Python 函数与面向对象

要点

函数默认值在定义时求值;可变默认参数会被后续调用复用。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 不强制一切皆方法。


练习与实践

练习一:参数设计

  1. 编写 create_user(name: str, /, email: str, *, role: str = "member") -> dict,分别用位置、关键字混合方式调用验证 / 与 * 的限制

练习二:建模购物车

  1. 用 @dataclass 实现 Item(name: str, price: float, qty: int)
  2. 实现 Cart 类:add(item)、total() -> float、魔术方法 __len__
  3. 让 Cart 支持 for item in cart(__getitem__ 或 __iter__)

评估标准:uv run python -c "from cart import Cart; c = Cart(); print(len(c))" 正常输出,且 ruff/mypy 无告警。


相关文档

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