面向对象(二)

前言

  • 承接《面向对象(一)》:那篇讲「怎么写一个类」,这篇讲类之间的关系和几种特殊方法——继承与 super()、访问权限与名字改写、类属性 / 类方法 / 静态方法、多态与鸭子类型、@property、可调用对象的判断。
  • 所有示例都在本机 Python 3.14.10 上实跑过,输出原样贴在代码块下面。
  • 数据持久化(pickle / json / shelve / sqlite3 / h5py)已拆到单独一篇《数据持久化存储》,这篇只留面向对象的内容。

类的继承

  • 基本继承

    子类自动拥有父类的方法和属性,在此基础上再加自己的东西。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Parent:
    def parent_method(self):
    print('这是父类方法')

    class Child(Parent): # 括号里写父类,就是继承
    def child_method(self):
    print('这是子类方法')

    c = Child()
    c.parent_method() # 类里没写,照样能调用
    c.child_method()

    输出:

    1
    2
    这是父类方法
    这是子类方法
  • 方法重写(override)

    子类里写一个同名方法就把父类的盖掉了;查找时沿 MRO 从子类往父类走,先命中谁用谁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Animal:
    def speak(self):
    return '动物发出声音'

    class Dog(Animal):
    def speak(self): # 同名即重写
    return '汪汪汪!'

    print(Animal().speak(), '|', Dog().speak())
    print('Dog 的实例同时也是 Animal:', isinstance(Dog(), Animal))

    输出:

    1
    2
    动物发出声音 | 汪汪汪!
    Dog 的实例同时也是 Animal: True
  • 用 super() 调父类

    super() 不是「父类」的硬编码,而是按 MRO 取下一个类;最常见的用法是在子类的 __init__ 里把父类负责的那部分初始化做掉。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Animal:
    def __init__(self, name):
    self.name = name # name 这个属性由父类创建

    class Dog(Animal):
    def __init__(self, name, breed):
    super().__init__(name) # 不调这句,self.name 根本不存在
    self.breed = breed

    d = Dog('旺财', '金毛')
    print(d.name, d.breed)
    print('实例字典:', list(vars(d)))

    输出:

    1
    2
    旺财 金毛
    实例字典: ['name', 'breed']
  • 多继承与 MRO

    Python 允许一个类同时继承多个父类。多个父类有同名方法时,按 MRO(方法解析顺序) 决定用谁,MRO 由 C3 线性化算法算出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Father:
    def skills(self):
    return '编程、修理'

    class Mother:
    def skills(self):
    return '烹饪、艺术'

    class Child(Father, Mother): # 括号里的顺序就是优先级
    pass

    c = Child()
    print('不重写时取到:', c.skills())
    print('MRO:', [k.__name__ for k in Child.__mro__])
    print('想两个都要就显式点名:', Father.skills(c), '+', Mother.skills(c))

    输出:

    1
    2
    3
    不重写时取到: 编程、修理
    MRO: ['Child', 'Father', 'Mother', 'object']
    想两个都要就显式点名: 编程、修理 + 烹饪、艺术

    把顺序换成 Child(Mother, Father),skills() 就会取到 烹饪、艺术。菱形继承也是同一套规则:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Base:
    def hi(self):
    return 'base'

    class Left(Base):
    pass

    class Right(Base):
    pass

    class Bottom(Left, Right): # Base 被间接继承两次
    pass

    print([k.__name__ for k in Bottom.__mro__])
    print(Bottom().hi())

    输出:

    1
    2
    ['Bottom', 'Left', 'Right', 'Base', 'object']
    base

    C3 保证 Base 排在 Left、Right 之后,不会被提前命中。如果写出无法线性化的继承关系(class C(A, B),而 B 又是 A 的父类),定义类时会直接抛 TypeError: Cannot create a consistent method resolution。

  • 混入类(Mixin)

    多继承的一种常见用法:父类只提供「能力」,不打算当成类型来用,习惯上名字带 Mixin。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import json

    class JsonMixin:
    def to_json(self):
    return json.dumps(self.__dict__, ensure_ascii=False)

    class Person:
    def __init__(self, name):
    self.name = name

    class JsonPerson(Person, JsonMixin): # 人是「是什么」,Mixin 是「会什么」
    pass

    p = JsonPerson('张三')
    print(p.to_json())
    print('不加 ensure_ascii=False 会长这样:', json.dumps(p.__dict__))

    输出:

    1
    2
    {"name": "张三"}
    不加 ensure_ascii=False 会长这样: {"name": "\u5f20\u4e09"}

    原笔记这里写的是 json.dumps(self.__dict__),注释却写输出 {"name": "张三"}——实际默认 ensure_ascii=True,非 ASCII 会被转义成 \uXXXX。

  • 抽象基类

    用 abc 模块规定「子类必须实现哪些方法」。它管的是实例化这一步:抽象方法没实现完,连对象都造不出来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    from abc import ABC, abstractmethod

    class Shape(ABC):
    @abstractmethod
    def area(self): # 只声明接口,不给实现
    ...

    class Circle(Shape):
    def __init__(self, radius):
    self.radius = radius

    def area(self): # 实现了才能实例化
    return 3.14 * self.radius ** 2

    class Half(Shape): # 忘了实现 area
    pass

    print('Circle(5):', Circle(5).area())
    try:
    Shape() # 抽象类本身不能被实例化
    except TypeError as e:
    print('实例化 Shape:', e)
    try:
    Half()
    except TypeError as e:
    print('实例化 Half:', e)

    输出:

    1
    2
    3
    Circle(5): 78.5
    实例化 Shape: Can't instantiate abstract class Shape without an implementation for abstract method 'area'
    实例化 Half: Can't instantiate abstract class Half without an implementation for abstract method 'area'
  • 检查继承关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Parent:
    pass

    class Child(Parent):
    pass

    c = Child()
    print('issubclass(Child, Parent):', issubclass(Child, Parent))
    print('issubclass(Parent, Child):', issubclass(Parent, Child))
    print('isinstance(c, Parent):', isinstance(c, Parent))
    print('一切类的根:', issubclass(Child, object))
    print('类本身是谁的实例:', type(Child).__name__)

    输出:

    1
    2
    3
    4
    5
    issubclass(Child, Parent): True
    issubclass(Parent, Child): False
    isinstance(c, Parent): True
    一切类的根: True
    类本身是谁的实例: type

    顺带区分:isinstance(x, C) 是「x 是 C 或 C 的子类的实例」;issubclass(A, B) 只比类,拿实例进去会 TypeError。

  • 继承里两个容易踩的坑

    1. 私有属性不会被子类「原样」继承。__x 在类定义时按所在类改名,子类里写 self.__x 会解析成 _子类名__x,取不到父类那个。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class Parent:
      def __init__(self):
      self.__secret = 42 # 真实键名是 _Parent__secret

      class Child(Parent):
      def peek(self):
      return self.__secret # 这里找的是 _Child__secret

      c = Child()
      try:
      c.peek() # 子类里这句找的是 _Child__secret
      except AttributeError as e:
      print('子类里按原名取:', e)
      print('显式写父类改名后的键:', c._Parent__secret)
      print('实例字典:', list(vars(c)))

      输出:

      1
      2
      3
      子类里按原名取: 'Child' object has no attribute '_Child__secret'
      显式写父类改名后的键: 42
      实例字典: ['_Parent__secret']

      要拿父类的值只能显式写 self._Parent__secret,或者由父类提供公开方法——后者才是正路。

    2. 子类不写 __init__ 时直接用父类的;一旦写了又忘了 super().__init__(),父类建的属性就全没了,而且这个错往往要到后面访问属性时才暴露。

