Day20 · 面向对象(二)继承、多态与高级特性

Day20 · 面向对象(二)继承、多态与高级特性

阶段:阶段一 · 核心基础 | 前置:Day19 | 技术基准:Python 3.15.0

学习目标

  • 掌握单继承、多继承与 MRO(方法解析顺序),会用 super()
  • 理解多态与鸭子类型
  • 会用 __slots__ 节省内存,用 @property 做属性校验与只读控制
  • 认识常用魔术方法(__len__、__getitem__、__eq__ 等)
  • 了解枚举类 enum.Enum,初步认识元类 type(可选了解)
  • 初步认识领域建模:区分实体与值对象,知道聚合根的作用,体会"用类把业务规则(不变式)封装起来"

一、继承(Inheritance)

1.1、为什么需要继承

继承 让子类(派生类)自动获得父类(基类)的属性和方法,还能再扩展自己的东西。它是实现代码复用和"is-a"关系的手段。语法:class 子类(父类):

python 复制代码
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} 发出声音")


class Dog(Animal):            # Dog 继承 Animal
    def bark(self):
        print(f"{self.name} 汪汪汪")


d = Dog("旺财")
d.speak()        # 输出:旺财 发出声音(继承来的)
d.bark()         # 输出:旺财 汪汪汪(自己的)

1.2、方法重写(Override)

子类可以重新定义父类的同名方法,覆盖掉父类版本:

python 复制代码
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} 发出声音")


class Cat(Animal):
    def speak(self):                 # 重写父类方法
        print(f"{self.name} 喵喵喵")


Cat("咪咪").speak()      # 输出:咪咪 喵喵喵(调的是子类版本)

1.3、super():调用父类方法

重写 __init__ 时,通常用 super() 先把父类的初始化做了,再补充子类自己的属性:

python 复制代码
class Animal:
    def __init__(self, name):
        self.name = name


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)      # 先调用父类的 __init__
        self.breed = breed          # 再补充子类属性


d = Dog("旺财", "柴犬")
print(d.name, d.breed)             # 输出:旺财 柴犬

1.4、多继承与 MRO

Python 支持一个类继承多个父类:class C(A, B):。当多个父类有同名方法时,按 MRO(Method Resolution Order,方法解析顺序) 决定调用谁。用 类名.__mro__ 可以查看顺序:

python 复制代码
class A:
    def hello(self):
        print("A")


class B(A):
    def hello(self):
        print("B")


class C(A):
    def hello(self):
        print("C")


class D(B, C):
    pass


print([c.__name__ for c in D.__mro__])
# 输出:['D', 'B', 'C', 'A', 'object']
D().hello()        # 输出:B(按 MRO,B 排在 C 前面)

小嘉提醒:多继承功能强大但容易绕晕,初学阶段优先用单继承;真要用多继承,理解 MRO 和 super() 的协作机制很重要。

二、多态与鸭子类型

2.1、多态

多态指:同一个方法调用,作用在不同对象上,表现出不同行为:

python 复制代码
class Dog:
    def speak(self):
        return "汪汪"


class Cat:
    def speak(self):
        return "喵喵"


def make_sound(animal):
    print(animal.speak())      # 不关心传进来的是什么,只要它会 speak


make_sound(Dog())      # 输出:汪汪
make_sound(Cat())      # 输出:喵喵

2.2、鸭子类型(Duck Typing)

Python 是动态类型语言,它"不看你是什么类型,只看你能不能做这件事"------"如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子"。上面的 make_sound 根本不要求参数是 Animal 的子类,只要对象有 speak 方法就能用:

python 复制代码
class Robot:                 # 跟 Animal 毫无关系
    def speak(self):
        return "嘀嘀"


make_sound(Robot())      # 输出:嘀嘀,照样能跑

三、__slots__ 节省内存

默认实例的属性存在一个字典里,字典比较占内存。如果一个类会创建海量实例,可以用 __slots__ 限定实例只能有哪些属性,并取消字典,大幅省内存:

python 复制代码
class Point:
    __slots__ = ("x", "y")        # 只允许 x、y 两个属性

    def __init__(self, x, y):
        self.x = x
        self.y = y


p = Point(3, 4)
print(p.x, p.y)        # 输出:3 4
# p.z = 99             # 报错!z 不在 __slots__ 里

小嘉提醒:__slots__ 是为"性能/内存"服务的优化手段,普通类不用加;加了之后实例不能再动态绑定新属性。

四、@property:属性的受控访问

4.1、为什么需要 property

Day19 说双下划线不是真私有。真正想"控制属性的读写、加校验",用 @property:把方法伪装成属性来访问。

python 复制代码
class Student:
    def __init__(self, name, score):
        self.name = name
        self.score = score        # 直接赋值,不做校验


s = Student("小嘉", -100)         # 分数居然能是负数!

4.2、用 @property 做校验

python 复制代码
class Student:
    def __init__(self, name, score):
        self.name = name
        self._score = score

    @property
    def score(self):                  # 读 score 时走这里
        return self._score

    @score.setter
    def score(self, value):           # 写 score 时走这里
        if not 0 <= value <= 100:
            raise ValueError("分数必须在 0~100 之间")
        self._score = value


s = Student("小嘉", 90)
print(s.score)          # 输出:90(像访问属性一样)
s.score = 95            # 像赋值一样,自动走 setter
# s.score = 200        # 抛出 ValueError

4.3、只读属性

只写 @property、不写 @score.setter,这个属性就变成只读:

python 复制代码
class Circle:
    def __init__(self, r):
        self._r = r

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


c = Circle(2)
print(c.area)          # 输出:12.56636
# c.area = 999        # 报错:can't set attribute(只读)

五、常用魔术方法(Magic Methods)

魔术方法是"前后各两个下划线"的特殊方法,会在特定时机被 Python 自动调用。Day19 见过 __init__、__str__,这里再列几个常用的:

魔术方法 何时被调用 作用
__len__ len(obj) 让对象支持 len()
__getitem__ obj[i] 让对象支持下标访问
__eq__ obj == 其他 自定义相等判断
__repr__ repr(obj) 官方字符串表示
python 复制代码
class Team:
    def __init__(self, name, members):
        self.name = name
        self.members = members

    def __len__(self):
        return len(self.members)        # 让 len(team) 可用

    def __getitem__(self, index):
        return self.members[index]      # 让 team[i] 可用

    def __eq__(self, other):
        return self.members == other.members   # 自定义 ==


t = Team("战队", ["小嘉", "阿明", "小红"])
print(len(t))              # 输出:3(自动调 __len__)
print(t[0])                # 输出:小嘉(自动调 __getitem__)
t2 = Team("备份队", ["小嘉", "阿明", "小红"])
print(t == t2)              # 输出:True(自动调 __eq__)

六、枚举类 enum

当一个变量只应该取有限几个固定值(比如"星期几""状态"),用 enum.Enum 比魔法字符串更安全、可读:

python 复制代码
from enum import Enum


class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3


c = Color.RED
print(c)               # 输出:Color.RED
print(c.name, c.value)  # 输出:RED 1

