Python 的生成器太奇怪了吧,传递的居然不是变量的内存地址指针,而是变量符号

2022-10-20 13:38:37 +08:00
 TongDu

大家先看两个两个例子,比如使用filter过滤出偶数

l = [1,2,3]
lf1 = filter(lambda x: x%2==0, l)
l = [4,5,6]
lf2 = filter(lambda x: x%2==0, l)
print(list(lf1)) # 结果 >>> [2]
print(list(lf2)) # 结果 >>> [4, 6]

比如计算平方

l = [1,2,3]
lf1 = (i**2 for i in l)
l = [4,5,6]
lf2 = (i**2 for i in l)
print(list(lf1)) # 结果 >>> [1, 4, 9]
print(list(lf2)) # 结果 >>> [16, 25, 36]

重点来了,如果我用这个切片,取出前两个元素,大家觉得结果是什么?

l = 'ABC'
l_f1 = (l[i] for i in range(2))
l = 'DEF'
l_f2 = (l[i] for i in range(2))
print(list(l_f1))
print(list(l_f2))

居然都是

['D', 'E']
['D', 'E']

这种情况下,传递给生成器(...)的变量 l 居然不是内存地址,而是一个符号。这太奇怪了吧,想不通。

3351 次点击
所在节点    Python
20 条回复
siluni
2022-10-20 13:46:06 +08:00
应该不是传了符号,而是 generator 本身是 lazy 的,在调用 list(l_f1)的时候才真正执行了运算,这时 l 已经指向第二个值了
lambdaq
2022-10-20 13:47:48 +08:00
l_f1 = 和 l_f2 = 后面如果是「小括号」是惰性求值的,你不 print 它就不计算。改成「方括号」就没问题了。

基础姿势。
stein42
2022-10-20 13:48:25 +08:00
生成器里面第一个表达式是延迟求值的。
相当于:
map(lambda i: l[i], range(2))
每次计算会去读 l 的值。

函数式编程需要避免赋值这些有副作用的操作。
TongDu
2022-10-20 13:52:11 +08:00
@siluni 我最开始也是那么理解的,但是前两个例子可以排除这样的情况。我想到了给函数传参,传入的应该就是内存地址。对于`(y[i] for i in x)`这样的生成器,x 应该是按照函数外传参进去的(第一第二个例子可佐证),而 y 不是传参进去的,而是属于在局部 local 域里面直接获取外部的变量。
deplivesb
2022-10-20 13:56:04 +08:00
clino
2022-10-20 13:56:25 +08:00
你先入为主了,这两种选择明显是取变量比取地址更自然
siluni
2022-10-20 13:56:42 +08:00
@TongDu 前两个例子是 iterable 的循环,但 range()生成的是 generator 。iterable 的值是一直在内存里的,generator 是调用 next()的时候才会计算的
apake
2022-10-20 14:17:08 +08:00
并不是 惰性求值的问题, 两个例子都是 惰性求值. 楼主的意思是 generator 的执行环境 并不像 函数那样 封装了一个 闭包环境.
whoami9894
2022-10-20 14:17:23 +08:00
(i**2 for i in l):

l 是创建生成器时闭包捕获的,i ** 2 是 f.next() 时计算的

***

(l[i] for i in range(2)):

闭包没有捕获任何变量,计算 l[i] 时从外部作用域获取 l

***

两个表达式对应的 opcode ,注意看 LOAD opcode