访问权限

  • 说明

    Python 没有 Java / C++ 那种 public / private 关键字,访问权限靠命名约定表达,原则是「约定优于强制」:除了带双下划线的名字会被改写之外,其他都能从外部访问到。

  • 公开成员(public)

    默认就是公开,外部随便访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Demo:
    def __init__(self):
    self.public_attr = 'I am public'

    def public_method(self):
    return 'Public method'

    obj = Demo()
    print(obj.public_attr)
    print(obj.public_method())

    输出:

    1
    2
    I am public
    Public method
  • 受保护成员(protected,单下划线)

    _x 只是提示「这是内部实现,别直接碰」,语言层面不做任何限制;唯一的实际效果是 from module import * 不会带上它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Demo:
    def __init__(self):
    self._protected = 'I am protected'

    def _hint(self):
    return 'Protected method'

    obj = Demo()
    print(obj._protected) # 照样能访问,只是不该这么写
    print(obj._hint())

    输出:

    1
    2
    I am protected
    Protected method
  • 私有成员(private,双下划线)

    __x 会被名字改写(name mangling)成 _类名__x,目的是避免和子类的属性撞名,不是安全机制。注意 __x__(前后都有双下划线)不算私有,不会被改写。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Demo:
    def __init__(self):
    self.__secret = 'I am private'

    def __peek(self):
    return 'Private method'

    def show(self): # 类内部用原名,编译时已经改好了
    print(self.__secret)
    print(self.__peek())

    obj = Demo()
    obj.show()
    try:
    obj.__secret
    except AttributeError as e:
    print('外部按原名取:', e)
    print('改写后照样能拿到:', obj._Demo__secret)
    print('实例字典里的真实键:', list(vars(obj)))

    输出:

    1
    2
    3
    4
    5
    I am private
    Private method
    外部按原名取: 'Demo' object has no attribute '__secret'
    改写后照样能拿到: I am private
    实例字典里的真实键: ['_Demo__secret']

    两个细节值得记住:

    • 一是改写只发生在类定义体内,所以在类外面写 obj.__secret,找的就是字面上的 __secret(报错信息里也是它),并不会帮你改成 _Demo__secret;
    • 二是 vars(obj) 里只有 _Demo__secret,说明属性在创建那一刻就已经改名,实例字典里根本不存在 __secret 这个键。
  • 特殊成员(前后都是双下划线)

    __init__、__str__、__len__ 这类不会被改写,它们是协议方法,由解释器在特定时机自动调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Demo:
    def __init__(self):
    self.value = 42

    def __str__(self):
    return f'Demo with value {self.value}'

    obj = Demo()
    print(obj) # print 触发 __str__
    print(obj.__str__()) # 也能直接调,一般没必要
    print(str(obj)) # str() 同样走 __str__
    print('没写 __repr__ 时 repr 形如 <__main__.Demo object at 0x地址>')

    输出:

    1
    2
    3
    4
    Demo with value 42
    Demo with value 42
    Demo with value 42
    没写 __repr__ 时 repr 形如 <__main__.Demo object at 0x地址>
  • 小结

    访问权限 命名约定 实际可访问性 使用建议
    公开 无前缀 任何地方 对外接口
    受保护 _x 任何地方(只是约定) 类内部和子类
    私有 __x 类内部(外部可用 _类名__x 拿到) 只给类自己用
    特殊 __x__ 任何地方 实现协议方法,别自造这种名字

    真要「藏」数据,Python 的答案是模块边界(不导出该名字)或者把属性做成 @property 加校验,而不是双下划线。