if c == Color.RED:
    print("这是红色")     # 输出:这是红色

用枚举的好处:拼写错误会立刻暴露,而不是悄悄变成"另一个字符串"。

七、元类 type 简介(选读)

7.1、"类本身也是对象"

在 Python 里"类本身也是对象"。平时用 class Dog: 定义类,背后其实是 type 这个"元类"在创建它。可以用 type(对象) 查看它是哪个类创建的:

python 复制代码
class Dog:
    pass


d = Dog()
print(type(d))       # 输出:<class '__main__.Dog'>
print(type(Dog))     # 输出:<class 'type'>(Dog 这个类是 type 造的)

类(Dog)和普通对象(d)都是对象 ,区别只在:d 是 Dog 的实例,Dog 是 type 的实例。type 是"造类的类",所以叫元类(metaclass)。

7.2、用 type() 三参数动态创建类

既然类可以用代码"造"出来,那 type 还提供了一种动态写法:type(类名, 父类元组, 属性字典)。它与 class 语句等价:

python 复制代码
# 等价于 class Dog: sound = "汪汪"
Dog = type("Dog", (), {"sound": "汪汪"})
d = Dog()
print(Dog.__name__, d.sound)          # Dog 汪汪

# 带继承:等价于 class Cat(Animal): sound = "喵"
Animal = type("Animal", (), {"kind": "动物"})
Cat = type("Cat", (Animal,), {"sound": "喵"})
print(Cat().kind, Cat().sound)        # 动物 喵

三个参数:① 类名(字符串)② 父类元组(可空)③ 类体命名空间字典。这是"动态生成类"最原始的手段,ORM、配置驱动的框架底层常这么造类。

7.3、自定义元类:class X(type) + metaclass= 指定

定义一个继承 type 的类,重写它的 __new__,就能在类被创建的那一刻拦截并修改它 。用 metaclass= 指定给某个类:

python 复制代码
class WithCreated(type):                      # 自定义元类:继承 type
    def __new__(mcs, name, bases, namespace): # mcs=元类本身,name=类名,namespace=类体字典
        namespace["created"] = True           # 在类真正生成前,给类体塞一个属性
        return super().__new__(mcs, name, bases, namespace)

class Product(metaclass=WithCreated):         # 指定用 WithCreated 造这个类
    pass

print(Product.created)                        # True(被元类自动加上的)

记忆点:元类的 __new__ 在"类对象"生成前运行 ,此时 namespace 是还没被冻结的字典,改它就能给新类"加料"。

7.4、三个典型应用场景

元类平时用不到,但看懂这三个场景,就能理解它在框架里到底干嘛用:

① 类注册表:自动收集所有子类(插件系统、命令分发常用)

python 复制代码
class Registry(type):
    registry = {}                                  # 类级字典,存"名字 -> 类"
    def __new__(mcs, name, bases, namespace):
        cls = super().__new__(mcs, name, bases, namespace)
        if name != "Base":                         # 基类自己不入册
            Registry.registry[name] = cls
        return cls

class Base(metaclass=Registry): pass
class A(Base): pass
class B(Base): pass
print(sorted(Registry.registry.keys()))            # ['A', 'B']

只要写了 class 某类(Base):,它就被自动登记,主程序不用手动 import 每一个------这就是很多框架"我啥都没写,插件却被识别到"的原理。

② 单例:让 类() 永远返回同一个对象 (Day37 用 __new__ 实现过单例,元类是另一种写法)

python 复制代码
class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):            # 拦截"实例化"这一步
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Logger(metaclass=Singleton): pass
print(Logger() is Logger())                        # True,多次"实例化"拿的是同一个对象

③ 收集类属性:像 ORM 那样把字段声明变成结构信息(Django 模型、dataclass 的思想雏形)

python 复制代码
class CollectAttrs(type):
    def __init__(cls, name, bases, namespace):     # 类生成后自动调用
        cls.attrs = [k for k in namespace if not k.startswith("__")]
        super().__init__(name, bases, namespace)

class User(metaclass=CollectAttrs):
    name = "小嘉"
    age = 18
print(User.attrs)                                  # ['name', 'age']

ORM 就是这么干的:你写 name = 字段(...),元类在类生成时把这些声明"读出来",翻译成建表语句。

小嘉提醒:区分两组容易混的 __new__------实例的 __new__ (Day37 单例里,控制"实例对象"怎么创建,__new__ 在 __init__ 之前)和元类的 __new__ (本节,控制"类对象"怎么创建,在 class 语句结束那一刻执行)。前者造的是实例,后者造的是类。

7.5、什么时候才真的需要元类

元类(metaclass)是 Python 里最深、也最少直接用的黑魔法之一。一句话判断标准:当你要给"一大批不同类"统一加同一个行为,而普通装饰器/继承/__init_subclass__ 都做不到时 ,才考虑它。日常开发基本用不到,先知道"类也是对象、type 是造类的类、metaclass= 能拦截造类过程"这三个概念即可,等以后遇到 ORM 框架(如 Django 模型)时再深入。

进阶方向:Python 3.6+ 提供了更轻量的 __init_subclass__,很多"以前只能靠元类"的需求(如自动给子类加属性)用它可以实现,不需要动 type。元类优先级最高,遇到 metaclass 冲突(父类元类不兼容)会报 TypeError,这也是框架里偶尔见到"元类冲突"报错的原因。

八、运算符重载与可迭代魔术方法

魔术方法还能让你自定义的对象"支持 +、被 for 遍历"这类内置操作------这叫运算符重载。

8.1、__add__:让对象支持 +

python 复制代码
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):               # 定义 + 的行为
        return Vector(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"


v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2)          # 输出:Vector(4, 6)(自动调 __add__)

8.2、__iter__:让对象能被 for 遍历

配合 __next__(Day15 讲过迭代器协议),自定义容器就能直接 for x in obj:

python 复制代码
class Countdown:
    def __init__(self, start):
        self.n = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.n <= 0:
            raise StopIteration
        self.n -= 1
        return self.n + 1


for num in Countdown(3):
    print(num, end=" ")
# 输出:3 2 1

小嘉提醒:魔术方法是"Python 把语言内置操作接到你对象上"的钩子------len() 背后是 __len__、+ 背后是 __add__、for 背后是 __iter__。理解这层映射,你就能写出用起来和 list/int 一样顺手的自定义类。

九、类型检查:isinstance 与 issubclass

继承体系里,经常要判断"一个对象是不是某个类的""一个类是不是另一个的子类"。两个内置函数正好干这个:

python 复制代码
class Animal:
    pass


class Dog(Animal):
    pass


d = Dog()
print(isinstance(d, Dog))          # 输出:True(d 是 Dog 的实例)
print(isinstance(d, Animal))       # 输出:True(d 也是父类 Animal 的实例)
print(isinstance(d, str))          # 输出:False

print(issubclass(Dog, Animal))     # 输出:True
print(issubclass(Dog, object))      # 输出:True(Python 3 里所有类最终都继承 object)

