类方法、静态方法与 property

前面 类与对象基础 提到过:Python 类里有三种方法(实例方法、类方法、静态方法),还有一个把方法伪装成属性的 @property。这些不是”高级技巧”,而是日常写业务经常用到的工具。这一篇讲透它们各自的定位、用法、和典型场景。

一、三种方法的对比

1
2
3
4
5
6
7
8
9
10
11
class Foo:
def instance_method(self):
"""实例方法:第一个参数 self(当前实例)。"""

@classmethod
def class_method(cls):
"""类方法:第一个参数 cls(类本身)。"""

@staticmethod
def static_method():
"""静态方法:不接收任何隐式参数。"""

调用:

1
2
3
4
5
6
f = Foo()

f.instance_method() # 实例方法:常规
f.class_method() # 类方法:可以从实例调
Foo.class_method() # 也可以从类调
Foo.static_method() # 静态方法:一般从类调

三者的核心区别:

类型 拿到的第一个参数 用途
实例方法 self(实例) 操作/查询单个实例
类方法 cls(类) 备用构造器、访问/修改类属性
静态方法 逻辑上跟类相关,但不需要实例或类的工具函数

二、classmethod:备用构造器

最常见的用途:给类提供”另一种创建方式”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Date:
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day

@classmethod
def from_string(cls, s):
"""从 'YYYY-MM-DD' 字符串创建。"""
y, m, d = map(int, s.split("-"))
return cls(y, m, d)

@classmethod
def today(cls):
import datetime
t = datetime.date.today()
return cls(t.year, t.month, t.day)


d1 = Date(2025, 9, 19)
d2 = Date.from_string("2025-09-19")
d3 = Date.today()
  • 常规 __init__ 只能接受一种参数形式;
  • classmethod 让类拥有多个构造入口
  • cls(y, m, d) 而不是 Date(y, m, d)——这样子类继承时也能用
1
2
3
4
class BeijingDate(Date):
pass

BeijingDate.from_string("2025-09-19") # 返回 BeijingDate 实例,不是 Date

三、classmethod:操作类状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Widget:
_count = 0

def __init__(self):
Widget._count += 1

@classmethod
def total(cls):
return cls._count

@classmethod
def reset(cls):
cls._count = 0

Widget()
Widget()
Widget()
print(Widget.total()) # 3
Widget.reset()
print(Widget.total()) # 0

四、staticmethod:类的工具函数

什么时候用 staticmethod?——“这个函数逻辑上属于这个类,但不需要访问 self 或 cls。”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Temperature:
def __init__(self, celsius):
self.celsius = celsius

@staticmethod
def c_to_f(c: float) -> float:
return c * 9 / 5 + 32

@staticmethod
def f_to_c(f: float) -> float:
return (f - 32) * 5 / 9


print(Temperature.c_to_f(100)) # 212.0

如果不放在类里,就是模块级函数——那也没错。静态方法只是把它**”归类”到类命名空间里**,让调用方一目了然是干嘛的。

建议:如果一个工具函数只跟某个类相关,用 staticmethod;如果和多个类都相关,就写成模块级函数。

五、@property:把方法伪装成属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Circle:
def __init__(self, r):
self.r = r

@property
def area(self):
return 3.14159 * self.r ** 2

@property
def diameter(self):
return 2 * self.r


c = Circle(3)
print(c.area) # 28.274.. 没有括号!
print(c.diameter) # 6
c.area = 100 # ❌ AttributeError: can't set attribute

好处:

  • 对外像属性,简洁;
  • 对内是方法,可以有逻辑;
  • 将来把字段变计算/反之,调用方代码不用改

六、setter 和 deleter

想让 property “可写”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Celsius:
def __init__(self, value):
self._value = value

@property
def value(self):
return self._value

@value.setter
def value(self, v):
if v < -273.15:
raise ValueError("温度不能低于绝对零度")
self._value = v

@value.deleter
def value(self):
print("重置温度")
self._value = 0


c = Celsius(20)
print(c.value) # 20
c.value = 100
try:
c.value = -500 # ValueError
except ValueError as e:
print(e)
del c.value # 重置温度

语法要点:三个装饰器函数同名,Python 会把它们串成 property 对象。

七、只读属性的常见用法

计算属性

1
2
3
4
5
6
7
8
9
10
11
class Rectangle:
def __init__(self, w, h):
self.w, self.h = w, h

@property
def area(self):
return self.w * self.h

@property
def perimeter(self):
return 2 * (self.w + self.h)

懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Report:
def __init__(self, data):
self._data = data
self._cache = None

@property
def summary(self):
if self._cache is None:
print("计算中...")
self._cache = self._expensive_calculation()
return self._cache

def _expensive_calculation(self):
return sum(self._data)

Python 3.8+ 有更优雅的 functools.cached_property

1
2
3
4
5
6
7
8
9
10
from functools import cached_property

class Report:
def __init__(self, data):
self._data = data

@cached_property
def summary(self):
print("计算中...")
return sum(self._data)

cached_property 第一次访问计算,之后直接返回缓存。写业务里非常好用。

八、property 用于验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class User:
def __init__(self, name, age):
self.name = name
self.age = age # 会走 setter

@property
def age(self):
return self._age

@age.setter
def age(self, value):
if not isinstance(value, int):
raise TypeError("age 必须是 int")
if not 0 <= value <= 150:
raise ValueError("age 不合理")
self._age = value


u = User("咖飞", 3)
u.age = 4 # ok
u.age = -1 # ValueError
u.age = "abc" # TypeError

九、property 的性能

property 每次访问都跑一遍函数,比直接访问属性慢一些(大概 5-10 倍)。对性能极敏感的场景(比如循环里访问几百万次)可以直接暴露字段。日常业务代码性能损耗可以忽略。

十、classmethod + property(Python 3.9-3.11)

Python 3.9 到 3.11 一度支持 @classmethod @property 叠加:

1
2
3
4
5
6
7
class Config:
_default = "prod"

@classmethod
@property
def env(cls):
return cls._default

但 Python 3.12 把这个组合废弃了(因为语义混乱)。以后请写成常规 classmethod(Config.env())或者用 metaclass。

十一、”数据类”的现代做法

如果你的类主要是”装数据+少量逻辑”,用 @dataclass 更简洁,前面 类与对象基础 讲过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from dataclasses import dataclass

@dataclass
class Book:
title: str
price: float
stock: int = 0

@property
def is_available(self):
return self.stock > 0

def sell(self, qty=1):
if qty > self.stock:
raise ValueError("库存不足")
self.stock -= qty

dataclass 帮你写好 __init____repr____eq__,你只专注业务逻辑。

十二、什么时候用什么

场景 选择
操作/查询单个实例 实例方法
备用构造器(多种入参形式) classmethod
修改/查询类级别的状态 classmethod
工具函数,逻辑上归属这个类,不需要 self / cls staticmethod
“属性但需要计算” @property
“属性且需要校验” property + setter
“计算一次就缓存” cached_property

十三、一个完整例子:员工类

综合展示四种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# employee.py
"""员工类,展示实例方法、classmethod、staticmethod、property 的组合。"""

from dataclasses import dataclass, field
from datetime import date
from functools import cached_property


class Employee:
_next_id = 1
company = "咖飞科技"

def __init__(self, name: str, hire_date: date, salary: float):
self.id = Employee._next_id
Employee._next_id += 1
self.name = name
self.hire_date = hire_date
self._salary = salary

# 实例方法
def raise_salary(self, percentage: float):
self._salary *= (1 + percentage / 100)

# 只读属性
@property
def salary(self):
return self._salary

# 计算属性
@property
def years_of_service(self):
return (date.today() - self.hire_date).days // 365

# 缓存属性
@cached_property
def title(self):
y = self.years_of_service
if y >= 5:
return "高级工程师"
if y >= 2:
return "工程师"
return "初级工程师"

# 备用构造器
@classmethod
def from_dict(cls, d: dict):
return cls(
name=d["name"],
hire_date=date.fromisoformat(d["hire_date"]),
salary=d["salary"],
)

# 类方法:改类属性
@classmethod
def set_company(cls, name: str):
cls.company = name

# 静态方法:工具
@staticmethod
def is_working_day(d: date) -> bool:
return d.weekday() < 5

def __repr__(self):
return f"Employee(id={self.id}, name={self.name!r}, title={self.title})"


if __name__ == "__main__":
e = Employee("咖飞", date(2020, 1, 1), 10000)
print(e)
print(f"工龄 {e.years_of_service} 年")
print(f"薪资 {e.salary}")

e2 = Employee.from_dict({
"name": "小王",
"hire_date": "2024-06-01",
"salary": 8000,
})
print(e2)

Employee.set_company("咖飞集团")
print(f"当前公司 {Employee.company}")

print(f"今天上班?{Employee.is_working_day(date.today())}")

十四、常见陷阱

  1. self.x@property x 冲突__init__self.x = ... 会调 setter,或者跟 property 名字冲突时报 AttributeError。
  2. 忘了写 setterc.value = 100 报 AttributeError。
  3. property 递归@x.setterself.x = value 会死循环。要 self._x = value
  4. classmethod 与 staticmethod 用反:需要访问 cls 就用 classmethod。
  5. cached_property 与继承:需要 __slots__ 时不能用 cached_property。
  6. @staticmethod 在类里省略:Python 允许普通函数放类里当”未绑定函数”,但不推荐——加 @staticmethod 意图更清晰。

十五、小结与延伸阅读

  • 三种方法:instance / class / static,分别拿到 self / cls / 无;
  • classmethod 常用于备用构造器和类级操作;
  • staticmethod 是”归属这个类的工具函数”;
  • @property 把方法伪装成属性,方便验证/计算/缓存;
  • setter 用 @x.setter
  • 只读属性 + 惰性求值用 functools.cached_property
  • 数据类首选 @dataclass

延伸阅读:

下一篇 抽象类与接口 我们讲怎么”强制”子类实现某些方法。

特殊方法(魔术方法)详解

Python 里名字前后都是双下划线的方法(__init____repr____len__……)叫特殊方法,也叫魔术方法(magic methods)dunder methods(double underscore 的缩写)。它们是让你自定义的类行为像内置类型的秘诀:让 len(obj)obj[k]for x in objobj1 + obj2with obj:str(obj) 都能正常工作。这一篇把最常用的 20 多个魔术方法一次讲清。

一、初始化与析构

1
2
3
4
5
6
class Foo:
def __init__(self, x): # 初始化
self.x = x

def __del__(self): # 垃圾回收时调用(一般不用)
print(f"删除 {self}")
  • __init__:实例创建后调用;
  • __new__:创建实例本身(很少重写);
  • __del__:不保证被调用,别用来关闭资源。

二、字符串表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point:
def __init__(self, x, y):
self.x, self.y = x, y

def __repr__(self):
return f"Point({self.x}, {self.y})"

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) # (3, 4) print 走 __str__

# 在容器里
print([p, p]) # [Point(3, 4), Point(3, 4)] 容器打印走 __repr__
  • __repr__必写,给开发者看,最好是”能复制回来重建对象”的字符串;
  • __str__:可选,给用户看,不写就 fallback 到 __repr__

三、比较运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

def __le__(self, other):
return (self.major, self.minor) <= (other.major, other.minor)

# __gt__ / __ge__ / __ne__ 可以补齐或让 Python 反推


v1 = Version(1, 5)
v2 = Version(2, 0)
print(v1 < v2) # True
print(v1 == v2) # False

懒办法@functools.total_ordering 自动生成所有比较:

1
2
3
4
5
6
7
8
9
10
11
12
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)

只写 __eq__ 和一个顺序方法,其余自动生成。

四、哈希与不可变

1
2
3
4
5
6
7
8
9
class Point:
def __init__(self, x, y):
self.x, self.y = x, y

def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)

def __hash__(self):
return hash((self.x, self.y))

