第12篇|面向对象一:类、实例、属性、方法
本篇定位:《Python 教程》专栏一「Python 从零入门」的第 12 篇。承接第 11 篇异常处理——自定义异常已经轻轻碰到了
class;本篇系统建立「类(类型对象)→ 实例 → 属性 / 方法」的心智模型,并为第 13 篇继承与 MRO、第 14 篇魔术方法与数据模型、以及后续dataclass/nn.Module/ Agent Tool 打地基。读完你将能够:说清为何用类把数据与行为捆在一起;区分类型对象与实例、
type(obj)与obj.class;正确写init,并知道new负责造壳;分清实例属性与类属性(含可变类属性陷阱);选用实例方法 /@classmethod/@staticmethod;用@property做受控访问;用最小自定义异常衔接第 11 篇;在 AI Infra / Agent 场景里把配置对象与 Tool 建成类。预计用时:阅读 35~45 分钟,动手 25~35 分钟。
上一篇你学会了用异常表达失败,并写过最小的 class ConfigError(Exception)。那一行 class 不是语法彩蛋——它打开了 Python 最核心的建模方式之一:面向对象(Object-Oriented Programming, OOP)。本篇不急着讲继承树和 MRO(那是第 13 篇),而是先把「一个类如何描述一类东西、一个实例如何带着自己的数据、方法如何绑定到实例」讲透。
和第 02 篇「一切皆对象」会合:类本身也是对象(类型对象);实例是类型对象“造出来”的对象。和第 07、08 篇会合:方法就是带着隐式第一个参数的函数;和第 09 篇会合:类通常住在模块里,用 from xxx import MyClass 导入。和第 11 篇会合:自定义异常就是继承 Exception 的小类——本篇会把它写完整一点,作为从异常到 OOP 的桥。
建议边读边开练习目录(uv init --no-package),每节代码单独跑一遍。阅读顺序:为何需要类 → 类型与实例 → 初始化与属性 → 三种方法 → property / 身份与相等 → AI Infra 场景。踩坑大全请过一遍,尤其是「可变类属性被所有实例共享」和「忘写 self」。
也可以先用一句话定心:类描述蓝图,实例带着数据;查属性先看实例再看类;方法靠绑定把 self 塞进去。 本篇把这句话拆开。
动手时建议用 type()、id()、dict 亲自“拆开”对象看内部——比只背术语记得牢。和第 02 篇一样,对象身份与命名空间仍是底层主线;本篇只是把“自定义类型”这条线接上。
和“全用字典 + 一堆函数”相比,本篇带来的变化是:名字、数据、行为、以及日后的继承扩展点,被收进同一个可导入的类型里。弄清蓝图与实例、属性查找与方法绑定,后面读 nn.Module、Pydantic 模型、Agent 的 Tool 注册表时,就不会只剩“这是个类吧”的模糊印象。
你也可以把本篇看成第 05 篇字典与第 07 篇函数的“产品化”:字典能存字段,函数能操作字段;类把两者绑成可复用的类型,并让解释器帮你做查找与绑定。第 13、14 篇会在这个产品上继续加继承与协议。
和第 09 篇合在一起看:一个模块可以导出多个类;一个类的实例可以在包的各处流转。训练脚本里的 Config、tokenizer wrapper、推理服务里的 Client,几乎都是「模块里的类 + 运行时的实例」。
动手练习时不妨准备两份对照:一份继续用 dict + 函数完成“用户问候”;另一份改成 class User。对比调用处的可读性、拼错字段时的报错位置、以及“再加一个方法”时要改几处文件。这种对比比抽象讨论“OOP 好不好”更有体感。
本篇刻意不一上来讲继承。许多教程把 class Dog(Animal) 当作第一印象,结果读者会以为“有类就是有继承树”。其实单类已经能解决大量问题:封装校验、提供方法、成为可导入的类型名。继承是扩展点,不是入场券——第 13 篇再发门票。
一、为什么需要类:把数据与行为捆在一起
1.1 字典 + 函数的上限
没有类时,常见写法是「字典当结构,函数当行为」:
def make_user(name: str, age: int) -> dict:
return {"name": name, "age": age}
def greet(user: dict) -> str:
return f"Hi, {user['name']}, age {user['age']}"
u = make_user("Ada", 36)
print(greet(u))
Hi, Ada, age 36
能跑,但有三处痛:字段名是字符串,拼错键要到运行时才炸;相关函数散落各处,导入列表越来越长;无法干净表达“这是一种 User”,也难挂校验、默认值与日后扩展点。
1.2 模块也不等于“一类东西的多个实例”
第 09 篇的模块适合放一份全局配置或工具函数。若你需要很多个同类对象(多条请求、多种 Tool、多个 batch 配置),模块级全局变量会打架——你需要的是可复制的蓝图,不是单例命名空间。
1.3 类:蓝图 + 出厂约定
类(class) 描述一类对象的结构与行为;实例(instance) 是按蓝图造出的具体对象。把字段写成属性、把操作写成方法,调用方写 user.greet() 而不是 greet(user)——数据与行为住在一起,类型名成为文档的一部分。
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def greet(self) -> str:
return f"Hi, {self.name}, age {self.age}"
u = User("Ada", 36)
print(u.greet())
print(type(u).__name__, u.__class__.__name__)
Hi, Ada, age 36
User User
AI Infra 视角:推理配置(
max_tokens、temperature)、Tokenizer 包装、Agent 的 Tool(.name/.run())用类建模后,校验、默认值、序列化与注册表都有挂钩。全用裸dict在原型期快,一到多工具、多模型就会散。
当然,不是所有东西都要上类。一次性的三行脚本、纯函数流水线、或第 09 篇那种“模块即单例配置”,用函数与字典往往更短。经验法则:当同一种结构会反复出现、并带着固定操作时,才值得提成类;当只有一份全局状态时,模块可能就够了。
再补一句和团队协作有关的:类名与方法名会成为接口文档。cfg.validate()、tool.run(query) 比 do_stuff(d) 更容易在代码评审里被讨论与测试。后面第 22 篇写 pytest 时,针对类的方法做单元测试也更自然。
二、class 语句:类型对象与实例
2.1 class 做了什么
执行 class User: ... 时,解释器创建一个类型对象(type object),名字绑定到 User。之后 User(...) 会走创建协议,得到实例。类本身也是对象——可以 print(User)、把它当参数传递、放进列表。