小嘉提醒:判断对象类型时,优先用 isinstance() 而不是 type(x) == Dog------因为 isinstance 会把"子类实例也算作父类",这才符合继承的语义。

十·补充、领域建模(Domain Modeling)

补.1、什么是领域建模

前面几天写的类,很多是"语法练习"(动物叫、图形面积、倒计时)。领域建模(Domain Modeling) 换一个视角:面向对象不是为了练语法,而是把现实业务里的概念、规则、关系"翻译"成代码------让代码里的类名、方法名,直接对应业务人员嘴里的话(这也叫"统一领域语言")。建模时最常区分三种构件:

构件 有没有独立身份 是否可变 靠什么判断相等 例子
实体 Entity 有唯一身份(编号/ID) 状态会变 编号相同才是同一个对象 用户、订单、读者、一本具体的馆藏图书
值对象 Value Object 没有独立身份 创建后不再改 内容相同就相等 金额、ISBN、地址、日期区间
聚合 Aggregate --- --- 把一组强相关的对象包成一个整体,只留一个"聚合根"当入口 图书馆(书+读者+借阅规则)、订单(订单项+收货地址)

小嘉提醒:判断一个概念是实体还是值对象,问自己两句:①它换了个名字/编号,算不算另一个东西?(是 → 实体)②它的某个属性被人在外面偷偷改掉,业务会出问题吗?(会 → 不可变的值对象)值对象一旦建好就不该改,要变就"换一个新的",这样错误无处藏身。

补.2、小例子:图书馆领域模型(纯标准库,本机实测)

下面用一个最简单的图书馆例子,把三种构件都落一遍:ISBN、Money 是值对象(不可变、按内容相等),Book、Customer 是实体(有编号、状态可变),Library 是聚合根(借书、还书的规则都由它守住)。

python 复制代码
# Author: XiaoJia

class ISBN:
    """值对象:书号,不可变,内容相同就相等。"""

    def __init__(self, code):
        code = str(code).strip()
        if len(code) != 13 or not code.isdigit():
            raise ValueError("ISBN 必须是 13 位数字")
        self._code = code

    @property
    def code(self):
        return self._code

    def __eq__(self, other):
        return isinstance(other, ISBN) and self._code == other._code

    def __hash__(self):
        return hash(self._code)

    def __repr__(self):
        return f"ISBN({self._code})"


class Money:
    """值对象:金额,不可变,金额与币种相同才相等。"""

    def __init__(self, amount, currency="CNY"):
        if amount < 0:
            raise ValueError("金额不能为负数")
        self._amount = float(amount)
        self._currency = currency

    @property
    def amount(self):
        return self._amount

    @property
    def currency(self):
        return self._currency

    def __eq__(self, other):
        return (isinstance(other, Money)
                and self._amount == other._amount
                and self._currency == other._currency)

    def __repr__(self):
        return f"Money({self._amount} {self._currency})"


class Book:
    """实体:每本书有自己的编号,编号相同才是同一本,状态可变(是否借出)。"""

    def __init__(self, book_id, title, isbn, price):
        self.book_id = book_id
        self.title = title
        self.isbn = isbn          # 值对象
        self.price = price        # 值对象
        self.borrowed = False

    def __repr__(self):
        return f"Book(id={self.book_id}, 《{self.title}》, 借出={self.borrowed})"


class Customer:
    """实体:读者,有读者号,状态可变(借了哪些书)。"""

    def __init__(self, customer_id, name):
        self.customer_id = customer_id
        self.name = name
        self.borrowed_books = []

    def borrow(self, book):
        self.borrowed_books.append(book)

    def give_back(self, book_id):
        self.borrowed_books = [b for b in self.borrowed_books if b.book_id != book_id]

    def __repr__(self):
        return f"Customer({self.customer_id}, {self.name})"


class Library:
    """聚合根:图书馆是一组书的整体入口,借书/还书都经由它,由它守住规则。"""

    def __init__(self, name):
        self.name = name
        self._books = {}

    def add_book(self, book):
        self._books[book.book_id] = book

    def lend(self, book_id, customer):
        book = self._books.get(book_id)
        if book is None:
            raise KeyError(f"馆内没有编号为 {book_id} 的书")
        if book.borrowed:
            raise RuntimeError(f"《{book.title}》已被借出,不能重复借")
        book.borrowed = True
        customer.borrow(book)
        print(f"{customer.name} 借走了《{book.title}》")

    def receive_back(self, book_id, customer):
        book = self._books.get(book_id)
        if book is None or not book.borrowed:
            raise RuntimeError(f"编号 {book_id} 的书不在馆内或未借出")
        book.borrowed = False
        customer.give_back(book_id)
        print(f"{customer.name} 归还了《{book.title}》")

    def show_books(self):
        for book in self._books.values():
            print(book)


lib = Library("小嘉图书馆")
lib.add_book(Book("B001", "Python 程序设计", ISBN("9787111588338"), Money(59.0)))
lib.add_book(Book("B002", "面向对象精要", ISBN("9787302456789"), Money(45.5)))

xiajia = Customer("C001", "小嘉")
lib.lend("B001", xiajia)
lib.show_books()
# 输出:小嘉 借走了《Python 程序设计》
# 输出:Book(id=B001, 《Python 程序设计》, 借出=True)
# 输出:Book(id=B002, 《面向对象精要》, 借出=False)

lib.receive_back("B001", xiajia)
lib.show_books()
# 输出:小嘉 归还了《Python 程序设计》
# 输出:Book(id=B001, 《Python 程序设计》, 借出=False)
# 输出:Book(id=B002, 《面向对象精要》, 借出=False)

# 值对象:内容相同就相等,跟是不是同一个对象无关
isbn1 = ISBN("9787111588338")
isbn2 = ISBN("9787111588338")
print("两个 ISBN 相等吗:", isbn1 == isbn2)      # 输出:两个 ISBN 相等吗: True
m1 = Money(59.0)
m2 = Money(59.0)
print("两笔金额相等吗:", m1 == m2)            # 输出:两笔金额相等吗: True

# 下面这些违规用法都会被挡在构造/聚合根这一层:
# ISBN("123")                 # 抛出 ValueError:ISBN 必须是 13 位数字
# Money(-5)                   # 抛出 ValueError:金额不能为负数
# lib.lend("B001", xiajia)     # 书已借出时再借,抛出 RuntimeError:不能重复借

小嘉建议:注意"不变式"(业务上永远成立的规则)放在哪里------金额不能为负写在 Money.__init__,书不能重复借写在聚合根 Library.lend,而不是散落在调用方各处。这样无论谁来借书,规则都不会被绕过。

补.3、领域建模给 Python 开发者的启示

  • 和 Day19/Day20 的关系:今天手写的东西,全是前面学过的工具------@property 把值对象做成只读、__eq__ 定义值对象"按内容相等"、__repr__ 让调试输出可读、封装把规则藏进类内部。领域建模不是新语法,而是"用这些语法去表达业务"。
  • 和真实项目的衔接:以后学到 Day44 的 Django ORM 模型、Day73 的 SQLAlchemy 声明式模型,本质就是把今天手写的实体类映射到数据库表;今天的 Book、Customer 将来会变成 ORM 里的 Model 类,而聚合根上的业务规则仍然要你自己写在方法里,ORM 只负责存取,不负责业务对错。

