要点
函数默认值在定义时求值;可变默认参数会被后续调用复用。dataclass 帮你生成方法,不改变这个基本对象语义。
1. 函数:参数系统的四种形态
def send(
to: str, # 位置参数
subject: str, # 可作为关键字传入
*, # * 之后的参数只能用关键字
cc: list[str] | None = None, # 关键字参数带默认值
**headers: str, # ** 收集任意关键字参数
) -> dict[str, object]:
return {"to": to, "subject": subject, "cc": cc, "headers": headers}
msg = send("a@x.com", "hi", cc=["b@x.com"], priority="high")
要点:
/与*:/之前只能按位置传,*之后只能按关键字传,中间两者皆可- 默认值求值时机:函数定义时求值一次,因此默认值绝不能是可变对象
- 类型注解:参数与返回值都标注,
**headers: str表示值为 str 的字典
可变默认参数陷阱(Python 面试第一题):
def add_tag(tag: str, tags: list[str] = []): # ❌ 所有调用共享同一个列表
tags.append(tag)
return tags
def add_tag(tag: str, tags: list[str] | None = None): # ✅ 标准解法
if tags is None:
tags = []
tags.append(tag)
return tags
把“默认值只创建一次”和 default_factory 放在同一个小程序里看会更直观。下例先显示错误函数的共享状态,再断言两次安全调用与两个 Task 实例都各自得到新列表;可保存为 functions_dataclass_demo.py 后执行 python functions_dataclass_demo.py。
from dataclasses import dataclass, field
def unsafe_add(tag: str, tags: list[str] = []) -> list[str]:
tags.append(tag)
return tags
def safe_add(tag: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(tag)
return tags
@dataclass
class Task:
title: str
tags: list[str] = field(default_factory=list)
def main() -> None:
print(f"unsafe={unsafe_add('one')},{unsafe_add('two')}")
first, second = safe_add("one"), safe_add("two")
a, b = Task("A"), Task("B")
a.tags.append("urgent")
assert first == ["one"] and second == ["two"]
assert b.tags == []
print(f"safe={first},{second}; tasks={a.tags},{b.tags}")
if __name__ == "__main__":
main()
💡 上面用到的
if tags is None是条件判断,下一章控制流才系统讲解。这里先记住惯例:可变默认参数一律用None占位,函数体内再判空重建——背下这个固定套路即可,原理下一章补齐。
更深入的错误案例见常见错误排查。
2. 类:定义你自己的类型
class Bookmark:
"""书签实体:演示类的标准写法。"""
total = 0 # 类属性:所有实例共享
def __init__(self, title: str, url: str) -> None:
self.title = title # 实例属性
self.url = url
Bookmark.total += 1
def describe(self) -> str: # 实例方法第一个参数恒为 self
return f"{self.title}"
@classmethod
def from_raw(cls, raw: str) -> "Bookmark":
"""工厂方法:cls 指向当前类。"""
title, url = raw.split("|")
return cls(title, url)
@staticmethod
def is_valid_url(url: str) -> bool:
"""工具函数:不接收 self/cls。"""
return url.startswith(("http://", "https://"))
bm = Bookmark("Python", "https://python.org")
print(bm.describe()) # [Python](https://python.org)
bm2 = Bookmark.from_raw("uv|https://astral.sh")
要点:
self显式写在参数里,是方法而非语法的体现__init__只负责初始化已创建好的对象,__new__才创建对象(极少自定义)- 单下划线前缀
self._cache是”内部使用”约定(无强制私有)
3. dataclass:数据建模的默认选择
纯存数据的类,用 @dataclass 自动生成 __init__/__repr__/__eq__:
from dataclasses import dataclass, field
@dataclass
class Bookmark:
title: str
url: str
tags: list[str] = field(default_factory=list) # 可变默认值的正确姿势
pinned: bool = False
bm = Bookmark("uv", "https://astral.sh")
print(bm) # Bookmark(title='uv', url='https://astral.sh', ...)
print(bm == Bookmark("uv", "https://astral.sh")) # True,按字段比较
常用变体:
| 装饰器 | 作用 |
|---|---|
@dataclass(frozen=True) | 阻止字段重新赋值;只有全部参与比较的字段都可哈希时,实例才可哈希、可作 dict 键 |
@dataclass(order=True) | 生成 <、>= 等比较方法 |
@dataclass(slots=True) | 启用 __slots__,省内存、禁止动态属性 |
字段级控制(field() 的 default_factory/repr/compare/init 参数全表、frozen 与排序细节)见 dataclass 速查。
💡 与 Pydantic 模型的分工:dataclass 管”程序内部数据”,Pydantic 管”边界校验”(API 输入/配置文件),见 FastAPI 速查。
4. 魔术方法:协议驱动的世界
Python 对象行为由双下划线”魔术方法”定义,框架和内置语法都在调用它们:
class Vector:
def __init__(self, x: float, y: float) -> None:
self.x, self.y = x, y
def __repr__(self) -> str: # repr(v) / REPL 显示
return f"Vector({self.x}, {self.y})"
def __add__(self, other: "Vector") -> "Vector": # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other: object) -> bool: # v1 == v2
return isinstance(other, Vector) and (self.x, self.y) == (other.x, other.y)
def __len__(self) -> int: # len(v)
return 2
def __getitem__(self, i: int) -> float: # v[0],顺带获得可迭代性
return (self.x, self.y)[i]
v1 + v2 并非语法糖的偶然:解释器将其翻译为 v1.__add__(v2)。完整协议目录见魔术方法与协议。
入门必会的五个:
| 魔术方法 | 触发场景 |
|---|---|
__init__ | 实例初始化 |
__repr__ | 调试显示(建议总是实现) |
__eq__ | == 比较 |
__len__ / __bool__ | len() / if obj: |
__getitem__ | 下标访问与 for 遍历 |
继承用”单继承 + 组合”:子类在 __init__ 中以 super().__init__(title, url) 复用父类初始化;需要鸭子类型灵活性时改用 typing.Protocol(见 typing 注解全表)。
最佳实践
dataclass 减少数据载体的初始化和比较样板,复杂不变量或不同身份语义可能需要普通类。函数的可变默认参数在定义时创建,dataclass 的可变字段通常用 default_factory 为每个实例生成独立对象。frozen=True 不会把 list、dict 等字段变成可哈希对象:例如本页 tags: list[str] 仍会让默认生成的 hash() 抛出 TypeError;需要作键时应使用 tuple 等不可变、可哈希字段,或按业务语义另行定义哈希。
repr 帮助调试,但不要把秘密字段直接展示;asdict 的递归转换也不保证任意成员都能被 JSON 序列化。创建两个实例只修改其中一个的列表,验证没有共享默认状态。
常见问题
Q1: @staticmethod 和模块级函数怎么选?
A: 与类有概念关联但不用实例/类状态的工具放 @staticmethod;否则直接写模块级函数,Python 不强制一切皆方法。
练习与实践
练习一:参数设计
- 编写
create_user(name: str, /, email: str, *, role: str = "member") -> dict,分别用位置、关键字混合方式调用验证/与*的限制
练习二:建模购物车
- 用
@dataclass实现Item(name: str, price: float, qty: int) - 实现
Cart类:add(item)、total() -> float、魔术方法__len__ - 让
Cart支持for item in cart(__getitem__或__iter__)
评估标准:uv run python -c "from cart import Cart; c = Cart(); print(len(c))" 正常输出,且 ruff/mypy 无告警。