Python中通过@classmethod 实现多态的示例

发布时间:2026/10/10 21:53:25
Python中通过@classmethod 实现多态的示例
前言先说清一个容易含混的地方Python 的多态polymorphism主要靠实例方法 鸭子类型实现——只要对象有对应的方法不看它属于哪个类。classmethod提供的是另一种形态可以叫「类级别的多态」方法拿到的第一个参数是类本身惯例命名为cls因此同一段代码在被不同子类调用时会作用到不同的类上构造出的也是那个子类的实例。这个说法需要一点限定classmethod本身不是多态开关它只是把作用对象从实例换成类。真正实现多态的是子类可以覆写override这些类方法加上cls始终指向实际被调用的那个类。所以本文的落点是怎么写类方法、cls是怎么传递的、为什么它能做到父类定义、子类各自表现不同。classmethod是内置的装饰器不需要 import。本文示例基于 Python 3.8涉及 3.9 / 3.10 / 3.11 变化的点会单独标注。一、三种方法的对照装饰器第一个参数调用方式能否访问实例属性典型用途无实例方法selfobj.m()/Cls.m(obj)能常规业务逻辑classmethodclsCls.m()/obj.m()不能除非传入实例备用构造器、工厂、类级配置staticmethod无Cls.m()/obj.m()不能与类相关但不依赖类状态的工具函数官方文档对类方法的描述是类方法接收类作为隐式的第一个参数就像实例方法接收实例一样。并且强调类方法既可以在类上调用C.f()也可以在实例上调用C().f()此时实例会被忽略只看它的类。二、cls 到底指向谁这是理解多态的关键cls不是写死在定义处的类而是调用发起处所属的类。# 适用于 Python 3.8class Shape:def __init__(self, name):self.name nameclassmethoddef from_dict(cls, data):工厂方法用 cls 构造而不是写死 Shape()。return cls(data[name])classmethoddef kind(cls):return cls.__name__class Circle(Shape):圆继承 Shape 但不覆写任何类方法。s Shape.from_dict({name: 形状})c Circle.from_dict({name: 圆})print(type(s).__name__) # Shapeprint(type(c).__name__) # Circle —— cls 是 Circleprint(Circle.kind()) # Circleprint(Shape.kind()) # Shape如果from_dict里写死return Shape(data[name])那么Circle.from_dict(...)得到的就是Shape实例——多态就此消失。写成cls(...)才是关键。三、在子类里覆写做到同一接口、不同表现继承加覆写就是多态的完整形态。子类可以覆写工厂方法也可以给它加校验。# 适用于 Python 3.8class Document:def __init__(self, title):self.title titleclassmethoddef from_title(cls, title):return cls(title)classmethoddef describe_source(cls):return 来自普通文档class Report(Document):classmethoddef from_title(cls, title):if not title.endswith(报告):title title 报告return cls(title) # cls 是 Reportclassmethoddef describe_source(cls):return 来自报表系统docs [Document.from_title(说明), Report.from_title(季度)]for d in docs:print(type(d).__name__, d.title, |, d.describe_source())同一个调用形式X.from_title(...)/X.describe_source()在不同类上给出不同行为与不同返回类型这就是类方法层面的多态。四、用 classmethod 做注册表与工厂更贴近实战的用法是注册表 工厂各子类在定义时自动登记工厂方法按名字创建对应子类的实例。# 适用于 Python 3.8class Codec:_registry {} # 类属性所有子类共享def __init_subclass__(cls, **kwargs): # 3.6子类定义时自动执行super().__init_subclass__(**kwargs)Codec._registry[cls.__name__.lower()] clsclassmethoddef create(cls, name):按名字创建对应子类的实例。target cls._registry.get(name.lower())if target is None:known , .join(sorted(cls._registry))raise ValueError(f未知编解码器 {name!r}可选{known})return target()def encode(self, text):raise NotImplementedErrorclass JsonCodec(Codec):def encode(self, text):return {text: repr(text) }class PlainCodec(Codec):def encode(self, text):return textcodec Codec.create(json) # 拿到 JsonCodec 实例print(type(codec).__name__) # JsonCodecprint(codec.encode(你好))print(type(Codec.create(plain)).__name__) # PlainCodecCodec.create(...)不关心具体是哪个子类创建出来的对象自动带上该子类的encode调用方只面向Codec的接口编程。五、在类方法里用 super 与继承父类逻辑零参数super()在类方法里同样生效因为cls就是当前类的第一个参数。# 适用于 Python 3.8class Base:classmethoddef create(cls, **kwargs):print(fBase.create 建的是 {cls.__name__})return cls(**kwargs)class Child(Base):classmethoddef create(cls, **kwargs):obj super().create(**kwargs) # 调用父类的类方法obj.extra 子类追加return objdef __init__(self, tag):self.tag tagc Child.create(tagA)print(type(c).__name__, c.tag, c.extra) # Child A 子类追加注意零参数super()依赖当前函数的第一个参数所以它在嵌套函数包括生成器表达式里不按预期工作——那时super()找不到对应的类与参数。关于版本细节还有两条自Python 3.10起类方法会继承方法属性__module__、__name__、__qualname__、__doc__、__annotations__并新增__wrapped__属性而让类方法包装property等描述符这个能力自3.11起被弃用、在3.13中被移除。常见坑点坑 1忘了cls参数。❌class A:classmethoddef make():return A()调用A.make()抛TypeError: make() takes 0 positional arguments but 1 was given。✅def make(cls):并且返回cls()而不是A()。坑 2在类方法里直接访问实例属性。❌classmethod def show(cls): return cls.name——cls.name找的是类属性name通常不存在抛AttributeError。✅ 类方法只处理类级别的东西需要实例属性就让方法变成实例方法或接收一个实例参数。坑 3把classmethod和staticmethod搞混。❌ 在staticmethod里写cls(...)——cls未定义NameError。✅ 需要cls就用classmethod确实与类状态无关才用staticmethod。坑 4工厂方法里写死类名。❌class Shape:classmethoddef from_dict(cls, data):return Shape(data[name]) # 写死子类调用得到父类实例✅return cls(data[name])让多态保留下来。坑 5通过cls改共享的类属性。❌class C:items []classmethoddef add(cls, x):cls.items.append(x) # 所有子类、所有实例共享同一个列表✅ 需要每个类独立时在类方法里判断并新建if items not in cls.__dict__: cls.items []或干脆别用类属性存可变状态。坑 6以为在实例上调用类方法会传实例。❌ 期待obj.describe()能拿到obj。✅ 类方法在实例上调用时实例被忽略传入的仍是type(obj)。需要实例就用普通方法。坑 7在嵌套函数或生成器表达式里用零参数super()。❌class Child(Base):classmethoddef create(cls):f lambda: super().create() # 嵌套作用域zero-arg super 失效return f()✅ 把它换成显式形式super(Child, cls).create()或者别把super()放进嵌套函数。坑 8用类方法包装property。❌classmethod里再套property—— 这个能力在 Python 3.11 被弃用、3.13 已移除在旧版本上也别依赖。✅ 需要类级别的只读属性时用普通类方法Cls.value()表达行为清晰且跨版本一致。总结要点结论第一个参数惯例叫cls指向实际被调用的那个类调用方式Cls.m()或obj.m()后者忽略实例只取其类实现多态的关键工厂方法里写cls(...)子类可覆写与静态方法区别classmethod拿到clsstaticmethod什么都不拿与实例方法区别类方法拿不到实例别在里面读实例属性super()零参数形式可用嵌套函数里不可用版本变化3.10 起继承方法属性并加__wrapped__3.11 弃用、3.13 移除包装 property一句话总结classmethod的价值不在于能不带实例调用而在于cls让父类写一次、子类各自生效——这正是多态在类层面上的表达。写工厂、写注册表、写需要继承链协作的构造逻辑时用它只是一个不用 self 的工具函数用staticmethod更贴切。Python 2 里classmethod同样存在但 Python 2 有新旧式类之分只有继承自object的新式类才能正确使用零参数super()Python 3 中所有类都是新式类。Python 2.7 自 2020 年 1 月 1 日停止维护新代码一律使用 Python 3。