Day20 · 面向对象(二)继承、多态与高级特性
Day20 · 面向对象(二)继承、多态与高级特性
阶段:阶段一 · 核心基础 | 前置:Day19 | 技术基准:Python 3.15.0
学习目标
- 掌握单继承、多继承与 MRO(方法解析顺序),会用
super() - 理解多态与鸭子类型
- 会用
__slots__节省内存,用@property做属性校验与只读控制 - 认识常用魔术方法(
__len__、__getitem__、__eq__等) - 了解枚举类
enum.Enum,初步认识元类type(可选了解) - 初步认识领域建模:区分实体与值对象,知道聚合根的作用,体会"用类把业务规则(不变式)封装起来"
一、继承(Inheritance)
1.1、为什么需要继承
继承让子类(派生类)自动获得父类(基类)的属性和方法,还能再扩展自己的东西。它是实现代码复用和"is-a"关系的手段。语法:class 子类(父类):
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)
子类可以重新定义父类的同名方法,覆盖掉父类版本:
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() 先把父类的初始化做了,再补充子类自己的属性:
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__ 可以查看顺序:
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、多态
多态指:同一个方法调用,作用在不同对象上,表现出不同行为:
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 方法就能用:
class Robot: # 跟 Animal 毫无关系
def speak(self):
return "嘀嘀"
make_sound(Robot()) # 输出:嘀嘀,照样能跑
三、__slots__ 节省内存
默认实例的属性存在一个字典里,字典比较占内存。如果一个类会创建海量实例,可以用 __slots__ 限定实例只能有哪些属性,并取消字典,大幅省内存:
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:把方法伪装成属性来访问。
class Student:
def __init__(self, name, score):
self.name = name
self.score = score # 直接赋值,不做校验
s = Student("小嘉", -100) # 分数居然能是负数!
4.2、用 @property 做校验
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,这个属性就变成只读:
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) |
官方字符串表示 |
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 比魔法字符串更安全、可读:
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(对象) 查看它是哪个类创建的:
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 语句等价:
# 等价于 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= 指定给某个类:
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、三个典型应用场景
元类平时用不到,但看懂这三个场景,就能理解它在框架里到底干嘛用:
① 类注册表:自动收集所有子类(插件系统、命令分发常用)
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__ 实现过单例,元类是另一种写法)
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 的思想雏形)
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__:让对象支持 +
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:
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
继承体系里,经常要判断"一个对象是不是某个类的""一个类是不是另一个的子类"。两个内置函数正好干这个:
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 是聚合根(借书、还书的规则都由它守住)。
# 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,可以直接和数字比较、参与运算:
# 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,成员之间可以 | 组合:
# 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"{成员}" 打印出来就是值本身:
# 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_ 可在查不到值时兜底:
# 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] |
删元素 |
# 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] 时)。自己判断一下就能让自定义序列支持切片:
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。
# 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__ 做初始化。
# 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__ 就不会被自动调用:
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 格式化 |
# 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__时__repr__;反过来不行。
十四、比较与哈希
14.1、比较六件套与 total_ordering
六个比较运算符对应六个钩子。只写 __eq__ 和 __lt__,再用 functools.total_ordering(Day18 讲过)就能自动补出其余四个:
# 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__:
# 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__、描述符都没有)时才兜底触发。
# 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__ 绕开:
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__
# 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__,父类省下的内存又还回来了:
# 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 的旧版报错长什么样(本机实测):
# 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这类,错误信息会列出对象上名字最接近的候选属性。
# 以下为 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__ |
** |
幂 |
# 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__ |
` | ` |
# 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) 出场:
# 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__
# 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"我可以当一个整数用",于是能直接当下标、参与切片,整数语义的类必须实现它:
# 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__。
# 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,就能把异常吞掉,程序不再向外抛:
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__ |
# 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 的位置模式匹配:
# 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__ |
方法解析顺序元组 |
# 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__多了嵌套层级,日志里打印嵌套类时更清楚。
练习题
- 写一个
Shape基类,再写Circle和Rectangle两个子类,都有area()方法;写一个函数total_area(shapes)用多态累加所有图形面积。 - 给
Student类用@property实现一个只读的is_adult属性(年龄 >= 18 返回 True)。 - 自定义一个
MyList类,通过__len__和__getitem__让它支持len()和下标访问,内部用一个列表存储。 - 用
enum.Enum定义一个Direction(UP/DOWN/LEFT/RIGHT),写个函数接收方向并打印对应中文。 - 思考题:什么是鸭子类型?它和"必须继承某个父类才能用"的静态语言思路有什么不同?
- 把今天的
Team类加上__repr__,让print([t])这样的容器输出也好看。 - 参照补.2 的图书馆模型,做一个简单"下单"模型:定义
Money值对象和Order聚合根(内含订单项),实现两条不变式——金额不能为负、没有任何订单项的空订单不能提交。 - 思考题:为什么 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__。
扩展阅读
- Python 官方:继承 https://docs.python.org/3/tutorial/classes.html#inheritance
- enum 模块文档:https://docs.python.org/3/library/enum.html
- 描述符与 property:https://docs.python.org/3/howto/descriptor.html

浙公网安备 33010602011771号