```
2 8 LOAD_NAME 1 (list)
10 LOAD_CONST 1 (<code object <genexpr> at 0x10256d3a0, file "<dis>", line 2>)
12 LOAD_CONST 2 ('<genexpr>')
14 MAKE_FUNCTION 0
16 LOAD_NAME 0 (l)
18 GET_ITER
20 CALL_FUNCTION 1
22 CALL_FUNCTION 1
24 POP_TOP

3 26 LOAD_NAME 1 (list)
28 LOAD_CONST 3 (<code object <genexpr> at 0x10256d450, file "<dis>", line 3>)
30 LOAD_CONST 2 ('<genexpr>')
32 MAKE_FUNCTION 0
34 LOAD_NAME 2 (range)
36 LOAD_CONST 4 (2)
38 CALL_FUNCTION 1
40 GET_ITER
42 CALL_FUNCTION 1
44 CALL_FUNCTION 1
46 POP_TOP
48 LOAD_CONST 5 (None)
50 RETURN_VALUE

Disassembly of <code object <genexpr> at 0x10256d3a0, file "<dis>", line 2>:
2 0 LOAD_FAST 0 (.0)
>> 2 FOR_ITER 14 (to 18)
4 STORE_FAST 1 (i)
6 LOAD_FAST 1 (i)
8 LOAD_CONST 0 (2)
10 BINARY_POWER
12 YIELD_VALUE
14 POP_TOP
16 JUMP_ABSOLUTE 2
>> 18 LOAD_CONST 1 (None)
20 RETURN_VALUE

Disassembly of <code object <genexpr> at 0x10256d450, file "<dis>", line 3>:
3 0 LOAD_FAST 0 (.0)
>> 2 FOR_ITER 14 (to 18)
4 STORE_FAST 1 (i)
6 LOAD_GLOBAL 0 (l)
8 LOAD_FAST 1 (i)
10 BINARY_SUBSCR
12 YIELD_VALUE
14 POP_TOP
16 JUMP_ABSOLUTE 2
>> 18 LOAD_CONST 0 (None)
20 RETURN_VALUE

```
MRlaopeng
2022-10-20 14:25:32 +08:00
因为 range(2) 生成的 [0,1]
amlee
2022-10-20 15:18:23 +08:00
生成器表达式中的 for 子句是立即计算的,这时候有闭包环境。

但是生成器表达式的小括号一开头的那个表达式是惰性计算的。

#这时候闭包的 i 变量存的是 l 中的值,表达式 i**2 是惰性计算的,放入 list()的时候才计算
lf1 = (i**2 for i in l)

#这时候闭包的 i 变量存的是 [0, 1] 中的值,表达式 l[i] 是惰性计算的,放入 list()的时候才计算
lf2 = (l[i] for i in range(2))


所以,在执行 print(list(lf2)) 之前,l 被重新赋值了,就会出现 op 说的情况。

这就是函数式编程为什么强调 immutable
westoy
2022-10-20 15:32:56 +08:00
>>> l = 'ABC'
>>> l_f1 = (lambda l=l: (l[i] for i in range(2)))()
>>> l = 'DEF'
>>> l_f2 = (lambda l=l: (l[i] for i in range(2)))()
>>> print(list(l_f2))
['D', 'E']
>>> print(list(l_f1))
['A', 'B']
crab
2022-10-20 15:44:33 +08:00
@westoy 为什么再多打印一次直接空列表了。
lucays
2022-10-20 15:53:29 +08:00
@crab generator 只能用一次,实时吐完就没了
milkpuff
2022-10-20 18:28:30 +08:00
>>> (c[i] for i in range(5))
<generator object <genexpr> at 0x000002A859CD4820>
>>> c
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined

当没有变量 c 时,也能正确创建 generator
nyxsonsleep
2022-10-20 22:02:57 +08:00
@crab 生成器特性
jurassic2long
2022-10-21 10:50:24 +08:00
这重点不在生成器,而是在:
i for i in l

l[i] for i in range(3)
的区别
brucmao
2022-10-21 11:22:16 +08:00
@westoy 看不明白 lambda l=l ,这里默认参数为啥这样传
```
l = "ABC"
l_f1 = (lambda l: (l[i] for i in range(2)))(l)

l = "DEF"
l_f2 = (lambda l: (l[i] for i in range(2)))(l)
print(list(l_f2))
print(list(l_f1))
```
westoy
2022-10-21 11:47:19 +08:00
@brucmao

效果一样的

我只是个人习惯把一次性的闭包参数做成默参传
zlstone
2022-10-21 12:03:32 +08:00
python 不存在取地址,底层一直传的就是 c struct 本身,只是因为 struct 里面存的是一个地址,所以看起来像是取地址的行为。

第一个示例传的就是两个不同的 list ,所以 filter 出来的东西是不一样的,如果这么写,这两个结果就是相同的
```
l = [1,2,3]
lf1 = filter(lambda x: x%2==0, l)
l.append(4)
lf2 = filter(lambda x: x%2==0, l)
print(list(lf1)) # 结果 >>> [2, 4]
print(list(lf2)) # 结果 >>> [2, 4]
```

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/888402

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX