最近在看《松本行弘的程序世界》,其中讲到Ruby和Javascript里的原型编程,觉得非常灵活。我记得之前看Python教程的时候也有提到过类似的机制,无奈当时不求甚解,现在只好补补课。
在原型编程中,对象的成员变量和方法都可以动态修改,通过复制已有的对象来实现代码的重用。这样不需要事先定义好的类就可以实现面向对象编程。要实现原型编程,首要条件就是对象的成员变量和方法可以动态修改。Python提供了setattr()和delattr()两个函数来动态修改对象的属性,也就是成员变量和方法。先看下面一段代码:
class Foo(object):
def do(self):
print("done")
o = Foo()
try:
print(o.bar)
except:
print("no attribute bar found")
setattr(o, "bar", 1)
try:
print(o.bar)
except:
print("no attribute bar found")
delattr(o, "bar")
try:
print(o.bar)
except:
print("no attribute bar found")
第一次print(o.bar)的时候,因为Foo类型的对象没有这个属性,所以会抛出异常;第二次打印的时候,已经通过setattr()添加了bar属性,所以可以成功;之后又用delattr()删除了属性,所以第三次打印又是异常。事实上,还有更简单的写法:
o.bar = 1 # 等价于setattr(o, "bar", 1)
del o.bar # 等价与delattr(o, "bar")
在Python里,每个自定义类的对象都有一个__dict__成员变量,这个字典里记录了这个对象的动态属性(在类定义之外添加的属性)。对于Foo类型的对象o,打印出来就是这样:
>>> o = Foo()
>>> o.bar = 1
>>> o.__dict__
{'bar': 1}
动态添加的bar在__dict__中,而类中定义的do不在。__dict__在对象外部是可读可写的,相当于一个public类型的成员变量。通过修改对象的__dict__,就可以修改对象的属性。所以前面的代码还有第三种写法:
o.__dict__["bar"] = 1 # 等价于setattr(o, "bar", 1)
del o.__dict__["bar"] # 等价与delattr(o, "bar")
Python的内建类型,比如int、str或者object是没有这个成员变量的,因此这些类型的对象是不能动态添加属性的。既然可以对象的属性可以动态增加,那么把一个函数赋值给o的一个成员变量,是不是就成了一个方法了呢?
def func():
print("hello")
o.say = func
o.say()
看起来好像问题已经解决了,不过其实没有那么简单。在类的成员方法中,第一个参数都是self(比如前面的do方法),通过它可以访问对象的成员变量,相当于C++的this。但是这里的func()函数并没有self参数,因此也没有办法引用成员变量。我们试试给func()添加一个self参数:
def func(self):
print("hello")
再把它设置为o的属性:
>>> o.say = func
>>> o.say()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() takes exactly 1 argument (0 given)
这次直接抛出异常,告诉我们func()函数缺少一个参数。而对于正常的成员方法,调用的时候是不需要显式的传递self参数的,这说明func()函数距离真正成为成员函数还差了那么一丢丢。用type()函数查看一下类型,可以发现do和say原来是不同的类型:
>>> type(o.do)
<type 'instancemethod'>
>>> type(o.say)
<type 'function'>
看起来只要把func转换instancemethod类型就可以了。Python的types包中提供了MethodType来完成这种转换[1]:
>>> from types import MethodType
>>> o.say = MethodType(func, o, Foo)
>>> o.say()
hello
到这里我们的目的已经达到了。不过我还想补充一点儿关于开放类的内容。《松本行弘的程序世界》第6章中讲解了Ruby的开放类。在Ruby中,已经声明的类可以在代码中动态的修改,可以添加和删除方法,或者给方法起别名。这种方法很有用,比如RoR里就通过AcitveSupport对Ruby的基本类型进行了扩展,所以可以像下面这样写代码:
2.weeks.ago
Python中其实也有类似的机制。在Python里,一切皆对象。所谓的类,也不过是一种特殊类型的对象罢了。先看看上面的Foo类,其实也有__dict__属性:
>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__module__': 'test', '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None})
不过这个__dict__的类型有点不一样,不是字典,而是dict_proxy,这个我们暂时不讨论,只要知道这里的__dict__是不能修改的就好了。
>>> Foo.__dict__['a'] = 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'dictproxy' object does not support item assignment
虽然这种方式不能用了,但是setattr()和Foo.bar = 1这两种写法还是可以的,因此类的属性也是可以动态增加的。动态增加Foo类的属性,是否会影响已经生成的Foo类对象呢?
>>> o = Foo()
>>> Foo.bar = 1
>>> o.bar
1
>>> Foo.bar = 2
>>> o.bar
2
答案是肯定的。反过来,修改对象o的属性,是否会影响到Foo呢?接着前面的代码继续执行:
>>> o.bar = 3
>>> Foo.bar
2
>>> Foo.bar = 4
>>> o.bar
修改对象o的bar属性不会影响类Foo的bar属性,并且当o的bar属性被修改之后,再次修改Foo.bar,就不会对o.bar产生影响了。这有点类似与copy-on-write机制。在没有修改o.bar的时候,对象o其实是共享了Foo类的bar属性,因此修改Foo.bar,o.bar也随之改变。一旦修改了o.bar之后,就创建了一个bar属性的副本,再修改Foo.bar就不会影响o.bar了。其实只要把整个过程中对象o的__dict__属性打印出来就很清楚了:
>>> Foo.bar = 2
>>> o.__dict__
{}
>>> o.bar = 3
>>> o.__dict__
{'bar': 3}
>>> Foo.bar = 4
>>> o.__dict__
{'bar': 3}
给Foo类增加bar属性的时候,o.__dict__是空。这是打印o.bar,由于o本身没有bar属性,就会找到Foo类的bar属性。而对o.bar进行了修改之后,o.__dict__里就多了一个key为“bar”的项目,也就是给o本身增加了一个bar属性,此后再打印o.bar,访问的就是这个属性,而不是Foo.bar了。
前面给类添加了成员变量,要添加方法也是一样的,先用types.MethodType()将函数转换成instancemethod类型,然后复制给某个属性。给类添加方法时,MethodType()的第二个参数可以填None,不指定任何对象。
总结一下,Python还是很灵活的,不过美中不足的是,前面讲的东西仅仅适用于自定义类型。对于Python的内建类型,比如int、str和object,不论是类型本身还是对象,都是不能动态修改的。所以Python也没有办法像前面RoR那么灵活的去编程。另外,这种灵活性其实是双刃剑,滥用的话会让代码变得很难读,因为读者找不到某些类或者对象的属性都是在哪里添加的,也就很难理解代码。
-
这个方法是在https://segmentfault.com/q/1010000004087006看到的,里面还提供另一种使用partial的实现方式,这里暂不讨论。 ↩