跳转至主要内容

Python编程

Python编程技巧:Monkey Patching实例分析与潜在风险

Sprite
发表于 2024年10月23日

Monkey Patching 是一种技术,允许你在运行时改变对象的行为。这是一种非常有用的特性,但也可能使我们的代码变得更加难以理解和调试,我们必须谨慎地使用 Monkey Patching 。

在本文中,我们将看到一些如何使用 Monkey Patching 快速解决特定问题的例子,以及讨论在较大项目环境中 Monkey Patching 会发生的后果。

Monkey Patching 与 Python 中的可变性概念密切相关。因为自定义对象是可变的,它们的属性可以被替换而不需要创建对象。

我们看一下这个例子:

class MyClass:
    a = 1
    b = '2'

然后我们可以像这样使用代码:

var1 = MyClass()
var2 = var1

var1.a = 2
var1.b = '3'

print(var2.a)
# 2
print(var2.b)
# '3'

我们使用 MyClass 创建一个对象,并称其为 var1 。然后我们将对象拷贝到另一个名为 var2 的变量中。我们改变了 var1 中存储的值,但观察到 var2 中存储的值也发生了改变。这仅仅是因为在 Python 中,变量只是一个引用。在 var2 = var1 行,我们只是拷贝了引用,但两个引用指向同一个对象实例。

Python 还允许你在类本身中更改属性,而不是在类的实例中。我们可以这样做:

var1 = MyClass()
var2 = MyClass()
print(var1.a)
# 1
MyClass.a = 2
print(var1.a)
# 2
print(var2.a)
# 2

我们看到的是,如果直接修改类的任何属性的值,实例会继承此更改。这既非常有用,又非常危险,因为你可能会修改你原本并不打算修改的对象的属性值。

还有下面一个重要的行为,即混合之前遵循的两种方法:

var1 = MyClass()
var2 = var1

var1.a = 2
var1.b = '3'

MyClass.a = 3

print(var1.a)
# 2
print(var2.a)
# 2

即使将属性 a 更改为 3 ,你也不会看到此更改出现在类的实例上。这是因为 Python 中可变和不可变数据类型背后的思想。由于你更改了 var1.a 的值,现在该属性指向的对象与类属性所指向的对象不同。

最后,我想指出的最后一种情况是,在修改属性 a 之前保留对其的引用会发生什么

var1 = MyClass()
var2 = var1

var3 = var1.a
[...]
print(var3)
# 3

我已经跳过了更改属性值的代码部分。现在你会发现,如果你将 var1.a 实际存储在变量 var3 中,当你直接更改类中存储的值时,这个变量实际上被修改了。所有这些行为实际上是合理的,如果你考虑到变量只存储对象的引用而不是对象本身,并且当你改变一个不可变变量时,你会创建一个新的引用。

上面的所有示例都与某种方式的猴子补丁有关。你可以看到,我们正在运行时更改类的值。我们已经强调在程序执行过程中而不是在定义本身中更改属性值的一些结果。

上面的示例如果我们考虑方法是与上面的示例中的 a 或 b 完全一样的属性时,可以进行扩展

class MyClass:
    a = 1
    b = '2'

    def get_value(self):
        return self.a

我们实例化这个类:

var1 = MyClass()
print(var1.get_value())

然后我们定义一个新函数,我们想用它来替换 get_value :

def get_new_value(cls):
    return cls.b

在上面的函数中,我已经用 cls 替换了 self ,只是为了更明显,但在你的情况下,你可以自由地使用更合适的关键字。然后我们替换方法:

MyClass.get_value = get_new_value

如果我们使用这个方法,将得到:

print(var1.get_value())
# 2

我们在 var1 定义后替换了 get_value 。如果我们定义一个新对象,得到相同的输出是符合预期的。

var2 = MyClass()
print(var2.get_value())
# 2

如果在更改方法之前我们定义了两个不同的对象,结果将是相同的。我们可以重写类的方法:

var1 = MyClass()
var2 = MyClass()

MyClass.get_value = get_new_value

print(var1.get_value())
print(var2.get_value())

文章开头的示例,当我们使用整数或字符串作为属性时仍然有效。 你可以检查如果复制对象,将其存储为新变量,然后覆盖该方法会发生什么。 方法和整数或字符串一样,没有什么特别的地方。 主要区别在于它们接受输入。

在上面的例子中,我们已经在类级别上替换了该方法。如果我们想在实例级别上替换该方法,那么方法会有些不同。如果我们在类级别进行替换,所有实例都会受到这些更改的影响,而这可能不是我们想要的。我们可以这样做:

import types