类属性

  • 说明

    写在类里、方法外的属性就是类属性:它属于类对象本身,被所有实例共享;实例里没有副本,读的时候沿 MRO 回落到类上找。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass:
    class_attribute = '我是类属性' # 类属性:挂在类对象上

    def __init__(self):
    self.instance_attribute = '我是实例属性' # 实例属性:挂在每个实例上

    print(MyClass.class_attribute) # 通过类名读
    obj = MyClass()
    print(obj.class_attribute) # 通过实例读,回落到类上
    print('在实例字典里吗:', 'class_attribute' in vars(obj))
    print('在类字典里吗:', 'class_attribute' in MyClass.__dict__)

    输出:

    1
    2
    3
    4
    我是类属性
    我是类属性
    在实例字典里吗: False
    在类字典里吗: True
  • 读得到不等于改得动

    通过实例给同名属性赋值,不会改类属性,而是在实例上新建一个属性,把类属性挡住。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Test:
    class_attr = 10

    t1 = Test()
    t2 = Test()

    t1.class_attr = 20 # 新建实例属性,不是改类属性

    print('t1:', t1.class_attr)
    print('t2:', t2.class_attr)
    print('Test:', Test.class_attr)
    Test.class_attr = 30 # 要真改类属性得走类名
    print('t2 现在:', t2.class_attr)
    print('t1 仍被自己挡着:', t1.class_attr)

    输出:

    1
    2
    3
    4
    5
    t1: 20
    t2: 10
    Test: 10
    t2 现在: 30
    t1 仍被自己挡着: 20
  • 可变类属性更危险

    类属性是 list / dict 这种可变对象时,实例上的 append 会真的改到类属性,所有实例一起变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Team:
    members = [] # 可变类属性:一个列表被全体共享

    def join(self, name):
    self.members.append(name) # 没有赋值语句,改的是类上那个列表

    a = Team()
    b = Team()
    a.join('小明')
    print('b 看到的:', b.members)
    print('类属性:', Team.members)
    print('a 自己有吗:', 'members' in vars(a))

    输出:

    1
    2
    3
    b 看到的: ['小明']
    类属性: ['小明']
    a 自己有吗: False

    默认值要放可变容器时,宁可写进 __init__(self.members = []),或者用 None 占位、在方法里再建。

  • 常见用途

    1. 类级常量:Circle.PI 这种,方法里用 self.PI 读。
    2. 统计实例数量:Person.count += 1 写在 __init__ 里(必须用类名改,self.count += 1 会变成给实例建属性)。
    3. 提供默认值:Car.wheels = 4,个别实例可以覆盖。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Person:
    nation = '中国' # 类属性:全体共享
    count = 0 # 用来计数

    def __init__(self, name):
    self.name = name
    self.nation = 'china' # 同名实例属性,会挡住类属性
    Person.count += 1 # 改类属性必须走类名

    p = Person('xiaoming')
    q = Person('xiaohong')
    print('类上的 nation:', Person.nation)
    print('实例上的 nation:', p.nation)
    print('已创建实例数:', Person.count)
    Person.city = '杭州' # 类属性可以动态添加
    print('动态加的:', q.city)

    输出:

    1
    2
    3
    4
    类上的 nation: 中国
    实例上的 nation: china
    已创建实例数: 2
    动态加的: 杭州
  • 几个特殊的类属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Person:
    """一个用来演示的人"""
    nation = '中国'

    def __init__(self, name):
    self.name = name

    print('类名:', Person.__name__)
    print('父类元组:', Person.__bases__)
    print('文档字符串:', Person.__doc__)
    print('__dict__ 的类型:', type(Person.__dict__).__name__)
    print('类里定义的名字:', sorted(k for k in Person.__dict__ if not k.startswith('__')))
    print('所属模块:', Person.__module__)

    输出:

    1
    2
    3
    4
    5
    6
    类名: Person
    父类元组: (<class 'object'>,)
    文档字符串: 一个用来演示的人
    __dict__ 的类型: mappingproxy
    类里定义的名字: ['nation']
    所属模块: __main__

    Person.__dict__ 是 mappingproxy(只读视图),往里塞键会 TypeError;要动态加属性就写 Person.city = ...。__init__ 是方法、不算「类里定义的数据属性」,但它同样在 Person.__dict__ 里,只是键名以双下划线开头所以被上面那行过滤掉了。

类方法

  • 说明

    用 @classmethod 装饰的方法,第一个参数是类本身(习惯写 cls)。它拿到的是「这次调用发生在哪个类上」,所以在子类上调用时 cls 就是子类——这是它和「把类名写死」的关键区别。

  • 语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass:
    class_attr = '类属性'

    @classmethod
    def show(cls):
    print(f'我是类方法,cls 是 {cls.__name__},能读 {cls.class_attr}')
    return cls() # 常见用法:顺手造一个实例

    MyClass.show() # 通过类调用
    obj = MyClass()
    obj.show() # 通过实例调用,cls 仍然是类

    输出:

    1
    2
    我是类方法,cls 是 MyClass,能读 类属性
    我是类方法,cls 是 MyClass,能读 类属性
  • 最典型的用途:替代构造器

    一个类需要好几种「入口」时,用类方法返回实例,比在 __init__ 里堆 if 清楚。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from datetime import date

    class Order:
    def __init__(self, oid, amount):
    self.oid = oid
    self.amount = amount

    @classmethod
    def from_dict(cls, d): # 入口 1:从字典建
    return cls(d['oid'], d['amount'])

    @classmethod
    def today(cls, oid, amount): # 入口 2:附带创建日期
    o = cls(oid, amount)
    o.created = date.today().isoformat()
    return o

    o1 = Order.from_dict({'oid': 'A1', 'amount': 30})
    o2 = Order.today('A2', 50)
    print(o1.oid, o1.amount)
    print(o2.oid, o2.amount, o2.created)

    输出(created 是当天日期):

    1
    2
    A1 30
    A2 50 2026-11-02
  • 继承时的差别:cls 会跟着变

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Base:
    tag = 'base'

    @classmethod
    def who(cls):
    return f'{cls.__name__} / {cls.tag}'

    @staticmethod
    def who_static():
    return f'Base / {Base.tag}' # 静态方法里把类名写死了

    class Sub(Base):
    tag = 'sub'

    print('Base.who():', Base.who())
    print('Sub.who():', Sub.who()) # cls 是 Sub,tag 也跟着变
    print('Sub.who_static():', Sub.who_static()) # 仍然是 Base,子类换不动它

    输出:

    1
    2
    3
    Base.who(): Base / base
    Sub.who(): Sub / sub
    Sub.who_static(): Base / base
  • 综合示例:用类方法拼一个平方和

    题目:设计一个数字类,有两个属性,能做加减乘除,再要求算两个数的平方和。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Number:
    def __init__(self, num1, num2):
    self.num1 = num1
    self.num2 = num2

    def add(self):
    return self.num1 + self.num2 # 两数之和

    def sub(self):
    return self.num1 - self.num2 # 两数之差

    def mul(self):
    return self.num1 * self.num2 # 两数之积;传 (n, n) 就是 n 的平方

    def div(self):
    if self.num2 == 0:
    return 0 # 简单起见除零给 0,生产环境建议抛异常
    return self.num1 / self.num2

    @classmethod
    def pingfanghe(cls, num1, num2): # 平方和:n1^2 + n2^2
    n12 = cls(num1, num1).mul() # 第一个数的平方
    n22 = cls(num2, num2).mul() # 第二个数的平方
    return cls(n12, n22).add() # 两个平方再相加

    print(Number.pingfanghe(3, 4))
    print(Number.pingfanghe(0.5, 1))

    输出:

    1
    2
    25
    1.25

    原笔记这里写的是 n1 = Number(num1, num1),换成 cls(...) 更合理:子类调用时才会用子类自己。

