使用元类动态添加方法或不使用元类动态添加方法

11

更新 - 2012/12/13

为了澄清一下 - 我的兴趣并不在于如何将方法添加到类中 - 如您可以在我的问题和人们的答案中看到的那样,有多种方法可以做到这一点(对自己的 Perl 深表歉意)。

我感兴趣的是学习使用不同方法向类添加方法的基本区别,而真正的大问题是为什么我需要使用元类。例如 Python 元类编程入门 指出:

可能是元类最常见的用途 [...]:添加、删除、重命名或替换已定义的类中的方法。

由于有更多的方法可以实现这一点,我感到困惑并正在寻找解释。

谢谢!

原始内容- 2012/12/12

我需要动态地向类(以及基于该类生成的新类)添加方法。我提出了两种方法,一种涉及元类,另一种则不需要。除了后者没有涉及“黑魔法”元类之外,我无法看出这两种方法给我带来任何区别 ;)

方法 1 #1 使用元类:

class Meta(type):
        def __init__(cls, *args, **kwargs):
                setattr(cls, "foo", lambda self: "foo@%s(class %s)" % (self,
                        cls.__name__))

class Y(object):
        __metaclass__ = Meta

y = Y()
y.foo() # Gives 'foo@<__main__.Y object at 0x10e4afd10>(class Y)'

第二种方法,#2 没有元类:

class Z(object):
        def __init__(self):
                setattr(self.__class__, "foo",
                        lambda self: "foo@%s(class %s)" %
                        (self, self.__class__.__name__))

z = Z()
z.foo() # Gives 'foo@<__main__.Z object at 0x10c865dd0>(class Z)'
据我所知,这两种方法在结果和“表现力”方面都是相同的。即使我尝试使用 type("NewClassY", (Y, ), {}) 或者 type("NewClassZ", (Z, ), {}) 来创建一个新类,我也会得到相同的预期结果,这两种方法之间没有区别。
因此,我想知道这两种方法是否存在任何“潜在”的差异,或者如果我使用#1#2是否会有什么问题,或者这只是一种语法糖?
PS:是的,我确实读了这里讨论Python元类和Pythonic数据模型的其他线程。

你可以使用这两种方法调用Y.fooZ.foo吗? - Blender
2
区别在于元类将在定义类时创建方法,而__init__方式将在实例化类时创建方法。还有多重继承(mixin),您可以使用它来添加方法。 - Jochen Ritzel
@Blender 不可以这样调用它们,因为它们不是“类方法”。 - TomasHeran
@JochenRitzel 这很有趣,也符合我的观察。但是是否存在一种方法无法实现而另一种可以实现的情况呢? - TomasHeran
2个回答

9

使用元类的明显原因是因为它们在类被知道时就提供了关于类的元数据,与对象的存在与否无关。很琐碎,对吧?那么,让我们展示一些我在您原来的ZY类上执行的命令,以便看到这意味着什么:

In [287]: hasattr(Y,'foo')
Out[287]: True

In [288]: hasattr(Z,'foo')
Out[288]: False

In [289]: Y.__dict__
Out[289]:
<dictproxy {..., 'foo': <function __main__.<lambda>>}>

In [290]: Z.__dict__
Out[290]:
<dictproxy {...}>

In [291]: z= Z()

In [292]: hasattr(Z,'foo')
Out[292]: True

In [293]: Z.__dict__
Out[293]:
<dictproxy {..., 'foo': <function __main__.<lambda>>}>

In [294]: y = Y()

In [295]: hasattr(Y,'foo')
Out[295]: True

In [296]: Y.__dict__
Out[296]:
<dictproxy {..., 'foo': <function __main__.<lambda>>}>

如您所见,第二个版本在声明后实际上会显著地改变类Z,这是您通常想要避免的。对于Python来说,在“类型”(类对象)上执行操作并使它们尽可能一致并不是一种罕见的操作,特别是在这种情况下(该方法实际上并不是在运行时动态的,而只是在声明时动态的)。
一个可以想到的应用是文档编写。如果您使用元类为foo添加docstring,则文档可能会通过__init__方法获取它,但这种情况非常不太可能。
这也可能导致难以发现的错误。考虑一个使用类的元信息的代码片段。在99.99%的情况下,这将在创建Z实例之后执行,但是那0.01%的情况可能会导致奇怪的行为,甚至崩溃。
在继承链中,这也可能变得棘手,您必须小心在哪里调用父类的构造函数。例如,像这样的类可能会引起问题:
class Zd(Z):
    def __init__(self):
        self.foo()
        Z.__init__(self)
a = Zd()
...
AttributeError: 'Zd' object has no attribute 'foo'

虽然这种方法工作得很好:

class Yd(Y):
    def __init__(self):
        self.foo()
        Y.__init__(self)
a = Yd()

在不小心的情况下,甚至在更复杂的层次结构中,MRO 不是显而易见的情况下,调用方法却没有使用相关的 __init__ 看起来可能很愚蠢。这种错误很难被发现,因为大多数情况下只要先调用了Z.__init__,Zd() 就会成功。


2
如果问题是动态添加方法,Python可以以一种非常直观的方式来处理。具体做法如下:
#!/usr/bin/python
class Alpha():

    def __init__(self):
        self.a = 10
        self.b = 100

alpha = Alpha()
print alpha.a
print alpha.b

def foo(self):
    print self.a * self.b * self.c

Alpha.c = 1000
Alpha.d = foo

beta = Alpha()
beta.d()

输出:

$ python script.py 
10
100
1000000

问候!

P.S.:我在这里看不到黑魔法的相似之处(:

编辑:

考虑到martineau的评论,我添加了data属性,而不是method function属性。我真的看不出在做Alpha.d = fooAlpha.d = lambda self: foo(self)之间有什么区别,除了我将使用一个lambda函数作为包装器来添加函数foo

方法的添加是相同的,python本身也将两个添加称为相同的名称:

#!/usr/bin/python
class Alpha():

    def __init__(self):
        self.a = 10
        self.b = 100

alpha = Alpha()
print alpha.a
print alpha.b

def foo(self):
    print self.a * self.b * self.c

Alpha.c = 1000

Alpha.d = lambda self: foo(self)
Alpha.e = foo

print Alpha.d
print Alpha.e

a = Alpha()
a.d()
a.e()

输出:

10
100
<unbound method Alpha.<lambda>>
<unbound method Alpha.foo>
1000000
1000000

如图所示,Python本身将两个结果都称为方法--唯一的区别在于一个是对函数 foo 的引用,另一个是对使用函数 foo 在其定义体中的lambda函数的引用。

如果我说错了什么,请纠正我。

祝好!


你的回答没有向类添加方法,只添加了数据属性,而不是方法函数属性。 - martineau
你介意举个例子说明什么是“方法函数属性”吗? - Rubens
1
Alpha.answer = lambda self: 42 - martineau
我根据你的回答添加了一个编辑,你介意看一下吗? - Rubens
谢谢大家,但我不是在寻找如何向类添加方法的方法。我感兴趣的事情是(也许我的问题措辞不够好,对此抱歉),一种方法(元类)是否从另一种方法(在类的__init__中添加方法并执行setattr(self.__class__. ...))根本上不同。@JochenRitzler 上面解释了一个区别。 - TomasHeran
TomasHeran: 不,你的问题措辞尤其是标题并不够好——实际上如果那是你真正想知道的内容,它们非常具有误导性。 - martineau

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