38. 理解

推导式是一种紧凑的语法,用于从另一个可迭代对象构建容器或类似生成器的迭代器。 CPython 将它们实现为具有自己的执行范围的编译代码对象。

常见形式:python [x * 2 for x in xs] {x * 2 for x in xs} {x: x * 2 for x in xs} (x * 2 for x in xs) 这些对应于:```text list comprehension set comprehension dict comprehension generator expression


## 38.1 列表理解

列表理解急切地构建一个列表。```python
ys = [x * 2 for x in xs]
```从概念上讲:```python
ys = []
for x in xs:
    ys.append(x * 2)
```结果是一个包含所有生成值的列表对象。

源形式更短,但运行时工作仍然是迭代、表达式求值和附加操作。

## 38.2 过滤

理解可以包括`if`条款。```python
ys = [x * 2 for x in xs if x > 0]
```从概念上讲:```python
ys = []
for x in xs:
    if x > 0:
        ys.append(x * 2)
```过滤器针对每个项目运行。如果条件为 false,则元素表达式不会针对该项目运行。

## 38.3 多个`for`条款

推导式可以包含嵌套循环。```python
pairs = [(x, y) for x in xs for y in ys]
```从概念上讲:```python
pairs = []
for x in xs:
    for y in ys:
        pairs.append((x, y))
```顺序是从左到右,与嵌套循环顺序匹配。

为了:```python
[(x, y, z) for x in xs for y in ys for z in zs]
```概念上的循环嵌套是:```python
result = []
for x in xs:
    for y in ys:
        for z in zs:
            result.append((x, y, z))
```## 38.4 多重过滤器

过滤器附加到它们出现的循环级别。```python
result = [x for x in xs if x > 0 if x % 2 == 0]
```从概念上讲:```python
result = []
for x in xs:
    if x > 0:
        if x % 2 == 0:
            result.append(x)
```具有多个循环:```python
result = [(x, y) for x in xs if x > 0 for y in ys if y > x]
```从概念上讲:```python
result = []
for x in xs:
    if x > 0:
        for y in ys:
            if y > x:
                result.append((x, y))
```顺序很重要,因为后面的子句可以使用前面子句绑定的名称。

## 38.5 理解范围

 Python 3 中,推导式有其自己的作用域。```python
x = 100
ys = [x for x in range(3)]
print(x)
```输出:```text
100
````x`内部推导式不会覆盖外部推导式`x`

从概念上讲,推导式的行为就像一个小的嵌套函数:```python
def _listcomp(iterable):
    result = []
    for x in iterable:
        result.append(x)
    return result

ys = _listcomp(range(3))
```这不是精确的源转换,但它解释了范围。

## 38.6 为什么推导式有自己的代码对象

推导式需要一个地方来存储其循环变量,而不会将它们泄漏到周围的作用域中。

CPython 通过将许多推导式编译成嵌套代码对象来解决这个问题。

例子:```python
def f(xs):
    return [x * 2 for x in xs]
```外部函数有一个代码对象。列表推导式有另一个代码对象存储在外部代码对象的常量中。

您可以检查一下:```python
def f(xs):
    return [x * 2 for x in xs]

for const in f.__code__.co_consts:
    print(type(const), const)
```一个常量通常是一个用于理解的嵌套代码对象。

## 38.7 反汇编列表理解

使用`dis`:

```python
import dis

def f(xs):
    return [x * 2 for x in xs]

dis.dis(f)
```然后检查嵌套的代码对象:```python
for const in f.__code__.co_consts:
    if hasattr(const, "co_code"):
        dis.dis(const)
```你会看到两层:```text
outer function:
    create comprehension function
    get iterator from xs
    call comprehension function
    return list

inner comprehension:
    build list
    iterate input
    compute x * 2
    append to list
    return list
```确切的字节码在 CPython 版本中发生变化,但形状保持不变。

## 38.8 列表追加优化

列表理解通常使用专门的列表附加字节码路径。

从概念上讲:```python
result.append(value)
``` CPython 可以避免每个追加的普通方法查找。

而不是对每个项目执行此操作:```text
load result.append
call append
```理解体可以使用内部附加操作。

从概念上讲:```text
LIST_APPEND
```这节省了重复的属性查找和方法调用的开销。

这是列表推导式通常比等效的 Python 级循环更快的原因之一`append`

## 38.9 集合理解

集合理解热切地构建集合。```python
unique = {x.lower() for x in words}
```从概念上讲:```python
unique = set()
for x in words:
    unique.add(x.lower())
```结果包含根据正常集合散列和相等的唯一元素。

集合推导式在内部使用集合添加行为,类似于列表推导式使用追加行为。

## 38.10 字典理解

字典理解热切地构建字典。```python
index = {item.id: item for item in items}
```从概念上讲:```python
index = {}
for item in items:
    index[item.id] = item
```如果出现重复的键,则后面的值会覆盖前面的值:```python
d = {x % 2: x for x in range(5)}
print(d)
```输出:```text
{0: 4, 1: 3}
```理解遵循正常的字典分配语义。

## 38.11 生成器表达式

生成器表达式是惰性的。```python
g = (x * 2 for x in xs)
```它不会立即建立一个列表。它创建一个类似生成器的对象,在迭代时计算值。```python
g = (x * 2 for x in range(3))

print(next(g))
print(next(g))
print(next(g))
```输出:```text
0
2
4
```筋疲力尽后,它会上升`StopIteration`

## 38.12 生成器表达式与列表理解

比较:```python
[x * 2 for x in xs]
```和:```python
(x * 2 for x in xs)
```|特色|列表理解 |生成器表达式 |
|---|---|---|
|评价|渴望|懒惰|
|结果 |列表 |生成器对象 |
|内存|存储所有结果 |存储执行状态 |
|迭代|可以多次迭代结果 |一击|
|语法 |方括号|括号|
|使用案例|现在需要所有值 |流值|

当您需要完整列表时,列表理解通常会更快。

当您想要流式传输值或提前停止时,生成器表达式通常更好。

## 38.13 生成器表达式的一次性性质

生成器表达式被消耗一次。```python
g = (x for x in range(3))

print(list(g))
print(list(g))
```输出:```text
[0, 1, 2]
[]
```第一个`list(g)`耗尽它。

如果您需要可重用的数据,请构建一个列表或另一个容器。

## 38.14 提前停止

生成器表达式对于提前停止的消费者很有用。```python
first = next(x for x in xs if x > 100)
```这只计算直到找到第一个匹配元素。

列表理解版本:```python
first = [x for x in xs if x > 100][0]
```在选择第一项之前构建完整的匹配列表。

对于大或无限输入,生成器表达式是正确的模型。

## 38.15 推导式和闭包

推导式可以捕获外部变量。```python
def scale(xs, factor):
    return [x * factor for x in xs]
```理解使用`factor`来自外部函数。

从概念上讲:```text
outer function frame:
    factor stored in local or cell

comprehension code object:
    reads factor as free variable
```当推导式需要访问外部作用域变量时,编译器会安排闭包单元。

## 38.16 循环变量绑定

循环变量属于理解范围。```python
def f():
    x = "outer"
    ys = [x for x in range(3)]
    return x, ys

print(f())
```输出:```text
('outer', [0, 1, 2])
```领悟里面,`x`是理解代码对象的局部。

外层`x`保持不变。

## 38.17 推导式中的赋值表达式

赋值表达式可以出现在推导式中。```python
result = [y for x in xs if (y := f(x)) > 0]
```绑定规则很微妙。赋值表达式绑定在包含范围内,而不是像循环变量那样绑定在隐式理解范围内。

例子:```python
def f(xs):
    result = [y for x in xs if (y := x * 2) > 3]
    return y, result
```领悟之后,`y`如果至少发生了一次赋值,则在包含函数作用域中可能是可见的。

存在此行为是因为赋值表达式旨在使指定的名称在某些表达式本地上下文之外可用。

## 38.18 嵌套推导式

一个推导式可以包含另一个推导式。```python
matrix = [[i * j for j in range(3)] for i in range(3)]
```从概念上讲:```python
matrix = []
for i in range(3):
    row = []
    for j in range(3):
        row.append(i * j)
    matrix.append(row)
```每个推导式都有自己的代码对象和范围。

因此,嵌套推导式可以创建嵌套的类似函数的执行层。

## 38.19 字典推导式

迭代字典会产生键。```python
keys = [k for k in d]
```使用值:```python
values = [v for v in d.values()]
```使用键值对:```python
pairs = [(k, v) for k, v in d.items()]
```常见的变换:```python
inverted = {v: k for k, v in d.items()}
```如果值重复,则后面的键会覆盖前面的键,因为字典键必须是唯一的。

## 38.20 理解和评估顺序

理解子句从左到右执行。```python
[(x, y) for x in xs for y in f(x)]
```对于每个`x`, `f(x)`被评估以产生内部可迭代。

从概念上讲:```python
result = []
for x in xs:
    for y in f(x):
        result.append((x, y))
