协慌网

登录 贡献 社区

向现有对象实例添加方法

我读过,可以在 Python 中向现有对象(即不在类定义中)添加方法。

我知道这样做并不总是一件好事。但是怎么可能呢?

答案

在 Python 中,函数和绑定方法之间存在差异。

>>> def foo():
...     print "foo"
...
>>> class A:
...     def bar( self ):
...         print "bar"
...
>>> a = A()
>>> foo
<function foo at 0x00A98D70>
>>> a.bar
<bound method A.bar of <__main__.A instance at 0x00A9BC88>>
>>>

绑定方法已 “绑定”(描述性)到一个实例,并且无论何时调用该方法,该实例都将作为第一个参数传递。

但是,作为类(而不是实例)的属性的可调用对象仍未绑定,因此您可以在需要时随时修改类定义:

>>> def fooFighters( self ):
...     print "fooFighters"
...
>>> A.fooFighters = fooFighters
>>> a2 = A()
>>> a2.fooFighters
<bound method A.fooFighters of <__main__.A instance at 0x00A9BEB8>>
>>> a2.fooFighters()
fooFighters

先前定义的实例也会被更新(只要它们本身没有覆盖属性):

>>> a.fooFighters()
fooFighters

当您要将方法附加到单个实例时,就会出现问题:

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

将函数直接附加到实例时,该函数不会自动绑定:

>>> a.barFighters
<function barFighters at 0x00A98EF0>

要绑定它,我们可以在类型模块中使用 MethodType 函数:

>>> import types
>>> a.barFighters = types.MethodType( barFighters, a )
>>> a.barFighters
<bound method ?.barFighters of <__main__.A instance at 0x00A9BC88>>
>>> a.barFighters()
barFighters

这次,该类的其他实例没有受到影响:

>>> a2.barFighters()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: A instance has no attribute 'barFighters'

通过阅读有关描述符元类编程的信息,可以找到更多信息。

前言 - 关于兼容性的说明:其他答案可能仅在 Python 2 中有效 - 此答案在 Python 2 和 3 中应该可以很好地工作。如果仅编写 Python 3,则可能会显式地从object继承,否则代码应保留在相同的。

向现有对象实例添加方法

我读过,可以在 Python 中向现有对象(例如不在类定义中)添加方法。

我了解这样做并非总是一个好的决定。但是,怎么可能呢?

是的,有可能 - 但不建议

我不建议这样做。这是一个坏主意。不要这样

这有两个原因:

  • 您将向执行此操作的每个实例添加一个绑定对象。如果您经常执行此操作,则可能会浪费大量内存。通常仅在调用的短时间内创建绑定方法,然后在自动垃圾回收时它们不再存在。如果手动执行此操作,则将具有一个引用绑定方法的名称绑定 - 这将防止使用时对其进行垃圾回收。
  • 给定类型的对象实例通常在该类型的所有对象上都有其方法。如果在其他位置添加方法,则某些实例将具有那些方法,而其他实例则不会。程序员不会期望如此,您冒着违反最不惊奇规则的风险。
  • 由于还有其他确实很好的理由不这样做,因此,如果这样做,您的声誉也会很差。

因此,除非您有充分的理由,否则我建议您不要这样做。这是更好的在类定义来定义的正确方法或类直接不太优选到猴的贴剂,是这样的:

Foo.sample_method = sample_method

由于具有指导意义,因此,我将向您展示一些实现此目的的方法。

怎么做

这是一些设置代码。我们需要一个类定义。可以将其导入,但这并不重要。

class Foo(object):
    '''An empty class to demonstrate adding a method to an instance'''

创建一个实例:

foo = Foo()

创建一个要添加到其中的方法:

def sample_method(self, bar, baz):
    print(bar + baz)

无效的方法(0)- 使用描述符方法__get__

在函数上进行点分查询会将该函数的__get__方法与实例一起调用,将对象绑定到该方法,从而创建一个 “绑定方法”。

foo.sample_method = sample_method.__get__(foo)

现在:

>>> foo.sample_method(1,2)
3

方法一 - types.MethodType

首先,导入类型,从中我们将获得方法构造函数:

import types

现在我们将方法添加到实例中。为此,我们需要types模块中的 MethodType 构造函数(已在上面导入)。

types.MethodType 的参数签名为(function, instance, class)

foo.sample_method = types.MethodType(sample_method, foo, Foo)

和用法:

>>> foo.sample_method(1,2)
3

方法二:词法绑定

首先,我们创建一个包装器函数,将方法绑定到实例:

def bind(instance, method):
    def binding_scope_fn(*args, **kwargs): 
        return method(instance, *args, **kwargs)
    return binding_scope_fn

用法:

>>> foo.sample_method = bind(foo, sample_method)    
>>> foo.sample_method(1,2)
3

方法三:functools.partial

局部函数将第一个自变量应用于函数(以及可选的关键字自变量),以后可以与其余自变量(和覆盖的关键字自变量)一起调用。因此:

>>> from functools import partial
>>> foo.sample_method = partial(sample_method, foo)
>>> foo.sample_method(1,2)
3

当您认为绑定方法是实例的部分功能时,这很有意义。

未绑定函数作为对象属性 - 为什么不起作用:

如果尝试以与将其添加到类中相同的方式添加 sample_method,则它不受实例约束,并且不会将隐式 self 作为第一个参数。

>>> foo.sample_method = sample_method
>>> foo.sample_method(1,2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sample_method() takes exactly 3 arguments (2 given)

我们可以通过显式传递实例(或其他任何方法,因为此方法实际上并不使用self参数变量)来使未绑定函数起作用,但是它与其他实例的预期签名不一致(如果我们是猴子,修补此实例):

>>> foo.sample_method(foo, 1, 2)
3

结论

现在,您知道可以执行此操作的几种方法,但是很认真地讲 - 不要这样做。

自 python 2.6 起不推荐使用 new模块,并在 3.0 版中将其删除,请使用类型

参见http://docs.python.org/library/new.html

在下面的示例中,我故意从patch_me()函数中删除了返回值。我认为提供返回值可能会使人相信 patch 返回了一个新对象,这是不正确的 - 它修改了传入的对象。可能这可以促进对猴子补丁的更严格的使用。

import types

class A(object):#but seems to work for old style objects too
    pass

def patch_me(target):
    def method(target,x):
        print "x=",x
        print "called from", target
    target.method = types.MethodType(method,target)
    #add more if needed

a = A()
print a
#out: <__main__.A object at 0x2b73ac88bfd0>  
patch_me(a)    #patch instance
a.method(5)
#out: x= 5
#out: called from <__main__.A object at 0x2b73ac88bfd0>
patch_me(A)
A.method(6)        #can patch class too
#out: x= 6
#out: called from <class '__main__.A'>