规则:

  • 一旦重写 __eq__,Python 会自动把 __hash__ 设为 None(不可哈希);
  • 想让实例能进 set / 做 dict key,就要显式定义 __hash__
  • __hash__ 必须与 __eq__ 一致:a == bhash(a) == hash(b)
  • 哈希基于不可变字段——可变字段变了,哈希失效。

五、算术运算

让实例支持 + - * / // 等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y

def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)

def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)

def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

def __repr__(self):
return f"Vector({self.x}, {self.y})"


v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
print(v1 * 3) # Vector(3, 6)

对应方法:

  • __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__
  • __iadd__, __isub__(就地版 +=
  • __radd__(当 x + self 时 x 不知道怎么处理,就调 self.__radd__(x)

六、容器协议

让实例像容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Deck:
def __init__(self):
self.cards = list(range(1, 53))

def __len__(self):
return len(self.cards)

def __getitem__(self, i):
return self.cards[i]

def __setitem__(self, i, v):
self.cards[i] = v

def __delitem__(self, i):
del self.cards[i]

def __contains__(self, x):
return x in self.cards


d = Deck()
print(len(d)) # 52
print(d[0]) # 1
print(3 in d) # True

只要有 __len____getitem__,Python 就把你的类当成序列——for x in objobj[i:j] 都能用。

七、迭代协议

想让实例可 for x in obj

1
2
3
4
5
6
7
8
9
10
11
12
13
class Countdown:
def __init__(self, start):
self.start = start

def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1

for x in Countdown(3):
print(x)
# 3 2 1

__iter__ 返回一个迭代器(或用 yield 让函数变生成器)。详细见 迭代器与可迭代对象

八、布尔与真值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Cart:
def __init__(self):
self.items = []

def __bool__(self):
return bool(self.items)

def __len__(self):
return len(self.items)


c = Cart()
if not c: # 空购物车为假
print("购物车空")

规则:Python 判真值时先看 __bool__,没有再看 __len__(返回 0 视为假),都没有默认为真。

九、上下文管理器 with

1
2
3
4
5
6
7
8
9
10
11
12
class Timer:
def __enter__(self):
import time
self.start = time.time()
return self

def __exit__(self, exc_type, exc_val, tb):
import time
print(f"耗时 {time.time() - self.start:.3f}s")

with Timer() as t:
sum(i*i for i in range(1_000_000))
  • __enter__ 返回值绑给 as 后的名字;
  • __exit__ 无论有没有异常都会被调用;
  • 返回 True 会吞掉异常

详细见 with 语句与上下文管理器

十、可调用对象 __call__

让实例像函数一样被调用:

1
2
3
4
5
6
7
8
9
10
class Multiplier:
def __init__(self, factor):
self.factor = factor

def __call__(self, x):
return x * self.factor


double = Multiplier(2)
print(double(5)) # 10

用来做”带状态的函数”——比闭包更结构化。装饰器有时也基于 __call__ 写成类。

十一、属性访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Foo:
def __getattr__(self, name):
"""访问不存在的属性时调用。"""
return f"没有 {name}"

def __setattr__(self, name, value):
"""设置任意属性都会经过这里。"""
print(f"设置 {name}={value}")
super().__setattr__(name, value)

def __delattr__(self, name):
pass


f = Foo()
f.x = 10 # 设置 x=10
print(f.x) # 10
print(f.abc) # 没有 abc

__getattr__ 只在找不到属性时才被调用(不是每次访问都触发)。想每次都拦截用 __getattribute__(很少用)。

十二、上下文长度与切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyList:
def __init__(self, data):
self.data = data

def __getitem__(self, key):
if isinstance(key, slice):
return MyList(self.data[key])
return self.data[key]

def __repr__(self):
return f"MyList({self.data})"


m = MyList([1, 2, 3, 4, 5])
print(m[1:3]) # MyList([2, 3])

判断 keyslice 还是 int,可以支持切片。

十三、比较相等的关键规则

a == b 的完整过程:

  1. a.__eq__(b)
  2. 若返回 NotImplemented,调 b.__eq__(a)
  3. 若都返回 NotImplemented,用身份判断(a is b)。

写自定义比较时对”不同类型”返回 NotImplemented 而不是 False:

1
2
3
4
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)

十四、__slots__(回顾)

前面 属性、方法与 self 讲过:

1
2
class Point:
__slots__ = ("x", "y")

省内存、防拼错。

十五、其他常见魔术方法

方法 触发
__format__ format(x) / f-string
__bytes__ bytes(x)
__index__ 整数化(切片 index 等)
__reversed__ reversed(x)
__abs__ abs(x)
__int__ __float__ int(x) / float(x)
__copy__ __deepcopy__ copy 模块调用
__enter__ __exit__ with 语句
__aenter__ __aexit__ async with
__aiter__ __anext__ async for

十六、一个完整例子:分数类

综合展示多种魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# fraction.py
"""分数类,展示常用魔术方法。"""

from math import gcd
from functools import total_ordering


@total_ordering
class Fraction:
def __init__(self, numer: int, denom: int = 1):
if denom == 0:
raise ZeroDivisionError("分母不能为 0")
# 约分
g = gcd(abs(numer), abs(denom))
sign = -1 if (numer < 0) ^ (denom < 0) else 1
self.numer = sign * abs(numer) // g
self.denom = abs(denom) // g

def __repr__(self):
return f"Fraction({self.numer}, {self.denom})"

def __str__(self):
return f"{self.numer}/{self.denom}" if self.denom != 1 else str(self.numer)

def __eq__(self, other):
if not isinstance(other, Fraction):
return NotImplemented
return self.numer * other.denom == other.numer * self.denom

def __lt__(self, other):
return self.numer * other.denom < other.numer * self.denom

def __hash__(self):
return hash((self.numer, self.denom))

def __add__(self, other):
return Fraction(
self.numer * other.denom + other.numer * self.denom,
self.denom * other.denom
)

def __sub__(self, other):
return Fraction(
self.numer * other.denom - other.numer * self.denom,
self.denom * other.denom
)

def __mul__(self, other):
return Fraction(self.numer * other.numer, self.denom * other.denom)

def __truediv__(self, other):
return Fraction(self.numer * other.denom, self.denom * other.numer)

def __neg__(self):
return Fraction(-self.numer, self.denom)

def __abs__(self):
return Fraction(abs(self.numer), self.denom)

def __float__(self):
return self.numer / self.denom

def __bool__(self):
return self.numer != 0


if __name__ == "__main__":
a = Fraction(1, 2)
b = Fraction(1, 3)
print(f"{a} + {b} = {a + b}") # 1/2 + 1/3 = 5/6
print(f"{a} - {b} = {a - b}") # 1/6
print(f"{a} * {b} = {a * b}") # 1/6
print(f"{a} / {b} = {a / b}") # 3/2
print(-a, abs(-a))
print(a > b) # True
print(float(a)) # 0.5
print(bool(Fraction(0))) # False

十七、常见陷阱

  1. 重写 __eq____hash__:实例不能进 set / dict。
  2. 哈希用可变字段:改变字段后 set 里再也找不到。
  3. __init__ 里 return 值:会 TypeError。
  4. __enter__ 忘 return selfwith X() as x: 里 x 是 None。
  5. __getattr____getattribute__ 混用:前者只在找不到时触发,后者每次访问都触发,容易死循环。
  6. 算术方法返回类型不一致Vector + Vector 应该返回 Vector,别返回 tuple。

十八、小结与延伸阅读

  • 魔术方法让类”表现得像内置类型”;
  • 至少写 __init____repr__
  • 想比较写 __eq__ + total_ordering
  • 想可哈希写 __hash__
  • 想运算符重载写 __add__ 等;
  • 想像容器写 __len__ / __getitem__
  • 想可迭代写 __iter__
  • with__enter__ / __exit__
  • 想可调用写 __call__

延伸阅读:

下一篇 类方法、静态方法与 property 我们讲三种方法风格的区别与用法。

继承与多态

继承是面向对象的三大支柱之一(封装、继承、多态)。Python 的继承机制既灵活又危险——它支持”多重继承”,能让你写出比 Java 更简洁的代码,但也可能让你陷入”钻石继承”的迷宫。这一篇讲清楚:怎么继承、super() 是什么、MRO 方法解析顺序、多态的本质、以及什么时候不该用继承(组合优于继承)。

一、继承的基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal:
def __init__(self, name):
self.name = name

def eat(self):
print(f"{self.name} 在吃")


class Dog(Animal):
def bark(self):
print(f"{self.name} 在叫")


d = Dog("旺财")
d.eat() # 旺财 在吃 继承来的
d.bark() # 旺财 在叫 自己的
  • class Dog(Animal)Animal 是父类(基类),Dog 是子类;
  • 子类继承父类的所有属性和方法;
  • 子类可以新增自己的方法;
  • 子类可以重写父类的方法。

二、super():调用父类方法

想在子类里”扩展”(而不是完全替换)父类的方法:

1
2
3
4
5
6
7
8
9
10
11
12
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) # 旺财 拉布拉多

super() 是 Python 里几乎所有子类都要用的东西。特别是 __init__ 里,忘了调用父类通常会漏掉某些初始化。

三、方法重写(override)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Animal:
def speak(self):
print("动物在叫")

class Dog(Animal):
def speak(self):
print("汪汪")

class Cat(Animal):
def speak(self):
print("喵喵")

Dog().speak() # 汪汪
Cat().speak() # 喵喵

这就是多态——同一个方法名 speak(),不同的类有不同的实现。

四、多态:以行为为中心

多态的本质:调用方不用关心具体类型,只要那个对象有对应方法就行

1
2
3
4
5
def make_sound(animal):
animal.speak()

make_sound(Dog())
make_sound(Cat())

make_sound 不检查 isinstance(animal, Animal)——只要能 .speak() 就行。这个思想在 Python 里叫 duck typing(鸭子类型)——“看起来像鸭子,叫起来像鸭子,那就是鸭子”。

1
2
3
4
5
class Robot:
def speak(self):
print("bzzt")

make_sound(Robot()) # 也能跑!Robot 根本没继承 Animal

跟 Java 严格的接口不同,Python 是”行为驱动”的。

五、isinstanceissubclass

判断继承关系:

1
2
3
isinstance(d, Dog)       # True
isinstance(d, Animal) # True,子类也算父类
issubclass(Dog, Animal) # True

推荐:接口检查用 isinstance。不用 type(x) == Dog,那样会漏掉子类。

六、多重继承

Python 支持一个类继承多个父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Swimmer:
def swim(self):
print("在游泳")

class Runner:
def run(self):
print("在跑步")

class Triathlete(Swimmer, Runner):
pass

t = Triathlete()
t.swim()
t.run()

优点:可以”组合”多种能力;
缺点:容易陷入”钻石继承”问题。

钻石继承与 MRO

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
def hi(self): print("A")

class B(A):
def hi(self): print("B")

class C(A):
def hi(self): print("C")

class D(B, C):
pass

D().hi() # B 还是 C?

Python 用 MRO(Method Resolution Order) 决定:

1
2
print(D.__mro__)
# (D, B, C, A, object)

Python 用 C3 线性化算法计算 MRO,规则:

  • 子类在父类前;
  • 多个父类按声明顺序;
  • 保证每个类只出现一次。

D().hi() 会输出 B,因为 MRO 里 B 在 C 之前。

建议:写代码时用少而清晰的继承(一层继承就够 90% 场景)。真需要多重继承,画个 MRO 图确认一下。

七、super() 的正确用法

在多重继承里,super() 不是”调用父类”——它是”沿着 MRO 找下一个“。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A:
def m(self):
print("A")

class B(A):
def m(self):
print("B")
super().m()

class C(A):
def m(self):
print("C")
super().m()

class D(B, C):
def m(self):
print("D")
super().m()

