如何在Python中为类动态创建类方法

81

如果我定义一个小的Python程序,如下:

class a():
    def _func(self):
        return "asdf"

    # Not sure what to resplace __init__ with so that a.func will return asdf
    def __init__(self, *args, **kwargs):
         setattr(self, 'func', classmethod(self._func))

if __name__ == "__main__":
    a.func

我收到了回溯错误。

Traceback (most recent call last):
  File "setattr_static.py", line 9, in <module>
    a.func
AttributeError: class a has no attribute 'func'

我想要弄清楚的是,如何在不实例化对象的情况下动态地给一个类设置一个类方法?


编辑:

这个问题的答案是:

class a():
    pass

def func(cls, some_other_argument):
    return some_other_argument

setattr(a, 'func', classmethod(func))

if __name__ == "__main__":
    print(a.func)
    print(a.func("asdf"))

返回以下输出

<bound method type.func of <class '__main__.a'>>
asdf

看一下类方法。Python中的静态方法与C++中的不同。 - SethMMorton
1
你没有在任何地方调用__init __(),所以它没有被执行也就不足为奇了。此外,你必须将新方法添加到上,而不是实例上。 - Sven Marnach
类似于 a.func = staticmethod(a._func) 这样的代码。 - Sven Marnach
1
@SvenMarnach 我如何在不实例化对象的情况下动态设置一个类方法? - user1876508
上面的编辑有一个错误:在setattr中,将“some_other_argument”替换为“func”。 - koriander
标记的重复内容是关于向单个实例添加方法,而不是类。 - OrangeDog
6个回答

80

你可以通过对类对象的简单赋值或对类对象使用setattr来动态地添加一个classmethod。这里我使用了Python的约定,即类名以大写字母开头,以减少混淆:

# define a class object (your class may be more complicated than this...)
class A(object):
    pass

# a class method takes the class object as its first variable
def func(cls):
    print 'I am a class method'

# you can just add it to the class if you already know the name you want to use
A.func = classmethod(func)

# or you can auto-generate the name and set it this way
the_name = 'other_func' 
setattr(A, the_name, classmethod(func))

11
这里有几个问题:
  • __init__只在创建实例时运行,例如obj = a()。这意味着当您执行a.func时,setattr()调用还没有发生。
  • 您无法从该类的方法中直接访问该类的属性,因此在__init__中使用_func不能直接生效,您需要使用self._funcself.__class__._func
  • self将成为a的一个实例,如果您在实例上设置属性,则该属性仅对该实例可用,而不适用于整个类。因此,即使调用了setattr(self,'func',self._func)a.func也将引发AttributeError
  • 以您当前的方式使用staticmethod不会产生任何影响,staticmethod将返回一个结果函数,它不会修改参数。因此,您需要像setattr(self,'func',staticmethod(self._func))一样做(但考虑到以上评论,这仍然无法实现您的目标)
那么现在的问题是,您实际上想要做什么?如果您真的想在初始化实例时向类添加属性,可以尝试以下方法:
class a():
    def _func(self):
        return "asdf"

    def __init__(self, *args, **kwargs):
        setattr(self.__class__, 'func', staticmethod(self._func))

if __name__ == '__main__':
    obj = a()
    a.func
    a.func()

然而,这还是有点奇怪。现在您可以访问a.func并无任何问题地调用它,但传递给a.funcself参数始终将是最近创建的a实例。我真的想不出任何理智的办法将实例方法_func()转换为静态方法或类方法。

由于您正在尝试动态添加函数到类中,也许以下内容更接近您实际想要做的事情?

class a():
    pass

def _func():
    return "asdf"

a.func = staticmethod(_func)  # or setattr(a, 'func', staticmethod(_func))

if __name__ == '__main__':
    a.func
    a.func()

我只是稍微澄清了一下我的问题。我想能够调用一个类方法而不必明确定义它。 - user1876508
请看我的编辑,最终的代码可能是你正在寻找的。 - Andrew Clark
如果多个模块调用此模块,staticmethod(_func)会为每个导入调用一次,还是只在模块的生命周期中调用一次? - user1876508
最后的代码示例:仅在模块加载时执行一次。 - eri

2

1. 基本思路:使用额外的类来保存方法

我发现了一种有意义的方法来完成这项工作:

首先,我们定义一个这样的基类:

class MethodPatcher:
    @classmethod
    def patch(cls, target):
        for k in cls.__dict__:
            obj = getattr(cls, k)
            if not k.startswith('_') and callable(obj):
                setattr(target, k, obj)

现在我们有了一个原始类:
class MyClass(object):
    def a(self):
        print('a')

接下来,我们定义想要添加到新的Patcher类中的新方法:

(在这种情况下,请勿以_开头命名方法)

class MyPatcher(MethodPatcher):
    def b(self):
        print('b')

然后调用:
MyPatcher.patch(MyClass)

因此,在原始的 MyClass 中添加了新方法 b(self)

obj = MyClass()
obj.a()  # which prints an 'a'
obj.b()  # which prints a 'b'

2. 使语法更简洁,我们使用类装饰器

现在如果我们已经声明了MethodPatcher,我们需要做两件事:

  • 定义一个ModelPatcher的子类ChildClass,其中包含要添加的额外方法
  • 调用ChildClass.patch(TargetClass)

因此,我们很快发现第二步可以通过使用装饰器来简化:

我们定义一个装饰器:

def patch_methods(model_class):
    def do_patch(cls):
        cls.patch(model_class)
    return do_patch

我们可以这样使用它:

@patch_methods(MyClass)
class MyClassPatcher(MethodPatcher):

    def extra_method_a(self):
        print('a', self)

    @classmethod
    def extra_class_method_b(cls):
        print('c', cls)

    # !!ATTENTION!! the effect on declaring staticmethod here may not work as expected:
    # calling this method on an instance will take the self into the first argument.
    # @staticmethod
    # def extra_static_method_c():
    #    print('c')

3. 组合起来

现在,我们可以将MethodPatcherpatch_method的定义放入一个单独的模块中:

# method_patcher.py

class MethodPatcher:
    @classmethod
    def patch(cls, target):
        for k in cls.__dict__:
            obj = getattr(cls, k)
            if not k.startswith('_') and callable(obj):
                setattr(target, k, obj)

def patch_methods(model_class):
    def do_patch(cls):
        cls.patch(model_class)
    return do_patch

因此,我们可以自由地使用它:

from method_patcher import ModelPatcher, patch_model

4. 最终方案:更简单的声明

很快我发现MethodPatcher类并不必要,而@patch_method装饰器可以完成工作,所以最终我们只需要一个patch_method

def patch_methods(model_class):
    def do_patch(cls):
        for k in cls.__dict__:
            obj = getattr(cls, k)
            if not k.startswith('_') and callable(obj):
                setattr(model_class, k, obj)
    return do_patch

使用方法如下:

@patch_methods(MyClass)
class MyClassPatcher:

    def extra_method_a(self):
        print('a', self)

    @classmethod
    def extra_class_method_b(cls):
        print('c', cls)

    # !!ATTENTION!! the effect on declaring staticmethod here may not work as expected:
    # calling this method on an instance will take the self into the first argument.
    # @staticmethod
    # def extra_static_method_c():
    #    print('c')

2
你可以用这种方式来做。
class a():
    def _func(self):
        return "asdf"

setattr(a, 'func', staticmethod(a._func))

if __name__ == "__main__":
    a.func()

如果这被用于模块中,那么每次使用类a时都会调用setattr吗? - user1876508
仅在第一个模块导入。 - eri
那么如果多个模块调用此模块,它只会调用一次setattr吗? - user1876508
2
只有第一次导入模块会连接到命名空间,第二次及更多的导入不会。 - eri
这个不太合理。它试图将_func既作为对象方法又作为类方法。更好的做法是在_func上直接使用@classmethod - tdelaney

2

您需要使用setattr(self, 'func', staticmethod(self._func))

您需要初始化类variable=a()以调用__init__。静态类中没有init。


如果我这样做的话,我会收到相同的回溯错误。 - user1876508
我相当确定我需要使用classmethod,但是你现在的方法对我来说行不通。 - user1876508
此方法适用于实例。 - eri

1

我使用的是Python 2.7.5,但以上解决方案对我来说无法正常工作。这是我最终得到的结果:

# define a class object (your class may be more complicated than this...)
class A(object):
    pass

def func(self):
    print 'I am class {}'.format(self.name)

A.func = func

# using classmethod() here failed with:
#       AttributeError: type object '...' has no attribute 'name'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接