Skip to content

进阶语法

推导式、生成器、装饰器、类型注解这几个语法不一定天天用,但看别人的代码、写稍大一点的项目时绕不开。先准备一份示例数据,后面所有例子都基于它:

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 关键字也能造生成器。yieldreturn 的区别在于: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 生成器配合聚合函数

sumanymax 这些内置函数能直接吃生成器:

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_higreet 指向同一个函数。

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                        # 返回新函数 wrapper

log_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、团队协作一次性脚本可以不写

这几个语法有个共同点:用了让代码更短更清楚才用,用了反而更难读就退回基础写法