0

0

Python类方法在继承中的身份识别与描述符协议解析

花韻仙語

花韻仙語

发布时间:2025-10-07 14:49:00

|

239人浏览过

|

来源于php中文网

原创

Python类方法在继承中的身份识别与描述符协议解析

本文深入探讨了Python中类方法在继承场景下的行为,特别是当它们作为列表元素进行比较时,其身份识别问题。核心在于Python的描述符协议导致每次访问类方法时都会创建新的方法对象,而非直接引用其底层函数。文章详细解释了这一机制,并通过示例代码展示了如何正确地在子类中排除父类方法,推荐使用方法名字符串进行比较,以确保逻辑的准确性。

Python类方法的身份识别与描述符协议

python中,当我们处理类方法(@classmethod装饰器修饰的方法)时,尤其是在涉及继承和动态比较的场景下,可能会遇到关于方法“身份”的困惑。一个常见的误解是,从类或实例中多次访问同一个类方法,会得到同一个方法对象。然而,事实并非如此。

Python内部通过描述符协议(Descriptor Protocol)来管理类方法、静态方法和实例方法的行为。当您通过 Parent.func1 这样的方式访问一个类方法时,Python会触发描述符协议,动态地创建一个新的方法对象。这个方法对象是绑定到特定类(或实例)的。这意味着,即使是多次访问同一个类上的同一个类方法,您得到的也是不同的方法对象。

我们可以通过Python内置的 id() 函数(返回对象的内存地址标识符)和 is 运算符(检查对象身份是否相同)来验证这一点:

class Parent:
    @classmethod
    def func1(cls):
        pass

class Child(Parent):
    pass

# 每次访问 Parent.func1 都会得到不同的方法对象
print(f"id(Parent.func1)第一次: {id(Parent.func1)}")
print(f"id(Parent.func1)第二次: {id(Parent.func1)}")
print(f"Parent.func1 is Parent.func1: {Parent.func1 is Parent.func1}") # 输出 False

# 父类和子类访问同一个方法,也得到不同的方法对象
print(f"id(Child.func1): {id(Child.func1)}")
print(f"Parent.func1 is Child.func1: {Parent.func1 is Child.func1}") # 输出 False

从上述输出可以看出,每次通过 Parent.func1 或 Child.func1 获取类方法时,都会生成一个具有不同 id 的新方法对象,因此它们彼此之间不 is 相同。

底层函数对象的统一性

尽管方法对象本身是动态生成的,但它们背后所引用的底层函数对象是相同的。这个底层函数对象是实际包含方法逻辑的函数定义,可以通过方法对象的 __func__ 属性访问。

立即学习Python免费学习笔记(深入)”;

class Parent:
    @classmethod
    def func1(cls):
        print("hello func1")

class Child(Parent):
    pass

# 验证底层函数对象是相同的
print(f"Parent.func1.__func__ is Child.func1.__func__: {Parent.func1.__func__ is Child.func1.__func__}") # 输出 True
print(f"Parent.func1.__func__ is Parent.func1.__func__: {Parent.func1.__func__ is Parent.func1.__func__}") # 输出 True

这表明,Parent.func1 和 Child.func1 虽然是不同的方法对象,但它们都指向同一个原始的 func1 函数定义。

原始代码中 NO_CALCULATE 列表的问题

理解了上述机制,我们就能解释为什么原始代码中的 NO_CALCULATE 列表无法按预期工作:

class Parent:
    @classmethod
    def func1(cls):
        print("hello func1")

    @classmethod
    def func2(cls):
        print("hello func2")

    @classmethod
    def func3(cls):
        print("hello func3")

    CALCULATE = [func1, func2, func3]
    NO_CALCULATE = []

    @classmethod
    def calculate_kpis(cls):
        for func in cls.CALCULATE:
            # 这里的 func 是在类定义时创建的方法对象
            # 而 NO_CALCULATE 列表中的 Parent.func1 也是一个方法对象
            # 但它们很可能不是同一个对象实例
            if func not in cls.NO_CALCULATE:
                func.__get__(cls)() # 这种调用方式也是可以简化的