十一、枚举类深入

第六节已经用 Enum 定义了最基础的枚举(Color.RED 这种"固定取值")。实际工程里还常遇到"枚举值要能和数字直接比""几个开关位要能组合""枚举本身就是个字符串"这些需求,enum 模块提供了几个更专用的变体(本机实测)。

11.1、IntEnum:既能当枚举,又能当 int

普通 Enum 的成员和它的值是两回事(Color.RED == 1 是 False)。IntEnum 的成员本身就是个 int,可以直接和数字比较、参与运算:

python 复制代码
# Author: XiaoJia
from enum import IntEnum

class Color(IntEnum):
    RED = 1
    GREEN = 2
    BLUE = 3

print(Color.RED == 1)        # True   IntEnum 成员就是 int
print(Color.RED + 1)          # 2      参与算术(结果退化为普通 int)
print(list(Color))            # [<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>]

11.2、Flag:可位运算组合的开关

当一个状态可以"同时开多个开关"(如文件权限 = 可读 | 可写),用 Flag,成员之间可以 | 组合:

python 复制代码
# Author: XiaoJia
from enum import Flag

class Perm(Flag):
    R = 4
    W = 2
    X = 1

p = Perm.R | Perm.W
print(p)                  # Perm.R|W
print(Perm.R in p)       # True   用 in 判断某个开关是否打开
print(Perm.X in p)       # False

11.3、StrEnum:枚举本身就是字符串(3.11+)

StrEnum 的成员既是枚举、又是字符串,== "red" 直接成立,f"{成员}" 打印出来就是值本身:

python 复制代码
# Author: XiaoJia
from enum import StrEnum

class ColorS(StrEnum):
    RED = "red"
    GREEN = "green"

print(ColorS.RED == "red")     # True
print(f"颜色={ColorS.RED}")    # 颜色=red   直接是字符串值,不是 ColorS.RED

11.4、auto 与别名、missing

auto() 让 Python 自动给成员赋值(从 1 递增);给不同成员赋相同的值 时,后写的会成为前一个的"别名"(同一个对象,不重复出现在 list 里);重写 _missing_ 可在查不到值时兜底:

python 复制代码
# Author: XiaoJia
from enum import Enum, auto

class State(Enum):
    READY = auto()
    RUNNING = auto()
    DONE = auto()
print(list(State))            # [<State.READY: 1>, <State.RUNNING: 2>, <State.DONE: 3>]

# 别名:CRIMSON 和 RED 值相同,它只是 RED 的另一个名字
class Color2(Enum):
    RED = 1
    CRIMSON = 1
    BLUE = 2
print(Color2.CRIMSON is Color2.RED)   # True
print(list(Color2))                    # [<Color2.RED: 1>, <Color2.BLUE: 2>]  别名不重复列
print(Color2(1))                       # Color2.RED

# _missing_:按值查找失败时的兜底
class Status(Enum):
    OK = 200
    @classmethod
    def _missing_(cls, value):
        return cls.OK
print(Status(999))                     # Status.OK   查不到就兜底成 OK

小嘉提醒:@enum.unique 装饰器可以"禁止别名",一旦有两个成员同值就报错,适合要求"每个枚举值必须唯一"的场景。第六节的基础 Enum 管"固定取值",本节这四个变体分别管"当 int 比""按位组合""本身是字符串""自动赋值与别名兜底"------按需求挑用即可。

十二、下标/映射协议:__getitem__/__setitem__/__delitem__

第五节用 Team 演示了只读的 __getitem__(team[i])。要让自定义类像 list/dict 一样完整支持"读、写、删"下标,需要三个钩子配合(本机实测)。

魔术方法 触发写法 作用
__getitem__(self, key) obj[key] 读 取元素;key 是 int、str 或 slice
__setitem__(self, key, value) obj[key] = v 写 设元素
__delitem__(self, key) del obj[key] 删元素
python 复制代码
# Author: XiaoJia
class MyDict:
    def __init__(self):
        self._data = {}
    def __getitem__(self, key):
        return self._data[key]          # d["a"] 走这里
    def __setitem__(self, key, value):
        self._data[key] = value         # d["a"] = 1 走这里
    def __delitem__(self, key):
        del self._data[key]             # del d["a"] 走这里

d = MyDict()
d["a"] = 1
print(d["a"])        # 输出:1
del d["a"]

__getitem__ 收到的 key 还可能是 slice 对象(当你写 obj[1:4] 时)。自己判断一下就能让自定义序列支持切片:

python 复制代码
class MySeq:
    def __init__(self, items):
        self._items = list(items)
    def __getitem__(self, index):
        if isinstance(index, slice):                 # 切片 -> 返回新序列
            return MySeq(self._items[index])
        return self._items[index]                    # 普通下标
    def __len__(self):
        return len(self._items)

s = MySeq([10, 20, 30, 40, 50])
print(s[1])          # 输出:20
print(s[1:4]._items)  # 输出:[20, 30, 40](切片拿到的是 MySeq,再取 _items 看内容)
print(s[-1])         # 输出:50

小嘉提醒:只实现 __getitem__ 的类就能被 for 遍历、被 in 判断------这是比 __iter__ 更宽松的"可迭代"兜底路径(Day15 讲迭代器时提过)。

十三、真值判定与构造表示

13.1、__bool__:if obj 时问一句"你真吗"

if obj:、bool(obj) 会先调 obj.__bool__();没有 __bool__ 时,再回退到 __len__()(长度非 0 就算真);两者都没有,默认恒为 True。

python 复制代码
# Author: XiaoJia
class LenBased:
    def __len__(self):
        return 0          # 没有 __bool__,真值由 __len__ 决定

class Neither:
    pass

print(bool(LenBased()))   # 输出:False(__bool__ 缺失 -> 回退 __len__ == 0)
print(bool(Neither()))    # 输出:True(两者都没有 -> 默认 True)

小嘉提醒:Python 2 里这个钩子叫 __nonzero__,Python 3 改名为 __bool__。__nonzero__ 是 Python 2 遗留名,Python 3 里写它无效------迁移老代码时记得改过来。

13.2、__new__:实例创建的第一步

元类那节(7.3/7.4)提过"实例的 __new__ 在 __init__ 之前"。这里系统讲:类名(...) 时,先调 __new__ 创建并返回实例,再把这个实例传给 __init__ 做初始化。

python 复制代码
# Author: XiaoJia
class Logger:
    def __new__(cls, *args, **kwargs):
        print("__new__ 创建实例")
        return super().__new__(cls)     # 必须 return 一个实例
    def __init__(self):
        print("__init__ 初始化")

Logger()
# 输出:__new__ 创建实例
# 输出:__init__ 初始化

关键陷阱:__new__ 返回的如果不是本类实例,__init__ 就不会被自动调用:

python 复制代码
class A:
    def __init__(self):
        print("A.__init__ 被调用了")

class B:
    def __new__(cls):
        return super().__new__(A)      # 返回的是 A,不是 B
    def __init__(self):
        print("B.__init__ 被调用了")   # 不会执行

b = B()
print(type(b).__name__)   # 输出:A(B.__init__ 根本没跑)

单例(Day37)就是靠重写 __new__、只造一个实例来实现的。

13.3、__repr__/__str__/__bytes__/__format__

方法 触发 用途
__repr__ repr(obj)、容器里打印、调试 "官方"表示,理想情况能 eval 还原对象
__str__ str(obj)、print(obj) "可读"表示,面向用户
__bytes__ bytes(obj) 转字节串
__format__ format(obj, spec)、f"{obj:spec}" 按 format_spec 格式化
python 复制代码
# Author: XiaoJia
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __repr__(self):
        return f"Point({self.x}, {self.y})"   # 可 eval 还原
    def __str__(self):
        return f"({self.x}, {self.y})"

p = Point(3, 4)
print(repr(p))          # 输出:Point(3, 4)
print(str(p))           # 输出:(3, 4)
print([p])              # 输出:[Point(3, 4)](容器内部用 repr)
print(eval(repr(p)))    # 输出:(3, 4)

class Color:
    def __init__(self, r, g, b):
        self.r, self.g, self.b = r, g, b
    def __format__(self, spec):
        if spec == "hex":
            return f"#{self.r:02X}{self.g:02X}{self.b:02X}"
        return f"Color({self.r},{self.g},{self.b})"

c = Color(255, 128, 0)
print(format(c, "hex"))   # 输出:#FF8000
print(f"{c}")             # 输出:Color(255,128,0)(format_spec 为空)

小嘉提醒:只写 __repr__ 就够 90% 场景------没写 __str__ 时 print 会自动回退用 __repr__;反过来不行。

十四、比较与哈希

14.1、比较六件套与 total_ordering

六个比较运算符对应六个钩子。只写 __eq__ 和 __lt__,再用 functools.total_ordering(Day18 讲过)就能自动补出其余四个:

python 复制代码
# Author: XiaoJia
from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, major, minor):
        self.major, self.minor = major, minor
    def __eq__(self, other):
        return (self.major, self.minor) == (other.major, other.minor)
    def __lt__(self, other):
        return (self.major, self.minor) < (other.major, other.minor)

v1, v2 = Version(3, 13), Version(3, 9)
print(v1 == v2)   # 输出:False
print(v1 > v2)    # 输出:True(total_ordering 用 __lt__ 推导)
print(min(v1, v2))  # 输出:Version(3, 9)
钩子 运算符 钩子 运算符
__lt__ < __gt__ >
__le__ <= __ge__ >=
__eq__ == __ne__ !=

14.2、__hash__ 的经典陷阱

一旦你定义了 __eq__,Python 会自动把类的 __hash__ 置为 None ,实例立刻变成"不可哈希"(不能放进 set / dict 键)。要恢复可哈希,必须同时显式定义 __hash__:

python 复制代码
# Author: XiaoJia
class PointEq:
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return self.x == other.x

print(PointEq.__hash__)        # 输出:None(定义 __eq__ 后自动置 None)
# hash(PointEq(1))             # 报错:TypeError: unhashable type: 'PointEq'

class PointHash:
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, PointHash) and self.x == other.x
    def __hash__(self):
        return hash(self.x)

s = {PointHash(1), PointHash(1), PointHash(2)}
print(len(s))   # 输出:2(内容相等的两个 PointHash(1) 被去重)

小嘉提醒:哈希一致性铁律------a == b 为真时,hash(a) 必须等于 hash(b) 。所以 __eq__ 用到了哪些字段,__hash__ 就应该只对这些字段取 hash。值对象(补.2 的 ISBN)正是这么做的。

十五、属性访问协议

Day19 讲过 getattr/hasattr/setattr/delattr 这些"反射函数"和 __dict__。本节讲拦截属性访问本身的四个钩子,外加 __slots__ 的继承注意(第三节讲过基础用法,这里只补衔接)。

15.1、__getattr__ vs __getattribute__

两者名字只差一个,但触发时机完全不同:

  • __getattribute__:每次访问属性都触发(包括正常属性),相当于一道总闸。
  • __getattr__:只有当属性真的找不到 (走完 __dict__、描述符都没有)时才兜底触发。
python 复制代码
# Author: XiaoJia
class Lazy:
    def __init__(self):
        self.real = "真实属性"
    def __getattr__(self, name):        # 只有找不到时才来
        return f"<兜底值:{name}>"

o = Lazy()
print(o.real)     # 输出:真实属性(正常找到,不触发 __getattr__)
print(o.missing)  # 输出:<兜底值:missing>(找不到 -> 兜底)

写 __getattribute__ 要特别防递归 ------在它里面写 self.name 又会触发它自己,必须用 object.__getattribute__ 绕开:

python 复制代码
class Guard:
    def __init__(self):
        self.done = True
    def __getattribute__(self, name):
        if name == "password":
            raise PermissionError("password 禁止访问")
        return object.__getattribute__(self, name)   # 防递归:走 object 那一层

g = Guard()
print(g.done)     # 输出:True
# g.password      # 抛出 PermissionError

15.2、__setattr__/__delattr__/__dir__

python 复制代码
# Author: XiaoJia
class Deep:
    def __setattr__(self, name, value):
        print(f"设置 {name} = {value!r}")
        object.__setattr__(self, name, value)   # 同样防递归
    def __delattr__(self, name):
        print(f"删除 {name!r}")
        object.__delattr__(self, name)

d = Deep()
d.x = 10     # 输出:设置 'x' = 10
del d.x      # 输出:删除 'x'

class Listed:
    def __dir__(self):           # 定制 dir() 的输出
        return ["only_this", "that_too"]

print(dir(Listed()))   # 输出:['only_this', 'that_too']

15.3、__slots__ 的继承注意

第三节演示了 __slots__ 省内存。容易踩的坑在继承上:子类如果不重新声明 __slots__,会重新多出 __dict__,父类省下的内存又还回来了:

python 复制代码
# Author: XiaoJia
class Base:
    __slots__ = ("a",)

class Sub(Base):
    __slots__ = ("b",)        # 子类也声明,整个继承链都无 __dict__

sb = Sub()
sb.a, sb.b = 1, 2
print(hasattr(sb, "__dict__"))   # 输出:False(父类+子类都声明了 slots)

class SubNoSlots(Base):
    pass                      # 子类不声明 -> 重新多出 __dict__

sn = SubNoSlots()
sn.d = 99
print(hasattr(sn, "__dict__"))   # 输出:True
print(sn.d)                      # 输出:99

15.4、3.15 视角:更聪明的 AttributeError 错误提示

本机实测(Python 3.13.16):下面"旧版报错"是我亲手跑出来的真实输出;3.15 的新提示行为本机 3.13.16 无法实测,按官方文档描述编写。

15.1 / 15.2 节讲了属性查找失败会抛 AttributeError。以前这种报错只有一句干巴巴的"找不到属性",排错全靠自己瞪眼睛。3.15 大幅升级了 AttributeError 的提示,和"属性访问"直接相关的有三类:

① 先看 3.13 的旧版报错长什么样(本机实测):

python 复制代码
# Author: XiaoJia
class Inner:
    area = 3.14
class Outer:
    inner = Inner()

o = Outer()

try:
    o.iner                     # 本意是 o.inner
except AttributeError as e:
    print(e)
    # 3.13 实测输出:'Outer' object has no attribute 'iner'

try:
    [].push(1)                 # 从其他语言转来的人极易写的
except AttributeError as e:
    print(e)
    # 3.13 实测输出:'list' object has no attribute 'push'

try:
    "abc".toUpperCase()
except AttributeError as e:
    print(e)
    # 3.13 实测输出:'str' object has no attribute 'toUpperCase'

d = {}
try:
    d.put("a", 1)
except AttributeError as e:
    print(e)
    # 3.13 实测输出:'dict' object has no attribute 'put'

try:
    (1, 2).append(3)
except AttributeError as e:
    print(e)
    # 3.13 实测输出:'tuple' object has no attribute 'append'

注意 3.13 的输出里没有任何"你是不是想写 xxx"的提示 ------错拼 iner、误用 list.push、写 str.toUpperCase、对 tuple 调 append,全部只给一句"没有这个属性"。

② 3.15 的新行为(按官方文档描述,未实测):

  • 对象成员透传建议 :当 o.inner.area 里的某一段拼错时,错误信息会顺着对象图往下走,直接提示你"Did you mean .inner.area"------而不是只说"Outer 没有 iner"。
  • 内置类型的跨语言方法名提示 :识别从其他语言(Java/JS 风格)误写的方法名并给建议:
    • list.push → 提示用 .append;
    • str.toUpperCase → 提示用 .upper;
    • dict.put(k, v) → 提示用 d[k] = v;
    • tuple.append → 提示 tuple 不可变、要改成 list。
  • delattr 失败时给相近属性名 :del obj.color 拼成 del obj.colo 这类,错误信息会列出对象上名字最接近的候选属性。
python 复制代码
# 以下为 3.15 官方文档描述的效果,本机 3.13.16 未实测:
# o.iner
# AttributeError: 'Outer' object has no attribute 'iner'. Did you mean: '.inner.area'?
#
# [].push(1)
# AttributeError: 'list' object has no attribute 'push'. Did you mean: 'append'?
#
# "abc".toUpperCase()
# AttributeError: 'str' object has no attribute 'toUpperCase'. Did you mean: 'upper'?

小嘉提醒:这是一类"对业务逻辑零侵入、但每天都在省排错时间"的改进------它不改任何运行时行为,只改错误信息的文案。从其他语言转 Python 的同学尤其受益:push/toUpperCase/put 这类肌肉记忆终于会被解释器当场纠正,而不是等到读源码才发现写错了方法名。

十六、算术与位运算符全家

8.1 节用 Vector 演示了 __add__ 的基础写法,这里不再重复示例,把全套算术/位运算钩子列清楚。

16.1、正向算术运算符