```这意味着后面的子句可以依赖于前面的循环变量。

仅当该输出值的所有循环和过滤子句都成功后,元素表达式才会运行。

## 38.21 副作用

推导式可能包含副作用,但通常应该用于生成值。

可能但风格不佳:```python
[print(x) for x in xs]
```这会构建一个列表`None`值只是为了执行打印。

更喜欢:```python
for x in xs:
    print(x)
```当结果很重要时使用理解式。

## 38.22 推导式中的异常

异常会正常传播。```python
result = [10 / x for x in xs]
```如果`x`为零,`ZeroDivisionError`传播并且理解停止。

除非在其他地方引用,否则部分构建的内部容器将被丢弃,而普通理解内部不会公开这些容器。

对于生成器表达式,异常会延迟发生:```python
g = (10 / x for x in xs)
```创造`g`不分裂。当请求有问题的项目时,会发生异常。

## 38.23 理解和`try`推导式不允许诸如以下的语句`try`直接在它们里面。

无效的:```python
[x for x in xs try ...]
```使用辅助函数:```python
def parse_or_none(x):
    try:
        return int(x)
    except ValueError:
        return None

values = [y for x in xs if (y := parse_or_none(x)) is not None]
```或者当异常处理是核心时使用普通循环:```python
values = []
for x in xs:
    try:
        values.append(int(x))
    except ValueError:
        pass
```## 38.24 异步推导式

里面`async def`, 推导式可以使用`async for````python
async def collect(stream):
    return [item async for item in stream]
```从概念上讲:```python
result = []
async for item in stream:
    result.append(item)
return result
```他们还可以使用`await`在元素表达式或过滤器中:```python
async def collect(xs):
    return [await process(x) for x in xs]
```异步理解编译为异步感知字节码,并可能在执行期间挂起。

## 38.25 异步生成器表达式

异步生成器表达式可以使用`async for````python
gen = (item async for item in stream)
```它产生一个类似异步生成器的对象,使用`async for`或者`anext````python
async for item in gen:
    ...
```执行模型将理解范围与异步迭代和协程挂起相结合。

## 38.26 理解和`locals()`因为理解有其自身的范围,`locals()`在类似理解的帮助程序中,看到的是理解局部变量,而不是周围的局部变量。

使用辅助函数比使用直接语法更容易看到这一点,因为推导式限制了语句。

重要规则:```text
loop variables in comprehensions do not leak into the surrounding scope
```对于普通代码,依赖该规则而不是细节`locals()`在实现创建的框架内。

## 38.27 推导式和后期绑定

推导式内部的闭包仍然可以显示后期绑定行为。```python
funcs = [lambda: x for x in range(3)]
print([f() for f in funcs])
```输出:```text
[2, 2, 2]
```每个 lambda 都以相同的理解变量结束`x`,其最终值为`2`

使用默认参数来捕获当前值:```python
funcs = [lambda x=x: x for x in range(3)]
print([f() for f in funcs])
```输出:```text
[0, 1, 2]
```理解范围可以防止向外泄漏,但它不会为闭包的每次迭代创建新的绑定。

## 38.28 推导式和参考生命周期

列表推导式在完成后不会保持其框架处于活动状态,除非有东西捕获它。

但是生成器表达式在暂停时保持其类似框架的状态处于活动状态。```python
g = (x * 2 for x in range(10))
```生成器表达式成立:```text
code object
iteration state
current iterator
locals
suspended frame state
```如果它捕获了一个大对象,该对象可能会保持活动状态,直到生成器耗尽或被丢弃。```python
def f():
    big = bytearray(100_000_000)
    return (x for x in range(3) if big is not None)

g = f()
```这里,`big`通过生成器表达式闭包保持活动状态。

## 38.29 理解和表现

列表推导式通常比等效循环更快,因为 CPython 可以使用专门的内部操作。

例子:```python
result = []
for x in xs:
    result.append(x * 2)
```与以下相比:```python
result = [x * 2 for x in xs]
```该推导式可以避免重复的 Python 级别的方法查找`append`

然而,性能取决于表达式、数据大小、Python 版本以及惰性是否重要。

通则:```text
use list/set/dict comprehensions when building that container directly
use generator expressions when streaming or stopping early
use explicit loops when control flow is complex
```## 38.30 理解力和可读性

当理解适合一个简单的转换时,它们是最清晰的。

好的:```python
names = [user.name for user in users]
```好的:```python
active = [user for user in users if user.active]
```通常过于密集:```python
result = [(a, b, c) for a in xs if p(a) for b in f(a) if q(b) for c in g(a, b) if r(c)]
```当存在许多子句、副作用、异常处理或复杂分支时,请使用显式循环。

## 38.31 CPython 对象流

对于列表理解:```python
[x * 2 for x in xs]
```CPython 从概念上执行:```text
create result list
get iterator from xs
loop:
    get next item
    store item in comprehension local x
    load x
    load constant 2
    multiply
    append to result list