D().m()
# D
# B
# C
# A
  • D 调 super() → 找 MRO 下一个 → B;
  • B 调 super() → C(不是 A!因为 MRO 是 D→B→C→A→object);
  • C 调 super() → A。

这个特性叫”协作式多重继承”,能让每个类都被访问一次。要用好,每个类都要写 super().method(...)

八、组合优于继承

继承的问题

  • 强耦合父类接口;
  • 深层继承树难理解;
  • “is-a” 关系有时不对(Rectangle vs Square 是经典反例)。

替代方案组合——把功能作为属性持有:

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ 继承强耦合
class Duck(Bird, Swimmer, Quacker):
...

# ✅ 组合更清晰
class Duck:
def __init__(self):
self.beak = Beak()
self.wings = Wings()
self.legs = Legs()

def fly(self):
self.wings.flap()

经验法则:只有 A 真的 B(is-a 关系)时才继承。A 只是 B(has-a)时用组合。

九、抽象方法(预告)

想强制子类实现某个方法,用 abc 模块:

1
2
3
4
5
6
7
8
9
10
11
12
from abc import ABC, abstractmethod

class Shape(ABC):
@abstractmethod
def area(self):
...

class Circle(Shape):
def area(self):
return 3.14 * self.r ** 2

Shape() # ❌ 不能实例化抽象类

完整讲解见 抽象类与接口

十、Python 里所有类都继承 object

即使不写 (object),Python 3 里所有类默认继承 object

1
2
3
4
5
class Foo:
pass

Foo.__mro__ # (Foo, object)
isinstance(Foo(), object) # True

object 提供了 __init____repr____hash__ 等默认行为。

十一、一个完整例子:图形类层次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# shapes.py
"""图形类的继承体系。"""

from abc import ABC, abstractmethod
from math import pi


class Shape(ABC):
"""所有图形的抽象基类。"""

@abstractmethod
def area(self) -> float:
...

@abstractmethod
def perimeter(self) -> float:
...

def describe(self) -> None:
print(f"{self.__class__.__name__}: 面积 {self.area():.2f}, "
f"周长 {self.perimeter():.2f}")


class Circle(Shape):
def __init__(self, r: float):
self.r = r

def area(self):
return pi * self.r ** 2

def perimeter(self):
return 2 * pi * self.r


class Rectangle(Shape):
def __init__(self, w: float, h: float):
self.w, self.h = w, h

def area(self):
return self.w * self.h

def perimeter(self):
return 2 * (self.w + self.h)


class Square(Rectangle):
def __init__(self, side: float):
super().__init__(side, side)


shapes = [Circle(3), Rectangle(4, 5), Square(6)]
for s in shapes:
s.describe()

十二、检查方法是否被重写

1
2
3
4
5
6
7
8
9
10
11
class A:
def hi(self): print("A")

class B(A):
pass

class C(A):
def hi(self): print("C")

B.hi is A.hi # True,B 没重写
C.hi is A.hi # False,C 重写了

十三、Mixin 模式

Mixin 是”提供小片功能的类”,通常不单独实例化,只用来被组合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class JSONMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)

class LoggableMixin:
def log(self, msg):
print(f"[{self.__class__.__name__}] {msg}")

class User(JSONMixin, LoggableMixin):
def __init__(self, name):
self.name = name

u = User("咖飞")
u.log("hello")
print(u.to_json())

Django、SQLAlchemy 等框架大量使用 mixin。

十四、常见陷阱

  1. 忘了 super().__init__():父类初始化没跑,属性缺失。
  2. 不理解 MRO 就用多重继承:某个方法调用了”意想不到”的父类版本。
  3. 过度继承:3+ 层的继承树几乎必然难维护。
  4. type(x) == Foo 遗漏子类:用 isinstance
  5. 重写父类方法签名不兼容:违反 LSP(里氏替换原则)。
  6. 在 Mixin 里访问父类未有的属性self.name 可能不存在。

十五、小结与延伸阅读

  • 继承用 class Sub(Base):
  • super().method(...) 调父类;
  • 多态 = 不同类实现同名方法,调用方无感;
  • Python 是鸭子类型,行为比类型更重要;
  • 多重继承用 MRO 决定查找顺序;
  • 组合优于继承;
  • Mixin 是提供片段功能的类;
  • 想强制子类实现方法用 abc。

延伸阅读:

下一篇 特殊方法(魔术方法)详解 我们讲让类”表现得像内置类型”的双下划线方法。

属性、方法与 self

上一篇 类与对象基础 里讲了类的骨架。这一篇要深入类里的”血肉”:属性怎么组织、方法怎么调用、self 背后到底发生了什么。你会看到 Python 的 OOP 比 Java 灵活得多:属性可以动态添加、方法可以借给别的对象、self 只是位置参数……理解这些能让你写出更 Pythonic 的类。

一、属性的三种来源

一个对象的属性可能来自三处:

  1. 实例属性self.x = ...)——每个实例独立;
  2. 类属性(在类体里赋值)——所有实例共享;
  3. 继承的属性——从父类来的。

访问 obj.x 时,Python 按 实例 → 类 → 父类 → 父父类 … 的顺序查找(MRO 顺序,下一篇讲)。

二、动态添加属性

Python 允许你在任何时候给一个实例加属性:

1
2
3
4
5
6
7
8
class Foo:
pass

f = Foo()
f.x = 10 # 动态加实例属性
f.greeting = "hi"

print(f.x, f.greeting)

灵活是灵活,但不建议在生产代码里滥用——IDE 无法补全,读代码的人一头雾水。规范做法是在 __init__ 里把所有实例属性声明清楚。

三、如何限制属性 —— __slots__

如果一个类会创建大量实例,或你希望”禁止使用者随便加属性”,用 __slots__

1
2
3
4
5
6
7
8
class Point:
__slots__ = ("x", "y")

def __init__(self, x, y):
self.x, self.y = x, y

p = Point(3, 4)
p.z = 5 # ❌ AttributeError

好处:

  • 每个实例内存大幅减少(不再需要 __dict__);
  • 属性名拼错立刻报错;
  • 属性访问略快。

代价:

  • 不能动态加属性;
  • 子类要自己声明 __slots__,否则会退回普通模式。

大规模建模(几百万实例)时非常有价值。日常小类不必强求。

四、__dict__:实例属性的字典

普通类的实例其实是个字典 + 一些别的:

1
2
3
4
5
6
7
8
9
class User:
def __init__(self, name, age):
self.name = name
self.age = age

u = User("咖飞", 3)
print(u.__dict__) # {'name': '咖飞', 'age': 3}
u.__dict__["city"] = "北京"
print(u.city) # 北京

这就是为什么 Python 属性这么”动态”——它们真的存在字典里。

五、hasattr / getattr / setattr / delattr

前面 常用内置函数汇总 提过:

1
2
3
4
5
6
7
8
u = User("咖飞", 3)

hasattr(u, "name") # True
getattr(u, "name") # '咖飞'
getattr(u, "gender", "未知") # '未知' 带默认值

setattr(u, "gender", "M") # 等价 u.gender = "M"
delattr(u, "age") # 等价 del u.age

反射用法适合写”框架/序列化/迁移工具”,日常业务不常用。

六、方法的绑定

看这段代码:

1
2
3
4
5
6
7
8
9
class Dog:
def bark(self):
print(f"{self.name} 在叫")

d = Dog()
d.name = "旺财"

print(d.bark) # <bound method Dog.bark of <Dog object ...>>
print(Dog.bark) # <function Dog.bark at 0x...>
  • Dog.bark普通函数
  • d.bark绑定方法——Python 自动把 d 绑定为 self

绑定发生在属性查找那一刻,用的是描述符协议(进阶话题,可以先不深究)。

七、方法可以互相调用

1
2
3
4
5
6
7
8
9
10
11
12
class Circle:
def __init__(self, r):
self.r = r

def diameter(self):
return 2 * self.r

def area(self):
return 3.14159 * self.r ** 2

def summary(self):
return f"半径 {self.r}, 直径 {self.diameter()}, 面积 {self.area():.2f}"

一个方法调用另一个用 self.method()——因为 self 就是当前实例,self.method 就是绑定方法。

八、property:把方法伪装成属性

有时候某个”属性”其实是计算出来的,你想让使用者像访问属性一样访问它,用 @property

1
2
3
4
5
6
7
8
9
10
11
class Circle:
def __init__(self, r):
self.r = r

@property
def area(self):
return 3.14159 * self.r ** 2

c = Circle(3)
print(c.area) # 28.27...,不加括号!
c.area = 100 # AttributeError: can't set

好处:

  • 使用者不用关心是”字段”还是”计算”;
  • 将来把字段改成计算属性时,调用方代码不用改;
  • 可以配合 setter/deleter 加验证。

完整的 property 讲解见 类方法、静态方法与 property

九、动态改方法

Python 里方法也是属性,一样可以动态改:

1
2
3
4
5
6
7
8
9
10
class Foo:
def bark(self):
print("汪")

def meow(self):
print("喵")

Foo.bark = meow # 整个类都改了

Foo().bark() # 喵

或者只改一个实例:

1
2
3
4
import types

f = Foo()
f.bark = types.MethodType(meow, f) # 只 f 变了

工程实践里几乎不用,但你知道能这么玩就好。

十、self 只是约定

前面说过——Python 里 self 不是关键字,你写别的也行:

1
2
3
4
5
6
7
8
9
class Foo:
def bar(this): # 用 this
print(this)

def baz(banana): # 用 banana
print(banana)

Foo().bar()
Foo().baz()

但请永远写 self。整个 Python 社区都这么写,标新立异只会让代码难读。

十一、方法的三种类型

Python 类里有三种方法:

  • 实例方法:第一个参数是 self,最常见;
  • 类方法@classmethod,第一个参数是 cls(类本身);
  • 静态方法@staticmethod,不接受隐式参数,本质就是”放在类里的普通函数”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Foo:
def instance_method(self):
print(f"实例方法,self={self}")

@classmethod
def class_method(cls):
print(f"类方法,cls={cls}")

@staticmethod
def static_method():
print("静态方法")


f = Foo()
f.instance_method() # 实例方法,self=...
Foo.class_method() # 类方法,cls=Foo
Foo.static_method() # 静态方法

具体用法见 类方法、静态方法与 property

十二、类属性 vs 实例属性的读写规则

1
2
3
4
5
6
7
8
9
10
class Foo:
count = 0 # 类属性

def __init__(self):
Foo.count += 1 # 修改类属性要用类名

def bump_wrong(self):
self.count += 1 # 等价 self.count = self.count + 1
# 读取时找到类属性 0,写入时创建实例属性 1
# 之后 self.count 都是实例属性,类属性没变

规则

  • 读属性:先实例,再类;
  • 写属性:永远写实例属性(除非用 ClassName.attr = ...)。

用类属性做”实例计数”、”共享配置”时要用类名写:

1
2
3
4
5
6
7
8
class Widget:
total = 0
def __init__(self):
Widget.total += 1

Widget()
Widget()
print(Widget.total) # 2

十三、封装的实际做法

Java 里”getter/setter”是标配。Python 完全不必

1
2
3
4
5
6
7
8
9
10
11
12
13
# ❌ Java 风格
class User:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, name):
self._name = name

# ✅ Python 风格
class User:
def __init__(self, name):
self.name = name

Python 提倡”直接访问属性”。如果将来需要加验证或计算,再改成 @property——调用方代码不用改一个字。

十四、__slots__ 与继承

__slots__ 需要从上到下都声明才有效:

1
2
3
4
5
class Parent:
__slots__ = ("a",)

class Child(Parent):
__slots__ = ("b",) # 只声明新的,不重复父类的

如果任何一层没写 __slots__,就会退回普通 __dict__ 模式。

十五、一个完整例子:银行账户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# bank.py
"""银行账户,展示属性、方法、property、类属性。"""


class BankAccount:
# 类属性
bank_name = "咖飞银行"
_next_id = 1000

