猴子修补 Python class

Monkeypatching a Python class

我想了解 Python classes 和对象是如何工作的。在 Perl 中它非常简单,在 package 中定义的每个 sub 都可以调用为静态、class 或对象方法(CLASS::funcCLASS->func$obj->func).乍一看,Python class 看起来像带有 bless 哈希的 Perl class(Python 中的 __dict__ 属性 class).但是在 Python 中我有点困惑。因此,为了更好地理解,我尝试对一个空 class 进行猴子修补,添加 3 个行为与静态、class 和对象方法完全相同的属性,但我无法得到它。

起初我创建了正常的 class 以获得基本结果:

def say(msg, x):
    print('*', msg, 'x =', x, 'type(x) =', type(x))


class A():
    @staticmethod
    def stt_m(x):
        say('stt_m', x)

    @classmethod
    def cls_m(x):
        say('cls_m', x)

    def obj_m(x):
        say('obj_m', x)

然后我创建了一个函数(称为 test),它尝试使用一个参数调用所有方法,如果失败(因为第一个参数可以是 class 或对象本身),则尝试再次调用 none 在输出行前面打印一个 'X',然后打印检测到的类型:

def test(obj):
    # Detect if obj is a class or an instantiated object
    what = 'Class' if type(obj) == type else 'Object'
    print()
    # Try to call static, class and object method getting attributes
    for a in ('stt_m', 'cls_m', 'obj_m'):
        meth = getattr(obj, a)
        try:
            meth(111)
        except:
            print('X', end='')
            meth()
        print(' ', what, a, meth)

使用默认 A class 及其对象调用 test

test(A)
test(A())

结果是:

* stt_m x = 111 type(x) = <class 'int'>
  Class stt_m <function A.stt_m at 0x7fb37e63c8c8>
X* cls_m x = <class '__main__.A'> type(x) = <class 'type'>
  Class cls_m <bound method A.cls_m of <class '__main__.A'>>
* obj_m x = 111 type(x) = <class 'int'>
  Class obj_m <function A.obj_m at 0x7fb37e63c9d8>

* stt_m x = 111 type(x) = <class 'int'>
  Object stt_m <function A.stt_m at 0x7fb37e63c8c8>
X* cls_m x = <class '__main__.A'> type(x) = <class 'type'>
  Object cls_m <bound method A.cls_m of <class '__main__.A'>>
X* obj_m x = <__main__.A object at 0x7fb37e871748> type(x) = <class '__main__.A'>
  Object obj_m <bound method A.obj_m of <__main__.A object at 0x7fb37e871748>>

因此,使用 class 或对象前缀调用 staticmethod,它们的行为与普通(命名空间)函数一样(接受 1 个参数)。无论使用哪种方式调用 classmethod,传递的第一个参数都是 class 对象。从 class 调用 objectmethod 的行为与普通函数一样,如果从对象调用,则第一个参数是对象本身。这后来看起来有点奇怪,但我可以接受。

现在,让我们尝试猴子修补一个空 class:

class B():
    pass

B.stt_m = lambda x: say('stt_m', x)
B.cls_m = types.MethodType(lambda x: say('cls_m', x), B)
B.obj_m = types.MethodType(lambda x: say('obj_m', x), B())

test(B)
test(B())

结果是:

* stt_m x = 111 type(x) = <class 'int'>
  Class stt_m <function <lambda> at 0x7fbf05ec7840>
X* cls_m x = <class '__main__.B'> type(x) = <class 'type'>
  Class cls_m <bound method <lambda> of <class '__main__.B'>>
X* obj_m x = <__main__.B object at 0x7fbf0d7dd978> type(x) = <class '__main__.B'>
  Class obj_m <bound method <lambda> of <__main__.B object at 0x7fbf0d7dd978>>

X* stt_m x = <__main__.B object at 0x7fbf06375e80> type(x) = <class '__main__.B'>
  Object stt_m <bound method <lambda> of <__main__.B object at 0x7fbf06375e80>>
X* cls_m x = <class '__main__.B'> type(x) = <class 'type'>
  Object cls_m <bound method <lambda> of <class '__main__.B'>>
X* obj_m x = <__main__.B object at 0x7fbf0d7dd978> type(x) = <class '__main__.B'>
  Object obj_m <bound method <lambda> of <__main__.B object at 0x7fbf0d7dd978>>

根据此模式,stt_m 的行为类似于普通 class 和 cls_m 的对象方法,而 obj_m 的行为类似于 class 的方法正常 class.

我可以用这种方式猴子修补静态方法吗?

您可以 monkey-patch 方法到 class,但它是这样完成的:

B.stt_m = staticmethod(lambda x: say('stt_m', x))
B.cls_m = classmethod(lambda x: say('cls_m', x))
B.obj_m = lambda x: say('obj_m', x)

您的 B.cls_m 版本没问题,但是您的 B.stt_m 创建了一个普通方法,并且您的 B.obj_m 将一个实例方法附加到新创建的 B(),但是然后 B() 被丢弃,你测试一个新的 B() 没有额外的方法。

通常不需要在Python中使用types.MethodType:

types.MethodType(function, object_)

等同于

function.__get__(object_)

哪个好一点,虽然也很少见。

还有(不相关但太简洁了,更不用说),在 Python 的新版本中,您的

print('*', msg, 'x =', x, 'type(x) =', type(x))

可以写成

print(f"* {msg} {x = } {type(x) = }")