图1 类(类型对象)与实例
class Point:
"""二维点。"""
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(Point) # 类型对象
print(p) # 实例(默认 repr 含地址)
print(type(p) is Point)
print(p.__class__ is Point)
print(type(Point)) # 类的类型是 type
<class '__main__.Point'>
<__main__.Point object at 0x...>
True
True
<class 'type'>
(地址因运行而异;关注 type(p) is Point 与 type(Point) is type。)
2.2 type(obj) 与 obj.class
对普通用户类实例,type(obj) 与 obj.class 通常指向同一类型对象。养成习惯:用 type(obj) 看“它是什么类型”;用 isinstance(obj, SomeClass) 做“是否属于某类型(含继承)”的判断——继承细节见第 13 篇,本篇先记住 isinstance 比裸比 type 更常用。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(isinstance(p, Point))
print(isinstance(3, int), type(3) is int)
True
True True
2.3 实例有自己的命名空间
第 02 篇讲过:对象有身份(id)。实例通常还有 dict,存放该实例自己的属性:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(sorted(p.__dict__.keys()))
p.label = "origin-ish" # 动态加点属性(灵活,但团队项目宜克制)
print(p.__dict__)
['x', 'y']
{'x': 3, 'y': 4, 'label': 'origin-ish'}
心智模型:类是共享的蓝图(方法、类属性住这里);实例是带私有袋子(dict)的具体物。
补充:并非所有实例都有普通的 dict(例如部分用 slots 的类型会省掉它以节省内存)——入门期你写的类默认都有。看到 slots 时知道“属性名被固定、省 dict”即可,细节可留到工程优化阶段。
调试时常用三件套:type(obj) 看类型,obj.dict 看实例状态,dir(obj) 看“能点出哪些名字”(含类上继承来的,较嘈杂)。本篇图 2 会把“名字从哪查到”画清楚;dir 只是便利清单,不是查找算法本身。
三、init、轻量 new,与两类属性
3.1 init:初始化,不是“构造函数”的全部
许多人把 init 叫构造函数。更精确的说法是:创建实例时,先由 new 分配(造出空壳对象),再由 init 初始化(往壳里填属性)。入门期你几乎只写 init;知道 new 存在即可——不可变类型、某些单例/池化会重写它,第 14 篇与进阶主题再展开。
class ModelConfig:
def __init__(self, name: str, max_tokens: int = 256):
if max_tokens <= 0:
raise ValueError("max_tokens must be positive")
self.name = name
self.max_tokens = max_tokens
cfg = ModelConfig("mini", 512)
print(cfg.name, cfg.max_tokens)
mini 512
init 里的 self 就是正在初始化的那个实例。self.name = name 把值放进该实例的命名空间。
3.2 实例属性 vs 类属性
- 实例属性:挂在每个实例上(通常在
init里赋值),彼此独立。 - 类属性:写在
class体里、方法之外,所有实例共享同一份(查属性时,实例没有才会落到类上)。