def __init__(self, owner: str, balance: float = 0):
self.id = BankAccount._next_id
BankAccount._next_id += 1
self.owner = owner
self._balance = balance # 私有约定,让外部走 property

@property
def balance(self) -> float:
return self._balance

@balance.setter
def balance(self, value: float) -> None:
if value < 0:
raise ValueError("余额不能为负")
self._balance = value

def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("金额必须 > 0")
self._balance += amount

def withdraw(self, amount: float) -> None:
if amount > self._balance:
raise ValueError("余额不足")
self._balance -= amount

def transfer(self, other: "BankAccount", amount: float) -> None:
self.withdraw(amount)
other.deposit(amount)

def __repr__(self):
return f"BankAccount(id={self.id}, owner={self.owner!r}, balance=¥{self._balance})"


if __name__ == "__main__":
a = BankAccount("咖飞", 1000)
b = BankAccount("小王", 500)

a.deposit(500)
a.transfer(b, 300)
print(a)
print(b)
print(f"银行:{BankAccount.bank_name}")

十六、常见陷阱

  1. 忘了 selfdef method(x) 会导致调用时报错。
  2. 给类属性 += 变成加实例属性:想改类属性用 ClassName.attr = ...
  3. __slots__ 与 dataclass 组合:3.10+ 用 @dataclass(slots=True)
  4. 过度封装:写一堆 getter/setter 不 Pythonic,直接暴露属性即可。
  5. 动态属性拼写错误f.nmae = "x" 不报错,但读的时候没有。用 __slots__ 或 dataclass 可以避免。
  6. 修改方法查找规则:一般别改,除非你在写元编程。

十七、小结与延伸阅读

  • 实例属性在 __init__ 里初始化,类属性写在类体;
  • self.x 读时查实例→类,写时只创建实例属性;
  • hasattr/getattr/setattr 是反射三件套;
  • __slots__ 减内存、防止乱加属性;
  • 方法可以互相调用,self.method() 就是绑定方法;
  • Python 不写 getter/setter,需要计算/验证时用 @property
  • self 只是约定,永远写 self。

延伸阅读:

下一篇 继承与多态 我们讲 OOP 的核心特性之一——从父类继承。

类与对象基础

到目前为止我们写的都是”函数式 + 过程式”的代码。真实项目里更常用面向对象编程(OOP)——用”类”把数据和行为打包在一起。Python 的 OOP 有一点特别:语法非常轻量,但概念完整,比 Java/C++ 更贴近”写脚本”的直觉。这一篇讲最基础的 class:类的定义、实例化、属性、方法、__init__self,以及一个新时代的强力工具 @dataclass

一、为什么需要类

看下面这段”过程式”代码:

1
2
3
4
5
6
7
8
9
10
users = []

def add_user(users, name, age):
users.append({"name": name, "age": age})

def get_name(user):
return user["name"]

def celebrate_birthday(user):
user["age"] += 1

问题:

  • user 只是普通字典,字段拼错不会报错;
  • 函数散落各处,跟数据没有关联;
  • 加一个字段(如 email)要修改多处;
  • 逻辑跟数据没有归属感。

把这些解决了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User:
def __init__(self, name, age):
self.name = name
self.age = age

def celebrate_birthday(self):
self.age += 1

def __repr__(self):
return f"User({self.name!r}, {self.age})"


u = User("咖飞", 3)
u.celebrate_birthday()
print(u) # User('咖飞', 4)
  • 数据(nameage)与行为(celebrate_birthday)绑在一起;
  • IDE 能自动补全 u. 后的方法;
  • 拼错字段立刻报错(u.nmae → AttributeError)。

二、定义一个类

1
2
3
4
5
6
7
8
9
10
11
12
class ClassName:
# 类属性(所有实例共享)
class_var = "shared"

# 构造方法:创建实例时被调用
def __init__(self, x, y):
self.x = x # 实例属性
self.y = y

# 实例方法
def method(self):
print(self.x, self.y)

命名规范:

  • 类名用 PascalCaseUserOrderServiceHTTPClient
  • 方法和属性用 snake_case。

三、self 到底是什么

Python 里方法的第一个参数惯例叫 self——它就是”当前实例“本身:

1
2
3
4
5
6
7
8
class Dog:
def bark(self):
print(f"{self.name} 在叫")

d = Dog()
d.name = "旺财"
d.bark() # 旺财 在叫
# 等价于 Dog.bark(d)

self 不是 Python 关键字,只是约定俗成。你写 this 也能跑,但没人会那么写。

四、__init__ 构造方法

__init__ 在实例被创建后立即调用,用来做初始化:

1
2
3
4
5
6
7
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

p = Point(3, 4)
print(p.x, p.y) # 3 4

没有显式定义 __init__ 时会用 object 的默认版本(不接收参数):

1
2
3
4
5
class Foo:
pass

Foo() # 可以
Foo(1) # TypeError

五、实例属性 vs 类属性

1
2
3
4
5
6
7
8
9
10
11
class Dog:
kind = "canine" # 类属性,所有实例共享

def __init__(self, name):
self.name = name # 实例属性

d1 = Dog("旺财")
d2 = Dog("小白")

print(d1.kind, d2.kind) # canine canine
print(d1.name, d2.name) # 旺财 小白

修改:

1
2
3
4
5
6
7
d1.name = "阿黄"           # 只改 d1

d1.kind = "labrador" # 给 d1 创建实例属性 kind(不影响 d2)
print(d2.kind) # canine

Dog.kind = "犬" # 通过类改,所有实例都变(除了 d1,因为它有实例属性覆盖)
print(d1.kind, d2.kind) # labrador 犬

规则:读取时先找实例,再找类;写入时永远是给实例。理解这一点能避免大部分”值为什么这样”的困惑。

⚠️ 陷阱:可变类属性

1
2
3
4
5
6
7
class Team:
members = [] # ❌ 危险

t1 = Team()
t2 = Team()
t1.members.append("咖飞")
print(t2.members) # ['咖飞'] !! 共享了

修复:在 __init__ 里初始化:

1
2
3
class Team:
def __init__(self):
self.members = [] # ✅ 每个实例独立

六、实例方法调用的两种写法

1
2
3
d = Dog("旺财")
d.bark() # 隐式,Python 把 d 作为 self 传入
Dog.bark(d) # 显式,等价

前者是标准写法,后者只在某些内省/元编程场景用。

七、__repr____str__

打印实例默认是 <__main__.Foo object at 0x...>,不友好。定义 __repr__ 让它好读:

1
2
3
4
5
6
7
8
class Point:
def __init__(self, x, y):
self.x, self.y = x, y

def __repr__(self):
return f"Point({self.x}, {self.y})"

print(Point(3, 4)) # Point(3, 4)
  • __repr__:给”程序员”看的,最好能”复制回来能重建对象”;
  • __str__:给”用户”看的,print(x) 走这个,没定义就 fallback 到 __repr__

至少定义 __repr__。日志、调试都会自动用到它。

八、@dataclass:写类的现代方式

Python 3.7 起,dataclasses 模块让你用一行装饰器自动生成 __init____repr____eq__

1
2
3
4
5
6
7
8
9
10
11
from dataclasses import dataclass

@dataclass
class User:
name: str
age: int
email: str = "" # 默认值

u = User("咖飞", 3)
print(u) # User(name='咖飞', age=3, email='')
u == User("咖飞", 3) # True

比手写省一大堆样板代码。大部分”只装数据 + 少量方法”的类都推荐用 dataclass

其他常用参数:

1
2
3
@dataclass(frozen=True)        # 不可变(属性不能改)
@dataclass(order=True) # 生成 <, <=, >, >= 比较方法
@dataclass(slots=True) # 3.10+,减少内存

九、类可以有方法

方法就是”定义在类里的函数”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance

def deposit(self, amount):
if amount <= 0:
raise ValueError("金额必须 > 0")
self.balance += amount

def withdraw(self, amount):
if amount > self.balance:
raise ValueError("余额不足")
self.balance -= amount

def __repr__(self):
return f"BankAccount({self.owner!r}, ¥{self.balance})"


a = BankAccount("咖飞", 100)
a.deposit(50)
a.withdraw(30)
print(a) # BankAccount('咖飞', ¥120)

方法总是至少一个参数 self(除非是 staticmethod/classmethod,下节讲)。

十、”私有”变量

Python 没有真正的”私有”,靠命名约定:

1
2
3
4
5
6
7
8
9
10
class Foo:
def __init__(self):
self.public = 1
self._protected = 2 # 单下划线:约定"外部别动"
self.__private = 3 # 双下划线:名字改写,避子类冲突

f = Foo()
print(f.public) # 1
print(f._protected) # 2,可访问但不建议
print(f._Foo__private) # 3,改写后的名字
  • 单下划线 _x约定 私有,能访问但读代码的人知道”别碰”;
  • 双下划线 __x(前双后无):name mangling,被改写成 _ClassName__x,主要用来避免子类意外覆盖。

日常写业务用单下划线就够了。双下划线只在写库或明确不想被覆盖时用。

十一、类里的常量

1
2
3
4
5
6
7
8
9
10
class Circle:
PI = 3.14159 # 类常量

def __init__(self, r):
self.r = r

def area(self):
return self.PI * self.r ** 2

print(Circle(3).area())

约定:常量用大写。

十二、类的生命周期钩子

除了 __init__,还有几个特殊方法:

1
2
3
4
5
6
7
8
9
10
11
12
class Foo:
def __new__(cls, *args, **kwargs):
"""创建实例(一般不用重写,除非做单例/元类)。"""
return super().__new__(cls)

def __init__(self, x):
"""初始化实例(更常见)。"""
self.x = x

def __del__(self):
"""垃圾回收时调用(一般不用)。"""
print(f"删除 {self}")
  • __new__ 用得极少,只在单例、不可变类、元类里需要;
  • __init__ 是最常用的;
  • __del__ 不保证被调用,不要依赖它做资源清理,用 with 上下文管理器。

十三、一个完整例子:图书类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# book.py
"""一个简单的图书类,展示 OOP 基础。"""

from dataclasses import dataclass


@dataclass
class Book:
title: str
author: str
year: int
price: float
stock: int = 0

def is_available(self) -> bool:
return self.stock > 0

def sell(self, qty: int = 1) -> None:
if qty > self.stock:
raise ValueError(f"库存不足:只有 {self.stock} 本")
self.stock -= qty

def restock(self, qty: int) -> None:
if qty <= 0:
raise ValueError("补货数量必须 > 0")
self.stock += qty

def discounted_price(self, rate: float) -> float:
"""rate 是折扣率 0-1,如 0.2 表示 8 折。"""
return self.price * (1 - rate)


if __name__ == "__main__":
b = Book("Python 编程", "Guido", 2020, 59.9, stock=10)
print(b)
b.sell(3)
print(f"卖出后库存 {b.stock}")
print(f"打八折 ¥{b.discounted_price(0.2):.2f}")

十四、isinstancetype

判断一个对象是不是某类的实例:

1
2
3
4
5
6
7
u = User("咖飞", 3)

isinstance(u, User) # True
isinstance(u, object) # True,所有类都继承 object

type(u) == User # True,但不建议这么写
type(u) is User # 同上

永远用 isinstance,它支持继承(下一篇 继承与多态 讲)。

十五、常见陷阱

  1. 忘了 selfdef method(x): 少了 self,调用时报错。
  2. 可变类属性共享:默认参数陷阱的兄弟,永远在 __init__ 里初始化可变属性。
  3. self.x vs x:函数里 x = 5 是局部变量,不是实例属性。
  4. __init__ 有返回值__init__ 必须返回 None,写 return 值会 TypeError。
  5. 在类外访问 __private:Python 用 name mangling 阻止直接访问,除非硬走 _ClassName__private
  6. 重写 __eq__ 忘了 __hash__:定义 __eq__ 会自动让 __hash__ = None(不可哈希)。dataclass 会自动处理。

