9. 类
命名空间与作用域、实例/类属性、方法绑定、继承与 MRO、私有化约定、迭代器与生成器——用 JS prototype 做类比但别套用错。
9.1 核心概念
- 类:描述行为与初始状态的模板。
- 实例:每个对象有自己的属性(通常挂在
__dict__)。 self:实例方法首参,调用时由解释器绑定。- 查找顺序:实例 → 类 → 基类链(MRO)。
9.2 命名空间与作用域
- LEGB:Local → Enclosing → Global → Builtins。
global:绑定模块级名字。nonlocal:写外层函数的名字。
二者都是声明「赋值写到哪」:global → 模块顶层,nonlocal → 外层函数的局部变量。
def scope_test():
spam = "test spam"
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
do_local()
print("After do_local:", spam) # test spam
do_nonlocal()
print("After do_nonlocal:", spam) # nonlocal spam
do_global()
# do_global 里的 global 只写模块级 spam;本函数局部 spam 不变
print("After do_global:", spam) # nonlocal spam
scope_test()
print("In global scope:", spam) # global spam9.3 类基础
class Dog:
kind = "canine" # 类属性:所有实例共享同一个 str 对象(别共享可变 list)
def __init__(self, name: str) -> None:
self.name = name
self.tricks: list[str] = []
def add_trick(self, trick: str) -> None:
self.tricks.append(trick)
d1 = Dog("Fido")
d2 = Dog("Buddy")
d1.add_trick("roll over")
print(d1.tricks) # ['roll over']
print(d2.tricks) # []反模式:tricks: list[str] = [] 写在类体上 → 所有实例共享同一张表。
反模式(anti-pattern)指的是:看起来很常见、甚至被随手写出来的写法,但在该场景下会引出可靠性与维护上的问题;
调用机制
d.add_trick(x) 等价于 Dog.add_trick(d, x)。
class Bag:
def __init__(self) -> None:
self.data: list[int] = []
def add(self, x: int) -> None:
self.data.append(x)
def add_twice(self, x: int) -> None:
self.add(x)
self.add(x)9.4 @property:把访问器伪装成属性
对齐「getter,但调用方不用写 ()」:
class Circle:
def __init__(self, r: float) -> None:
self.r = r
@property
def area(self) -> float:
return 3.1415926 * self.r * self.r
c = Circle(2)
print(c.area) # 不是 c.area()9.5 @classmethod 与 @staticmethod
class Point:
def __init__(self, x: int, y: int) -> None:
self.x, self.y = x, y
@classmethod
def origin(cls) -> "Point":
return cls(0, 0)
@staticmethod
def manhattan(a: "Point", b: "Point") -> int:
return abs(a.x - b.x) + abs(a.y - b.y)注解里的 "Point" 是带引号的类型注解(前向引用):对类型检查器来说仍然表示 类 Point,不是「字符串类型」。类体里尚未结束定义时就能引用自身,所以常见这种写法。
@classmethod:第一个参数是类本身(约定写 cls)。上面 origin 用 cls(0, 0) 创建实例,子类继承时仍会生成子类的实例。常见用途:备用构造函数、按类配置生成对象。
@staticmethod:解释器不自动塞 self 或 cls;只是挂在类名下的普通函数,调用写成 Point.manhattan(a, b)。实例若需要,由参数显式传入(如上例的 a、b),而不是靠绑定到「当前对象」。
对照前端:classmethod ≈ 静态方法里仍能拿到「构造函数」(类似工厂里用 this 指向派生类 ctor 的场景);staticmethod ≈ 挂在类/对象上的纯函数,只为归类命名空间。
9.6 继承与 super()
class Animal:
def speak(self) -> str:
return "..."
class Cat(Animal):
def speak(self) -> str:
return "Meow"
class Animal2:
def __init__(self, name: str) -> None:
self.name = name
class Dog2(Animal2):
def __init__(self, name: str, breed: str) -> None:
super().__init__(name)
self.breed = breed
cat = Cat()
print(isinstance(cat, Cat), isinstance(cat, Animal)) # True True
print(issubclass(Cat, Animal)) # Trueisinstance(obj, Cls):判断某个对象是不是 Cls 的实例(沿继承也算)。issubclass(A, B):判断类 A 是不是类 B 的子类(或同一类);两个参数都是类型,不能把实例当作第一个参数。
MRO(C3)
class A:
tag = "A"
class B(A):
tag = "B"
class C(A):
tag = "C"
class D(B, C):
pass
print(D.__mro__) # 解释器如何线性化多重基类
d = D()
print(d.tag) # 跟 MRO:先到 B多重继承里常见「菱形」:B、C 同继承 A,D 再继承 B、C。MRO(方法解析顺序,由 C3 线性化得到)记在 D.__mro__ 里。解析实例属性(如 tag)时沿这条链从左到右找第一个定义,故 d.tag 为 "B"(B 排在 C 前)。
9.7 「私有」与名称改写
class Mapping:
def __init__(self, iterable: list[int]) -> None:
self.items: list[int] = []
self.__update(iterable)
def update(self, iterable: list[int]) -> None:
self.items.extend(iterable)
__update = update # 私有别名:子类不易误触
m = Mapping([1, 2, 3])
# m.__update # AttributeError —— 被改写为 _Mapping__update9.8 迭代器与生成器
下面两段都在做同一件事:按索引从尾到头遍历字符串,产出 'f','l','o','g'。上一段是手写迭代器协议(__iter__ / __next__);下一段是生成器函数(yield),语义等价、代码更短。
class Reverse:
def __init__(self, data: str) -> None:
self.data = data
self.index = len(data)
def __iter__(self) -> "Reverse":
return self
def __next__(self) -> str:
if self.index == 0:
raise StopIteration
self.index -= 1
return self.data[self.index]
print(list(Reverse("golf"))) # ['f', 'l', 'o', 'g']
def reverse_gen(data: str):
for i in range(len(data) - 1, -1, -1):
yield data[i]
print(list(reverse_gen("golf"))) # ['f', 'l', 'o', 'g']9.9 dataclass(3.7+,工程常用)
存几个字段的类,手写要重复 __init__ / __repr__。@dataclass:按 id: int 这类注解自动生成构造和可读 print。slots=True 可选:只允许声明过的属性、略省内存;初学可省略。
from dataclasses import dataclass
@dataclass(slots=True)
class User:
id: int
name: str
u = User(1, "Ada")
print(u) # User(id=1, name='Ada')仅有注解、或手写构造但没写「打印格式」时:
class OnlyAnnot:
id: int
name: str
# OnlyAnnot(1, "Ada") # TypeError:注解不会自动生成 __init__,不能这样构造
class Handwritten:
def __init__(self, id: int, name: str) -> None:
self.id = id
self.name = name
print(Handwritten(1, "Ada")) # 默认:<__main__.Handwritten object at 0x...>,看不到 id/name说明:OnlyAnnot 那种写法没有 @dataclass 时,类体里的 id: int 只是标注,不会帮你写出构造函数,也就没法用位置参数直接构造。Handwritten 虽能 print,但用的是对象默认展示,不含字段内容;需要可读字段时要么手写 __repr__,要么用上面的 @dataclass。
9.10 速记
| JS | Python |
|---|---|
this | 显式 self |
| 类字段 | 分清类共享 vs 实例字段,别共享可变默认 |
extends | 多继承 + MRO |
| 迭代协议 | __iter__ / yield |
权威延伸:9. Classes