跳到主要内容

装饰器深入理解:从原理到实战

· 阅读需 5 分钟
Destiny
Python 学习者

装饰器是 Python 里「能让你代码立刻看起来很专业」的特性,也是初学者最容易卡住的坎。这篇笔记不打算抄一遍教程,而是从原理讲清楚——一旦理解了「函数就是对象」这件事,所有装饰器语法都顺理成章。

一、原理:函数就是对象

Python 里函数是一等对象(first-class object),意味着它可以:

  • 赋值给变量
  • 作为参数传入
  • 作为返回值返回
  • 装进容器里
def greet(name: str) -> str:
return f"hello, {name}"

say = greet # 赋值给另一个名字
print(say("destiny")) # hello, destiny

funcs = [greet, str.upper]
print(funcs[0]("world")) # hello, world
备注

理解装饰器的关键,不是去记 @decorator 的语法,而是先接受「函数就是普通的值」。后面所有装饰器都是这个事实的自然推论。

二、@ 语法糖到底做了什么

先看一个「手动」装饰的例子:

def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper

def hello(name):
return f"hello, {name}"

hello = shout(hello) # 关键一步:把原函数替换成 wrapper
print(hello("destiny")) # HELLO, DESTINY

@shout 就是上面 hello = shout(hello) 的语法糖:

@shout
def hello(name):
return f"hello, {name}"

# 等价于:def hello(...): ... 然后 hello = shout(hello)
注意

很多教程只讲到这里就停了,但你立刻会发现一个坑:hello__name__ 变成了 wrapperhelp(hello) 也丢了原函数的文档。这就是 functools.wraps 存在的理由——下一节讲。

三、functools.wraps:保留元信息

写装饰器永远要加 @wraps,否则调试时你会痛不欲生:

from functools import wraps

def shout(func):
@wraps(func) # ← 关键
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper

@shout
def hello(name):
"""打招呼"""
return f"hello, {name}"

print(hello.__name__) # hello,而不是 wrapper
print(hello.__doc__) # 打招呼

@wraps 把原函数的 __name____doc____module____wrapped__ 都复制到 wrapper 上,调试器、Sphinx、IDE 才能正确显示信息。

四、带参数的装饰器

参数化装饰器本质是「多嵌一层」——外层接收参数、返回真正的装饰器:

from functools import wraps

def repeat(times: int):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(times=3)
def ping():
print("pong", end=" ")

ping() # pong pong pong

记忆要点:普通装饰器是两层函数,带参数装饰器是三层函数

五、类装饰器

类也可以做装饰器,用 __call__ 让实例可调用即可。这种写法在需要「状态」时更清晰:

class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
wraps(func)(self) # 也可以保留元信息

def __call__(self, *args, **kwargs):
self.count += 1
print(f"第 {self.count} 次调用")
return self.func(*args, **kwargs)

@CountCalls
def task():
print("do work")

task(); task(); task()

也可以反过来:装饰一个类——比如自动给所有方法加 @staticmethod,或自动生成 __repr__dataclass 就是典型的类装饰器。

六、常用内置装饰器速览

from functools import lru_cache, cached_property, wraps
from dataclasses import dataclass

@lru_cache(maxsize=128) # 自动缓存返回值,按参数缓存
def fib(n: int) -> int:
return n if n < 2 else fib(n-1) + fib(n-2)

@dataclass(slots=True) # 自动生成 __init__/__repr__/__eq__
class Point:
x: float
y: float

class Image:
@cached_property # 第一次访问后缓存,后续直接返回
def size(self) -> tuple[int, int]:
# 假设这里读文件、做计算
return (1920, 1080)

@staticmethod / @classmethod # 方法类型控制
@property # getter
提示

3.9+ 推荐用 functools.cache,它等价于 lru_cache(maxsize=None),无大小限制且略快。3.12 还新增了 @override 用于标注重写。

七、实战:带参数的缓存装饰器

把上面的概念用起来,自己实现一个「带 TTL 的缓存」:

from functools import wraps
from time import monotonic
from typing import Callable, Any