十六、小结与延伸阅读

  • 类是”数据 + 行为”的打包工具;
  • class Foo: + def __init__(self, ...): 是最基础组合;
  • self 是当前实例,方法第一个参数总是它;
  • 实例属性在 __init__ 里赋值,类属性写在类体;
  • 可变类属性会共享,用 __init__ 初始化;
  • 至少定义 __repr__ 便于调试;
  • 优先用 @dataclass 写数据类。

延伸阅读:

下一篇 属性、方法与 self 我们把类里的”细节”讲深。

pip 与虚拟环境 venv

学 Python 的人几乎都会踩这两个坑:

  1. 依赖冲突:A 项目要 numpy==1.20,B 项目要 numpy==1.25,两个项目一起装到系统 Python 里,A 用起来 B 就崩;
  2. 环境污染:随手 pip install 装了 100 多个包,之后不知道哪个是哪个项目要的,也不敢删。

答案就是 虚拟环境(venv)——每个项目一个独立的 Python 依赖沙箱。配合 pip,你能优雅地管理任意多个项目的依赖。这一篇讲透 pip、venv、requirements.txt、pyproject.toml,以及现代化的替代品(uv、poetry)。

一、pip 是什么

pip 是 Python 官方推荐的包管理工具,用来安装、升级、卸载第三方库。Python 3.4+ 自带 pip。

1
2
pip --version
python -m pip --version # 更保险,明确用当前 Python 的 pip

二、pip 常用命令

安装 / 卸载 / 升级

1
2
3
4
5
6
7
8
pip install requests                # 装最新版
pip install requests==2.31.0 # 指定版本
pip install "requests>=2.28" # 版本范围
pip install "requests[security]" # 装 extras
pip install -r requirements.txt # 从需求文件批量装

pip install -U requests # 升级
pip uninstall requests # 卸载

查询

1
2
3
4
pip list                    # 已安装的包
pip show requests # 详情
pip freeze # 用于生成 requirements.txt
pip search xxx # 已废弃,用 https://pypi.org 网页搜

换源

pip 默认从 PyPI 下载,国内速度慢。换成清华源:

1
2
3
4
5
# 一次性
pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple

# 全局设置
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

三、为什么需要虚拟环境

没有虚拟环境的世界

1
2
pip install django==3.2      # 项目 A 用
pip install django==4.2 # 项目 B 用 → 装完 A 就崩了

系统只有一个 Python,装到里面就冲突。

有虚拟环境的世界

1
2
项目 A/.venv/  ← django 3.2
项目 B/.venv/ ← django 4.2

每个项目独立,互不干扰。

四、venv:官方虚拟环境工具

Python 3.3+ 自带 venv,无需安装。

创建

在项目根目录:

1
2
3
4
5
# Windows
python -m venv .venv

# macOS/Linux
python3 -m venv .venv

会创建一个 .venv/ 目录,里面装着一份独立的 Python 解释器和标准库链接。

激活

激活后当前 shell 的 python/pip 都指向这个虚拟环境:

1
2
3
4
5
6
7
8
# Windows PowerShell / cmd
.venv\Scripts\activate

# Windows Git Bash
source .venv/Scripts/activate

# macOS/Linux
source .venv/bin/activate

激活后命令提示符前会多出 (.venv)

1
(.venv) $ pip install requests

退出

1
deactivate

删除虚拟环境

直接删掉整个 .venv/ 目录即可,没有其他”注册”要清理。

五、requirements.txt:项目依赖清单

记录依赖:

1
pip freeze > requirements.txt

生成的文件类似:

1
2
3
4
5
certifi==2024.2.2
charset-normalizer==3.3.2
idna==3.6
requests==2.31.0
urllib3==2.2.1

恢复依赖(在别人的电脑或另一个虚拟环境):

1
pip install -r requirements.txt

freeze 的问题

pip freeze 把间接依赖(依赖的依赖)也列了出来,太啰嗦。更规范的做法:

  • 顶层依赖写在 requirements.txt(只写你直接用的);
  • 锁定所有版本写在 requirements-lock.txt(由 pip freeze 或 pip-tools 生成,用来重现环境)。

或者用现代工具 pip-tools

1
2
3
4
5
6
7
8
pip install pip-tools
# requirements.in(人写)
requests
pandas>=2.0

# 生成锁文件
pip-compile requirements.in # → requirements.txt(含所有依赖)
pip-sync requirements.txt # 同步环境

六、pyproject.toml:现代项目配置

requirements.txt 太朴素,现代 Python 用 pyproject.toml 一站式描述项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"requests>=2.28",
"pydantic~=2.0",
]

[project.optional-dependencies]
dev = ["pytest", "black", "mypy"]

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

然后:

1
2
pip install -e .              # 装项目本身
pip install -e ".[dev]" # 带上 dev 依赖

七、常用第三方工具

uv(Rust 写的极速 pip 替代品,2024 起流行)

1
2
3
pip install uv
uv venv # 一秒创建虚拟环境
uv pip install -r requirements.txt # 比 pip 快 10-100 倍

poetry

以前很火的项目管理器,功能全但配置繁琐。新人可以先跳过。

conda(Anaconda)

科学计算/AI 生态用得多,能装 non-Python 依赖(C 库、CUDA 等)。用 conda 就别混用 pip,容易乱。

八、pipx:全局安装 CLI 工具

有些命令行工具(blackruffipythonawscli)你希望全局可用独立环境

1
2
pipx install black
pipx install ruff

pipx 会为每个工具建一个隐藏虚拟环境,暴露一个命令,互不干扰。

九、依赖版本约束语法

1
2
3
4
5
6
7
requests            # 任意版本
requests==2.31.0 # 精确
requests>=2.28 # 大于等于
requests~=2.28.0 # 兼容版本:2.28.* 都行,不含 2.29
requests<3 # 小于
requests>=2.28,<3 # 组合
requests!=2.29.0 # 不等于

~=语义化版本的推荐写法——允许”补丁号”升级,禁止”次版本号”变化。

十、常见工作流

新项目

1
2
3
4
5
6
mkdir myproject && cd myproject
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install --upgrade pip
pip install requests pandas
pip freeze > requirements.txt

别人的项目

1
2
3
4
5
git clone <url>
cd <repo>
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

IDE 里挑选解释器

  • VS Code:Ctrl+Shift+P → “Python: Select Interpreter” → 选 .venv/bin/python
  • PyCharm:Settings → Python Interpreter → Add → Existing Environment。

十一、.gitignore 里加什么

永远不要把虚拟环境目录、缓存、编译产物提交进 git:

1
2
3
4
5
.venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/

十二、镜像源与私有源

全局设置镜像

1
2
3
4
5
6
# Linux/macOS: ~/.pip/pip.conf
# Windows: %APPDATA%\pip\pip.ini

[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
trusted-host = pypi.tuna.tsinghua.edu.cn

项目级镜像

在项目根目录放 pip.conf,只在这个项目里生效。

私有 PyPI(企业内网)

1
pip install --extra-index-url https://pypi.company.com/simple mypkg

十三、依赖冲突诊断

pip install 有时候会报 “cannot resolve dependencies” 或者装完运行时报导入错误。诊断步骤:

  1. pip list 查看已装的版本;
  2. pip show <包名> 查看依赖关系;
  3. pip check 让 pip 检查依赖冲突;
  4. pipdeptree 看依赖树:
1
2
3
pip install pipdeptree
pipdeptree # 打印所有依赖树
pipdeptree -w silence # 只显示冲突

十四、一个完整例子:搭建 Web 项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 1. 建立虚拟环境
mkdir myweb && cd myweb
python -m venv .venv
source .venv/bin/activate

# 2. 装依赖
pip install --upgrade pip
pip install fastapi uvicorn[standard]

# 3. 写代码
cat > main.py << 'EOF'
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def hello():
return {"msg": "hello 咖飞"}
EOF

# 4. 运行
uvicorn main:app --reload

# 5. 保存依赖
pip freeze > requirements.txt

# 6. 提交前的 .gitignore
cat > .gitignore << 'EOF'
.venv/
__pycache__/
*.pyc
EOF

一个新同事拿到你的仓库,只需要:

1
2
3
4
5
git clone <repo> && cd myweb
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn main:app

就能跑起来。

十五、常见陷阱

  1. 忘记激活虚拟环境pip install 装到系统 Python 了。永远看命令提示符前是否有 (.venv)
  2. 多个 Python 版本共存pip 有时装到别的 Python 上。用 python -m pip install ... 强制绑定到当前 python。
  3. 在 requirements.txt 里锁到过旧版本:不主动升级容易积累技术债,也可能有安全漏洞。定期 pip list --outdated 检查。
  4. 虚拟环境跨机器复制.venv/ 有硬编码路径,不能复制到别的电脑。永远靠 requirements.txt 重建。
  5. conda 和 pip 混用:容易乱。选一个坚持用。
  6. 用 sudo pip install:Linux 上千万别 sudo,会污染系统 Python。永远用虚拟环境。

十六、小结与延伸阅读

  • 每个项目一个虚拟环境,永远不要污染系统 Python;
  • python -m venv .venv 创建,source .venv/bin/activate 激活;
  • pip installpip freezepip install -r requirements.txt 三剑客;
  • pyproject.toml 描述项目;
  • 想更快用 uv,想更完善用 poetry
  • CLI 工具用 pipx
  • 别把 .venv/ 提交进 git。

延伸阅读:

到这里模块三(函数与模块)7 篇结束! 下一篇进入 模块四:面向对象编程,从 类与对象基础 开始。

模块与包管理

到现在为止我们所有代码都写在一个 .py 文件里。真实项目怎么可能这么单调?一个中等规模项目动辄几十上百个 .py 文件,怎么组织、怎么导入、怎么发布?答案就是模块(module)包(package)。这一篇要把 import 的所有姿势、__init__.py__name__ 是什么、相对导入 vs 绝对导入、循环导入等问题一次讲清楚。

一、什么是模块

一个 .py 文件就是一个模块。模块是 Python 组织代码的最小单位。

1
2
3
4
5
6
7
8
# math_utils.py
PI = 3.14159

def add(a, b):
return a + b

def multiply(a, b):
return a * b

在另一个文件里用它:

1
2
3
4
5
# main.py
import math_utils

print(math_utils.PI)
print(math_utils.add(3, 4))

只要在同一个目录(或 PYTHONPATH 里),import 就找得到。

二、import 的四种姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 导入整个模块
import math
math.sqrt(16)

# 2. 起别名
import numpy as np
np.array([1, 2])

# 3. 从模块中导入具体名字
from math import sqrt, pi
sqrt(16)
pi

# 4. 起别名的具体名字
from math import sqrt as square_root
square_root(16)

# 5. (不推荐)导入所有
from math import *
sqrt(16) # 直接用

推荐用 import 模块from 模块 import 具体名字。避免 from x import *——它会把 x 里所有公开名字塞进当前命名空间,容易冲突且不好追踪。

三、模块的搜索路径

import mymodule 时 Python 按顺序找:

  1. 当前脚本所在目录
  2. PYTHONPATH 环境变量里的目录;
  3. 标准库目录
  4. site-packages(pip 安装的第三方包)。

查看当前搜索路径:

1
2
import sys
print(sys.path)

想加自定义路径:

1
sys.path.append("/path/to/dir")     # 临时加

不过更好的做法是把代码组织成”包”或者做 pip install -e .(下面讲)。

四、什么是包

一个包含 __init__.py 的文件夹就是一个包。包可以嵌套模块和子包:

1
2
3
4
5
6
7
8
9
myproject/
├── main.py
└── mypkg/
├── __init__.py
├── utils.py
├── models.py
└── sub/
├── __init__.py
└── helper.py

在 main.py 里:

1
2
3
import mypkg.utils
from mypkg.models import User
from mypkg.sub.helper import format_

__init__.py 的作用

  1. 告诉 Python “这是一个包”(Python 3.3+ 不严格要求,但强烈建议保留);
  2. 可以在里面写”包被导入时执行的代码”;
  3. 可以定义 __all__ 控制 from pkg import * 的行为;
  4. 可以在里面”提升”子模块的名字,方便使用者:
1
2
3
4
5
6
# mypkg/__init__.py
from .utils import add
from .models import User

# 使用方:
# from mypkg import add, User ← 而不用 from mypkg.utils import add

五、绝对导入 vs 相对导入

在包内部导入其他模块,两种写法:

1
2
3
4
5
6
7
8
# mypkg/sub/helper.py

# 绝对导入
from mypkg.utils import add

# 相对导入
from ..utils import add # 上一级
from . import buddy # 同一级
  • .:当前包;
  • ..:上一级包;
  • ...:上上级包(一般用不到)。

PEP 8 建议绝对导入优先。相对导入只在包内部用,且路径较深时可以简化。

注意:相对导入只能在包内部使用,直接 python helper.py 会报错。得用 python -m mypkg.sub.helper

六、__name__if __name__ == "__main__":

每个模块都有一个 __name__ 属性:

  • 当模块被直接执行python xxx.py)时,__name__ == "__main__"
  • 当模块被 import 时,__name__ 是模块名(如 "mypkg.utils")。