静态方法

  • 说明

    用 @staticmethod 装饰的方法不会自动收到 self 或 cls,它就是一个放在类命名空间里的普通函数,只是逻辑上属于这个类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class MathUtils:
    factor = 10

    @staticmethod
    def add(x, y):
    return x + y # 拿不到 self,也拿不到 cls

    @staticmethod
    def scale(x):
    return x * MathUtils.factor # 想读类属性只能把类名写死

    print(MathUtils.add(2, 3))
    calc = MathUtils()
    print(calc.scale(4)) # 实例调用和类调用等价

    输出:

    1
    2
    5
    40
  • 静态方法能不能访问类属性

    能,但要显式写类名,而且写了就没有多态:子类改了同名类属性,从子类调用静态方法读到的还是父类那份。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Base:
    factor = 3

    @staticmethod
    def via_static(x):
    return x * Base.factor # 写死 Base

    @classmethod
    def via_class(cls, x):
    return x * cls.factor # 跟着调用方走

    class Sub(Base):
    factor = 10

    s = Sub()
    print('静态方法:', s.via_static(2), '| 类方法:', s.via_class(2))
    print('Base 上调用:', Base.via_static(2), Base.via_class(2))

    输出:

    1
    2
    静态方法: 6 | 类方法: 20
    Base 上调用: 6 6

    所以「静态方法不能访问类属性」这个说法不准确:不能自动访问(没有 self / cls 可依赖),硬写类名是可以的,但也就放弃了继承体系里的多态。

  • 三种方法对比

    特性 实例方法 类方法 静态方法
    装饰器 无 @classmethod @staticmethod
    第一个参数 self(实例) cls(类) 无自动传参
    读实例属性 可以 不可以 不可以
    读类属性 可以(self.x 回落) 可以(cls.x,随子类变) 只能写死类名,随子类不变
    调用方式 一般通过实例 类或实例都行 类或实例都行
    典型用途 对象行为 替代构造器、操作类状态 与类相关的工具函数
  • 什么时候用哪个

    • 方法里要用到 self → 实例方法。
    • 要用到类本身(改类属性、造实例、给子类留扩展点)→ 类方法。
    • 两者都不用到,只是这个函数归在这个类名下更合适 → 静态方法;否则就该是模块级函数,不必塞进类里。