class Child(Parent):
    NO_CALCULATE = [Parent.func1]  # 移除此计算

if __name__ == "__main__":
     p1 = Child()
     p1.calculate_kpis()

在 Child 类中,NO_CALCULATE = [Parent.func1]。这里的 Parent.func1 在列表定义时被解析为一个方法对象。然后,在 calculate_kpis 方法的循环中,for func in cls.CALCULATE: 迭代出的 func 也是方法对象。当执行 if func not in cls.NO_CALCULATE: 时,Python会尝试比较 func 和 NO_CALCULATE 列表中的元素。由于方法对象没有自定义的相等比较逻辑,Python默认使用身份比较(即 is 运算符)。

因为 func (例如 Child.func1 迭代出来的对象) 和 NO_CALCULATE 列表中的 Parent.func1 是两个不同的方法对象实例(即使它们指向同一个底层函数),所以 func not in cls.NO_CALCULATE 会评估为 True,导致 func1 仍然被执行。

解决方案与最佳实践

为了正确地在子类中排除父类方法,我们应该避免直接比较方法对象。有以下两种更可靠的方法:

星火作家大神
星火作家大神

星火作家大神是一款面向作家的AI写作工具

下载

1. 推荐方案:通过方法名称字符串进行比较

最清晰、最Pythonic的方法是使用方法的名称(字符串)进行比较。将 NO_CALCULATE 列表中的元素改为方法名的字符串形式。

class Parent:
    @classmethod
    def func1(cls):
        print("Parent func1 executed")

    @classmethod
    def func2(cls):
        print("Parent func2 executed")

    @classmethod
    def func3(cls):
        print("Parent func3 executed")

    # CALCULATE 列表仍然存储方法对象
    CALCULATE = [func1, func2, func3]
    NO_CALCULATE = [] # 父类默认不排除任何方法

    @classmethod
    def calculate_kpis(cls):
        for func in cls.CALCULATE:
            # 使用方法名进行比较
            if func.__name__ not in cls.NO_CALCULATE:
                # 对于类方法,直接 func(cls) 调用即可
                func(cls)

class Child(Parent):
    # 子类排除 'func1',通过方法名字符串指定
    NO_CALCULATE = ["func1"]

if __name__ == "__main__":
     print("--- Calling calculate_kpis on Child instance ---")
     p1 = Child()
     p1.calculate_kpis()
     # 预期输出:
     # Parent func2 executed
     # Parent func3 executed

在这个修正后的代码中,Child.NO_CALCULATE 包含字符串 "func1"。在 calculate_kpis 方法中,func.__name__ 会获取当前迭代到的方法名称字符串,然后与 cls.NO_CALCULATE 列表中的字符串进行比较。这种比较是基于值的,因此能够准确地排除指定的方法。

2. 替代方案:通过底层函数对象 __func__ 进行比较

虽然不如方法名字符串直观,但也可以通过比较方法对象的 __func__ 属性来解决问题,因为 __func__ 属性指向的是同一个底层函数对象。

class Parent:
    @classmethod
    def func1(cls):
        print("Parent func1 executed")

    @classmethod
    def func2(cls):
        print("Parent func2 executed")

    @classmethod
    def func3(cls):
        print("Parent func3 executed")

    CALCULATE = [func1, func2, func3]
    NO_CALCULATE = []

    @classmethod
    def calculate_kpis(cls):
        # 预先提取 NO_CALCULATE 中方法的底层函数对象
        excluded_funcs = [f.__func__ for f in cls.NO_CALCULATE]
        for func in cls.CALCULATE:
            if func.__func__ not in excluded_funcs:
                func(cls)

class Child(Parent):
    # 存储父类方法对象的 __func__ 属性
    NO_CALCULATE = [Parent.func1] # 这里的 Parent.func1 会在列表创建时解析为一个方法对象