利用这个特性可以让一个文件”既是模块又是脚本”:

1
2
3
4
5
6
7
8
9
# math_utils.py
def add(a, b):
return a + b

def _demo():
print(add(3, 5))

if __name__ == "__main__":
_demo()
  • python math_utils.py → 运行 _demo()
  • import math_utils → 只导入函数,不运行 _demo

这是 Python 最重要的惯用法之一,每个脚本都建议加这行

七、导入时”执行”vs “定义”

一个模块被 import 时,Python 会从头到尾执行一遍

1
2
3
4
5
# mymod.py
print("模块被加载!")
x = 42
def f():
print("f 被调用")
1
2
import mymod                    # 打印 "模块被加载!"
import mymod # 什么都不打印,Python 缓存了
  • 第一次 import 会执行整个模块;
  • 之后的 import 直接用缓存(存在 sys.modules);
  • 想强制重新加载:importlib.reload(mymod)(调试用,生产别用)。

推论:模块顶层不要写”重活”(比如连数据库、下载文件)——那会拖慢所有 import 它的地方。把重活放在函数里,被调用时才做。

八、__all__:控制 from x import *

在模块顶层写 __all__ = ["公开名字1", "公开名字2"]

1
2
3
4
5
6
# utils.py
__all__ = ["add", "multiply"]

def add(a, b): return a + b
def multiply(a, b): return a * b
def _internal(): pass # 私有

from utils import * 只会导入 addmultiply_internal 不会。

惯例:下划线开头的名字(_helper)表示”私有”,就算没写 __all__from x import * 也不会导入它们。

九、常见标准库模块

Python 标准库巨大,日常写代码经常用到的:

模块 用途
os, sys, pathlib 系统、路径、文件
re 正则表达式
datetime, time 日期时间
json, csv 数据格式
collections 高级数据结构(deque, Counter 等)
itertools, functools 迭代与函数工具
math, random, statistics 数学与随机
logging 日志
subprocess 调用外部命令
threading, multiprocessing, asyncio 并发
urllib, http 网络(更常用的是第三方 requests)
argparse 命令行参数
unittest 测试(更常用 pytest)

十、循环导入

循环导入是新手项目结构乱的时候常见的错误:

1
2
3
4
5
6
7
# a.py
import b
def foo(): b.bar()

# b.py
import a
def bar(): a.foo()

Python 加载时会陷入循环,可能报 ImportError

解决办法

  1. 重构代码——把共享的东西抽到第 3 个模块 common.py,让 a 和 b 都 import common
  2. 函数内部导入(延迟到调用时才 import):
1
2
3
def bar():
from a import foo # 这时 a 已经加载好了
foo()
  1. 只 import 模块,不 from … import 具体名字——通常能规避。

十一、包的组织结构

一个规范的项目结构大致:

1
2
3
4
5
6
7
8
9
10
11
12
13
myproject/
├── pyproject.toml # 项目配置
├── README.md
├── src/ # 源代码
│ └── mypkg/
│ ├── __init__.py
│ ├── models.py
│ ├── utils.py
│ └── cli.py # 命令行入口
├── tests/ # 测试
│ ├── test_models.py
│ └── test_utils.py
└── requirements.txt # 依赖

十二、pip install -e .:开发模式安装

写好一个包后,想在别的项目里 import 它,可以:

  1. 复制目录——粗暴不推荐;
  2. pip install . —— 装到 site-packages,修改源码后要重装;
  3. pip install -e . —— 开发模式,改源码立即生效,最常用。

需要在项目根目录准备一个 pyproject.toml

1
2
3
4
5
6
7
[project]
name = "mypkg"
version = "0.1.0"

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

然后:

1
pip install -e .

十三、常见 import 陷阱

陷阱 1:文件名与标准库冲突

1
2
3
my_project/
├── math.py # ❌ 遮蔽了标准库的 math
└── main.py

import math 在 main.py 里可能会拿到你的 math.py别用标准库同名的文件名(math、json、time、random、os 等)。

陷阱 2:直接跑相对导入的文件

1
2
3
mypkg/
├── __init__.py
└── sub.py # 里面有 from . import xxx

python mypkg/sub.py 会报 ImportError: attempted relative import。用 python -m mypkg.sub 才对。

陷阱 3:sys.path 里没有项目根

在 IDE 里没问题,命令行下报 ModuleNotFoundError。解决:

  • 在项目根目录 python -m myproject.main
  • 或者用 pip install -e .
  • 别硬改 sys.path

陷阱 4:__init__.py 里 import 太多

1
2
# __init__.py
from .heavy_module import ... # 一 import 就变得很慢

包被 import 时,__init__.py立刻执行。控制它的开销,不要 import 昂贵的东西。

陷阱 5:绝对/相对导入混用

同一文件里既有 from mypkg.x import ... 又有 from .x import ...,容易搞混。建议一个包内统一使用一种风格

十四、一个完整例子:小型 CLI 包

1
2
3
4
5
6
7
8
9
todocli/
├── pyproject.toml
├── src/
│ └── todocli/
│ ├── __init__.py
│ ├── storage.py
│ └── cli.py
└── tests/
└── test_storage.py

storage.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# src/todocli/storage.py
"""持久化 TODO 列表到 JSON 文件。"""
import json
from pathlib import Path

DEFAULT_FILE = Path.home() / ".todocli.json"


def load(path: Path = DEFAULT_FILE) -> list[str]:
if not path.exists():
return []
return json.loads(path.read_text(encoding="utf-8"))


def save(items: list[str], path: Path = DEFAULT_FILE) -> None:
path.write_text(json.dumps(items, ensure_ascii=False, indent=2),
encoding="utf-8")

cli.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# src/todocli/cli.py
"""命令行入口。"""
import sys
from . import storage # 相对导入


def main():
items = storage.load()
if len(sys.argv) < 2:
for i, t in enumerate(items, 1):
print(f"{i}. {t}")
return

cmd = sys.argv[1]
if cmd == "add":
items.append(" ".join(sys.argv[2:]))
storage.save(items)
elif cmd == "done":
idx = int(sys.argv[2]) - 1
items.pop(idx)
storage.save(items)


if __name__ == "__main__":
main()

init.py

1
2
3
4
5
# src/todocli/__init__.py
from .storage import load, save
from .cli import main

__all__ = ["load", "save", "main"]

安装:pip install -e .,运行:python -m todocli.cli add 学 Python

十五、小结与延伸阅读

  • 一个 .py 是模块,含 __init__.py 的文件夹是包;
  • import 有绝对、相对两种,包内优先绝对;
  • if __name__ == "__main__": 让文件既是模块又是脚本;
  • 模块顶层代码只在第一次 import 时执行;
  • 循环导入靠”重构 + 延迟导入”解;
  • pip install -e . 做本地开发;
  • 避免文件名与标准库冲突。

延伸阅读:

下一篇 pip 与虚拟环境 venv 我们讲怎么装第三方库和隔离项目依赖。

装饰器入门

装饰器(decorator)是 Python 里”看起来很魔法,其实很简单”的语法。你在 Flask、Django、FastAPI、pytest 的代码里到处都能看到 @ 符号,那就是装饰器。前面 作用域与闭包匿名函数 lambda 与高阶函数 讲的东西合体,就是装饰器。这一篇讲清楚装饰器的原理、语法糖 @functools.wraps 的作用,进阶部分(带参数、类装饰器、lru_cache)留到 装饰器进阶

一、装饰器是什么

一句话定义:装饰器是一个”接受函数、返回函数”的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def uppercase(func):
def wrapper():
result = func()
return result.upper()
return wrapper


def greet():
return "hello"


greet = uppercase(greet) # 手动装饰
print(greet()) # HELLO

语义:”用 uppercase 包装一下 greet,让它返回值自动大写。”

二、@ 语法糖

@decorator 就是”把下面这个函数传给 decorator,再把返回值绑回同名”:

1
2
3
4
5
6
7
8
@uppercase
def greet():
return "hello"

# 等价于
def greet():
return "hello"
greet = uppercase(greet)

@ 只是让代码更短、意图更明显,本质没差。

三、带参数的被装饰函数

真实场景函数都有参数,wrapper 用 *args**kwargs 通吃:

1
2
3
4
5
6
7
8
9
10
11
12
13
def uppercase(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper() if isinstance(result, str) else result
return wrapper


@uppercase
def greet(name):
return f"hello {name}"


print(greet("咖飞")) # HELLO 咖飞

四、functools.wraps:保留原函数元信息

被装饰后,函数的名字和 docstring 会被替换:

1
2
3
4
5
6
7
@uppercase
def greet(name):
"""给某人打招呼"""
return f"hello {name}"

print(greet.__name__) # 'wrapper' ← 不是 greet 了!
print(greet.__doc__) # None

functools.wraps 修复这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import functools

def uppercase(func):
@functools.wraps(func) # ← 加这一行
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper() if isinstance(result, str) else result
return wrapper

@uppercase
def greet(name):
"""给某人打招呼"""
return f"hello {name}"

print(greet.__name__) # 'greet'
print(greet.__doc__) # '给某人打招呼'

规则:只要写装饰器,就无脑加 @functools.wraps(func)

五、经典装饰器:计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
import functools


def timed(func):
"""打印函数耗时。"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} 用了 {elapsed:.4f}s")
return result
return wrapper


@timed
def compute(n):
return sum(i * i for i in range(n))


compute(1_000_000)

六、经典装饰器:日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import functools
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger(__name__)


def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
log.info(f"调用 {func.__name__}(args={args}, kwargs={kwargs})")
try:
result = func(*args, **kwargs)
except Exception as e:
log.exception(f"{func.__name__} 抛出 {e}")
raise
log.info(f"{func.__name__} 返回 {result!r}")
return result
return wrapper


@logged
def divide(a, b):
return a / b


divide(10, 2)
divide(10, 0) # 会抛异常,日志里会记录

七、经典装饰器:重试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import functools
import time


def retry(times=3, delay=1):
"""遇到异常重试。"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exc = e
print(f"[{attempt}] {func.__name__} 失败:{e}")
if attempt < times:
time.sleep(delay)
raise last_exc
return wrapper
return decorator


@retry(times=3, delay=1)
def flaky_api():
import random
if random.random() < 0.7:
raise RuntimeError("network error")
return "ok"


print(flaky_api())

这已经是”带参数的装饰器”——外层 retry(times, delay) 返回真正的装饰器。三层嵌套是这个模式的标配。带参数装饰器我们在 装饰器进阶 深入讲。

八、内置装饰器

Python 有几个常用装饰器:

@staticmethod / @classmethod / @property

在类里用,我们在 类方法、静态方法与 property 讲。

@functools.lru_cache / @functools.cache(Python 3.9+)

自动缓存函数结果:

1
2
3
4
5
6
7
8
9
10
11
from functools import lru_cache, cache

@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)

