11

更新 - 2012/12/13

只是为了澄清一下-我对如何向类添加方法的方式不太感兴趣-正如您在下面我的问题和人们的回答中所看到的那样,有不止一种方法可以做到这一点(面颊上的舌头和帽子提示我的 Perl 自我)。

我感兴趣的是了解使用不同方法向类添加方法的根本区别是什么,而真正的大问题是为什么我需要使用元类。例如A Primer on Python Metaclass Programming指出:

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

而且由于有更多方法可以做到这一点,我很困惑并寻找解释。

谢谢!

原创 - 2012/12/12

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

使用元类的方法#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 数据模型中的元类的线程。

4

2 回答 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使用元类,文档可能会通过__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()

在没有使用相关的情况下调用方法可能看起来很愚蠢,__init__但是当你不小心时可能会发生这种情况,当然在 MRO 不是立即显而易见的更复杂的层次结构中。并且很难发现这个错误,因为大多数情况下 Zd() 会Z.__init__在之前的某个地方被调用时成功。

于 2012-12-13T13:17:23.100 回答
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

问候!

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

编辑:

考虑到 martineau 的评论,我要添加data attributes,而不是method function attributes。我真的看不出做Alpha.d = fooand之间没有区别Alpha.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 本身将两个结果添加都命名为方法——唯一的区别是一个是对 function 的引用,另一个是对在其定义主体foo中使用该函数的 lambda 函数的引用。foo

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

问候!

于 2012-12-12T23:24:16.187 回答