图2 属性查找:实例字典 → 类字典
class Counter:
kind = "demo" # 类属性
def __init__(self, start=0):
self.value = start # 实例属性
a, b = Counter(1), Counter(2)
print(a.kind, b.kind, Counter.kind)
print(a.value, b.value)
a.value = 10
print(a.value, b.value) # 互不影响
demo demo demo
1 2
10 2
3.3 可变类属性陷阱
若类属性是可变对象(列表、字典、集合),所有实例默认共享同一份——改一个等于改全家。这是经典坑。

图3 可变类属性被共享
class Bag:
items = [] # 危险:类属性且可变
def add(self, x):
self.items.append(x)
b1, b2 = Bag(), Bag()
b1.add("a")
print(b2.items) # ['a'] —— 吃惊吗?
['a']
正确做法:可变状态放进实例属性,在 init 里创建:
class BagSafe:
def __init__(self):
self.items = []
def add(self, x):
self.items.append(x)
s1, s2 = BagSafe(), BagSafe()
s1.add("a")
print(s1.items, s2.items)
['a'] []
不可变类属性(字符串、数字、元组)作“同类共享的常量/默认标签”是合理用法;可变共享只在你明确需要共享状态时才用。
属性查找顺序可以先记成一句话:先查实例,再查类型(类),再沿继承链往上(继承链的完整规则是 MRO,第 13 篇)。因此:
- 读
a.kind时,若a.dict没有kind,就会读到Counter.kind; - 写
a.kind = "x"时,默认是在实例上创建/遮盖,不会改类上那份(除非你用特殊描述符,进阶话题)。
这解释了为何“改 a.kind 不影响 b.kind”,以及为何可变类属性的 .append 特别危险——你没有遮盖,你是原地修改了共享对象。
init 里做校验是好习惯:非法参数立刻 raise ValueError 或自定义异常,避免“半初始化对象”在系统里流浪。与第 11 篇的 fail-fast 一致:配错比默默用错超参更安全。
四、方法三兄弟:实例、类、静态
4.1 实例方法与 self
写在类里的普通函数,经实例访问时会绑定:第一个参数自动传入该实例,惯例叫 self。

图4 方法绑定:函数 → 绑定方法
class Greeter:
def __init__(self, name):
self.name = name
def hello(self, title="Hi"):
return f"{title}, {self.name}"
g = Greeter("Lin")
print(g.hello())
print(g.hello("Hello"))
# 等价理解:Greeter.hello(g, "Hello")
print(Greeter.hello(g, "Hello"))
Hi, Lin
Hello, Lin
Hello, Lin
g.hello 是绑定方法;Greeter.hello 是原始函数。忘写 self 会导致调用时参数个数对不上——见踩坑大全。
4.2 @classmethod:第一个参数是类
需要“靠类型工作”而不是“靠某个实例”时用类方法:工厂构造、备选构造器、访问类属性等。
class ModelConfig:
default_max = 256
def __init__(self, name: str, max_tokens: int):
self.name = name
self.max_tokens = max_tokens
@classmethod
def from_dict(cls, data: dict):
return cls(data["name"], data.get("max_tokens", cls.default_max))
cfg = ModelConfig.from_dict({"name": "mini"})
print(cfg.name, cfg.max_tokens)
mini 256
cls 指向类本身(这里是 ModelConfig)。第 13 篇继承后,cls 可能是子类——工厂会自动造出子类实例,这是 @classmethod 相对硬编码类名的优势。
4.3 @staticmethod:不接 self / cls
逻辑上放在类的命名空间里,但既不读实例也不读类时,可用静态方法。它就是普通函数,换了住址。
class TokenUtils:
@staticmethod
def clamp(n: int, lo: int, hi: int) -> int:
return max(lo, min(hi, n))
print(TokenUtils.clamp(999, 1, 512))
print(TokenUtils.clamp(0, 1, 512))
512
1
| 种类 | 装饰器 | 第一参数 | 典型用途 |
|---|---|---|---|
| 实例方法 | (无) | self 实例 | 读写实例状态 |
| 类方法 | @classmethod | cls 类 | 工厂、备选构造、改类级配置 |
| 静态方法 | @staticmethod | 无 | 与类主题相关的纯函数 |
经验:默认写实例方法;需要工厂再用 classmethod;若既不碰 self 也不碰 cls,也可考虑模块级函数(第 09 篇)——静态方法不是必须。
绑定还可以从“函数也是对象”的角度理解(第 08 篇闭包与第 02 篇对象模型的交汇):通过实例取出方法时,描述符协议会把函数包成绑定方法,下次调用少传一个显式实例参数。你暂时不必背“描述符”一词,只要记住:obj.method 已经记住了 obj。
class Greeter:
def __init__(self, name):
self.name = name
def hello(self, title="Hi"):
return f"{title}, {self.name}"
g = Greeter("Lin")
bound = g.hello
print(bound.__self__ is g)
print(bound.__func__ is Greeter.hello)
print(bound("Hey"))
True
True
Hey, Lin
读库代码时若见到 cls、self 混用,先问:这个方法需要实例状态吗?需要子类类型吗?两者都不需要时,它可能该是静态方法或模块函数。
五、property、身份与相等、自定义异常桥接
5.1 @property:用属性语法做受控访问
有时你想让调用方写 obj.x,但读写时要校验或动态计算——用 property:
class Port:
def __init__(self, value: int):
self._value = value # 约定:单下划线=内部
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, v: int):
if not (1 <= v <= 65535):
raise ValueError("port out of range")
self._value = v
p = Port(8080)
print(p.value)
p.value = 3000
print(p.value)
8080
3000
class Port:
def __init__(self, value: int):
self._value = value
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, v: int):
if not (1 <= v <= 65535):
raise ValueError("port out of range")
self._value = v
p = Port(8080)
try:
p.value = 70000
except ValueError as e:
print(e)
port out of range
只读属性可以只定义 getter、不写 setter。第 14 篇还会见到更多“看起来像属性、背后是协议”的钩子。

@property受控访问的Agent Tool素描。左侧是@property读写流,包括p.value和p.value = 3000操作,以及getter和setter方法,其中setter有校验后写入,非法操作会引发ValueError。右侧是Tool类(Agent),包含.name、.description属性和.run(query)方法。底部说明同一套OOP零件,配置用属性+校验,工具用方法+名字,错误用自定义异常类型,第13篇用继承扩展多种Tool,第28篇接入Agent循环。图5 Agent Tool:属性与 run 方法
5.2 身份 vs 相等(衔接第 02 篇)
id(obj)/is:是不是同一个对象。==:值是否相等(默认同is;许多类型会重载,第 14 篇讲eq)。
class ModelConfig:
def __init__(self, name: str, max_tokens: int):
self.name = name
self.max_tokens = max_tokens
a = ModelConfig("mini", 256)
b = ModelConfig("mini", 256)
print(a is b) # False:两个实例
print(a == b) # 未定制 __eq__ 时通常也是 False
print(id(a) != id(b))
False
False
True
入门期先分清「同一个」和「长得像」。配置对象、缓存键是否该按值相等,留给第 14 篇与第 18 篇 dataclass。
5.3 最小自定义异常类(桥接第 11 篇)
第 11 篇的预告片现在可以写完整一点:
class ConfigError(Exception):
"""配置非法或缺失时抛出。"""
def load_max_tokens(raw: dict) -> int:
if "max_tokens" not in raw:
raise ConfigError("missing max_tokens")
try:
n = int(raw["max_tokens"])
except (TypeError, ValueError) as e:
raise ConfigError("max_tokens not an int") from e
if n <= 0:
raise ConfigError(f"max_tokens must be positive, got {n}")
return n
print(load_max_tokens({"max_tokens": "128"}))
try:
load_max_tokens({})
except ConfigError as e:
print(type(e).__name__, e)
128
ConfigError missing max_tokens
这就是 OOP 的最小收益之一:用类型名表达领域失败,上层 except ConfigError 即可挂策略。继承树怎么铺,见第 13 篇。
property 与异常常常一起出现:setter 里校验失败就抛;调用方可以像使用普通属性一样书写,同时在边界上捕获领域错误。不必把所有字段都做成 property——没有校验逻辑时,公开实例属性更简单;出现“读写要拦一道”时再升级。
和身份/相等相关的实务提醒:把自定义对象放进 set / 当 dict 键之前,需要稳定的 hash / eq 约定(第 05、14 篇)。本篇先做到“别默认以为两个配置实例 == 会比字段”。调试缓存命中、请求去重时,先打印 id 与关键字段,避免误判。
六、AI Infra / Agent:配置对象与 Tool 类
6.1 模型配置对象
把超参收进类,校验放在 init 或 property,比满天飞的裸字典更易维护:
class ConfigError(Exception):
pass
class GenConfig:
def __init__(self, model: str, temperature: float = 0.7, max_tokens: int = 256):
if not model:
raise ConfigError("model name empty")
if not (0.0 <= temperature <= 2.0):
raise ConfigError("temperature out of range")
if max_tokens <= 0:
raise ConfigError("max_tokens must be positive")
self.model = model
self.temperature = temperature
self.max_tokens = max_tokens
def to_dict(self) -> dict:
return {
"model": self.model,
"temperature": self.temperature,
"max_tokens": self.max_tokens,
}
g = GenConfig("mini-chat", temperature=0.2, max_tokens=64)
print(g.to_dict())
{'model': 'mini-chat', 'temperature': 0.2, 'max_tokens': 64}
6.2 Tokenizer / Wrapper 素描
包装第三方对象时,类提供稳定门面:内部以后可换实现,调用方仍写 tok.encode(text)。
class FakeTokenizer:
"""演示用:按空白切分。真实项目会包 Hugging Face / 自研 tokenizer。"""
def __init__(self, unk: str = "<unk>"):
self.unk = unk
def encode(self, text: str) -> list[str]:
return text.split() or [self.unk]
def decode(self, tokens: list[str]) -> str:
return " ".join(tokens)
tok = FakeTokenizer()
ids = tok.encode("hello agent world")
print(ids)
print(tok.decode(ids))
['hello', 'agent', 'world']
hello agent world
6.3 Agent Tool:.name 与 .run()
第 28 篇会做最小 Agent 循环;现在先用类固定工具接口:
class Tool:
def __init__(self, name: str, description: str):
self.name = name
self.description = description
def run(self, query: str) -> str:
raise NotImplementedError("subclass or replace run")
class EchoTool(Tool):
def run(self, query: str) -> str:
return f"echo: {query}"
tools = {t.name: t for t in [EchoTool("echo", "echo the query")]}
obs = tools["echo"].run("ping")
print(obs)
echo: ping
(EchoTool 轻轻用了继承——第 13 篇正餐;本篇只需体会「Tool 是带名字与 run 的对象」。也可用组合:传入 callable,不必子类。)
AI Infra 视角:vLLM / SGLang 的采样参数、Engine 配置,PyTorch 的
nn.Module,本质都是「类型对象 + 实例状态 + 方法」。先练好本篇的属性与方法,后面读源码时才能分清“类级注册表”和“实例级 buffer”。
把三块拼起来:Config 管旋钮,Tokenizer/Wrapper 管编解码门面,Tool 管可调用能力——Agent 雏形的骨架就有了。异常类型(11)决定失败如何上报;类(本篇)决定成功路径上对象如何协作。
再给一个“组合优于过早继承”的小例子:Tool 不一定非要子类——也可以持有一个函数:
class CallableTool:
def __init__(self, name: str, fn):
self.name = name
self._fn = fn
def run(self, query: str) -> str:
return self._fn(query)
t = CallableTool("ping", lambda q: f"pong:{q}")
print(t.name, t.run("x"))
ping pong:x
这在 Agent 框架里很常见:注册表里是对象,对象 internally 调函数或 HTTP。第 13 篇的继承适合“共享大量实现、偶尔覆盖”;第 08 篇的函数与闭包适合“行为是热插拔的”。两者都是面向对象工具箱的一部分——类负责边界,不必迷信深层继承树。
写推理服务时,还会见到“客户端类”:class LLMClient: 在 init 里保存 base_url / api_key,方法 chat(messages) 发请求。密钥放实例属性(或从环境读取后赋给实例),而不是类属性——否则测试多客户端时容易串配置。这和第 10 篇“路径与配置从哪来”、第 26 篇 HTTP 客户端一脉相承。
七、踩坑大全
7.1 属性与可变共享
坑 1:可变类属性当“每实例列表”。 现象:b2.items 里出现 b1 加的元素。解决:在 init 里 self.items = []。
坑 2:用可变对象作默认参数,再赋给 self.x。 现象:与第 07 篇相同——默认列表被共享。解决:默认用 None,函数体内新建。
坑 3:误以为 a.kind = "x" 改的是类属性。 现象:只在实例 a 上遮盖(shadow)了类属性,b.kind 仍是旧值。解决:改共享值应写 Counter.kind = ...;遮盖前先分清意图。
7.2 方法与 self
坑 4:实例方法忘写 self。 现象:TypeError: takes 0 positional arguments but 1 was given。解决:第一参数留 self。
坑 5:在方法里写 name = name 却不 self.name = name。 现象:局部变量转瞬即逝,实例上没有属性。解决:要持久化的状态赋给 self.*。
坑 6:把 @classmethod 写成实例方法去“造实例”。 现象:必须先有实例才能调用工厂,逻辑倒置。解决:备选构造器用 @classmethod,内部 return cls(...)。
7.3 property 与异常
坑 7:在 property 的 setter 里抛过于宽泛的 Exception。 现象:上层难以细分策略。解决:抛 ValueError / 自定义 ConfigError(第 11 篇刀法)。
坑 8:公开属性与 _value 双源真理。 现象:有人绕过 setter 写 obj._value = -1。解决:约定与文档;真正要强封装可了解名称改写(进阶),团队规范往往比语言强制更重要。
7.4 平台与工程
坑 9(通用):用 type(x) == SomeClass 做分支。 现象:子类实例进不来(第 13 篇后更痛)。解决:优先 isinstance(x, SomeClass)。
坑 10(Agent):Tool 不用类、只用函数字典,后期要加超时/权限时改不动。 现象:每个工具一套临时约定。解决:最小 Tool 接口(name / run);细节可渐进。
坑 11(GPU 服务器):把巨大模型权重误挂成类属性。 现象:多实例幻想“各有一份”时实际共享或导入期爆显存。解决:重量级资源在 init / 显式 load() 里按实例管理,并清楚生命周期。
坑 12(通用):在 init 里调用尚可被子类重写的方法且依赖“子类已初始化完毕”。 现象:第 13 篇继承后更易踩——父类 init 调 self.reset(),子类 reset 用到子类才赋值的字段。解决:入门期让 init 只做赋值与校验;复杂复位逻辑等对象建完再调。知道风险即可。
# 演示坑 4 的典型报错形态(不要在业务里留这种定义)
class Broken:
def oops():
return 1
try:
Broken().oops()
except TypeError as e:
print(type(e).__name__ + ":", e)
TypeError: Broken.oops() takes 0 positional arguments but 1 was given
八、小结
合上文章后,试着用自己的话解释:“实例属性找不到时会发生什么”以及“g.hello 和 Greeter.hello 差在哪”——讲得清,才算真的会了。
- 类 vs 实例:类是类型对象(蓝图);实例是具体对象,通常有自己的
dict。 - 创建:
new造壳,init填数据;入门期写好init与校验即可。 - 属性:实例属性彼此独立;类属性共享;可变类属性默认坑,可变状态放
init。 - 方法:实例方法绑
self;@classmethod绑cls(工厂);@staticmethod无绑定,也可用模块函数替代。 - property:用属性语法做读写校验与派生值;身份
is/id与相等==先分清。 - 桥接与场景:
class ConfigError(Exception)承接第 11 篇;Config / Tokenizer / Tool 是 AI Infra 与 Agent 的日常造型。
把故事连起来:对象与引用(02)→ 函数与作用域(07/08)→ 模块(09)→ 异常类型(11)→ 本篇用类把数据与行为收成可导入的类型 → 下一篇(13)讲继承、多态与 MRO → 第 14 篇用魔术方法接入 Python 数据模型。再往后,dataclass(18)、nn.Module(40)、最小 Agent(28)都会反复用到今天的刀法。
若只能带走一句话:蓝图在类,状态在实例;查找先实例后类;方法靠绑定传入 self/cls。
也可以用“三个不要”自检:不要把可变默认状态挂成类属性;不要忘 self;不要用 type(...) == 堵死继承。
再留一个自测:画出属性查找顺序;说明为何 Bag.items = [] 会共享;写出 from_dict 的 @classmethod;解释 a is b 与 a == b 在未定制 eq 时的差异——四问都顺,本篇就站稳了。
本篇技能会立刻复利:读开源库时先看类属性里有没有可变对象;写配置时把校验放进 init / property;设计 Agent 工具时先定 name + run,再填实现。先把“类型对象 / 实例 / 绑定”练准,比背三十个设计模式名词更划算。
九、课后练习
- (基础) 定义
class Book:init(self, title, pages),方法summary(self)返回"{title} ({pages}p)"。造两本书并打印。 - (基础) 给
Book增加类属性lang = "zh";改其中一个实例的lang,打印两实例与类的lang,解释发生了什么。 - (基础) 复现可变类属性陷阱:
class Team: members = [],两个实例add不同名字,打印两者的members;再改成安全版对比。 - (理解) 为
ModelConfig写@classmethod from_dict与实例方法to_dict;用字典走一圈往返。 - (理解) 实现
class Ratio:属性value用@property+ setter,仅允许0.0~1.0;越界抛ValueError。 - (理解) 写
class ConfigError(Exception): ...与函数parse_batch(raw: dict) -> int:缺键 / 非正数抛ConfigError,非法 int 用raise ConfigError(...) from e。 - (进阶) 定义
Tool基类(name、description、run),再写UpperTool:run返回大写字符串;放进dict[str, Tool]按名调用。 - (进阶 · AI Infra) 实现
GenConfig(含temperature/max_tokens校验)与FakeTokenizer.encode/decode;再写函数preview(cfg, tok, text)打印cfg.to_dict()与 encode 结果。思考:哪些该是实例属性,有没有误用类属性?
下一篇预告
类让你能描述一种东西;真实项目里常常是“一种东西的变体”——EchoTool / SearchTool 共享接口却不同实现。下一篇《第13篇|面向对象二:继承、多态、MRO》会系统讲子类、方法重写、super()、以及 C3 线性化(MRO)如何决定“找不到属性时去哪继续找”。本篇的属性查找图,到时会延长成一条链。
专栏一「Python 从零入门」的后续路线:面向对象二(13)→ 魔术方法与数据模型(14)。带着本篇的「蓝图 / 实例 / 绑定」直觉,读训练代码时先看状态挂在实例还是类上、工厂是不是 classmethod、Tool 有没有稳定接口,再钻模型细节。
若你想提前热身:把本篇的 Tool / EchoTool 再加一个 LenTool(返回字符串长度),用同一个 dict[str, Tool] 调度;观察两子类如何共用 name 约定。做完后第 13 篇谈多态时你会带着具体场景,而不是抽象 UML。
也可以把第 11 篇的 ConfigError 与本篇的 GenConfig 叠在一起:非法配置抛领域异常,合法配置变成清晰对象——异常定义失败协议,类定义成功路径上的数据形态——专栏一后半的主线就连上了。
若时间充裕,再多做一件事:对任意实例打印 type(obj)、obj.class、obj.dict、type(obj).dict.keys()(可先 list(...)),用笔标注“哪份是实例状态、哪份是类上的方法”。你会同时复习第 02 篇的对象模型与第 09 篇的命名空间——这些都是日后啃 nn.Module 与 Agent 框架的零件。
