在Python中,如何进行类扩展(猴子补丁)?
class Foo(object):
pass
foo = Foo()
def bar(self):
print 'bar'
Foo.bar = bar
foo.bar() #bar
如果你是从JavaScript过来的,可能知道如果给一个“类”的原型添加了某个属性,那么这个“类”的所有实例都会在它的原型链上拥有这个属性。因此,不需要对任何实例或“子类”进行修改。
那么,像Python这样的基于类的语言是如何实现“猴子补丁”的呢?
2 个回答
我刚刚读了一堆文档,关于如何解析 foo.bar
的整个过程大概是这样的:
- 我们能通过以下过程找到
foo.__getattribute__
吗?如果可以,就用foo.__getattribute__('bar')
的结果。- 查找
__getattribute__
不会导致无限递归,但它的实现可能会。 - 实际上,我们总能在新式对象中找到
__getattribute__
,因为object
提供了一个默认实现——不过这个实现是按照上述过程进行的。;) - 如果我们在
Foo
中定义了一个__getattribute__
方法,并访问foo.__getattribute__
,那么foo.__getattribute__('__getattribute__')
会被调用!但这并不意味着会无限递归——只要你小心;)
- 查找
bar
是 Python 运行时提供的“特殊”属性名吗(例如__dict__
、__class__
、__bases__
、__mro__
)?如果是,就用这个。bar
在foo.__dict__
字典中吗?如果是,就用foo.__dict__['bar']
。foo.__mro__
存在吗(也就是说,foo
实际上是一个类)?如果是,- 对于
foo.__mro__
[1:] 中的每个基类base
:- (注意第一个会是
foo
本身,我们已经搜索过了。) bar
在base.__dict__
中吗?如果是:- 让
x
等于base.__dict__['bar']
。 - 我们能找到(再次递归,但不会造成问题)
x.__get__
吗?- 如果可以,就用
x.__get__(foo, foo.__class__)
。 - (注意,函数
bar
本身也是一个对象,Python 编译器会自动给函数一个__get__
属性,设计用来这样使用。) - 否则,就用
x
。
- 如果可以,就用
- 让
- (注意第一个会是
- 对于
- 对于
foo.__class__.__mro__
的每个基类base
:- (注意这个递归不是问题:那些属性应该总是存在,并且属于“由 Python 运行时提供”的情况。
foo.__class__.__mro__[0]
总是foo.__class__
,也就是我们例子中的Foo
。) - (注意,即使
foo.__mro__
存在,我们也会这样做。这是因为类也有一个类:它的名字是type
,它提供了计算__mro__
属性的方法。) bar
在base.__dict__
中吗?如果是:- 让
x
等于base.__dict__['bar']
。 - 我们能找到(再次递归,但不会造成问题)
x.__get__
吗?- 如果可以,就用
x.__get__(foo, foo.__class__)
。 - (注意,函数
bar
本身也是一个对象,Python 编译器会自动给函数一个__get__
属性,设计用来这样使用。) - 否则,就用
x
。
- 如果可以,就用
- 让
- (注意这个递归不是问题:那些属性应该总是存在,并且属于“由 Python 运行时提供”的情况。
- 如果我们仍然没有找到可以使用的东西:我们能通过之前的过程找到
foo.__getattr__
吗?如果可以,就用foo.__getattr__('bar')
的结果。 - 如果一切都失败了,就
raise AttributeError
。
bar.__get__
实际上并不是一个函数——它是一个“方法包装器”——但你可以想象它大致是这样实现的:
# Somewhere in the Python internals
class __method_wrapper(object):
def __init__(self, func):
self.func = func
def __call__(self, obj, cls):
return lambda *args, **kwargs: func(obj, *args, **kwargs)
# Except it actually returns a "bound method" object
# that uses cls for its __repr__
# and there is a __repr__ for the method_wrapper that I *think*
# uses the hashcode of the underlying function, rather than of itself,
# but I'm not sure.
# Automatically done after compiling bar
bar.__get__ = __method_wrapper(bar)
顺便说一下,__get__
自动附加到 bar
上的“绑定”是你必须明确指定 self
参数的原因之一。在 JavaScript 中,this
是神奇的;而在 Python 中,绑定到 self
的过程才是神奇的。;)
是的,你可以在自己的对象上显式设置一个 __get__
方法,并让它在将类属性设置为对象的实例后,从另一个类的实例中访问时执行特殊操作。Python 是极其反射的。:) 但如果你想学习如何做到这一点,并真正理解这个情况,你有很多 阅读要做。;)
真正的问题是,怎么可能不这样呢?在Python中,类本身就是一种重要的对象。访问类的实例(也就是对象)里的属性时,Python会先查找这个实例的属性,如果找不到,就会去查找这个类的属性,再找它的父类属性(按照一定的顺序)。这些查找都是在程序运行时进行的(在Python中,所有事情都是在运行时处理的)。如果你在创建了一个实例之后给类添加了一个新属性,这个实例仍然能“看到”这个新属性,因为没有什么会阻止它。
换句话说,这一切之所以能这样工作,是因为Python不会缓存属性(除非你的代码自己做了缓存),也因为它不使用负缓存、影子类或者其他会限制这种行为的优化技术(即使某些Python实现会这样做,它们也会考虑到类可能会改变),而且一切都是在运行时进行的。