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
tabris17
V2EX  ›  Python

Python 闭包不支持修改 upvalue,有什么替代的解决方案?

  •  
  •   tabris17 · 2016-01-19 09:20:31 +08:00 · 6347 次点击
    这是一个创建于 3231 天前的主题,其中的信息可能已经有所发展或是发生改变。

    如下代码:

    def test_closure():
        x = 1
        def closure():
            x = 2
        closure()
        print x
    test_closure()
    

    x 仍然是 1

    第 1 条附言  ·  2016-01-19 10:13:37 +08:00
    总结一下:

    1 、通过调用栈去查找上层函数的本地变量。这个好像闭包执行时上下文环境不在申明的上下文环境就不行了
    2 、用引用类型将 upvalue 封装一下。比如: list 、 dict 、 object 。
    3 、将 upvalue 作为函数的属性
    第 2 条附言  ·  2016-01-19 10:14:27 +08:00
    漏了
    4 、 python3 的 nonlocal 关键字
    67 条回复    2016-01-19 23:03:00 +08:00
    est
        1
    est  
       2016-01-19 09:22:21 +08:00   ❤️ 1
    暴力遍历 call frame 修改局部变量。动态语言你值得拥有。
    tabris17
        2
    tabris17  
    OP
       2016-01-19 09:28:03 +08:00
    @est ……难道没有优雅一些的方法么?

    不能修改 upvalue 岂不是连 PHP 都不如了? Python 当自强啊
    est
        3
    est  
       2016-01-19 09:31:19 +08:00
    >>> a=123
    >>> def c():
    ... sys._getframe().f_back.f_locals['a'] = 456
    ...
    ...
    >>> a
    123
    >>> c
    <function c at 0x7f69c98df050>
    >>> c()
    >>> a
    456
    est
        4
    est  
       2016-01-19 09:32:39 +08:00
    如此定义个函数:

    upvalue = sys._getframe().f_back.f_locals

    你想怎么改就怎么改。改别的进程里的 php 变量都可以做到。
    clino
        5
    clino  
       2016-01-19 09:34:55 +08:00   ❤️ 1
    在 3 有新增关键字搞定这种情况

    用 2 我面对楼主这种情况,一般用的是这样的方式

    def test_closure():
    ----class v:pass
    ----v.x = 1
    ----def closure():
    --------v.x = 2
    ----closure()
    ----print v.x
    test_closure()
    zhuangzhuang1988
        6
    zhuangzhuang1988  
       2016-01-19 09:36:35 +08:00   ❤️ 1
    def test_closure():
    x = [1]
    def closure():
    x[0] = 2
    closure()
    print x[0]
    test_closure()
    arcas
        7
    arcas  
       2016-01-19 09:38:01 +08:00
    python 3 nonlocal 关键字
    clino
        8
    clino  
       2016-01-19 09:38:34 +08:00
    另外不是"闭包不支持修改 upvalue"
    而是在
    def closure():
    ----x = 2
    这里的赋值有声明的作用,所以 python 认为这里声明了一个新的局部变量
    arcas
        9
    arcas  
       2016-01-19 09:40:04 +08:00   ❤️ 1
    def func1():
    func1.x = 100
    def func2():
    func1.x = 200
    func2()
    print func1.x
    tabris17
        10
    tabris17  
    OP
       2016-01-19 09:53:01 +08:00
    @clino 你这是语法上的解释,本质就是 python 不支持修改 upvalue
    tabris17
        11
    tabris17  
    OP
       2016-01-19 10:04:49 +08:00
    @est 你这个办法,万一上层调用已经返回了咋办?
    tabris17
        12
    tabris17  
    OP
       2016-01-19 10:05:21 +08:00
    @arcas 看来要换 3
    vanxining
        13
    vanxining  
       2016-01-19 10:05:51 +08:00 via Android
    学习了。另外, upvalue 是 Lua 的说法?
    tabris17
        14
    tabris17  
    OP
       2016-01-19 10:08:50 +08:00
    @vanxining 不是,闭包所引用的函数外部变量就称作 upvalue
    kkwezard
        15
    kkwezard  
       2016-01-19 10:18:00 +08:00
    @tabris17 我怎么只在 lua 里看到过 upvalue 这种说法。
    xuboying
        16
    xuboying  
       2016-01-19 10:22:00 +08:00
    我也遇到了一样的问题

    这里有个 remedy

    https://segmentfault.com/q/1010000004211385
    tabris17
        17
    tabris17  
    OP
       2016-01-19 10:22:04 +08:00
    @kkwezard lua 的叫法比较学术呗
    clino
        18
    clino  
       2016-01-19 10:26:37 +08:00 via Android
    @tabris17 可是我给的代码就不是在修改 upvalue 了吗
    tabris17
        19
    tabris17  
    OP
       2016-01-19 10:27:13 +08:00
    @vanxining
    @kkwezard

    除了 lua ,其他语言说到闭包只是强调了变量的作用域。但是实际上是有个专门术语来称呼这个 inherit variables from the parent scope 的,就是 upvalue 。

    以下摘自 wiki 的闭包词条:

    在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。
    tabris17
        20
    tabris17  
    OP
       2016-01-19 10:28:53 +08:00
    @clino 你给的代码是修改 upvalue 引用的对象而已,不是修改了 upvalue 。 upvalue 本身是不可变的
    est
        21
    est  
       2016-01-19 10:34:21 +08:00
    @tabris17 你给我演示一下如何返回了?
    clino
        22
    clino  
       2016-01-19 10:38:13 +08:00 via Android
    @tabris17 你要这么理解也行 不过明明变了但又说不可变 你的描述是很糟糕的
    tabris17
        23
    tabris17  
    OP
       2016-01-19 10:39:51 +08:00
    @est

    import sys

    def test_closure():
    ----x = 1
    ----def closure():
    --------print sys._getframe().f_back.f_locals
    ----closure()
    ----return closure

    closure = test_closure()
    closure()
    tabris17
        24
    tabris17  
    OP
       2016-01-19 10:42:20 +08:00
    @clino 变了以及不变,两个主语不同,变的是 upvalue 引用的对象,不变的是 upvalue
    clino
        25
    clino  
       2016-01-19 10:45:40 +08:00 via Android
    upvalue 指向的值变了情况 你说 upvalue 没变 这是很误导人的
    jmc891205
        26
    jmc891205  
       2016-01-19 10:58:39 +08:00
    这是 python 作用域导致的问题
    由于 python 规定在内部函数里给变量赋值的时候会屏蔽外部函数的同名变量,所以你给的例子里, closure 里的 x 已经不是对 test_closure 里的 x 的引用了。在我看来,这个例子中的 closure 并不是一个闭包。
    clino
        27
    clino  
       2016-01-19 11:06:34 +08:00
    @jmc891205 不是作用域 不信你只打印不赋值看看
    clino
        28
    clino  
       2016-01-19 11:08:46 +08:00
    @jmc891205 没仔细看你已经说了赋值的情况了
    est
        29
    est  
       2016-01-19 11:11:05 +08:00
    @tabris17 呃,我再想一下,其他语言如果一个函数都返回了,你还能修改这函数里面的局部变量?
    jmc891205
        30
    jmc891205  
       2016-01-19 11:11:26 +08:00
    @clino 不赋值的话外层函数的变量就不会被屏蔽咯 这也是 python 对变量作用域的规定吧
    tabris17
        31
    tabris17  
    OP
       2016-01-19 11:12:00 +08:00
    @est 是啊。 Javascript 、 PHP (通过引用实现)、 Lua 都支持
    jmc891205
        32
    jmc891205  
       2016-01-19 11:12:23 +08:00
    @clino 这。。。麻烦你下次回别人帖子的时候 先把别人的帖子看完
    Mark3K
        33
    Mark3K  
       2016-01-19 11:13:39 +08:00
    我觉得闭包挺好用的,但是不应该这样使用, 我一般只在闭包里面使用外层作用域的变量,如果你需要改变外层作用域的变量,应该使用 x=closure()的方式。
    tabris17
        34
    tabris17  
    OP
       2016-01-19 11:17:52 +08:00
    @est

    function test_closure() {
    var x = 1;
    function closure1() {
    x = 2;
    }
    function closure2() {
    console.log(x);
    }
    return [closure1, closure2];
    }
    closures = test_closure();
    (closures[0])();
    (closures[1])();
    ethego
        35
    ethego  
       2016-01-19 11:20:01 +08:00
    ```python
    def test_closure():
    test_closure.x = 1
    def closure():
    test_closure.x = 2
    closure()
    print x
    test_closure()
    ```
    你再试试
    nooper
        36
    nooper  
       2016-01-19 11:20:18 +08:00
    python3.
    ethego
        37
    ethego  
       2016-01-19 11:21:19 +08:00
    因为 python 的闭包确实不是完整的闭包,变量的查找规则不依赖于闭包。
    xuboying
        38
    xuboying  
       2016-01-19 11:26:22 +08:00
    @ethego
    这样写是错的,如下例子,结果是 2 4 6 6 6 6 ,希望的结果是 2 4 6 6 4 2
    >def outer(s):
    > if s == 4:
    > return
    > outer.OuterVar = s
    > def inner():
    > outer.OuterVar = outer.OuterVar * 2
    > inner()
    > print outer.OuterVar ,
    > outer(s+1)
    > print outer.OuterVar ,
    >if __name__ == "__main__":
    > outer(1)

    正确的方法是用一个类来封装
    >class Namespace: pass
    >
    >def outer(s):
    > if s == 4:
    > return
    > ns = Namespace()
    > ns.OuterVar = s
    >
    > def inner():
    > ns.OuterVar = ns.OuterVar * 2
    > inner()
    >
    > print ns.OuterVar,
    >
    > outer(s+1)
    >
    > print ns.OuterVar,
    >if __name__ == "__main__":
    > outer(1)
    xuboying
        39
    xuboying  
       2016-01-19 11:28:18 +08:00
    重新排版
    @ethego
    这样写是错的,如下例子,结果是 2 4 6 6 6 6 ,希望的结果是 2 4 6 6 4 2

    def outer(s):
       if s == 4:
         return
       outer.OuterVar = s
       def inner():
         outer.OuterVar = outer.OuterVar * 2
       inner()
       print outer.OuterVar ,
       outer(s+1)
       print outer.OuterVar ,
    if __name__ == "__main__":
       outer(1)

    正确的方法是用一个类来封装

    class Namespace: pass

    def outer(s):
       if s == 4:
         return
       ns = Namespace()
       ns.OuterVar = s

       def inner():
         ns.OuterVar = ns.OuterVar * 2
       inner()

       print ns.OuterVar,

       outer(s+1)

       print ns.OuterVar,
    if __name__ == "__main__":
       outer(1)
    ethego
        40
    ethego  
       2016-01-19 11:30:48 +08:00
    @xuboying
    ```python
    def test_closure():
    ----def closure():
    --------closure.x = 2
    ----closure.x = 1
    ----closure()
    ----print x

    test_closure()
    ```
    你再试试
    ethego
        41
    ethego  
       2016-01-19 11:32:33 +08:00
    @xuboying
    ```python
    def test_closure():
    ----def closure():
    --------closure.x = 2
    ----closure.x = 1
    ----closure()
    ----print b.x

    test_closure()
    ```
    print 有个小错误,改正了,你再试试
    est
        42
    est  
       2016-01-19 11:34:44 +08:00
    @tabris17 服。
    ethego
        43
    ethego  
       2016-01-19 11:40:35 +08:00
    @xuboying 不需要用一个类来封装,闭包函数也是动态创建的,函数也是对象
    ethego
        44
    ethego  
       2016-01-19 11:47:12 +08:00
    支持完整的闭包和不支持都没什么影响,能完成的功能一样可以完成,只是你心里舒服一些罢了,有人说 python 不纯,不是纯的函数式编程,卧槽,失望了,还不如 php 。我想说,你语言层面再纯,哪怕是 haskell 那样纯洁的小天使,编译成汇编机器码,一样是面向过程的,你追求的纯净的函数式编程只是镜花水月空中楼阁罢了。
    BlackKey
        45
    BlackKey  
       2016-01-19 11:49:21 +08:00   ❤️ 1
    PEP 227 里面解释了这个, Rebinding names in enclosing scopes 部分
    简单来说就是他们不愿意(懒得)搞这个
    xuboying
        46
    xuboying  
       2016-01-19 11:52:59 +08:00
    @ethego b.x 什么鬼?
    print b.x
    NameError: global name 'b' is not defined
    ethego
        47
    ethego  
       2016-01-19 11:53:53 +08:00   ❤️ 1
    @xuboying
    ```python
    def test_closure():
    ----def closure():
    --------closure.x = 2
    ----closure.x = 1
    ----closure()
    ----print closure.x

    test_closure()
    ```
    这里就一个 x ,你还不知道用哪个?
    xuboying
        48
    xuboying  
       2016-01-19 11:57:01 +08:00
    @BlackKey 这个功能还真的是挺重要的,对于喜欢写递归的人来说,递归内的函数可以正确访问父函数的是保证逻辑正确的前提。 perl , js 都能很好的实现
    如果因为不能处理好递归而被人要求去专用其他语言真的是和很伤心的事情
    不过还好还是有 workaround 了
    xuboying
        49
    xuboying  
       2016-01-19 12:07:52 +08:00
    @ethego 你这个写法也是对的!!!但是必须把函数放在最开头,而且这个变量从父函数变量变成子函数变量了,我不知道这样做是否妥当(感觉上逻辑变了),也不知道能不能解决需要访问父父函数变量的情况(虽然我从来没有这么尝试过)

    def outer(s):
       if s == 4:
         return
       def inner():
         inner.x = inner.x * 2
       inner.x = s
       inner()
       print inner.x,
       outer(s+1)
       print inner.x,
    if __name__ == "__main__":
       outer(1)


    result: 2 4 6 6 4 2
    BlackKey
        50
    BlackKey  
       2016-01-19 12:31:37 +08:00
    @xuboying 我挺喜欢 Python 的,但有一点有时还是觉得挺烦的。他们经常喜欢让你接受他们认为好的方式,比如他们不提倡让你用匿名函数,结果 Python 匿名函数的就一直特别弱
    xuboying
        51
    xuboying  
       2016-01-19 12:36:43 +08:00
    @BlackKey 众所周知, Perl 和 Python 的文化在很多方面是不同的。 PEP20 中提到了其中一点, There should be one-- and preferably only one --obvious way to do it.

    然而却弄出了 Python 2 和 Python 3
    clino
        52
    clino  
       2016-01-19 12:41:23 +08:00
    @jmc891205 我的理解不是屏蔽,而是认为声明了一个新的局部变量导致访问不到外面的那一个同名的
    shyling
        53
    shyling  
       2016-01-19 13:13:28 +08:00 via iPad
    函数式还修改外部变量?函数自己的返回值呢?
    ethego
        54
    ethego  
       2016-01-19 13:15:48 +08:00
    @xuboying 正因为只选择一种最好的方式,所以才要彻底割裂 python 2 不好的地方。
    musicx
        55
    musicx  
       2016-01-19 13:22:46 +08:00
    我其实好奇是什么样的场景需要修改闭包外的值。。。而且要求不能用引用不能用将闭包返回值赋给原变量这两种方法。。。
    tabris17
        56
    tabris17  
    OP
       2016-01-19 13:35:09 +08:00
    @shyling
    @musicx

    可以试试接触下 javascript ,回过头来你会发现不能修改 upvalue 才是很奇怪的设定
    jmc891205
        57
    jmc891205  
       2016-01-19 13:36:56 +08:00
    @clino 我所说的“屏蔽”就是你说的这个意思
    xuboying
        58
    xuboying  
       2016-01-19 14:20:40 +08:00
    @ethego 如果 python 有这个自信,那么鉴别它做的好坏的方法是是否其他语言学习了他的精华,当你 porting python 到其他语言的时候没有很大的麻烦;如果大家都不像他,他也只能孤芳自赏了。
    ethego
        59
    ethego  
       2016-01-19 14:33:31 +08:00
    @xuboying 然而 javascript 越来越像 python 也是事实,看看 ecmas6 的写法就知道了
    fy
        60
    fy  
       2016-01-19 14:44:27 +08:00
    我刚想说 nolocal
    alsotang
        61
    alsotang  
       2016-01-19 14:47:22 +08:00
    2.x 确实不支持 nonlocal ,还是用方法 2 吧。用个 dict 之类的,修改里面的属性。反正这个变量也是在函数内部用的, return 出去的时候该返回什么还是返回什么。
    xuboying
        62
    xuboying  
       2016-01-19 15:07:54 +08:00
    @ethego 我倒不是 Python 黑,相反很多时候用 python ,只是更赞同 TMTOWTDI , PEP20 那句让人觉得小家子气。 python 能被别的语言认同当然也是好事。
    ethego
        63
    ethego  
       2016-01-19 15:24:06 +08:00   ❤️ 1
    @xuboying 追求理论的和谐可以试试 haskell 或者 ruby
    fy
        64
    fy  
       2016-01-19 15:42:53 +08:00
    2 被续命了太久,早应迁移了。 3 解决了很多实际问题,但 3.0-3.2 都不是太理想, 3.3 以后就很顺了。
    shyling
        65
    shyling  
       2016-01-19 15:43:15 +08:00
    @tabris17 实际上接触 javascript 更多一点。。

    var x =1;
    x=2;
    es6 有了块作用域是不是要再加个 let x=3;?
    语法是为了简化代码,让代码更易读的。
    tabris17
        66
    tabris17  
    OP
       2016-01-19 16:15:58 +08:00
    @shyling 块作用域解决了闭包的这个问题

    for (var i=0; i< 4;i++){
    setTimeout(function(){console.log(i);},1);
    }

    for (let i=0; i< 4;i++){
    setTimeout(function(){console.log(i);},1);
    }
    kaneg
        67
    kaneg  
       2016-01-19 23:03:00 +08:00
    Java 也类似。 匿名类如果要引用外部类的变量,该外部变量需要声明为 final 的。所以一个变通的办法就是创建一个数组,在匿名类中修改数组中的元素已达到修改外部变量的目的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2647 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 82ms · UTC 11:51 · PVG 19:51 · LAX 03:51 · JFK 06:51
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.