给 Python 已存在的类动态的添加方法(method)

monkeypatching

By Damnever on May 7, 2015

2015.11.2: python tornado中如何给每个服务器请求动态加上装饰器?@Damnever 的回答

下面是正题。。。 ***

动态添加实例属性的时候,这样:obj.a = 1setattr(obj, 'a', 1)就可以了,so easy!

但是,动态添加方法的时候,问题来了。


这里动态的给一个实例添加一个方法:

>>> class A(object):
...     pass
...
>>> def hello(self):
...    print "hello"
...
>>> a = A()
>>> a.hello = hello

调用一下,What the hell??

>>> a.hello()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: hello() takes exactly 1 argument (0 given)

对于实例来说只是function!连method都不是…

>>> a.hello
<function hello at 0x7ff1693ab758>
>>> a.hello(1)
hello

这是什么情况我也不得而知…


参考 stackoverflow 上的最高票回答Adding a Method to an Existing Object

对于一个类型(非实例)来说,它的属性都是unbound的:

>>> class A(object):
...     def hello(self):
...         print "hello"
...
>>> A.hello
<unbound method A.hello>
>>> A().hello
<bound method A.hello of <__main__.A object at 0x7f171e986810>>

所以可以通过类型来动态添加一个方法,对于实例来说,它是bound的,任何这个类型的实例都可以访问:

>>> class A(object):
...     pass
...
>>> def hello(self):
...     print 'hello'
...
>>> A.hello = hello
>>> A().hello()
hello
>>> A.hello
<unbound method A.hello>
>>> A().hello
<bound method A.hello of <__main__.A object at 0x7f62edae7810>>

另一种方式是使用types.MethodType,这一种只对当前实例起作用:

>>> import types
>>> class A(object):
...     pass
...
>>> def hello(self):
...     print hello
...
>>> a = A()
>>> a.hello = types.MethodType(hello, a)
>>> a.hello
<bound method ?.hello of <__main__.A object at 0x7fd8a318a290>>
>>> A.hello
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'A' has no attribute 'hello'

types模块是 2.6 版之后new模块的替代(Python 3里已经不存在 new 模块了),所以new里面的函数types模块都有替代,如:

>>> b = types.InstanceType(A)
>>> b
<__main__.A instance at 0x7fd8a318c488>
>>> b.hello = types.UnboundMethodType(hello, A)
>>> b.hello
<bound method ?.hello of <class __main__.A at 0x7fd8a315cc80>>
>>> A.hello
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: class A has no attribute 'hello'

这里忍不住要看一下types源码,很简短,但是你懂得… 涉及到type的,脑子里都是空白一片


这里还是不懂是什么机制,不过另一个答案Adding a Method to an Existing Object提到了descriptor protocol

描述器是属性, 实例方法, 静态方法, 类方法和 super 的背后的实现机制。

参考Descriptor HowTo Guide及译文Python描述器引导(翻译)


另外动态的添加staticmethodclassmethod的方式:

>>> class A(object):
...     pass
...
>>> def hello():
...     print 'hello'
...
>>> def world(cls):
...     print 'world'
...
>>> A.hello = staticmethod(hello)  # setattr(A, 'hello', staticmethod(hello))

>>> A.world = classmethod(world)  # setattr(A, 'hello', classmethod(world))

>>> a = A()
>>> a.hello()
hello
>>> a.world()
world
>>> a.hello  # A.hello 也是 function hello

<function hello at 0x7f39b4bf3758>
>>> a.world  # A.world 也是 bound method classobj.world

<bound method classobj.world of <class __main__.A at 0x7f39b4bf0258>>

话说我是怎么掉进这个坑的?其实只是为了搞一个奇葩的装饰器…

这也正是动态语言的强大之处:

class AddHigh(object):
	""" 给长方形添加高变成长方体 """
    def __init__(self, cls, z):
        self._cls = cls
        self._z = z

    def __call__(self, *args, **kwargs):
    	# 重写面积计算方式

        def _area(this, *args, **kwargs):
            return (self._z * this._x + self._z * this._y + this._x * this._y) * 2

        self._cls.area = _area
        obj = self._cls(*args, **kwargs)
        return obj


class Rectangle(object):
	""" 长方形 """
    def __init__(self, x, y):
        self._x = x
        self._y = y

    def area(self):
        return self._x * self._y

if __name__ == '__main__':
    a = Rectangle(1, 2)
    print a.area()
    b = AddHigh(Rectangle, 3)(1, 2)
    print b.area()

# 2

# 22