V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
TongDu
V2EX  ›  Python

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

  •  1
     
  •   TongDu · 2022-10-20 13:38:37 +08:00 · 3159 次点击
    这是一个创建于 551 天前的主题,其中的信息可能已经有所发展或是发生改变。

    大家先看两个两个例子,比如使用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 居然不是内存地址,而是一个符号。这太奇怪了吧,想不通。

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

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

    函数式编程需要避免赋值这些有副作用的操作。
    TongDu
        4
    TongDu  
    OP
       2022-10-20 13:52:11 +08:00
    @siluni 我最开始也是那么理解的,但是前两个例子可以排除这样的情况。我想到了给函数传参,传入的应该就是内存地址。对于`(y[i] for i in x)`这样的生成器,x 应该是按照函数外传参进去的(第一第二个例子可佐证),而 y 不是传参进去的,而是属于在局部 local 域里面直接获取外部的变量。
    deplivesb
        5
    deplivesb  
       2022-10-20 13:56:04 +08:00   ❤️ 3
    clino
        6
    clino  
       2022-10-20 13:56:25 +08:00
    你先入为主了,这两种选择明显是取变量比取地址更自然
    siluni
        7
    siluni  
       2022-10-20 13:56:42 +08:00
    @TongDu 前两个例子是 iterable 的循环,但 range()生成的是 generator 。iterable 的值是一直在内存里的,generator 是调用 next()的时候才会计算的
    apake
        8
    apake  
       2022-10-20 14:17:08 +08:00   ❤️ 1
    并不是 惰性求值的问题, 两个例子都是 惰性求值. 楼主的意思是 generator 的执行环境 并不像 函数那样 封装了一个 闭包环境.
    whoami9894
        9
    whoami9894  
       2022-10-20 14:17:23 +08:00   ❤️ 7
    (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
        10
    MRlaopeng  
       2022-10-20 14:25:32 +08:00
    因为 range(2) 生成的 [0,1]
    amlee
        11
    amlee  
       2022-10-20 15:18:23 +08:00   ❤️ 5
    生成器表达式中的 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
        12
    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
        13
    crab  
       2022-10-20 15:44:33 +08:00
    @westoy 为什么再多打印一次直接空列表了。
    lucays
        14
    lucays  
       2022-10-20 15:53:29 +08:00   ❤️ 1
    @crab generator 只能用一次,实时吐完就没了
    milkpuff
        15
    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
        16
    nyxsonsleep  
       2022-10-20 22:02:57 +08:00   ❤️ 1
    @crab 生成器特性
    jurassic2long
        17
    jurassic2long  
       2022-10-21 10:50:24 +08:00
    这重点不在生成器,而是在:
    i for i in l

    l[i] for i in range(3)
    的区别
    brucmao
        18
    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
        19
    westoy  
       2022-10-21 11:47:19 +08:00
    @brucmao

    效果一样的

    我只是个人习惯把一次性的闭包参数做成默参传
    zlstone
        20
    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]
    ```
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5422 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 07:19 · PVG 15:19 · LAX 00:19 · JFK 03:19
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.