在Python中,如何进行类扩展(猴子补丁)?

14 投票
2 回答
1435 浏览
提问于 2025-04-16 19:12
class Foo(object):
  pass

foo = Foo()
def bar(self):
  print 'bar'

Foo.bar = bar
foo.bar() #bar

如果你是从JavaScript过来的,可能知道如果给一个“类”的原型添加了某个属性,那么这个“类”的所有实例都会在它的原型链上拥有这个属性。因此,不需要对任何实例或“子类”进行修改。

那么,像Python这样的基于类的语言是如何实现“猴子补丁”的呢?

2 个回答

10

我刚刚读了一堆文档,关于如何解析 foo.bar 的整个过程大概是这样的:

  • 我们能通过以下过程找到 foo.__getattribute__ 吗?如果可以,就用 foo.__getattribute__('bar') 的结果。
    • 查找 __getattribute__ 不会导致无限递归,但它的实现可能会。
    • 实际上,我们总能在新式对象中找到 __getattribute__,因为 object 提供了一个默认实现——不过这个实现是按照上述过程进行的。;)
    • 如果我们在 Foo 中定义了一个 __getattribute__ 方法,并访问 foo.__getattribute__,那么 foo.__getattribute__('__getattribute__') 被调用!但这并意味着会无限递归——只要你小心;)
  • bar 是 Python 运行时提供的“特殊”属性名吗(例如 __dict____class____bases____mro__)?如果是,就用这个。
  • barfoo.__dict__ 字典中吗?如果是,就用 foo.__dict__['bar']
  • foo.__mro__ 存在吗(也就是说,foo 实际上是一个类)?如果是,
    • 对于 foo.__mro__[1:] 中的每个基类 base
      • (注意第一个会是 foo 本身,我们已经搜索过了。)
      • barbase.__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__ 属性的方法。)
    • barbase.__dict__ 中吗?如果是:
      • x 等于 base.__dict__['bar']
      • 我们能找到(再次递归,但不会造成问题) x.__get__ 吗?
        • 如果可以,就用 x.__get__(foo, foo.__class__)
        • (注意,函数 bar 本身也是一个对象,Python 编译器会自动给函数一个 __get__ 属性,设计用来这样使用。)
        • 否则,就用 x
  • 如果我们仍然没有找到可以使用的东西:我们能通过之前的过程找到 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 是极其反射的。:) 但如果你想学习如何做到这一点,并真正理解这个情况,你有很多 阅读要做。;)

11

真正的问题是,怎么可能不这样呢?在Python中,类本身就是一种重要的对象。访问类的实例(也就是对象)里的属性时,Python会先查找这个实例的属性,如果找不到,就会去查找这个类的属性,再找它的父类属性(按照一定的顺序)。这些查找都是在程序运行时进行的(在Python中,所有事情都是在运行时处理的)。如果你在创建了一个实例之后给类添加了一个新属性,这个实例仍然能“看到”这个新属性,因为没有什么会阻止它。

换句话说,这一切之所以能这样工作,是因为Python不会缓存属性(除非你的代码自己做了缓存),也因为它不使用负缓存、影子类或者其他会限制这种行为的优化技术(即使某些Python实现会这样做,它们也会考虑到类可能会改变),而且一切都是在运行时进行的。

撰写回答