• Python进阶:猴子补丁


    猴子补丁是一项允许在运行时更改对象行为的技术。 它是一个非常有用的功能,但它也可能使你的代码更难以理解和调试,因此,在实现猴子补丁程序时必须谨慎。

    猴子补丁的用法

    猴子补丁与Python中的灵活性紧密相关。 自定义对象是可变的,因此可以替换其属性而无需创建该对象的新副本。

    class A:
        def speak(self):
            return "hello"
    
    def speak_patch(self):
        return "world"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们在最开始的地方做了猴子补丁,即使用speak_patch这个函数去取代掉原来的speak。这时我们调用speak函数就变成了调用speak_patch函数了。

    >>> A.speak = speak_patch  # 做了猴子补丁,替换了原来的speak函数
    >>> c = A()
    >>> c.speak()
    world
    
    • 1
    • 2
    • 3
    • 4

    再来看一个例子:
    我们定义一个类,在类中我们不实现__len__方法,而在类外我们实现一个可以计算列表长度的方法。

    class A:
        def __init__(self, array):
            self._list = array
    
        # def __len__(self): 
        #     return len(self._list)
    
    
    def length(obj):
        return len(obj._list)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    下面我们做一个猴子补丁,将类中的__len__方法替换为length函数。

    >>> A.__len__ = length
    >>> a = A([1, 2, 3])
    >>> len(a)
    3
    
    • 1
    • 2
    • 3
    • 4

    注意:该有的参数还是要有,但名称可以不一样,类中原本应该是是self,在类外的实现可以任意写 (例如obj),因为会在做猴子补丁的时候自己去对应起来。但是取长度时,就必须要写obj._list

    可以看到,上面类中并没有定义__len__的方法,但当我们做了猴子补丁以后,仍然可以正确的调用len函数。经过以上的部分,就可以成功的在运行时,实际上实现类中的__len__方法。那为什么不一开始就实现 __len__呢?像是:

    class A:
        def __init__(self, array):
            self._list = array
    
        def __len__(self): 
            return len(self._list)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    原因是我们不希望去修改类中的源代码,而后者的修改也只是暂时性的,像用在测试时

    当然,猴子补丁不会允许你任意的使用,它是有限制的,它限制你不能给内置类型打补丁。比如要给str这个对象打补丁,就会报错。

    >>> def find(self, sub, start=None, end=None):
    ...     return 'ok'
    ...
    
    >>> str.find = find
    Traceback (most recent call last):
      File "", line 1, in <module>
    TypeError: can't set attributes of built-in/extension type 'str'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里的原因是可以保证内置的功能都是原本的,避免有人去打补丁后,导致后续一堆奇怪的问题。需要切记,猴子补丁不可乱用!

    types.MethodType

    很类似猴子补丁,来看一个types.MethodType的例子,

    import types
    
    class A:
        def speak(self):
            return "hello"
    
    
    def speak_patch(self):
        return "world"
    
        
    >>> a = A()
    >>> a.speak = types.MethodType(speak_patch, a)
    >>> a.speak()
    'world'
    >>> a2 = A()
    >>> a2.speak()  # 对于a2我们并没有修改,所以仍然是hello
    'hello'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    既然提到了types.MethodType,那也就来简单的学一下。

    1. types.MethodType的作用之—:添加实例方法

    我们先定义一个类和一个函数

    import types
    
    class cla(object):
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def prii(self):
            print("pri")
    
    
    def f1(self):
        print("f1")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们利用types.MethodType来为类添加实例方法f1

    >>> c = cla("zhangsan", 18)
    >>> c.prii()
    pri
    >>> c.f1 = types.MethodType(f1, c)
    >>> c.f1()
    f1
    >>> func = types.MethodType(f1, c)
    >>> func()
    f1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以看到,func()一样可以正常的执行。所以这个动态添加的办法和原生的属性还是有所区别的。执行了types.MethodType(f1,c)之后,相当于产生了一个特殊返回值,这个返回值指向f1函数,里面已经被默认传递了c对象作为参数,只要使用这个返回值,就可以当做是调用了对象的方法。c.f1=types.MethodType(f1,c)这么写之后,再调用c.f1(),完全是为了字面上的符合,符合这个操作的原意而已。

    1. types.MethodType的作用之二:添加静态方法

    在使用静态方法时,类中的self将不会再进行传值,此时,静态方法已经和类没什么关系了。 需要通过修饰器@staticmethod来进行修饰,静态方法不需要多定义参数。
    还是上面定义的cla类和f1函数,我们接下来定义一个静态方法test

    @staticmethod
    def test():
        print(":static")
    
    
    >>> c = cla("zhangsan", 18)
    >>> c.test = test()
    >>> c.test()
    :static
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里不需要绑定,因为静态方法不需要任何的参数,只要赋值了就直接用。但注意需要使用类名来调用。

    1. types.MethodType的作用之三:添加类方法

    下面定义一个类方法clsm,类方法也是直接赋值调用就行。

    @classmethod
    def clsm(cls):
        print("classmethod call")
    
    
    >>> cla.clsm = clsm
    >>> cla.clsm()
    classmethod call
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    动态添加可以做到什么呢?一款APP,在没有进行整个APP的情况下,里面的部分功能变了。即我部分的功能代码存在一个文件中,我在后台默默更换这些文件,下次你再打开APP,对应的功能或者样式就变了。这是不是就是类似猴子补丁呢?

    什么时候用去猴子补丁?

    猴子补丁非常强大,它显示了Python的灵活性。下面就来思考一下何时用猴子补丁。

    作为一般规则,最好不要打猴子补丁。例如,如果要更改程序的行为,则可以为要更改的类定义子类。猴子补丁的问题在于程序的行为变得更加难以理解,要追溯行为的变化变得非常复杂。但是,有时可能会有很大的好处。例如,使用numpy计算快速傅里叶变换可能比使用其他实现慢。想象一下你想使用PyFFTW,但是不想重写所有程序。这时你可以猴子补丁你的代码!

    请参阅下面的示例:

    import pyfftw
    import numpy
    numpy.fft = pyfftw.interfaces.numpy_fft
    
    • 1
    • 2
    • 3

    现在,无论何时使用numpy提供的FFT例程,它们都会被PyFFTW的例程自动替换。这可能会对你的程序产生巨大的影响,并且只用了一行代码!

    另外,常见的情况是测试。有时,你想在缺乏某些功能的环境中测试代码,或者由于测试实际上是在修改实时数据库,因此想防止这种情况。在这种情况下,可以在进行测试之前更改与数据库通信的方法。 即在测试程序时先避免与设备通信。

    总之,究竟如何实现此行为将取决于具体情况。

    参考

  • 相关阅读:
    猿创征文|Java之static关键字的应用【工具类、代码块和单例】
    java计算机毕业设计科研团队管理系统MyBatis+系统+LW文档+源码+调试部署
    【以太网硬件十七】什么!?网线边传数据还能边供电?
    数字化转型之数字资产知识库(springboot+es+vue+neo4j)
    使用IDEA创建一个SpringBoot项目
    GPT出现Too many requests in 1 hour. Try again later.
    Java加密2-MAC 对称密钥加密
    uniapp微信小程序获取屏幕宽高
    键盘方向键移动当前选中的table单元格,并可以输入内容
    【Java 面试题】经典 Java 面试题 200 问(下)
  • 原文地址:https://blog.csdn.net/qq_37085158/article/details/126392950