if __name__ == "__main__":
     print("--- Calling calculate_kpis on Child instance (using __func__) ---")
     p1 = Child()
     p1.calculate_kpis()

这种方法需要额外处理 NO_CALCULATE 列表,将其中的方法对象转换为其 __func__ 属性,然后进行比较。虽然可行,但通常不如直接使用方法名字符串清晰。

关于类方法调用 func.__get__(cls)() 的简化

在原始代码中,类方法的调用使用了 func.__get__(cls)()。对于类方法,这种显式使用描述符协议的方式是不必要的。当 func 是一个类方法对象时,直接调用 func(cls) 即可,Python会自动处理绑定逻辑。

# 原始代码中的调用方式
func.__get__(cls)()

# 简化后的调用方式(推荐)
func(cls)

这使得代码更简洁,更符合Python的惯用法。

总结

Python的描述符协议是理解类方法、静态方法和实例方法行为的关键。在继承和动态方法管理中,重要的是要认识到每次访问类方法时都会创建一个新的方法对象。因此,直接比较方法对象(如 method1 is method2 或 method1 in list_of_methods)通常不会按预期工作。

为了在子类中可靠地排除父类方法,推荐的方法是:

  1. 在 NO_CALCULATE 等排除列表中存储方法的名称字符串
  2. 在运行时,通过比较 func.__name__ 属性来判断是否应该执行该方法。
  3. 对于类方法的调用,直接使用 func(cls) 形式即可。

遵循这些最佳实践,可以确保在复杂的类层次结构中,类方法的管理和调用逻辑是清晰、准确且健壮的。

相关专题

更多
python开发工具
python开发工具

php中文网为大家提供各种python开发工具,好的开发工具,可帮助开发者攻克编程学习中的基础障碍,理解每一行源代码在程序执行时在计算机中的过程。php中文网还为大家带来python相关课程以及相关文章等内容,供大家免费下载使用。

769

2023.06.15

python打包成可执行文件
python打包成可执行文件

本专题为大家带来python打包成可执行文件相关的文章,大家可以免费的下载体验。

661

2023.07.20

python能做什么
python能做什么

python能做的有:可用于开发基于控制台的应用程序、多媒体部分开发、用于开发基于Web的应用程序、使用python处理数据、系统编程等等。本专题为大家提供python相关的各种文章、以及下载和课程。

764

2023.07.25

format在python中的用法
format在python中的用法

Python中的format是一种字符串格式化方法,用于将变量或值插入到字符串中的占位符位置。通过format方法,我们可以动态地构建字符串,使其包含不同值。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

659

2023.07.31

python教程
python教程

Python已成为一门网红语言,即使是在非编程开发者当中,也掀起了一股学习的热潮。本专题为大家带来python教程的相关文章,大家可以免费体验学习。

1325

2023.08.03

python环境变量的配置
python环境变量的配置

Python是一种流行的编程语言,被广泛用于软件开发、数据分析和科学计算等领域。在安装Python之后,我们需要配置环境变量,以便在任何位置都能够访问Python的可执行文件。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

549

2023.08.04

python eval
python eval

eval函数是Python中一个非常强大的函数,它可以将字符串作为Python代码进行执行,实现动态编程的效果。然而,由于其潜在的安全风险和性能问题,需要谨慎使用。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

579

2023.08.04

scratch和python区别
scratch和python区别

scratch和python的区别:1、scratch是一种专为初学者设计的图形化编程语言,python是一种文本编程语言;2、scratch使用的是基于积木的编程语法,python采用更加传统的文本编程语法等等。本专题为大家提供scratch和python相关的文章、下载、课程内容,供大家免费下载体验。

730

2023.08.11

AO3中文版入口地址大全
AO3中文版入口地址大全

本专题整合了AO3中文版入口地址大全,阅读专题下面的的文章了解更多详细内容。

1

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 10.4万人学习

Django 教程
Django 教程

共28课时 | 3.3万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号