
在Python中,当我们访问一个类方法(或其他类型的绑定方法,如实例方法)时,Python并不会返回一个静态的、预先存在的对象。相反,它会动态地创建一个新的“方法对象”。这个行为是Python的“描述符协议”(Descriptor Protocol)在起作用。
@classmethod装饰器实际上是将一个普通函数转换成了一个classmethod描述符。当通过类(如Parent.func1)或实例(如p1.func1)访问这个描述符时,描述符的__get__方法会被调用,它会返回一个新的、绑定到相应类或实例的方法对象。由于每次访问都会触发__get__方法并生成新对象,因此即使是多次访问同一个类方法,它们在内存中的身份(即id()值)也是不同的。
例如,考虑以下代码:
class Parent:
@classmethod
def func1(cls):
print("hello func1")
class Child(Parent):
pass
# 每次访问 Parent.func1 都会得到不同的方法对象
print(f"Parent.func1 的 ID: {id(Parent.func1)}")
print(f"Parent.func1 的 ID: {id(Parent.func1)}")
print(f"Child.func1 的 ID: {id(Child.func1)}")
# 比较两个方法对象的身份,结果为 False
print(f"Parent.func1 is Parent.func1: {Parent.func1 is Parent.func1}")
print(f"Parent.func1 is Child.func1: {Parent.func1 is Child.func1}")运行上述代码,你会发现每次id()的输出都不同,并且is运算符的比较结果都是False。这表明Parent.func1和Child.func1,甚至两次访问Parent.func1所得到的方法对象,都不是同一个对象。
立即学习“Python免费学习笔记(深入)”;
尽管方法对象本身是动态创建的,但它们所封装的底层函数对象却是唯一的。这个底层函数可以通过方法对象的__func__属性来访问。对于同一个逻辑函数,无论它是通过父类还是子类访问,其__func__属性都指向同一个函数对象。
class Parent:
@classmethod
def func1(cls):
print("hello func1")
class Child(Parent):
pass
# 比较底层函数对象的身份,结果为 True
print(f"Parent.func1.__func__ is Child.func1.__func__: {Parent.func1.__func__ is Child.func1.__func__}")这个特性对于需要识别方法身份的场景至关重要。
在提供的原始代码中,NO_CALCULATE列表存储的是Parent.func1这个特定的方法对象。然而,在calculate_kpis方法中,循环遍历CALCULATE列表时,每次func变量都会获取一个新的方法对象。
class Parent:
@classmethod
def func1(cls):
print("hello func1")
# ... 其他方法 ...
CALCULATE = [func1, func2, func3]
NO_CALCULATE = []
@classmethod
def calculate_kpis(cls):
for func in cls.CALCULATE:
# 这里的 func 是一个新创建的方法对象
# 而 cls.NO_CALCULATE 中存储的也是一个方法对象
# 由于身份不同,func not in cls.NO_CALCULATE 总是为 True
if func not in cls.NO_CALCULATE:
func.__get__(cls)() # 调用方式问题,将在下文优化
class Child(Parent):
NO_CALCULATE = [Parent.func1] # 这里存储的是 Parent.func1 的一个特定方法对象
if __name__ == "__main__":
p1 = Child()
p1.calculate_kpis()当Child.calculate_kpis被调用时,cls.CALCULATE中的func1(即Child.func1)是一个动态生成的新方法对象。而cls.NO_CALCULATE中存储的是Parent.func1在定义Child类时的一个特定方法对象。由于这两个方法对象在内存中的身份不同,func not in cls.NO_CALCULATE的判断始终为True,导致func1仍然被执行。
要正确地实现方法的排除逻辑,我们需要比较方法对象的某种稳定标识,而不是它们动态生成的对象身份。有两种推荐的解决方案:
通过比较方法对象的__func__属性,我们可以判断它们是否代表同一个逻辑函数。
class Parent:
@classmethod
def func1(cls):
print("hello func1 from Parent")
@classmethod
def func2(cls):
print("hello func2 from Parent")
@classmethod
def func3(cls):
print("hello func3 from Parent")
# CALCULATE 存储的是方法对象,其 __func__ 属性指向底层函数
CALCULATE = [func1, func2, func3]
NO_CALCULATE_FUNCS = [] # 存储要排除的底层函数对象
@classmethod
def calculate_kpis(cls):
for func_obj in cls.CALCULATE:
# 比较 func_obj 的底层函数是否在排除列表中
if func_obj.__func__ not in cls.NO_CALCULATE_FUNCS:
func_obj(cls) # 优化后的调用方式
class Child(Parent):
# 将 Parent.func1 的底层函数添加到排除列表
NO_CALCULATE_FUNCS = [Parent.func1.__func__]
if __name__ == "__main__":
print("--- 使用 __func__ 比较 ---")
p1 = Child()
p1.calculate_kpis()
# 预期输出:
# hello func2 from Parent
# hello func3 from Parent更简洁且通常更推荐的方法是直接比较方法名称字符串。这种方法不仅可以避免方法对象的动态性问题,而且在某些场景下(例如,当方法来自不同的基类但名称相同,且你希望排除所有同名方法时)更为灵活。
class Parent:
@classmethod
def func1(cls):
print("hello func1 from Parent")
@classmethod
def func2(cls):
print("hello func2 from Parent")
@classmethod
def func3(cls):
print("hello func3 from Parent")
CALCULATE = [func1, func2, func3]
NO_CALCULATE_NAMES = [] # 存储要排除的方法名称字符串
@classmethod
def calculate_kpis(cls):
for func_obj in cls.CALCULATE:
# 比较方法名称字符串是否在排除列表中
if func_obj.__name__ not in cls.NO_CALCULATE_NAMES:
func_obj(cls) # 优化后的调用方式
class Child(Parent):
# 将要排除的方法名称字符串添加到排除列表
NO_CALCULATE_NAMES = ["func1"]
if __name__ == "__main__":
print("\n--- 使用 __name__ 比较 ---")
p1 = Child()
p1.calculate_kpis()
# 预期输出:
# hello func2 from Parent
# hello func3 from Parent原始代码中使用了func.__get__(cls)()来调用类方法。虽然这种方式在功能上等同于func(cls),但它暴露了描述符协议的底层细节,使得代码不够简洁和直观。
当func已经是一个通过描述符协议获取到的、绑定到cls的方法对象时,它本身就具有正确的调用行为。因此,直接使用func(cls)是更标准、更清晰的调用方式。
通过理解Python方法对象的动态性及其底层工作原理,开发者可以避免常见的错误,并编写出更加健壮和可维护的代码。
以上就是Python类方法:理解其动态绑定与身份识别机制的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号