多态特性

  • 说明

    多态(Polymorphism)指同一个调用作用于不同类型的对象时产生不同结果。Python 是多态默认的语言:变量没有类型,对象才有,函数只看「你有没有这个方法」。

  • 实现方式一:继承 + 方法重写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Animal:
    def speak(self):
    return '……'

    class Dog(Animal):
    def speak(self):
    return '汪汪!'

    class Cat(Animal):
    def speak(self):
    return '喵喵!'

    def animal_sound(animal):
    print(animal.speak()) # 不关心具体类型

    animal_sound(Dog())
    animal_sound(Cat())

    输出:

    1
    2
    汪汪!
    喵喵!
  • 实现方式二:鸭子类型(推荐)

    Python 更推崇「走起来像鸭子、叫起来像鸭子,那就是鸭子」:不要求继承关系,只要有对应方法就能进同一个函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Dog:
    def speak(self):
    return '汪汪!'

    class Cat:
    def speak(self):
    return '喵喵!'

    class Car: # 跟动物毫无继承关系
    def speak(self):
    return '滴滴!'

    def make_sound(obj):
    print(obj.speak())

    for o in (Dog(), Cat(), Car()):
    make_sound(o)

    输出:

    1
    2
    3
    汪汪!
    喵喵!
    滴滴!

    想要「显式一点」的约束,用 typing.Protocol(3.8+)声明接口,运行时不强制、静态检查会管;或者用上一节的抽象基类,运行时直接拦。

  • 实现方式三:运算符重载

    同一个 + 作用在数字、字符串、自定义对象上表现不同,靠的是特殊方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Vector:
    def __init__(self, x, y):
    self.x = x
    self.y = y

    def __add__(self, other): # v1 + v2 走这里
    return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, k): # v * 2
    return Vector(self.x * k, self.y * k)

    def __rmul__(self, k): # 2 * v,不写就会 TypeError
    return self * k

    def __str__(self):
    return f'Vector({self.x}, {self.y})'

    v1 = Vector(2, 3)
    v2 = Vector(4, 5)
    print(v1 + v2)
    print(v1 * 2)
    print(2 * v1)

    输出:

    1
    2
    3
    Vector(6, 8)
    Vector(4, 6)
    Vector(4, 6)
  • 多态的价值

    • 调用方不写类型判断:新增一个类只要能提供同名方法,老代码不用改。
    • 可测试:传一个假的对象进去就能测,不需要真连数据库。
    • 接口统一:配合 Protocol / 抽象基类,把「约定」写进代码里。

    代价是 Python 没有编译期的「你有没有漏写 override」检查。3.12 起可以加 @typing.override 显式标注「我是重写」,漏写或父类签名变了会被类型检查器报错:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from typing import override

    class Animal:
    def speak(self):
    return '……'

    class Dog(Animal):
    @override # 只给静态检查器看,运行时不拦
    def speek(self): # 拼错了,mypy 会报 override 无效
    return '汪汪!'

    print('运行时照样能跑:', Dog().speek())

    输出:

    1
    运行时照样能跑: 汪汪!

