Appearance
进阶语法
推导式、生成器、装饰器、类型注解这几个语法不一定天天用,但看别人的代码、写稍大一点的项目时绕不开。先准备一份示例数据,后面所有例子都基于它:
python
# 一份文章列表,后面推导式、生成器、装饰器的例子都用它
articles = [
{"title": "Python 入门", "author": "张三", "status": "已发布", "content": "Python 是一门..."},
{"title": "Go 并发", "author": "李四", "status": "草稿", "content": "Go 的 goroutine..."},
{"title": "Rust 内存安全", "author": "张三", "status": "已发布", "content": "Rust 的所有权..."},
]每个元素是一个字典,有 title(标题)、author(作者)、status(状态)、content(正文)四个键。
一、推导式
从一个列表生成另一个列表,传统写法是空列表 + for 循环 + append:
python
# 传统写法
titles = []
for article in articles:
titles.append(article["title"])
# titles = ['Python 入门', 'Go 并发', 'Rust 内存安全']
# 推导式:一行搞定
titles = [article["title"] for article in articles]读法从左到右:article["title"] 是要放进新列表的东西,for article in articles 是从哪取。连起来就是"对 articles 里每个 article,取它的 title,组成新列表"。
带条件筛选——只取已发布的:
python
published = [a["title"] for a in articles if a["status"] == "已发布"]
# published = ['Python 入门', 'Rust 内存安全']if 在最后,把不满足条件的元素过滤掉。
1 字典推导式
花括号加冒号,生成字典:
python
# {标题: 状态} 的字典
status_map = {a["title"]: a["status"] for a in articles}
# {'Python 入门': '已发布', 'Go 并发': '草稿', 'Rust 内存安全': '已发布'}2 集合推导式
花括号没冒号,生成集合(自动去重):
python
# 所有作者,去重
authors = {a["author"] for a in articles}
# {'张三', '李四'}推导式适合简单的一行转换。逻辑一旦复杂——嵌套循环、多个 if、循环体里还要打印日志——写成普通 for 循环更清楚。一行推导式塞三四个条件,过两个月自己都读不懂。
二、生成器
列表推导式 [... for ...] 会一次性把所有结果算出来放进内存。数据量小没问题,但如果是几十万行日志、上百万个数字,内存吃不消。
生成器把方括号换成圆括号 (... for ...),不提前算完,用到一个算一个:
python
# 列表:一次性算出 100 万个值,全放内存
squares_list = [x * x for x in range(1000000)]
# 生成器:不算,等着被问才算
squares_gen = (x * x for x in range(1000000))生成器不像列表那样能直接 print 看到全部内容。要取值,用内置函数 next()——它的作用就是"从生成器里取下一个值":
python
print(next(squares_gen)) # 0,算出第一个
print(next(squares_gen)) # 1,算出第二个
print(next(squares_gen)) # 4,算出第三个每调一次 next(),生成器才算一个值给你。它像流水线——你要一个我算一个,不提前囤货。文件几个 G 的场景下,这个特性能避免内存爆掉。
1 yield——函数变生成器
除了圆括号写法,函数里用 yield 关键字也能造生成器。yield 跟 return 的区别在于:return 返回后函数就结束了;yield 返回一个值但暂停函数,下次再要值时从暂停的位置继续:
python
def read_lines(path):
"""逐行读文件,每次只交出一行。"""
with open(path, encoding="utf-8") as f:
for line in f:
yield line.strip() # 交出这一行,暂停
# for 循环会自动反复调 next(),直到生成器没值了
for line in read_lines("notes.txt"):
print(line)for line in read_lines("notes.txt") 看着像普通循环,但底层每次迭代都在调 next()——生成器交出一行,循环体处理一行,处理完再要下一行。内存里始终只有一行,不管文件多大。
2 生成器配合聚合函数
sum、any、max 这些内置函数能直接吃生成器:
python
# 所有文章正文的字数总和
total = sum(len(a["content"]) for a in articles)
# 有没有文章正文超过 10 个字
has_long = any(len(a["content"]) > 10 for a in articles)len(a["content"]) for a in articles 就是一个生成器(圆括号被 sum() 和 any() 省略了)。好处是不用先建一个列表再求和,省内存。
三、函数是一等对象
这一节是装饰器的前置知识——在 Python 里,函数跟整数、字符串一样,能赋值给变量、能当参数传、能当返回值。理解了这一点,装饰器才讲得通。
1 函数能赋值给变量
python
def greet(name):
return f"hello {name}"
say_hi = greet # 把函数赋给另一个变量
print(say_hi("张三")) # hello 张三greet 后面不加括号,得到的是函数本身(不是调用结果)。say_hi 和 greet 指向同一个函数。
2 函数能当参数传
函数能传给另一个函数:
python
def call_twice(func, value):
"""把 func 对 value 调用两次。"""
return func(value), func(value)
result = call_twice(greet, "张三")
print(result) # ('hello 张三', 'hello 张三')call_twice 接收一个函数 func 当参数,在内部调用它。接收函数当参数的函数,叫高阶函数。
3 函数能当返回值
函数里也能造一个新函数返回出去:
python
def make_greeter(prefix):
"""根据 prefix 造一个打招呼的函数。"""
def greeter(name):
return f"{prefix} {name}"
return greeter # 把内部函数返回出去
shout = make_greeter("HELLO")
print(shout("张三")) # HELLO 张三
whisper = make_greeter("psst")
print(whisper("张三")) # psst 张三make_greeter 返回的是它内部定义的 greeter 函数。每次调 make_greeter("HELLO"),都得到一个带固定 prefix 的新函数。
这三条(赋值、当参数、当返回值)是理解装饰器的地基。装饰器本质就是"接收一个函数、返回一个新函数"的高阶函数。
四、装饰器
装饰器在不改动原函数代码的前提下,给它套一层额外逻辑。先看最简单的例子——一个记录调用日志的装饰器:
python
def log_call(func):
"""接收一个函数,返回一个包了日志逻辑的新函数。"""
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__},参数 {args}")
result = func(*args, **kwargs) # 调用原函数
print(f"{func.__name__} 执行完,结果 {result}")
return result
return wrapper # 返回新函数 wrapperlog_call 做的事:接收原函数 func,在内部定义 wrapper(先打印日志、再调原函数、再打印结果),最后返回 wrapper。
@ 语法糖是应用装饰器的简写:
python
@log_call
def format_article(title, author):
return f"{title} —— {author}"
# @log_call 等价于这一行:
# format_article = log_call(format_article)
print(format_article("Python 入门", "张三"))
# 调用 format_article,参数 ('Python 入门', '张三')
# format_article 执行完,结果 Python 入门 —— 张三
# Python 入门 —— 张三@log_call 放在 def format_article 上面,等价于定义完之后执行 format_article = log_call(format_article)——用包装后的 wrapper 替换原来的函数。之后每次调 format_article,实际跑的是 wrapper(先日志、再原函数、再日志)。
装饰器最常见的用途是日志、计时、重试、权限检查这些横切逻辑——十几个函数都需要"执行前记日志",装饰器写一遍,到处复用。
1 带参数的装饰器
上面的 @log_call 不带参数。如果想让装饰器自己接收参数(比如 @retry(times=3)),需要多嵌套一层。这个三层结构是装饰器里最绕的部分,逐层拆开看:
python
def retry(times=3):
"""第一层:接收装饰器的参数(times)。"""
def decorator(func):
"""第二层:接收被装饰的函数(func)。"""
def wrapper(*args, **kwargs):
"""第三层:实际执行的替换函数,包含重试逻辑。"""
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as exc:
print(f"第 {attempt} 次失败:{exc}")
if attempt < times:
import time
time.sleep(1)
raise RuntimeError(f"重试 {times} 次后仍失败")
return wrapper # 第二层返回第三层
return decorator # 第一层返回第二层三层各自干什么:
- 第一层
retry:接收装饰器的参数(times=3),返回一个真正的装饰器decorator - 第二层
decorator:接收被装饰的函数func,返回包装函数wrapper - 第三层
wrapper:实际替换原函数的那个,里面有重试逻辑
python
@retry(times=3)
def fetch_article(article_id):
"""从接口拉文章,可能失败。"""
...
# 等价于:
# fetch_article = retry(times=3)(fetch_article)@retry(times=3) 先调 retry(times=3) 拿到 decorator,再用 decorator 包装 fetch_article。这样 wrapper 里就能用到 times 这个参数了。
这个三层结构第一遍看会觉得绕,写两三次就顺了。关键是记住:多一层是为了让装饰器自己也能接收参数。
五、类型注解
Python 变量不需要声明类型,但给函数参数和返回值加上类型注解,代码自带说明书:
python
def format_article(title: str, author: str) -> str:
return f"{title} —— {author}"title: str 表示参数应该是字符串,-> str 表示返回字符串。类型注解不影响运行——Python 运行时不检查类型,传个整数进去也不会报错。它的作用是给人看、给编辑器看(编辑器能据此提示和检查)。
1 常用类型
python
# 基本类型
name: str = "张三"
count: int = 0
ratio: float = 0.95
enabled: bool = True
# 列表、字典
articles: list[dict] = [{"title": "..."}]
title_to_status: dict[str, str] = {"Python 入门": "已发布"}
# 可以是多种类型(Python 3.10+)
def find_article(article_id: int) -> dict | None:
...dict | None 表示"返回字典或 None"——调用方一眼就知道要用 if result is None 判断。
2 为什么写类型注解
不写也能跑。但写了之后,函数签名就是一份接口文档——不用读函数体就知道要传什么、返回什么。现代 Python 项目(FastAPI、Pydantic)大量依赖类型注解做数据校验,这个习惯越早养成越省事。
六、这些语法什么时候用
| 语法 | 适合场景 | 别滥用 |
|---|---|---|
| 推导式 | 一行的简单转换、筛选 | 嵌套三层、带多个 if 时改回普通循环 |
| 生成器 | 大数据逐个处理、读大文件 | 数据量小用列表更直观 |
| 装饰器 | 日志、计时、重试这类横切逻辑 | 业务逻辑别塞进装饰器,难追 |
| 类型注解 | 函数签名、公共 API、团队协作 | 一次性脚本可以不写 |
这几个语法有个共同点:用了让代码更短更清楚才用,用了反而更难读就退回基础写法。