钩子 运算符 说明
__add__ + 加法
__sub__ - 减法
__mul__ * 乘法
__matmul__ @ 矩阵乘(3.5+)
__truediv__ / 真除法(结果带小数,与 // 区分)
__floordiv__ // 整除
__mod__ % 取模
__divmod__ divmod() 返回 (商, 余) 元组
__pow__ ** 幂
python 复制代码
# Author: XiaoJia
class Number:
    def __init__(self, v):
        self.v = v
    def __add__(self, o): return Number(self.v + o.v)
    def __sub__(self, o): return Number(self.v - o.v)
    def __mul__(self, o): return Number(self.v * o.v)
    def __truediv__(self, o): return Number(self.v / o.v)      # /
    def __floordiv__(self, o): return Number(self.v // o.v)    # //
    def __mod__(self, o): return Number(self.v % o.v)
    def __divmod__(self, o): return (Number(self.v // o.v), Number(self.v % o.v))
    def __pow__(self, o): return Number(self.v ** o.v)
    def __repr__(self): return f"Number({self.v})"

a, b = Number(10), Number(3)
print(a + b, a - b, a * b)     # 输出:Number(13) Number(7) Number(30)
print(a / b, a // b, a % b, a ** b)  # 输出:Number(3.33...) Number(3) Number(1) Number(1000)
q, r = divmod(a, b)
print(q, r)                    # 输出:Number(3) Number(1)

16.2、位运算符

钩子 运算符 钩子 运算符
__lshift__ << __rshift__ >>
__and__ & __xor__ ^
__or__ ` `
python 复制代码
# Author: XiaoJia
class Bits:
    def __init__(self, v): self.v = v
    def __lshift__(self, o): return Bits(self.v << o.v)
    def __rshift__(self, o): return Bits(self.v >> o.v)
    def __and__(self, o): return Bits(self.v & o.v)
    def __xor__(self, o): return Bits(self.v ^ o.v)
    def __or__(self, o): return Bits(self.v | o.v)
    def __repr__(self): return f"Bits({self.v})"

print(Bits(10) << Bits(1), Bits(10) >> Bits(1))   # 输出:Bits(20) Bits(5)
print(Bits(10) & Bits(3), Bits(10) ^ Bits(3), Bits(10) | Bits(3))  # Bits(2) Bits(9) Bits(11)

十七、反向运算符与就地运算符

17.1、反向运算符全家(14 个)

正向方法是 a + b 时左操作数 a 的方法。当 a 不支持该运算、且 a 和 b 类型不同时,Python 会回退去问右操作数 b 要一个"反向"方法------名字就是正向方法前面加个 r:

正向 反向 正向 反向
__add__ __radd__ __sub__ __rsub__
__mul__ __rmul__ __matmul__ __rmatmul__
__truediv__ __rtruediv__ __floordiv__ __rfloordiv__
__mod__ __rmod__ __divmod__ __rdivmod__
__pow__ __rpow__ __lshift__ __rlshift__
__rshift__ __rrshift__ __and__ __rand__
__xor__ __rxor__ __or__ __ror__

为什么需要它?因为 2 * obj 里 2 是内置 int,它不知道怎么乘你的自定义类------这时就轮到 obj.__rmul__(2) 出场:

python 复制代码
# Author: XiaoJia
class Scalar:
    def __init__(self, v): self.v = v
    def __repr__(self): return f"Scalar({self.v})"

s = Scalar(5)
# 2 * s      # 没定义 __rmul__ 时 -> TypeError: unsupported operand type(s) for *: 'int' and 'Scalar'

class Scalar2:
    def __init__(self, v): self.v = v
    def __mul__(self, o): return Scalar2(self.v * o)
    def __rmul__(self, o): return Scalar2(self.v * o)   # 左操作数不支持时回退到它
    def __repr__(self): return f"Scalar2({self.v})"

s2 = Scalar2(5)
print(2 * s2)     # 输出:Scalar2(10)(补了 __rmul__ 才正常)
print(s2 * 3)     # 输出:Scalar2(15)

小嘉提醒:写了反向方法要保证 a + b 和 b + a 结果一致(交换律),不然用户写 2 * obj 和 obj * 2 得到不同结果,很反直觉。

17.2、就地(增强赋值)运算符全家(13 个)

a += b 大致等价于 a = a.__iadd__(b)。就地方法约定返回 self (原地改);没定义就地方法时,Python 自动回退到普通 __add__ 并把结果重新赋值:

__iadd__ __isub__ __imul__ __imatmul__ __itruediv__ __ifloordiv__ __imod__ __ipow__ __ilshift__ __irshift__ __iand__ __ixor__ __ior__

python 复制代码
# Author: XiaoJia
class Bag:
    def __init__(self, items): self.items = list(items)
    def __iadd__(self, other):       # += 原地修改,返回 self
        self.items.extend(other.items)
        return self
    def __repr__(self): return f"Bag({self.items})"

b1 = Bag([1, 2])
b2 = Bag([3, 4])
orig = b1
b1 += b2
print(b1)               # 输出:Bag([1, 2, 3, 4])
print(b1 is orig)       # 输出:True(__iadd__ 原地改,还是原对象)

class OnlyAdd:
    def __init__(self, v): self.v = v
    def __add__(self, o): return OnlyAdd(self.v + o.v)   # 只定义普通 +
    def __repr__(self): return f"OnlyAdd({self.v})"

o1 = OnlyAdd(1)
orig = o1
o1 += OnlyAdd(2)       # 没有 __iadd__ -> 回退 o1 = o1.__add__(...)
print(o1, o1 is not orig)   # 输出:OnlyAdd(3) True(换成了新对象)

小嘉提醒:这就是为什么 list 的 += 是原地改(快),而 int/str 的 += 会生成新对象------可变类型实现 __iadd__ 返回 self,不可变类型不实现它、靠 __add__ 返回新对象。

十八、一元运算与数值转换

钩子 触发 钩子 触发
__neg__ -obj __pos__ +obj
__abs__ abs(obj) __invert__ ~obj
__complex__ complex(obj) __int__ int(obj)
__float__ float(obj) __index__ 切片下标 / bin/hex/oct / operator.index
__round__ round(obj[, n]) __trunc__ math.trunc
__floor__ math.floor __ceil__ math.ceil

__index__ 比较特殊:它告诉 Python"我可以当一个整数用",于是能直接当下标、参与切片,整数语义的类必须实现它:

python 复制代码
# Author: XiaoJia
import math, operator
class CN:
    def __init__(self, v): self.v = v
    def __neg__(self): return CN(-self.v)
    def __abs__(self): return CN(abs(self.v))
    def __invert__(self): return CN(~self.v)
    def __complex__(self): return complex(self.v, 0)
    def __int__(self): return int(self.v)
    def __float__(self): return float(self.v)
    def __index__(self): return self.v        # 供切片下标使用
    def __round__(self, ndigits=0): return CN(round(self.v, ndigits))
    def __repr__(self): return f"CN({self.v})"

x = CN(-5)
print(-x, abs(x), ~x)              # 输出:CN(5) CN(5) CN(4)
print(int(CN(3.9)), float(CN(3)), complex(CN(3)))   # 3 3.0 (3+0j)
print(round(CN(3.14159), 2))       # 输出:CN(3.14)

data = [100, 200, 300, 400]
print(data[CN(2)])                  # 输出:300(__index__ 让它能当下标)
print(operator.index(CN(7)))        # 输出:7
print(math.trunc(CN(3.9)), math.floor(CN(3.9)), math.ceil(CN(3.1)))  # 3 3 4

小嘉提醒(3.15 弃用,新代码勿踩):自 Python 3.15 起,__index__()/__int__() 返回非 int 、__float__() 返回非 float 、__complex__() 返回非 complex 的写法被弃用,3.20 起将强制要求返回对应类型(或其严格子类)。上面示例里 __int__ 返回 int(self.v)、__float__ 返回 float(self.v)、__complex__ 返回 complex(...) 都是合规写法------别图省事返回别的类型。另外 complex(complex_obj) 这种"把复数再当实参传进去"的构造方式也已弃用,复数只应作为单个位置参数构造。

还有个 bool 上的坑:自 3.12 起,对 True/False 用位反转 ~(走 bool.__invert__)被弃用------~True 得 -2、~False 得 -1,结果反直觉,官方建议改用 not x。注意这只针对 bool 类型本身,你自己定义的类里 __invert__ 不受影响。

十九、上下文管理:__enter__/__exit__

with 语句的背后就是这对钩子。执行流程:进 with 时调 __enter__(返回值赋给 as 变量),出 with 时(无论正常结束还是异常)调 __exit__。

python 复制代码
# Author: XiaoJia
class MyCtx:
    def __enter__(self):
        print("进入 with")
        return self
    def __exit__(self, exc_type, exc_value, tb):
        # 三个参数:异常类型、异常对象、堆栈;没异常时都是 None
        print(f"退出 with,exc_type={exc_type.__name__ if exc_type else None}")
        return False        # 返回 False(或 None):异常照常向外抛

with MyCtx():
    print("with 体内")
# 输出:进入 with / with 体内 / 退出 with,exc_type=None

__exit__ 若返回 True,就能把异常吞掉,程序不再向外抛:

python 复制代码
class Swallow:
    def __enter__(self): return self
    def __exit__(self, exc_type, exc_value, tb):
        print(f"捕获到 {exc_type.__name__}: {exc_value}")
        return True        # 吞掉异常

with Swallow():
    raise ValueError("boom")
print("异常被吞,程序继续走")
# 输出:捕获到 ValueError: boom / 异常被吞,程序继续走

小嘉提醒:文件操作的 with open(...)(Day29)就是这个协议------__exit__ 里做关闭文件,保证不管中间有没有异常都会关。平时不要滥用 return True 吞异常,除非你真知道自己在兜什么。

二十、异步协议、match_args 与 buffer 协议

20.1、异步版三对钩子

钩子 用途 对应同步
__await__ await obj 让对象可被 await
__aiter__/__anext__ async for __iter__/__next__
__aenter__/__aexit__ async with __enter__/__exit__
python 复制代码
# Author: XiaoJia
import asyncio

class AsyncCounter:
    def __init__(self, n): self.n = n
    def __aiter__(self):
        self.i = 0
        return self
    async def __anext__(self):
        if self.i >= self.n:
            raise StopAsyncIteration      # 异步版 StopIteration
        self.i += 1
        return self.i

class AsyncFile:
    async def __aenter__(self):
        print("异步进入")
        return self
    async def __aexit__(self, *args):
        print("异步退出")

async def main():
    async with AsyncFile():
        async for num in AsyncCounter(3):
            print("async for 拿到:", num)

asyncio.run(main())
# 输出:异步进入 / async for 拿到: 1 / 2 / 3 / 异步退出

小嘉提醒:这是 Day32 已有 asyncio 内容的延伸------协程、任务调度在那边讲,这里只把"怎么让自己的类支持 async for / async with"这层协议补上。

20.2、__match_args__(3.10+)与 buffer 协议(3.12+)

__match_args__ 声明类的哪些属性参与 match 的位置模式匹配:

python 复制代码
# Author: XiaoJia
class Point:
    __match_args__ = ("x", "y")
    def __init__(self, x, y):
        self.x, self.y = x, y

p = Point(3, 4)
match p:
    case Point(3, y):       # 位置模式,按 __match_args__ 绑定
        print("x==3, y =", y)   # 输出:x==3, y = 4

buffer 协议(3.12+)由 __buffer__/__release_buffer__ 实现,让对象能像 bytes/bytearray 一样被 memoryview 零拷贝访问。本机 3.13 实测:memoryview 实例确实带 __buffer__(hasattr(memoryview(b"x"), "__buffer__") 为 True),自定义类要实现它属于较底层的 C 扩展话题,初学知道有这回事即可。

二十一、类对象的特殊属性

第七节讲了"类本身也是对象、由 type 创建"。类对象自己还带一组特殊属性,常配合 isinstance/issubclass(第九节)和 MRO(1.4)一起用:

属性 含义
__name__ 类名(不带外层)
__qualname__ 嵌套类/函数的完整限定名
__module__ 定义所在的模块名
__doc__ 文档字符串
__bases__ 直接基类元组(所有父类)
__base__ 最近的那个直接基类(单继承下常用)
__mro__ 方法解析顺序元组
python 复制代码
# Author: XiaoJia
class Outer:
    class Inner:
        """Inner 的文档字符串"""

class A: pass
class B: pass
class C(A, B): pass          # 多继承

print([c.__name__ for c in C.__bases__])   # 输出:['A', 'B'](全部直接基类)
print(C.__base__.__name__)                  # 输出:A(最近的直接基类,与 __bases__ 不同)
print([c.__name__ for c in C.__mro__])     # 输出:['C', 'A', 'B', 'object']
print(Outer.Inner.__qualname__)            # 输出:Outer.Inner(完整限定名)
print(Outer.Inner.__name__)                 # 输出:Inner(只带内层名)
print(Outer.Inner.__module__)              # 输出:__main__
print(repr(Outer.Inner.__doc__))           # 输出:'Inner 的文档字符串'

小嘉提醒:多继承下 __base__ 只取元组第一个、__bases__ 才列全------调试 MRO 问题时两者对照着看。__qualname__ 比 __name__ 多了嵌套层级,日志里打印嵌套类时更清楚。

练习题

  1. 写一个 Shape 基类,再写 Circle 和 Rectangle 两个子类,都有 area() 方法;写一个函数 total_area(shapes) 用多态累加所有图形面积。
  2. 给 Student 类用 @property 实现一个只读的 is_adult 属性(年龄 >= 18 返回 True)。
  3. 自定义一个 MyList 类,通过 __len__ 和 __getitem__ 让它支持 len() 和下标访问,内部用一个列表存储。
  4. 用 enum.Enum 定义一个 Direction(UP/DOWN/LEFT/RIGHT),写个函数接收方向并打印对应中文。
  5. 思考题:什么是鸭子类型?它和"必须继承某个父类才能用"的静态语言思路有什么不同?
  6. 把今天的 Team 类加上 __repr__,让 print([t]) 这样的容器输出也好看。
  7. 参照补.2 的图书馆模型,做一个简单"下单"模型:定义 Money 值对象和 Order 聚合根(内含订单项),实现两条不变式------金额不能为负、没有任何订单项的空订单不能提交。
  8. 思考题:为什么 ISBN、金额要做成"不可变的值对象",而读者、图书要做成"有编号、状态可变的实体"?换成业务里的"收货地址",你觉得它更像哪一类?

小结

  • 继承让子类复用并扩展父类;重写用 super() 先调父类逻辑;多继承按 MRO 顺序解析方法。
  • 多态 = 同一调用不同行为;鸭子类型 = 不看类型、只看"能不能做这件事"。
  • __slots__ 限定实例属性、省内存;@property 把方法变成属性,做校验和只读控制。
  • 魔术方法:__len__(len)、__getitem__(下标)、__eq__(==)、__repr__(表示)。
  • enum.Enum 管理固定取值,比魔法字符串更安全。
  • 类本身也是对象,由 type 元类创建;type(名字, 父类元组, 命名空间) 可动态造类,自定义元类用 class X(type) + metaclass=,在类生成前拦截加料;典型场景有类注册表、单例、收集类属性(ORM 雏形),日常不常用,3.6+ 可用更轻量的 __init_subclass__。
  • 领域建模:实体有身份、状态可变,值对象无身份、不可变、按内容相等,聚合把强相关对象包成整体并由聚合根守住业务不变式。
  • 枚举变体:IntEnum 成员即 int 可直接比较运算;Flag 可位运算组合开关;StrEnum(3.11+)成员即字符串;auto() 自动赋值,同值成员互为别名(is 同一对象、不重复列出),_missing_ 给按值查不到时兜底。
  • 下标协议:__getitem__/__setitem__/__delitem__ 让自定义类像 list/dict 一样读写删,切片时 __getitem__ 收到 slice 对象。
  • 真值与构造:if obj 先走 __bool__、缺失回退 __len__、再缺失默认 True(__nonzero__ 是 Python 2 遗留名);__new__ 在 __init__ 前创建实例,返回非本类实例时 __init__ 不调用;__repr__ 求可 eval 的官方表示、__str__ 求可读,__format__ 接 f-string 的 format_spec。
  • 比较与哈希:六件套配合 total_ordering 只需写 __eq__+__lt__;定义 __eq__ 后 __hash__ 自动置 None,须显式重设且保证相等对象 hash 相同。
  • 属性访问:__getattr__ 找不到才兜底、__getattribute__ 每次都触发(用 object.__getattribute__ 防递归);__slots__ 的子类不重新声明会重新多出 __dict__。
  • 运算符全家:正向算术(含 @//////divmod/**)与位运算各有钩子;反向 __radd__ 等 14 个解决 2 * obj 这类左操作数不支持的回退;就地 __iadd__ 等 13 个约定返回 self 原地改,未定义时回退普通运算符。
  • 一元与数值转换:__neg__/__abs__/__invert__ 等一元钩子,__index__ 让对象能当下标;3.15 起 __int__/__float__/__complex__ 返回非对应类型被弃用;bool 上用 ~ 自 3.12 弃用,改用 not。
  • 上下文管理:__enter__/__exit__ 支撑 with,__exit__ 返回 True 可吞掉异常;异步版 __await__/__aiter__/__anext__/__aenter__/__aexit__ 支撑 await/async for/async with。
  • 其他协议与类属性:__match_args__(3.10+)参与 match 位置模式,buffer 协议(3.12+)接 memoryview;类对象自带 __name__/__qualname__/__module__/__doc__/__bases__/__base__/__mro__。

扩展阅读