属性方法

  • 说明

    用 @property 装饰器把方法伪装成属性:读的时候自动执行 getter,赋值时执行 setter,del 时执行 deleter。它是 Python 实现 getter / setter 的常规做法,也让「先写字段、以后再加拉逻辑」不至于破坏调用方代码。

  • 基本用法:getter / setter / deleter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class MyClass:
    def __init__(self, value):
    self._value = value # 真正存数据的地方,用单下划线表示内部

    @property
    def value(self): # getter:读 obj.value 时触发
    print('Getting value')
    return self._value

    @value.setter
    def value(self, new_value): # setter:写 obj.value = ... 时触发
    print('Setting value')
    self._value = new_value

    @value.deleter
    def value(self): # deleter:del obj.value 时触发
    print('Deleting value')
    del self._value

    obj = MyClass(10)
    print(obj.value)
    obj.value = 20
    del obj.value
    try:
    obj.value
    except AttributeError as e:
    print('删掉之后再读:', type(e).__name__, e)
    print('实例字典:', obj.__dict__)

    输出:

    1
    2
    3
    4
    5
    6
    7
    Getting value
    10
    Setting value
    Deleting value
    Getting value
    删掉之后再读: AttributeError 'MyClass' object has no attribute '_value'
    实例字典: {}

    注意最后两行:del obj.value 删的是 _value,getter 还是会跑(所以先打印 Getting value),然后在 self._value 这一行抛 AttributeError。报错信息指向 _value 而不是 value,排查时别被带偏。

  • 典型用法:校验 + 计算属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    class Circle:
    def __init__(self, radius):
    self.radius = radius # 这里就已经走 setter 了

    @property
    def radius(self):
    return self._radius

    @radius.setter
    def radius(self, value):
    if value <= 0:
    raise ValueError('Radius must be positive')
    self._radius = value

    @property
    def diameter(self): # 只读的计算属性
    return 2 * self._radius

    @property
    def area(self):
    return 3.14159 * (self._radius ** 2)

    c = Circle(5)
    print(c.radius, c.diameter, c.area)
    c.radius = 10 # 赋值会走校验
    print(c.radius, c.diameter, c.area)
    try:
    c.radius = -1
    except ValueError as e:
    print('非法值:', e)
    try:
    c.diameter = 30 # 只给了 getter,没给 setter
    except AttributeError as e:
    print('只读属性:', e)

    输出:

    1
    2
    3
    4
    5 10 78.53975
    10 20 314.159
    非法值: Radius must be positive
    只读属性: property 'diameter' of 'Circle' object has no setter
  • 注意事项

    • getter 不能收额外参数(obj.value 没有传参的地方),要参数就别用 property。
    • 别在 property 里做耗时或有副作用的操作:读代码的人以为在取字段,看不出它在发请求。
    • __init__ 里 self.radius = radius 会走 setter,校验逻辑因此对构造参数也生效——通常是好事,但要知道有这回事。
    • 一堆字段都要校验时,__set_name__ 描述符或 dataclasses + 手动校验比写十几个 @property 更省事。