# Python 3.9+ 无界版
@cache
def fib(n): ...

注意lru_cache 要求参数可哈希(不能是 list/dict)。

@functools.total_ordering

给类补齐比较方法:定义 __eq__ 和一个顺序方法(如 __lt__),自动生成剩下 3 个。见 特殊方法

@dataclass

自动生成 __init____repr__ 等,见 类与对象基础

九、多层装饰器

装饰器可以叠加,从下往上执行:

1
2
3
4
5
6
@a
@b
@c
def f(): ...

# 等价:f = a(b(c(f)))

调用 f() 时,实际是 a 的 wrapper 最外层调用。想象成”洋葱”——最外层先接触,逐层剥入到 f。

十、装饰类的方法

装饰器可以用在类的方法上,但要注意 self

1
2
3
4
5
6
7
8
9
10
11
12
13
def logged(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs): # self 也在 *args 里
print(f"[LOG] {self.__class__.__name__}.{func.__name__}")
return func(self, *args, **kwargs)
return wrapper

class Service:
@logged
def run(self, task):
print(f"运行 {task}")

Service().run("build")

def wrapper(*args, **kwargs) 也行,self 就是 args[0]

十一、装饰器的调试小技巧

被装饰后调试有时候会迷惑:

1
2
3
4
5
@timed
def foo(): ...

foo.__wrapped__ # 就是原来的 foo(wraps 提供的)
foo.__name__ # 'foo'

如果你想临时”取消装饰”,可以 foo.__wrapped__() 调用原函数。

十二、装饰器的常见错误

错误 1:忘了写 wrapper

1
2
def bad(func):
return func() # ❌ 立刻调用了!应该 return func 或 return wrapper

装饰器必须返回一个函数,不是”函数的返回值”。

错误 2:wrapper 没接收参数

1
2
3
4
5
6
7
8
9
def bad(func):
def wrapper(): # ❌ 没有 *args, **kwargs
return func()
return wrapper

@bad
def add(a, b): return a + b

add(1, 2) # TypeError: wrapper() takes 0 positional

错误 3:忘了 return

1
2
3
4
5
def bad(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs) # ❌ 忘了 return
return wrapper

被装饰的函数返回值丢了。

错误 4:忘了 @functools.wraps

被装饰后 func.__name__func.__doc__ 都乱了,pytest / Sphinx / 日志都会受影响。

十三、一个完整例子:权限校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# permission.py
"""装饰器实现的简单权限校验。"""

import functools


class Unauthorized(Exception):
pass


current_user = {"name": "guest", "roles": {"user"}}