def cached(ttl: float = 60.0):
"""带过期时间的缓存装饰器,单位秒。"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
store: dict[tuple, tuple[float, Any]] = {}
hits = misses = 0

@wraps(func)
def wrapper(*args, **kwargs):
nonlocal hits, misses
key = (args, tuple(sorted(kwargs.items())))
now = monotonic()

if key in store:
ts, val = store[key]
if now - ts < ttl:
hits += 1
return val
else:
del store[key]

misses += 1
val = func(*args, **kwargs)
store[key] = (now, val)
return val

wrapper.cache_info = lambda: {"hits": hits, "misses": misses, "size": len(store)} # type: ignore[attr-defined]
return wrapper
return decorator

@cached(ttl=0.5)
def slow_square(x: int) -> int:
print(f" computing {x}...")
return x * x

print(slow_square(4)) # computing 4... → 16
print(slow_square(4)) # 直接返回 16(命中缓存)
print(slow_square.cache_info()) # {'hits': 1, 'misses': 1, 'size': 1}

这个例子同时用到了:带参数装饰器、@wraps、闭包共享状态、动态给函数加属性。看懂它,装饰器就基本过关了。

小结

装饰器不是黑魔法,本质就一句话:用函数返回函数,用 @ 把原函数替换掉。理解之后,去看 FastAPI 的 @app.get("/")pytest@pytest.mark.parametrize、Django 的 @login_required,会发现它们都遵循同一个套路。

下一篇我们会讲生成器与上下文管理器——和装饰器一样,它们也是「写出 Pythonic 代码」的三大件之一。

用 uv 打造高效的 Python 开发工作流

· 阅读需 4 分钟
Destiny
Python 学习者

如果你还在用 python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt 这套三连击,那么这篇文章会改变你的工作流。本教程默认使用 uv 作为依赖管理工具,本文解释为什么,以及怎么用。

一、uv 是什么

uv 是 Astral 公司(也就是做 Ruff 那家)用 Rust 写的 Python 包管理工具,定位上同时是:

  • pip 的替代(安装包,但快 10–100 倍)
  • venv 的替代(一行命令创建虚拟环境)
  • pip-tools 的替代(依赖锁定、解析)
  • poetry / rye / pyenv 的部分替代(项目管理、Python 版本管理)

它的核心卖点是——快到你会怀疑「是不是没装上」。

提示

uv 的设计哲学是「兼容现有生态」:它依然生成标准的 pyproject.tomlrequirements.txtuv.lock,你随时可以退回用 pip,不会被困住。

二、为什么选择 uv

我之前用 poetry,后来转 rye,最后定在 uv。理由有三个:

  1. 速度:装一个 Django 项目,pip 要 30 秒,uv 只要 3 秒。开发体验直接质变。
  2. 一个工具搞定一切:装 Python、建虚拟环境、装包、跑脚本、锁定依赖,全部一个命令搞定,不用 pyenv + venv + pip 三件套。
  3. 全局工具安装uv tool install ruff 装全局 CLI 工具,类似 pipx,比 npm 全局装包还顺手。

三、核心命令速查

# Python 版本管理
uv python install 3.12 # 安装 Python 3.12
uv python list # 列出可用版本

# 虚拟环境(也兼容旧用法)
uv venv # 在当前目录创建 .venv
uv venv --python 3.12 # 指定 Python 版本

# 包管理(pip 的替代)
uv pip install requests # 装 pip 包
uv pip install -r requirements.txt
uv pip list

# 项目管理(推荐用法,替代 poetry)
uv init my-project # 初始化一个新项目
uv add fastapi # 加依赖到 pyproject.toml
uv add --dev pytest # 加开发依赖
uv remove fastapi # 移除依赖
uv sync # 按 lockfile 安装/同步环境
uv lock # 重新解析依赖生成 uv.lock
uv run python main.py # 在项目环境里跑命令(自动 sync)
uv run pytest # 跑测试
备注

uv pip installuv add 是两个层次的东西:前者只是「快速版 pip」,不动 pyproject.toml;后者会把依赖写进项目配置并更新 uv.lock。在项目里优先用 uv add

四、与 pip / venv / poetry 的对比

能力pip + venvpoetryuv
速度极快
虚拟环境手动自动自动
依赖锁定需 pip-tools内置内置
Python 版本管理不支持不支持支持
全局工具安装需 pipx不支持支持
兼容 pyproject.toml部分
注意

poetry 的 lock 文件解析在某些场景下会比较慢,且对私有源支持较弱。如果你的项目依赖很多、CI 跑得很频繁,迁移到 uv 后构建时间通常能减半。

五、实战:用 uv 创建一个完整项目

下面用 uv 把本教程里的一个 demo 项目跑起来:

# 1. 初始化项目
uv init study-demo
cd study-demo

# 2. 指定 Python 版本并创建环境
uv python pin 3.12

# 3. 加入生产依赖
uv add fastapi 'uvicorn[standard]'
uv add httpx

# 4. 加入开发依赖
uv add --dev pytest pytest-asyncio ruff

# 5. 跑起来(不需要手动 activate,uv run 自动处理)
uv run uvicorn main:app --reload

# 6. 跑测试
uv run pytest -v

# 7. 别人 clone 项目后,只要一句
uv sync

执行完,项目结构长这样:

study-demo/
├── .python-version # uv python pin 写入
├── .venv/ # uv venv 自动创建
├── pyproject.toml # uv init 生成,uv add 维护
├── uv.lock # uv lock 生成,锁定所有依赖
├── README.md
└── src/
└── study_demo/
└── __init__.py
提示

.venv/ 加进 .gitignore,但一定要把 uv.lock 提交到 git。lockfile 是保证「同事和我装的是同一套版本」的关键,千万别忽略它。

六、与现有 requirements.txt 项目共存

uv 不强制你迁移。如果是老项目,可以这样平滑过渡:

# 老 requirements.txt 直接同步
uv venv
uv pip install -r requirements.txt

# 之后再慢慢迁移到 uv add / pyproject.toml

小结

uv 不是「又一个包管理器」,而是把 pyenv + venv + pip + pip-tools + pipx 这一堆工具用 Rust 重写并整合成一个二进制。装上之后,我再也不用记忆「这个项目用 poetry,那个项目用 pipenv」——统一用 uv 就够了。

下一篇我们正式进入 Python 语法本身,从最基础的变量和类型注解开始。

现代 Python 你应该知道的 10 个特性

· 阅读需 4 分钟
Destiny
Python 学习者

很多人写 Python 还停留在 3.6 时代的写法:Optional[int]os.path.joinif-elif-else 一长串。其实从 3.10 开始,Python 已经悄悄变成了一门「看起来很像新语言」的语言。下面这 10 个特性,是我在日常代码里几乎天天都会用到的。本教程默认 Python 3.12+,所以放心大胆地用。

1. match-case:结构化模式匹配(3.10)

不再是简单的 switch,它支持解构:

def describe(point: tuple[int, int]) -> str:
match point:
case (0, 0):
return "原点"
case (x, 0):
return f"在 x 轴上,x={x}"
case (0, y):
return f"在 y 轴上,y={y}"
case (x, y):
return f"普通点 ({x}, {y})"

print(describe((3, 0))) # 在 x 轴上,x=3
提示

match-case 还能匹配类实例、字典、序列,配合「守卫表达式」(case Point(x, y) if x == y) 非常强大。

2. 海象运算符 :=(3.8,但 3.10 才流行起来)

赋值的同时返回值,省掉一次重复调用:

import re

if (m := re.search(r"\d+", "订单号: 9527")) is not None:
print(f"提取到数字:{m.group()}") # 提取到数字:9527

3. 类型注解 X | Y(3.10)

终于不用再写 Union[int, str]Optional[int]

def find_user(user_id: int) -> dict | None:
if user_id == 1:
return {"name": "destiny"}
return None

# 等价于 Optional[dict] / Union[dict, None]

4. dataclass + slots(3.10)

一行 slots=True 让 dataclass 用 __slots__,省内存又禁止动态属性:

from dataclasses import dataclass

@dataclass(slots=True)
class Point:
x: float
y: float

p = Point(1.0, 2.0)
# p.z = 3.0 # AttributeError,防止手滑加错字段

5. f-string 嵌套与调试输出(3.12)

3.12 终于支持在 f-string 的 {} 里再写 f-string,=调试语法也更好用:

user = {"name": "destiny", "level": 5}
width = 12

# 嵌套 f-string
print(f"{f"{user['name']:^{width}}"}") # destiny

# 调试语法 = 直接打印变量名和值
print(f"{user=}") # user={'name': 'destiny', 'level': 5}

6. TaskGroup:结构化并发(3.11)

asyncio.gather 的更安全替代,子任务出错能一起取消:

import asyncio

async def fetch(url: str) -> str:
await asyncio.sleep(0.1)
return f"data from {url}"

async def main() -> None:
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(fetch("api/a"))
t2 = tg.create_task(fetch("api/b"))
print(t1.result(), t2.result())

asyncio.run(main())
注意

TaskGroup 一旦有任何子任务抛错,会取消其它所有子任务,并抛出 ExceptionGroup。这正是我们想要的行为——并发任务要么一起成功,要么一起失败。

7. ExceptionGroup / except*(3.11)

为并发场景设计的「一组异常一起处理」:

def risky(x: int) -> None:
if x < 0:
raise ValueError(f"负数:{x}")

try:
risky(-1)
risky(-2)
except* ValueError as eg:
for e in eg.exceptions:
print("处理:", e)

8. type 语句:定义类型别名(3.12)

TypeAlias 的语法糖,还能定义泛型别名:

type Vector = list[float]
type Matrix[T] = list[list[T]]

def norm(v: Vector) -> float:
return sum(x * x for x in v) ** 0.5

# 泛型别名
m: Matrix[int] = [[1, 2], [3, 4]]

9. pathlib.Path.walk()(3.12)

终于不用再写 os.walk,而且能拿到 dir_entry 做更高效过滤:

from pathlib import Path

root = Path(".")
for dirpath, dirnames, filenames in root.walk():
if "__pycache__" in dirnames:
dirnames.remove("__pycache__") # 剪枝,不再深入
for name in filenames:
if name.endswith(".py"):
print(dirpath / name)
提示

Path.walk() 还支持 top_down=False,自底向上遍历——删空目录时非常有用。

10. Self 类型 & override 装饰器(3.11/3.12)

写「返回自己类型」的方法不再痛苦:

from typing import Self, override

class Shape:
def scale(self, factor: float) -> Self:
return self # 子类调用会自动返回子类类型

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

@override
def scale(self, factor: float) -> Self:
self.r *= factor
return self

@override 会在父类没有同名方法时静态报错,防止手滑写错方法名。

小结

这 10 个特性里,真正改变我写代码习惯的是三个:match-case(替代 if-elif 链)、X | Y 类型注解(让类型写起来不痛苦)、TaskGroup(让并发代码更安全)。其余几个则是「锦上添花」,但堆在一起,已经让 Python 3.12 读起来像一门全新的语言。

如果你还在用 3.9 写代码,强烈建议升级到 3.12——体验的提升不只是「快一点」,而是「写法完全不同」。

我的 Python 学习路径规划

· 阅读需 4 分钟
Destiny
Python 学习者

学一门语言,最难的不是语法,而是不知道「下一步该学什么」。我最初学 Python 时,把官方教程从头读到尾,结果读完依旧写不出一个像样的项目。后来我换了思路:先建立地图,再按图索骥地走,效率立刻翻倍。这篇文章就是我的那张地图,也是本教程的章节骨架。

一、为什么需要一份「学习路径」

Python 的知识面非常宽:可以做 Web、可以搞数据、可以写脚本、可以搞运维。如果没有路径,很容易陷入「今天看一点爬虫,明天看一点 Django」的碎片化学习,半年下来什么都没学透。

备注

学习路径不是要把所有东西都学完,而是让你在任意时刻都知道「我现在在哪、下一步去哪」。一旦有了坐标系,每一份新知识都能挂到合适的分支上。

二、本教程的章节结构

我把整个学习过程拆成五个阶段,由浅入深:

  1. 基础语法:变量、数据类型、控制流、函数。目标是能独立写出 50 行以内的小工具。
  2. 面向对象与模块化:类、继承、组合、模块、包。理解 import 背后到底发生了什么。
  3. 进阶特性:装饰器、生成器、上下文管理器、类型注解。这是「写出 Pythonic 代码」的分水岭。
  4. 工程化能力:虚拟环境、依赖管理(uv)、测试(pytest)、项目结构。让你写的代码能给别人用。
  5. 领域深入:选一个方向(Web / 数据 / 自动化)深耕,把前面四阶段的知识用到真实场景里。
提示

不要跳过第 3 阶段。很多初学者觉得「能跑就行」,但装饰器和生成器恰恰是阅读优秀开源代码的门槛。跳过它们,你看不懂 FastAPI、看不懂 Django ORM,也写不出优雅的工具。

三、循序渐进的方法论

1. 用「输出」倒逼「输入」

每学一个特性,立刻写一段代码验证。比如学到 dataclass,就试着把家里的书架建模出来:

from dataclasses import dataclass, field

@dataclass(slots=True)
class Book:
title: str
author: str
tags: list[str] = field(default_factory=list)

def summary(self) -> str:
return f"《{self.title}》—— {self.author}"

shelf = [Book("流畅的 Python", "Luciano Ramalho"), Book("Python Cookbook", "David Beazley")]
for book in shelf:
print(book.summary())

写完这段代码,你对 dataclass、类型注解、field 默认值、slots 的理解会立刻立体起来。

2. 「三次重复」原则

一个知识点,至少在三个不同场景下用过,才算真正掌握。比如 pathlib.Path

  • 第一次:读一个配置文件;
  • 第二次:批量重命名目录下的图片;
  • 第三次:在单元测试里构造临时目录。
注意

最常见的失败模式是「教程疲劳」——连续看几个小时视频却不动手。我的建议:每 30 分钟必须写一段代码,哪怕只是把教程里的例子敲一遍。手感是练出来的,不是看出来的。

四、给学习者的三条建议

第一,版本不要太旧。 本教程默认 Python 3.12+,因为 match-casetype 语句、TaskGroup 这些特性会显著改变你的写法。在新版本上学习,能少走很多弯路。

第二,读源码比看教程更重要。 当你掌握基础后,挑一个体量适中的库(比如 richhttpx)通读它的源码,你会看到真正的工程代码长什么样。

第三,建立自己的「错误笔记本」。 每次踩坑都记录下来:报错信息、原因、解决方法。三个月后回看,你会发现自己已经迈过了一个又一个坎。

五、下一步

我会按上面的五阶段,逐步更新本教程的每一章。如果你也刚好在学 Python,欢迎跟着笔记一起走。我们慢慢来,但每一步都走扎实。

学习不是赛跑,是长途徒步。重要的不是今天走了多远,而是明天还想继续走。