函数判断

  • 问题

    怎么判断一个对象能不能像函数那样被调用?

  • 方法一:内置 callable()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def my_func():
    pass

    class MyClass:
    def __call__(self): # 实例有了这个方法就可调用
    pass

    print(callable(my_func))
    print(callable(MyClass)) # 类本身也可调用:调用就是建实例
    print(callable(MyClass()))
    print(callable('hello'))
    print(callable(123))

    输出:

    1
    2
    3
    4
    5
    True
    True
    True
    False
    False

    官方文档的说法是:callable() 返回 True 只表示「看起来可调」,真调用仍可能失败;返回 False 则一定调不动。另外它有一段历史——Python 3.0 把它移除了,3.2 又加了回来(原笔记写成「Python 2 中曾被移除」,方向反了)。

  • 方法二:查 __call__

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def is_callable(obj):
    return hasattr(obj, '__call__')

    print(is_callable(lambda x: x + 1))
    print(is_callable([1, 2, 3]))

    class C:
    pass

    print('普通类的实例有 __call__ 吗:', hasattr(C(), '__call__'))
    print('但类本身有:', hasattr(C, '__call__'))

    输出:

    1
    2
    3
    4
    True
    False
    普通类的实例有 __call__ 吗: False
    但类本身有: True

    类本身有 __call__ 是因为它的类型是 type,实例化走的是元类的 __call__,所以 hasattr(C, '__call__') 为真而 hasattr(C(), '__call__') 为假。

  • 方法三:collections.abc.Callable

    类型检查场景用这个,它能和 isinstance 以及类型注解配合。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from collections.abc import Callable

    def my_func():
    return 42

    class C:
    def __call__(self):
    return 1

    print(isinstance(my_func, Callable))
    print(isinstance(C(), Callable))
    print(isinstance(int, Callable)) # 类是可调用的
    print(isinstance(123, Callable))

    输出:

    1
    2
    3
    4
    True
    True
    True
    False

    注意 Callable 的 isinstance 检查只看有没有 __call__,是「结构」判断,不看继承关系。注解里现在推荐直接写 collections.abc.Callable[[int], str],typing.Callable 从 3.9 起就属于历史写法了。

  • 别用 inspect.isfunction() 判断可调用

    它只认 def / lambda 造出来的纯函数对象,其他可调用对象一律 False。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from inspect import isfunction

    def my_func():
    pass

    class CallableClass:
    def __call__(self):
    pass

    print(isfunction(my_func)) # 纯函数
    print(isfunction(lambda x: x)) # lambda 也算
    print(isfunction(print)) # 内置函数
    print(isfunction(int)) # 类
    print(isfunction(str.upper)) # 方法描述符
    print(isfunction(CallableClass())) # 实现了 __call__ 的实例
    print(isfunction(my_func.__call__)) # 绑定方法

    输出:

    1
    2
    3
    4
    5
    6
    7
    True
    True
    False
    False
    False
    False
    False
  • 怎么选

    方法 覆盖范围 典型用例
    callable(obj) 最广:函数、类、方法、有 __call__ 的实例 通用判断,优先用它
    isinstance(obj, Callable) 同 callable(),但能进类型注解 静态检查 / 声明接口
    hasattr(obj, '__call__') 同上,但会触发属性查找 一般不必自己写
    inspect.isfunction(obj) 只认 def / lambda 确实只接受纯函数时
    inspect.isroutine(obj) 函数 + 方法 + 内置函数 想放宽一点时用

    实际写法:先 callable() 兜住,再在调用处捕获 TypeError,比穷举类型可靠。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def safe_call(func, *args, **kwargs):
    if not callable(func):
    raise TypeError(f'对象 {func!r} 不可调用')
    return func(*args, **kwargs)

    print(safe_call(print, 'Hello, World!'))
    try:
    safe_call('not a function')
    except TypeError as e:
    print(e)

    输出:

    1
    2
    3
    Hello, World!
    None
    对象 'not a function' 不可调用
本文结束 感谢您的阅读
正在加载今日诗词....