return result list
```列表对象在构建时保存在理解框架内。

对于字典理解:```python
{x: x * 2 for x in xs}
```流程是:```text
create result dict
iterate xs
compute key
compute value
store key-value pair
return dict
```## 38.32 理解代码对象名称

理解代码对象具有内部名称,例如:```text
<listcomp>
<setcomp>
<dictcomp>
<genexpr>
```你可以在回溯和内省中看到它们。

例子:```python
def f(xs):
    return [10 / x for x in xs]

f([2, 1, 0])
```回溯可能包括`<listcomp>`因为异常发生在理解代码对象内部。

这表明理解执行有其自己的框架式上下文。

## 38.33 推导式中的回溯

如果异常发生在推导式内部,则回溯可以包括外部函数和推导式。```python
def f(xs):
    return [10 / x for x in xs]

f([1, 0])
```除零发生在理解代码内部。

从概念上讲:```text
frame f
    calls <listcomp>
        division by zero
```这是理解代码对象的另一个明显效果。

## 38.34 理解变量的生命周期

理解范围内存在理解循环变量。

完成后:```python
def f():
    result = [x for x in range(3)]
    return "x" in locals()

print(f())
```这将返回:```text
False
```循环变量`x`没有成为当地人`f`

在理解框架内,`x`在理解运行时就存在。

## 38.35 生成器表达式参数快捷方式

生成器表达式可以作为唯一的参数传递给函数,而无需额外的括号。```python
total = sum(x * x for x in xs)
```这相当于:```python
total = sum((x * x for x in xs))
```但如果有多个参数,则需要括号:```python
result = func((x for x in xs), other)
```这是语法级别的便利。运行时对象仍然是一个生成器表达式。

## 38.36 推导式和内置函数

推导式通常与内置函数配对。

示例:```python
sum(x for x in xs)
any(x > 0 for x in xs)
all(x.valid for x in items)
max(score(x) for x in xs)
```这些使用生成器表达式,并且在某些情况下可以提前停止。`any`在第一个真值处停止。`all`在第一个假值处停止。`sum`消耗整个发电机。

选择生成器表达式可以避免构建不必要的中间列表。

## 38.37 常见误解

|误会 |正确型号 |
|---|---|
|理解只是在相同范围内进行语法重写 |它通常有自己的嵌套代码对象和范围 |
|循环变量泄漏到外部作用域 | Python 3 中,它没有 |
|生成器表达式构建一个元组 |它创建一个生成器对象 |
|列表理解总是更好 |生成器表达式更适合流式传输和提前停止 |
|推导式无法捕获外部变量 |他们可以通过闭包捕获|
|推导式中的每个 lambda 捕获不同的循环变量 |它们通常共享相同的理解变量绑定 |
|创建生成器表达式时发生异常 |当它被消耗时它们就会发生|
|字典理解保留重复的键 |后面的值会覆盖前面的值 |

## 38.38 阅读策略

从以下开始:```python
def f(xs):
    return [x * 2 for x in xs if x > 0]
```检查:```python
import dis

dis.dis(f)

for const in f.__code__.co_consts:
    if hasattr(const, "co_code"):
        print(const.co_name)
        dis.dis(const)
```然后比较:```python
def g(xs):
    return (x * 2 for x in xs if x > 0)
```追踪:```text
outer function bytecode
nested comprehension code object
iteration setup
local loop variable
filter jump
append or yield operation
return value
closure variables
```然后学习集合、字典、嵌套和异步理解。

## 38.39 章节总结

推导式是用于构建列表、集合、字典或类似生成器的迭代器的编译执行单元。它们以表达式的形式结合了迭代、过滤、表达式求值、绑定和容器构造。

核心模型是:```text
evaluate outer iterable
    
create comprehension execution scope
    
iterate
    
apply filters
    
compute element, key-value pair, or yielded value
    
append, add, store, or yield
    
return container or generator object
```列表、集合和字典理解是热切的。生成器表达式是惰性的。 CPython 使用嵌套代码对象、帧状态、闭包处理、专用字节码操作和正常异常传播来实现这些构造。