class MyClass:
    a = 1
    b = '2'

    def get_value(self):
        return self.a

def get_new_value(cls):
    return cls.b

var1 = MyClass()
var2 = MyClass()
var1.get_value = types.MethodType(get_new_value, var1)
print(var1.get_value())
# 2
print(var2.get_value())
# 1

在这个例子中,你可以看到我们已经改变了 var1 的方法的行为,但是没有改变 var2 的方法。请注意,我们在脚本开头导入了 types 。其余部分与我们已经完成的相同,只有一个例外,当我们替换 get_value 方法时。因为我们正在改变一个实例的方法,所以它需要是正确的类型。

>>> type(get_new_value)
<class 'function'>
>>> type(MyClass.get_value)
<class 'function'>
>>> type(var1.get_value)
<class 'method'>

方法和函数的主要区别在于,方法的第一个参数是实例本身( self )。因此,在替换实例上的函数之前,我们必须将函数转换为方法。

Monkey Patching 猴子补丁

最后要讨论一种模式是在模块级别进行猴子补丁。到目前为止,我们使用的属性和方法都属于自定义类,在名为 module.py 的文件中,我们可以添加以下内容:

def print_variable(var):
    print(var)

在另一个名为 script.py 的文件中添加:

import module

var1 = 1

AE_module.print_variable(var1)
# 1
def print_plus_one(var):
    print(var+1)

AE_module.print_variable = print_plus_one
AE_module.print_variable(var1)
# 2

你看到了猴子补丁也可以用在模块上。当你尝试实现这种补丁时,必须注意 Python 中导入发生的顺序。如果你使用__init__.py 文件来加载模块,并且它们之间存在一些依赖关系,当你进行猴子补丁时,可能程序已经太迟了。类似于当你修改对象的属性值,然后在类级别更改该值时发生的情况。

我们创建一个新文件,命名为 module2.py 并添加以下内容:

import module

def another_print(var):
    module.print_variable(var+1)

可以看到我们从原始模块中使用了 print_variable 。我们只是在打印之前增加了 +1 。我们可以修改文件 script.py 以包含这个新模块:

import module
import module2

var1 = 1

module.print_variable(var1)
# 1

def print_plus_one(var):
    print(var+1)

module.print_variable = print_plus_one

module.print_variable(var1)
# 2
module2.another_print(var1)
# 3

上面通过改变我们主要脚本上的 print_variable ,我们也改变了第二模块上正在发生的事情。了解这种模式后,我们可以开始使用这种模式来开发做更多事情:

  • 修复第三方库的bug:当使用的库有bug,但短时间内无法修复时,猴子补丁可以快速替代出错的部分。
  • 临时修改功能:在调试过程中,开发者可以在不重新编译或重新加载代码的情况下,临时修改某个功能。
  • 增强功能:为现有类或函数添加额外的行为或特性,而不需要更改底层代码。
  • 热更新:在系统运行时修改部分逻辑,避免重新启动服务。

什么时候(不)要使用 Monkey Patch

猴子补丁非常强大,它展示了 Python 的灵活性。一切都源于对不同数据类型的理解以及在 Python 中变量的含义。然而,要理解在自己的程序中何时使用这些模式可能会非常困难。

通常情况下,最好不要进行猴子补丁。如果你想改变程序的行为,例如,你可以为你想要修改的类定义子类。猴子补丁的问题在于程序的行为变得更加难以理解。在上面的例子中,当你调用 module2.another_print 时,你会看到一个非常难以理解的输出。如果你检查模块,你不会明白为什么会得到 3 而不是 2 。追溯行为变更的位置非常复杂。如果你检查变量,你会发现没有任何问题,并且 var1 仍然是 1 。

但是,有时也可能会有很大的好处。例如,使用 numpy 计算快速傅里叶变换可能比其他实现更慢。想象一下,你想使用 PyFFTW,但又不想重新编写所有程序。你可以对代码进行 monkey-patch 操作!

import pyfftw
import numpy

numpy.fft = pyfftw.interfaces.numpy_fft

现在,当我们使用 numpy 提供的 FFT 例程时,它们将自动被 PyFFTW 的替代所取代。这可能会对你的程序产生巨大影响,而且只需要一行代码!尽管这是一个特殊的例子,但还有其他情况下你可能需要考虑 Monkey Patching。通常的情况是测试。有时候,你希望在缺少某些功能的环境中测试你的代码,或者你希望防止因为测试而实际修改了活动数据库。在这种情况下,在进行测试之前,你可以更改与数据库通信的方法。

写作不易,欢迎关注

分类:
标签:

评论已关闭。