def require_role(role: str):
"""要求当前用户拥有指定 role。"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if role not in current_user["roles"]:
raise Unauthorized(
f"{current_user['name']} 缺少 {role} 权限")
return func(*args, **kwargs)
return wrapper
return decorator


@require_role("admin")
def delete_user(id: int):
print(f"删除用户 {id}")


@require_role("user")
def read_profile():
print("查看资料")


read_profile() # ok
try:
delete_user(42) # 缺 admin,抛异常
except Unauthorized as e:
print(e)

# 升权限
current_user["roles"].add("admin")
delete_user(42) # ok

Flask 的 @login_required、Django 的 @permission_required 都是这个思路的产业级实现。

十四、装饰器 vs 上下文管理器 vs 中间件

三者常做类似的事情(”包一层”):

  • 装饰器:包”函数”;
  • 上下文管理器(with):包”代码块”;
  • 中间件:包”请求-响应流”。

选择规则:

  • 想给一个函数加”计时/日志/权限/重试” → 装饰器;
  • 想给一段代码加”资源打开/关闭” → 上下文管理器;
  • 想在框架级给每个请求加逻辑 → 中间件。

十五、小结与延伸阅读

  • 装饰器就是”接受函数、返回函数”的函数;
  • @decorator 只是 func = decorator(func) 的语法糖;
  • wrapper 用 *args, **kwargs 通吃参数;
  • 一定要 @functools.wraps(func) 保留元信息;
  • 常见装饰器:timed、logged、retry、cached、permission;
  • 装饰器可以叠加,从下往上;
  • 带参数装饰器需要三层嵌套。

延伸阅读:

下一篇 模块与包管理 我们要跳出单文件,学会把代码组织成模块和包。

匿名函数 lambda 与高阶函数

Python 支持函数式编程的核心武器有两个:lambda(匿名函数)和高阶函数(把函数当参数或返回值的函数)。前面 函数定义与调用 里说过”函数是一等公民”,这一篇就把这条规则的用法讲透——lambdamapfilterreducesortedkeyfunctools.partial,用完之后你的代码会明显变短、变优雅。

一、lambda 语法

lambda 定义一个匿名的、只能写一个表达式的函数:

1
2
3
4
5
6
7
8
# 传统函数
def double(x):
return x * 2

# lambda
double = lambda x: x * 2

print(double(5)) # 10

结构:lambda 参数列表: 表达式。没有 return,表达式的值就是返回值。

限制

  • 只能一个表达式,不能有语句if x: return y 不行);
  • 不能有 return
  • 不能有多行;
  • 匿名——没有名字(虽然可以赋给变量)。

多参数:

1
2
3
4
5
add = lambda a, b: a + b
add(3, 4) # 7

# 默认参数、*args、**kwargs 也都支持
f = lambda x, y=10, *args, **kwargs: (x, y, args, kwargs)

二、什么时候用 lambda

lambda 的唯一价值是”传给别的函数当参数”——用完即扔。

1
2
3
4
5
6
7
8
# 排序:按字符串长度
sorted(["banana", "apple", "cherry"], key=lambda s: len(s))

# 过滤:只要正数
list(filter(lambda x: x > 0, [-2, -1, 0, 1, 2]))

# 映射:加一
list(map(lambda x: x + 1, range(5)))

不建议给 lambda 起名

1
2
3
4
5
6
# ❌ 反面教材:赋名字给 lambda
double = lambda x: x * 2

# ✅ 直接 def 更好
def double(x):
return x * 2

Guido 本人和 PEP 8 都认为——如果 lambda 需要名字,那你应该用 def。

三、高阶函数:函数作为参数

Python 内置了几个经典高阶函数:mapfilterreducesortedmin/maxany/all

map(func, iter)

对每个元素调用 func:

1
2
3
4
5
6
7
list(map(str.upper, ["a", "b", "c"]))       # ['A', 'B', 'C']
list(map(len, ["hi", "hello", "world"])) # [2, 5, 5]
list(map(lambda x: x*2, [1, 2, 3])) # [2, 4, 6]

# 多个可迭代对象:并行取
list(map(lambda a, b: a + b, [1, 2, 3], [10, 20, 30]))
# [11, 22, 33]

filter(pred, iter)

保留返回真的元素:

1
2
3
4
5
list(filter(lambda x: x > 0, [-2, -1, 0, 1, 2]))     # [1, 2]
list(filter(str.isalpha, ["abc", "12", "hi"])) # ['abc', 'hi']

# pred 为 None 时相当于用 bool
list(filter(None, [0, 1, "", "hi", []])) # [1, 'hi']

reduce(func, iter, initial)

累积计算,需要从 functools 导入:

1
2
3
4
5
6
7
8
9
10
11
from functools import reduce

# 求和:等价 sum([1, 2, 3, 4])
reduce(lambda a, b: a + b, [1, 2, 3, 4]) # 10
reduce(lambda a, b: a + b, [1, 2, 3, 4], 100) # 110(带初始值)

# 找最大:等价 max
reduce(lambda a, b: a if a > b else b, nums)

# 拼字符串
reduce(lambda a, b: a + b, ["hi ", "there ", "friend"])

reduce 曾经是 Python 内置的,Python 3 里移到了 functools,Guido 认为大部分场景用推导式或者 sum/min/max 就够,不需要 reduce。

map/filter vs 推导式

现代 Python 更推荐列表推导式

1
2
3
4
5
# map + filter
list(map(lambda x: x*2, filter(lambda x: x > 0, nums)))

# 推导式(更清晰)
[x*2 for x in nums if x > 0]

推导式一般更好读。map/filter 适合”已有函数不用 lambda”的场景:

1
2
3
4
5
# ✅ 简洁
list(map(str.upper, words))

# 推导式反而多打几个字
[w.upper() for w in words]

四、sorted / min / max 的 key

高阶函数最常见的用法:给排序函数传 key。

1
2
3
4
5
6
7
8
9
10
11
words = ["Banana", "apple", "cherry"]
sorted(words) # ['Banana', 'apple', 'cherry'] 按 ASCII
sorted(words, key=str.lower) # ['apple', 'Banana', 'cherry'] 忽略大小写
sorted(words, key=len) # 按长度

people = [("张三", 25), ("李四", 20), ("王五", 30)]
sorted(people, key=lambda p: p[1]) # 按年龄
sorted(people, key=lambda p: -p[1]) # 按年龄降序

# 多字段:先按第一个,再按第二个
records.sort(key=lambda r: (r.grade, r.name))

operator 更快:

1
2
3
4
5
from operator import itemgetter, attrgetter

sorted(people, key=itemgetter(1)) # 按第 1 项(年龄)
sorted(users, key=attrgetter("age")) # 按 .age 属性
sorted(data, key=itemgetter("category", "price")) # 多 key

itemgetter/attrgetter 是 C 实现,比 lambda 快很多,代码也更清晰。

五、functools.partial:偏函数

partial 固定函数的一部分参数,产生新函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from functools import partial

def power(base, exp):
return base ** exp

square = partial(power, exp=2) # 固定 exp=2
cube = partial(power, exp=3)

print(square(5)) # 25
print(cube(3)) # 27

# 也可以固定位置参数
add_10 = partial(lambda a, b: a + b, 10)
print(add_10(5)) # 15

用途:创建”预配置”的函数。特别适合传给 map/filter/sort 等高阶函数。

六、operator 模块:函数化的运算符

operator 模块把运算符变成函数,可以传参:

1
2
3
4
5
6
7
8
from operator import add, mul, gt, itemgetter, methodcaller

reduce(add, [1, 2, 3, 4]) # 10
reduce(mul, [1, 2, 3, 4]) # 24
list(filter(partial(gt, 5), [1, 3, 5, 7, 9])) # 小于 5 的(因为 gt(5, x))

# 调用方法
sorted(words, key=methodcaller("lower"))

七、函数作为返回值:函数工厂

1
2
3
4
5
6
7
8
9
def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}"
return greet

hello = make_greeter("Hello")
hi = make_greeter("Hi")
print(hello("咖飞")) # Hello, 咖飞
print(hi("小王")) # Hi, 小王

这本质就是闭包(作用域与闭包 讲过)。

八、装饰器预告

高阶函数最重要的应用就是装饰器——一个”接受函数、返回新函数”的函数:

1
2
3
4
5
6
7
8
9
10
def uppercase(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper

@uppercase
def greet(name):
return f"hello {name}"

print(greet("咖飞")) # HELLO 咖飞

我们在 装饰器入门 会详细讲。

九、函数式编程的边界

Python 支持函数式编程,但它不是纯函数式语言。跟 Haskell 或 Lisp 相比:

  • Python 更强调命令式和面向对象;
  • 没有尾调用优化,不适合大量递归;
  • lambda 有限制,不能像 Haskell 那样自由;
  • 推导式在很多场景比 map/filter 更 Pythonic。

建议:把函数式当”工具”,不当”信仰”。合适的地方用,别为了”函数式”而扭曲代码。

十、一个综合例子:数据管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# pipeline.py
"""对一批数据做多步转换,展示 lambda / map / filter / sorted / reduce 的组合。"""

from functools import reduce
from operator import itemgetter, mul

records = [
{"name": "咖飞", "score": 88, "gender": "M", "class": "A"},
{"name": "小王", "score": 75, "gender": "M", "class": "B"},
{"name": "小李", "score": 92, "gender": "F", "class": "A"},
{"name": "小张", "score": 60, "gender": "M", "class": "B"},
{"name": "小陈", "score": 95, "gender": "F", "class": "A"},
]

# 1. 挑 A 班
class_a = filter(lambda r: r["class"] == "A", records)

# 2. 只留 name 和 score
projected = map(lambda r: (r["name"], r["score"]), class_a)

# 3. 按分数降序
ranked = sorted(projected, key=itemgetter(1), reverse=True)

print("A 班排行榜:", ranked)

# 4. 所有女生分数乘积(示意 reduce)
females = filter(lambda r: r["gender"] == "F", records)
prod = reduce(mul, map(itemgetter("score"), females))
print("女生分数积:", prod)

# 5. 及格用户的名字(推导式版)
passed = [r["name"] for r in records if r["score"] >= 60]
print("及格:", passed)

# 6. 平均分
avg = sum(map(itemgetter("score"), records)) / len(records)
print(f"平均分:{avg:.1f}")

十一、常见陷阱

  1. lambda 里想写多行/if-return:不行,改回 def。
  2. 给 lambda 命名:PEP 8 反对。
  3. 循环里创建 lambda 的晚绑定:见 作用域与闭包
  4. map/filter 结果是迭代器filter(...) 只能遍历一次,多次遍历要 list()
  5. reduce 空序列reduce(add, []) 抛错,除非给 initial。
  6. partial 位置参数只能从左到右固定:想固定中间的参数用关键字:partial(f, y=10)
  7. operator.itemgetter 传多个索引:返回元组:itemgetter(0, 2)("abc") == ('a', 'c')

十二、小结与延伸阅读

  • lambda 只写一个表达式,一般用来传给别的函数;
  • 高阶函数:map / filter / reduce / sorted / min / max / any / all;
  • 现代 Python 更爱推导式,除非用现成函数;
  • operator.itemgetter / attrgetter 是 sort key 的最佳选择;
  • functools.partial 用来”预配置”函数;
  • 函数是一等公民 → 可以进容器、传参、返回;
  • 别过度函数式,能推导式就推导式。

延伸阅读:

下一篇 装饰器入门 我们把”高阶函数 + 闭包”合体,写出真正的 Pythonic 装饰器。

作用域与闭包

一段代码里 x = 5 的这个 x 到底”在哪里”?跨函数写代码后,你会开始遇到 “为什么 x 在这里能读、那里改不了?” 的疑问。答案就在作用域(scope)——Python 定位变量名的规则。理解 LEGB 规则和闭包,你才能真正驾驭函数、装饰器、生成器这些高级特性。这一篇讲透作用域、globalnonlocal、闭包与”闭包变量陷阱”。

一、LEGB 规则

Python 找一个变量名,按四层依次查找:

  1. L (Local):当前函数的局部;
  2. E (Enclosing):外层函数(如果嵌套定义)的局部;
  3. G (Global):模块顶层;
  4. B (Built-in):Python 内置命名空间。

找不到就 NameError

1
2
3
4
5
6
7
8
9
10
11
12
x = "global"

def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local
inner()
print(x) # enclosing

outer()
print(x) # global

内置层次的例子:

1
print("hello")     # print 在 built-in 里

如果你 def print(...) 定义了自己的 print,就会遮蔽内置。所以别用 listdictsum 这些做变量名。

二、局部作用域

函数内部赋值的变量默认是局部

1
2
3
4
5
6
7
8
x = 10

def f():
x = 20 # 新建局部 x,不影响外部
print(x) # 20

f()
print(x) # 10

规则:只要在函数体里有 x = ... 这种赋值语句,x 在整个函数里都被视为局部。哪怕这行在很下面:

1
2
3
4
5
x = 10

def f():
print(x) # ❌ UnboundLocalError!
x = 20 # 因为这一行,x 在整个函数里都是局部

要么全部使用外部 x(不赋值),要么显式声明 global

三、global:修改全局变量

在函数里想改全局变量,用 global

1
2
3
4
5
6
7
8
9
count = 0

def increment():
global count
count += 1

increment()
increment()
print(count) # 2

别滥用 global——它会让代码难测、难维护。多数情况改成”返回值 + 调用方赋值”更好:

1
2
3
4
5
def next_count(current):
return current + 1

count = 0
count = next_count(count)

四、nonlocal:修改外层函数变量

闭包(下面详细讲)常常需要修改外层函数的变量,但不是全局的,这时用 nonlocal

1
2
3
4
5
6
7
8
9
10
11
12
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc

c = counter()
print(c()) # 1
print(c()) # 2
print(c()) # 3

如果不写 nonlocaln += 1 会被当成”新建局部 n”,导致 UnboundLocalError。

nonlocalglobal 的区别

  • global 找到模块顶层的变量;
  • nonlocal 找到最近的外层函数的变量。

五、什么是闭包

闭包(closure)= “函数 + 它捕获的外层变量“。当外层函数返回内层函数时,内层函数记住了它的定义环境。

1
2
3
4
5
6
7
8
9
10
def make_multiplier(factor):
def multiplier(x):
return x * factor # 捕获了外层的 factor
return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5)) # 10
print(triple(5)) # 15
  • double 是一个函数,它”记住”了 factor=2
  • triple 是同一份代码,但”记住”的 factor=3
  • 两者互不影响。

六、闭包能干什么

1. 参数化的函数

1
2
3
4
5
6
7
8
9
10
def make_greeter(greeting):
def greet(name):
return f"{greeting}{name}"
return greet

hello = make_greeter("你好")
morning = make_greeter("早上好")

print(hello("咖飞")) # 你好,咖飞
print(morning("小王")) # 早上好,小王

2. 装饰器的底层机制

装饰器本质就是”接受函数,返回函数”,天然就是闭包:

1
2
3
4
5
6
7
8
def timed(func):
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 用了 {time.time()-start:.3f}s")
return result
return wrapper

wrapper 捕获了 func,就是闭包。

3. 保存状态(比类轻量)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def make_counter():
count = 0
def inc():
nonlocal count
count += 1
return count
def reset():
nonlocal count
count = 0
return inc, reset

inc, reset = make_counter()
print(inc(), inc(), inc()) # 1 2 3
reset()
print(inc()) # 1

七、闭包变量的可见性

闭包捕获变量本身(更准确说是”cell”),不是那一刻的值。所以外层变量后来变,闭包看到的也变:

1
2
3
4
5
6
7
8
9
def outer():
x = 10
def inner():
print(x)
x = 20 # 修改外层 x
return inner

f = outer()
f() # 20,不是 10!

inner 捕获的是 x 这个引用,而不是数值 10。

八、”晚绑定”陷阱:循环里的闭包

这是 Python 最经典的坑之一:

1
2
3
4
5
6
7
8
funcs = []
for i in range(3):
funcs.append(lambda: i)

for f in funcs:
print(f())
# 期待 0 1 2
# 实际 2 2 2

原因:所有 lambda 捕获同一个变量 i,循环结束时 i 是 2,所以调用时全部返回 2。

修复 1:默认参数(快照当前值)

1
2
3
4
5
6
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # 默认值在 def 时求值

for f in funcs:
print(f()) # 0 1 2

修复 2:立即调用工厂函数

1
2
3
4
5
funcs = []
for i in range(3):
def make(x):
return lambda: x
funcs.append(make(i))

修复 3:偏函数 functools.partial

1
2
from functools import partial
funcs = [partial(lambda x: x, i) for i in range(3)]

这个坑几乎所有教程都会警告——记住它。

九、检查闭包变量

闭包的”记忆”存在 __closure__ 属性里:

1
2
3
4
5
6
def make(x):
return lambda: x

f = make(42)
print(f.__closure__) # (cell object,)
print(f.__closure__[0].cell_contents) # 42

一般写业务代码不需要用它,但了解原理有助于理解装饰器等高级机制。

十、模块作用域 vs 类作用域

模块顶层的变量属于模块作用域(G 层)。

类体里的变量是个特殊存在——它属于”类的命名空间”,但方法内部访问不到(不属于 LEGB 的 E 层):

1
2
3
4
5
class Foo:
x = 10 # 类属性
def method(self):
print(x) # ❌ NameError!方法看不到 x
print(self.x) # ✅ 得走 self

初学者遇到这个会很困惑。规则:类体不是外层作用域,方法内部只能通过 selfFoo.x 访问类属性。

十一、变量的生命周期

  • 局部变量:函数调用时创建,返回时销毁;
  • 闭包捕获的变量:只要闭包函数还存在,就活着;
  • 全局变量:程序运行期间一直活;
  • 循环变量:Python 里 for 变量会泄漏到外层
1
2
3
for x in range(3):
pass
print(x) # 2!

推导式的循环变量不泄漏(3.x),但普通 for 会。

十二、检查名字:locals / globals

1
2
3
4
5
6
def f():
a, b = 1, 2
print(locals()) # {'a': 1, 'b': 2}

f()
print(globals()) # 模块顶层的所有名字,是个字典

调试时偶尔用一下,业务代码不要动 locals() 的返回值(有些实现里改它没效果)。

十三、一个综合例子:简单缓存

用闭包实现一个”最多缓存 N 个结果”的装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# cache.py
"""闭包实现的极简 LRU 风格缓存。"""

from collections import OrderedDict


def bounded_cache(maxsize: int = 128):
"""返回一个装饰器,为被装饰函数加缓存。"""
def decorator(func):
cache = OrderedDict()

def wrapper(*args):
if args in cache:
# 命中:把它移到末尾表示"最近用"
cache.move_to_end(args)
return cache[args]

result = func(*args)
cache[args] = result

# 超出容量,弹掉最久未用的
if len(cache) > maxsize:
cache.popitem(last=False)
return result

wrapper.cache = cache # 挂在函数上便于调试
return wrapper
return decorator


@bounded_cache(maxsize=3)
def slow_square(n):
print(f" 正在计算 {n} 的平方")
return n * n


for x in [1, 2, 3, 1, 4, 2]:
print(x, "->", slow_square(x))

print("缓存内容:", dict(slow_square.cache))

输出:

1
2
3
4
5
6
7
8
9
10
11
  正在计算 1 的平方
1 -> 1
正在计算 2 的平方
2 -> 4
正在计算 3 的平方
3 -> 9
1 -> 1 # 缓存命中
正在计算 4 的平方
4 -> 16
2 -> 4 # 缓存命中
缓存内容: {3: 9, 1: 1, 4: 16, 2: 4}

这个例子融合了闭包、可变默认对象、装饰器概念,本质就是一个 mini 版的 functools.lru_cache

十四、常见陷阱

  1. 函数里给全局变量赋值忘了 globalx = ... 会新建局部。
  2. 循环里的闭包晚绑定:改成默认参数或立即工厂。
  3. UnboundLocalError:函数里既读又赋值同一个名字,全成了局部。
  4. nonlocal 找不到目标:外层必须已有这个名字,不能凭空创建。
  5. 类作用域不参与 LEGB:方法内直接写类属性名会 NameError。
  6. for 变量泄漏for x in range(3): pass; print(x) 会输出 2。

十五、小结与延伸阅读

  • LEGB:Local → Enclosing → Global → Built-in;
  • global 修改全局,nonlocal 修改外层函数变量;
  • 闭包 = 函数 + 它记住的外层变量;
  • 闭包记住的是变量的引用,不是快照;
  • 循环里创建闭包要小心晚绑定;
  • 尽量少用 global,闭包比 global 更”局部化”;
  • 类作用域不参与 LEGB。

延伸阅读:

下一篇 匿名函数 lambda 与高阶函数 我们讲 Python 函数式编程的两个主角。