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

Python 作用域问题,int 型变量为什么会有些特殊呢

  •  
  •   whoops · 2019-12-08 19:04:02 +08:00 · 5962 次点击
    这是一个创建于 1815 天前的主题,其中的信息可能已经有所发展或是发生改变。

    是这样的,做练习时用闭包实现一个计数器,使用整型变量会报错

    UnboundLocalError: local variable 'cnt' referenced before assignment
    

    代码如下:

    def counter():
       cnt = 0
       def add_one():
           cnt += 1
           return cnt
       return add_one
    a=counter()
    print(a()) 
    #把整型变量换成列表就可以
    def counter():
       cnt = [0]
       def add_one():
           cnt[0] += 1
           return cnt[0]
       return add_one
    a=counter()
    print(a()) 
    

    初学不才,请教一下大家

    第 1 条附言  ·  2019-12-09 20:55:24 +08:00
    看完目前所有讨论,真的受益匪浅。特别是很多朋友非常细致详细的留言。谢谢你们。
    目前我的知识水平还不能够完全吸收这些知识,我也在不断学习,并不断回看你们的解答,进行反复思考。
    43 条回复    2019-12-09 16:46:29 +08:00
    silkriver
        1
    silkriver  
       2019-12-08 19:24:43 +08:00
    内层作用域里要先写 nonlocal cnt
    lspvic
        2
    lspvic  
       2019-12-08 19:28:11 +08:00 via Android   ❤️ 1
    在 add_one 中加上 nonloccal cnt
    add_one 中第一句相当于 cnt=cnt+1,
    函数中有赋值语句必须申明为 global 或 nonlocal 才表示为全局或对应作用域的变量,否则是为本地变量
    所以表现为本地变量 cnt 在声明前引用
    lucays
        3
    lucays  
       2019-12-08 19:28:30 +08:00
    list 是全局的,int 是局部的
    NeinChn
        4
    NeinChn  
       2019-12-08 19:42:53 +08:00
    +1,感觉 Python 有点 trick
    直观看出来的区别就是前者没办法创建 func_closure,后者创建了 func_closure,并且在 cell 内引用了 counter 内的数组
    具体为啥两者会有区别,这个估计得从 Python 的实现 /定义上看
    Herobs
        5
    Herobs  
       2019-12-08 19:52:05 +08:00 via Android
    #2 已经解释了,但是为什么列表可以,因为实际上列表的例子里并没有修改列表本身,只是调用了他的一个方法 __setitem__
    111qqz
        6
    111qqz  
       2019-12-08 19:54:34 +08:00   ❤️ 2
    可以参考 fluent python 的第七章"the nonlocal declaration"一节.

    ![python_fluent.png]( https://i.loli.net/2019/12/08/h9exqwcN4JSBLkg.png)

    区别在于 int 是 immutable 的,而 list 不是.
    crella
        7
    crella  
       2019-12-08 20:07:29 +08:00 via Android
    如果是 ruby 的话,两种方法都不可以。不管是数组还是普通变量都要声明为全局~
    superrichman
        8
    superrichman  
       2019-12-08 20:28:32 +08:00
    @lucahhai list 并不是全局的
    我用 globals()和 locals()把外层函数和内层函数的全局变量和本地变量都打出来看了一下.

    内层函数可以直接访问到外层的可变对象(list dict set 等), 但是无法访问到不可变对象(str int tuple 等). 还有就是如果外层函数有参数(比如 def counter(num) 的 num), 内层函数也可以访问到这些参数.

    从测试结果上看, 外层函数的参数和可变对象会自动变成内层函数的本地变量. 如果要在内层函数访问外层的不可变对象, 需要用 nonlocal 进行修饰, 或者把不可变对象存到可变对象里进行间接的传递(就像第二个函数一样, 不是直接传 0 而是把 0 放进 list).
    NeinChn
        9
    NeinChn  
       2019-12-08 20:47:37 +08:00
    @superrichman
    int/str/tuple 也可以被传递到闭包内的,只要不修改的情况下,打印 locals 可以看到
    ethego
        10
    ethego  
       2019-12-08 21:34:33 +08:00
    python 最开始并没有正确实现闭包,并不是 int 类型的问题,在 python 3 下可以在闭包内使用 nonlocal 声明闭包变量。
    wuwukai007
        11
    wuwukai007  
       2019-12-08 21:57:55 +08:00 via Android
    就相当于全局变量,函数内修改要声明下,计数器用生成器实现更简单吧
    superrichman
        12
    superrichman  
       2019-12-08 22:15:44 +08:00
    @NeinChn 不, 看不到的, 我试过了

    def counter():
    cnt = 0
    name = 'counter'
    cals = (2, 3, 4, 5, 6)
    print('outer locals', locals())

    def add_one():
    print('inner locals', locals())
    return add_one

    a = counter()
    a()

    输出结果

    outer locals {'cnt': 0, 'name': 'counter', 'cals': (2, 3, 4, 5, 6)}
    inner locals {}
    NeinChn
        13
    NeinChn  
       2019-12-08 22:21:15 +08:00
    @superrichman 因为你没用到。
    你的 add_one()里面 print 一下 cals,locals 就会有值了
    superrichman
        14
    superrichman  
       2019-12-08 22:36:32 +08:00
    @NeinChn 真的可以诶, 好神奇. 用 print 的时候能读到外层的 cals, 并且之后能在 lcoals 能看到, 但是要对 cals 操作就抛异常. emmm... 这是把外层的不可对象当成了只读的数据? 有点迷.
    NeinChn
        15
    NeinChn  
       2019-12-08 22:47:24 +08:00
    @superrichman 猜测就是不能修改引用而已。只是 Python 的异常太迷了。
    love
        16
    love  
       2019-12-08 23:08:36 +08:00 via Android
    这个挺好理解的吧,主要是 python 没有一个类似 js 的 var 声明变量操作符,而是把首次赋值就当成是当前作用域变量声明了,所以内层的函数相当于新声明了一个变量。
    ipwx
        17
    ipwx  
       2019-12-08 23:43:47 +08:00   ❤️ 1
    @ethego 什么鬼说法。
    @NeinChn 这和引用无关,因为 Python 根本没有“引用”这个概念。
    @superrichman 一切的关键都在于你在第一个例子里面,cnt 这个 **标识符** 在 add_one 局部函数里面,相对于外部 counter 函数,它的含义发生了变化。标识符变成了一个新的变量,而你此时 cnt += 1 相当于 cnt = cnt + 1,在右值表达式里面引用了这个新的变量。而这个新的变量在这句话执行完之前还没有创建,所以就会出错。

    但是第二个例子里面,add_one 函数里面的 cnt 这个 **标识符** counter 函数里面的含义是一致的。cnt[0] += 1 相当于 cnt[0] = cnt[0] + 1,在右值表达式里面,此时 cnt 这个标识符存在,所以可以引用。
    - - - -

    **标识符** 这个概念就是 **标识符**,既不是“引用”,也不是“变量”,而就是“标识符”。要深刻理解这一点,你们可能需要一点编译原理,以及写编译器的实践过程。
    ipwx
        18
    ipwx  
       2019-12-08 23:45:20 +08:00
    @superrichman 你在第一个例子里面,遇到的报错,就和你随手写 a=a 或者 a+=1 一样的。都是 a 这个变量还没有存在。
    ethego
        19
    ethego  
       2019-12-09 00:08:25 +08:00
    @ipwx 什么叫什么鬼说法,Python 2 没有正确实现闭包是众所周知的事实。
    ethego
        20
    ethego  
       2019-12-09 00:14:54 +08:00
    所以作为对函数上下文查找的修正,在 global 和 local 之间加入了 nonlocal 的关键字,来在不破坏兼容下实现正确的闭包 t 实现,即递归向上层 context 查找变量。
    ethego
        21
    ethego  
       2019-12-09 00:17:19 +08:00   ❤️ 1
    https://mail.python.org/pipermail/python-dev/2003-October/039214.html
    可以自己进来看看 guido 是怎么样承认这个错误了
    ethego
        22
    ethego  
       2019-12-09 00:28:08 +08:00
    和你说的右值里使用了未定义变量没有关系,add_one 作为一个闭包函数是要向上查找变量定义的,这个语义目前在绝大多数现代语言里都是一样的。实际上 Python 只是无法写而不是不能读,你试试改成 `print(cnt+1)` 你看看能不能不赋值直接使用 cnt,明显不是未定义的问题。
    FrankHB
        23
    FrankHB  
       2019-12-09 01:44:28 +08:00   ❤️ 1
    @ipwx 基本意思分析对了,但引入了一些跟 Py 不见得有关的更麻烦的问题……不太容易直接说明白。
    就干脆都过一遍吧。顺便当 FAQ 草稿。

    首先,这里的问题,跟编译不编译没有关系。
    所谓的编译原理只是顺带提到这些内容,因为这其实是语言设计而不是编译这种实现的先决知识,但没专门 PL 的同学就只能勉为其难一下了。

    其次,这里的问题的知识背景,只要是典型的有所谓变量(variable) 的语言,纯解释实现一样普遍适用。
    典型的这类语言中变量以源代码中的的特定片段,即变量名(variable name) ,通常以文本的形式提供,源代码中的实际表示通常就是字符串。
    源代码的文本通过词法分析识别出作为词素(lexeme) 的实例,然后被分析归类成为某一类记号(token) ,这是附加语法用途的文本以外的构造,在语法分析中完成。
    被作为变量名的记号是标识符。(有的语言在此之前还有预处理阶段,其中的类似的词素也叫标识符,但不属于记号—— 例如,C 的预处理记号在语法分析中被区分出表示变量名的普通的标识符记号,以及语法意义上的关键字。)
    在典型的语言(排除文本宏替换这类 DSL )中,用户实际一般使用是和语法规则区分规定的语义规则明确的抽象,而非语法构造。
    标识符在对应的语法处理之后就已经确定存在。(当然,语法上的处理不一定要求是全局 AOT 的形式,这个另当别论。)这和标识符表示什么含义是两回事。
    用标识符去代替标识符指称的实体(这里是变量)讨论会显得稀里糊涂,因为实际的处理方式不唯一,而且经常依赖之后的语义处理过程,处理后的内容也没法一一对应(允许一一对应的平凡逻辑还浪费了语言允许的抽象能力,通常就是应该在语言设计中避免的)。

    第三,大多数用户在这里没有区分清楚所谓“变量”所指的确切含义,于是稀里糊涂程度翻倍。
    虽然不少语言设计中根本没说清楚什么叫变量,一般地,变量区分于其它语言概念的关键性质就是保证存在标识变量的变量名。
    注意:
    1.变量总是被命名。语言层面上没有所谓的“匿名变量”,因为这是逻辑上的自相矛盾。
    2.变量名是标识符,但反过来不一定,因为标识符指称实体,但不一定命名变量。它可能是特殊的语法构造,如宏。
    3.变量(的值)是不是支持可被修改,对是不是变量无关紧要。像纯函数式语言中就有不可修改的变量。(但是近来很多语言设计者会误用可修改的对象作为变量的含义,另当别论。)
    4.有的语言中,函数名被单独区分,剩下的实体叫做对象(object) (注意先来后到,这和面向对象毫无关系),特指明确需要存储资源的、可以“储存”值(value) 并可能明确支持修改值的实体,如 C (题外话,ISO C 直接回避了“变量”的概念)。其它一些语言不强调这点,可以把函数也作为对象。
    实际上严格定义(如 IEC 2382 )中变量可被形式化为命名变量的标识符、指称(denote) 的实体(entity) 和上下文信息的元组。
    其中,上下文一般能明确被指称的实体在不同位置中不冲突,也就是源代码中允许引入相同的标识符指称不同实体。
    为了消歧义,可以利用上下文中不同的作用域(scope) ,通过名称解析(name resolution) 明确某个标识符作为变量名无歧义的指称到底是同名变量的哪一个。这是语义分析中的一种基本操作。
    而用户使用一个变量,既用的是变量名,也可以仅是变量指称的实体。日常所谓的“变量”可能只是指后者,都是严格意义上的变量。为了突出变量构成中的实体以外的作用,可以强调为变量绑定(variable binding) (这也可以是实现名称解析时使用的数据结构之一)。而绑定一个变量则指在程序中引入变量绑定的操作。
    用户阅读源代码,看到的首先是语法上的标识符,然后也需要人肉做名称解析以完成可能需要的消歧义,以确定变量到底指称什么实体,才能明白含义。
    人肉实现名称解析,它的结果在字面含义上,就是实体的引用(reference) 。
    虽然一般用户不一定意识到这点,但实际上语言的机制比一般人直接见名知意复杂得多。这是因为语言的规则要求明确性,需要处理所有情形,又要和其它语法一致。
    所以典型的语言中,这不是简单的语法替换过程,而是以标识符构成表达式(expression) ,对表达式求值(evaluate) 之后确定表达式具有的值(value) 。
    标识符构成的表达式的求值具有这样的性质:若被求值的表达式中的标识符指称一个实体,则求值后表达式的值引用被指称的实体。
    这个意义上,表达式的值同样也是所谓的引用(reference) 。
    虽然具体语言设计中不一定提供一等引用(first-class reference) 给用户,但只要是通过表达式求值而不是直接语法替换的形式提供变量名称解析、同时需要区分变量同一性(identity) (基本上只要不是纯函数语言就不可能回避)的语言设计,不可能避免等价于这里的引用的概念。例如,C 没提供引用,但它有左值(lvalue) 。
    所以语法意义上标识符确实就只是标识符,但唐突和对象或引用割裂开来,是无助于分清楚这些理由的。

    最后,对比上面的通用设计框架,顺带看看 Python 的具体规定。(以下照搬 3.8 的文档,URL 略。)
    2.3. Identifiers and keywords
    Identifiers (also referred to as names) are described by the following lexical definitions.
    ...
    [一坨具体语法略。]
    ……果然比上面说得还简单。
    3.1. Objects, values and types
    Objects are Python’s abstraction for data. All data in a Python program is represented by objects or by relations between objects. (In a sense, and in conformance to Von Neumann’s model of a “stored program computer,” code is also represented by objects.)
    这里对象用的是存储实体的概念。
    The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter’s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.)
    ...
    Python 不提供一等引用,但引用在语言规范中就没被回避。(否则一坨 reference-counting 就更没法说了。)
    4.2. Naming and binding
    Names refer to objects. Names are introduced by name binding operations.
    The following constructs bind names: formal parameters to functions, import statements, class and function definitions (these bind the class or function name in the defining block), and targets that are identifiers if occurring in an assignment, for loop header, or after as in a with statement or except clause. The import statement of the form from ... import * binds all names defined in the imported module, except those beginning with an underscore. This form may only be used at the module level.
    A target occurring in a del statement is also considered bound for this purpose (though the actual semantics are to unbind the name).
    Each assignment or import statement occurs within a block defined by a class or function definition or at the module level (the top-level code block).
    If a name is bound in a block, it is a local variable of that block, unless declared as nonlocal or global. If a name is bound at the module level, it is a global variable. (The variables of the module code block are local and global.) If a variable is used in a code block but not defined there, it is a free variable.
    Each occurrence of a name in the program text refers to the binding of that name established by the following name resolution rules.
    4.2.2. Resolution of names
    A scope defines the visibility of a name within a block. If a local variable is defined in a block, its scope includes that block. If the definition occurs in a function block, the scope extends to any blocks contained within the defining one, unless a contained block introduces a different binding for the name.
    When a name is used in a code block, it is resolved using the nearest enclosing scope. The set of all such scopes visible to a code block is called the block’s environment.
    When a name is not found at all, a NameError exception is raised. If the current scope is a function scope, and the name refers to a local variable that has not yet been bound to a value at the point where the name is used, an UnboundLocalError exception is raised. UnboundLocalError is a subclass of NameError.
    If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block. This can lead to errors when a name is used within a block before it is bound. This rule is subtle. Python lacks declarations and allows name binding operations to occur anywhere within a code block. The local variables of a code block can be determined by scanning the entire text of the block for name binding operations.
    到这里为止,LZ 的问题也好引用的理解也罢,包括具体报错的理由,应该都比较明确了。
    Python 的设计和一众小作坊设计一样共享一个经典糟粕——赋值这个依赖已有对象的(修改对象的值)操作和引入变量绑定这两种逻辑上根本不同(绑定原则上要求指定的变量绑定不存在,赋值要求变量绑定在之前必须存在)的操作混在一起了。
    所以在人肉解析名称之前,读者还需要多做一次消歧义,先确定这到底是个真正(纯粹)的赋值,还是带变量绑定的所谓赋值。否则就可能出现 LZ 这样的稀里糊涂。
    虽然这个例子里要消歧义很简单(都不用照抄 Python 实现的语义,重写 cnt = 0 成为类似 let cnt = 0 这样的伪码,总之能跟后面真正的赋值区分清楚即可),使用户一般性地被迫阅读至少整个块才能做到确定有哪些局部变量绑定是明显糟烂的设计。
    (正常的设计中,变量绑定是单独的操作,这往往被设计成变量的初始化声明的语法单独提供。Python 文档在这方面倒有些自知之明,知道是 subtle,但实际上为了使“块中到处可用”也不需要这样的设计,把声明改成表达式即可。)
    FrankHB
        24
    FrankHB  
       2019-12-09 05:04:26 +08:00   ❤️ 2
    随便一刷新还有新回复……

    @ethego 这说法明显有问题。算了,再过一遍原始问题好了……

    LZ 的例子中为什么“替换成列表”就没有错误?看起来是纯粹的语义问题,但实际按 Python reference 理解,还就直接被语法上下文决定了。
    注意 += 左边的 cnt 是个标识符,而 cnt[0] 不是。因此这两种情形直接被 assignment statement 的语法区别对待了,适用不同的语义规则:
    执行 cnt += 1 会被解释为先限制 cnt 为局部变量再要求引用局部变量 cnt 的这个绑定构造,而被引用的局部变量 cnt 此时没有绑定成功自然失败;
    执行 cnt[0] += 1 会被解释成不引入变量绑定的 augmented assignment statement,于是 cnt 就是先前 nearest enclosing scope 引入的变量。
    这里 spec 中 Binding of names 一节所谓的 targets that are identifiers if occurring in an assignment 的 assignment 没说就不能是 augmented assignment statement,也没有链接到具体章节,后面 Assignment statements 这节还包含 Augmented assignment statements 这小节,所以这里所谓的 assignment 是包含 augmented assignment statement 的(乃至 3.8 新增的 assignment expression )——尽管 assignment_stmt 和 augmented_assignment_stmt 作为文法元素是并列的,也没有拿 normal assignment statement 以外就不适用来辩解的余地。
    于是这跟什么列表是不是 immutable 没直接关系( Python 中什么东西是不是 immutable 根本取决于 object type,这里涉及的反正都没不允许 mutate 根本就不用管)。
    只是因为恰巧这些奇葩的语义规则的组合导致了只能 read-only 可用的假象,所以 GvR 发现这里自己挖坑需要补。不过 其实根本也没反省到家,所以也就是多糊了个关键字而已。
    (所谓想要照搬 Scheme 也是扯淡,虽然 Scheme 的 top-level 和 local 确实也有很二的问题,但 Scheme 里可没类似 += 的东西给一致性添乱,就算 SRFI-17 也不是这样整的。)

    题外话:
    虽然“未定义变量”的说法不那么靠谱(随手写 a = a 或者 a += 1 还真不一样,上下文相关),但 UnboundLocalError 这个错误就表示不可能和名称解析失败没关系——而且就实现提供的错误消息来说,还真有关系……
    纠结实现,UNBOUNDLOCAL_ERROR_MSG 和 NAME_ERROR_MSG 是不同的错误消息。只有后者才会字面上纠结“定义”,画风是这样的:
    UnboundLocalError: local variable 'cnt' referenced before assignment
    NameError: name 'cnt' is not defined
    然后 UnboundLocalError is a subclass of NameError,所以说成“未定义”,倒也算无可厚非。

    另外,引起这样折腾的也并不只是 GvR 承认错误这么简单了:
    https://stackoverflow.com/questions/30157655
    (光是这个反直觉设计就不要指望“和大多数语言”一样了。)
    hehheh
        25
    hehheh  
       2019-12-09 05:21:03 +08:00
    我记得 int 是可以取,可是不能修改的,直觉上感觉和 immutable 有关系,可是如果你写 cnt[:] = [1]也不会报错。所以应该是其他原因,懒得去查为什么了。
    hehheh
        26
    hehheh  
       2019-12-09 05:22:38 +08:00
    然后这种情况下 list 在 locals 和 globals 里都能看见,immutable 对象就看不见。感觉这个设计有点奇怪。
    ethego
        27
    ethego  
       2019-12-09 11:15:40 +08:00
    @FrankHB
    你错了,替换成列表没有问题的原因在于对于 list 成员的操作亦然属于只读 list 而不是写 list,对于目前的 python 来说在没有 nonlocal 的情况下仅仅只是不能对变量本身进行操作,而对 list 成员的操作并不涉及 list 变量本身 —— 它只是一个富指针。

    除非你认为 Guido 说的有问题,那你找他去,上面就是 Guido 觉得需要加入 nonlocal 关键字的观点与原因。
    ethego
        28
    ethego  
       2019-12-09 11:27:37 +08:00
    不好意思没仔细看你后面的结论,我认为你说的把声明和赋值放在一起是导致让人混淆是对的,但并不是问题的根本原因。来看看 Rust 是怎么做对这道题的:
    https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2d41aeaaaba8bf4aae2306ea9cdf77f0
    ethego
        29
    ethego  
       2019-12-09 11:31:37 +08:00
    ethego
        30
    ethego  
       2019-12-09 11:55:19 +08:00   ❤️ 1
    理论上来说在声明或赋值时,如果在本地的 context 没有查找到定义的变量都直接按照闭包语义向上查找就可以了,只是这样做会引入额外的开销。加入 nonlocal 关键字,还是把赋值和声明用 let 或者 var 之类的东西区分开只是一个问题的两种解决途径而已。
    ethego
        31
    ethego  
       2019-12-09 12:05:53 +08:00
    https://gist.github.com/ethe/cad17be3abf81935f61f20bd2937fc5c.js
    <script src="https://gist.github.com/ethe/cad17be3abf81935f61f20bd2937fc5c.js"></script>
    看下 Ruby 是怎么在不区分声明与赋值下做对的。
    FrankHB
        32
    FrankHB  
       2019-12-09 14:19:33 +08:00
    @FrankHB 我就是清楚“替换成列表没有问题的原因在于对于 list 成员的操作亦然属于只读 list 而不是写 list”(实际上还不仅是对 list )才有这里的奇葩规则混在一起的结论,因为真没发现能通过依赖这条路径就可以推出这里出的问题——这是目前 Python reference 里直接明确的东西。
    反过来,即便认为和 immutability 直接扯上关系的推理更直白,也覆盖不了 reference 里已有的描述,特别是不能解释清楚什么时候出现具体什么错误,一样至少需要这里用到的名称解析规则。
    所以需要改动 reference 才能支持你的推理。
    (我其实我同意按你想要的说法在直觉上还更容易让人明白一点;但 Py 特么现在就用的不是这个理由,那就凉拌吧,我也就不去找麻烦了……)
    GvR 的邮件里的说法不管是否正确地反映了他想要的东西,都不可能取代正式文档的表述(而且这人在 PL 的一般议题上误会和偷换概念不是没前科的,要他支持 PTC,大讲了一通 TCO 如何如何,最终却认怂 feel educated 了)。
    至于“这道题”,这就是送分题(区分 mutable,或至少按默认局部作用域规则一致地 capture ),Python 在搞混 binding construct/assignment 的情况下继续找事情,所以说特别奇葩。

    题外话,关于子对象 mutablity 一般应该怎么搞的问题还是挺麻烦的:
    https://www.v2ex.com/t/626109#reply76
    FrankHB
        33
    FrankHB  
       2019-12-09 14:21:26 +08:00
    ethego
        34
    ethego  
       2019-12-09 14:37:27 +08:00
    @FrankHB 不区分声明与赋值并不是在闭包中无法写被闭包捕获变量的理由,你看上面 Ruby 和我的解释就知道了,Python 这么做纯粹就是因为 Guido **希望** 对被捕获变量只读。这点在邮件里被 Guido 很明白地表达了。
    ethego
        35
    ethego  
       2019-12-09 14:50:40 +08:00
    而 Guido 在后来他自己对闭包的理解错了,而 Scheme 那种才是对的,而如果希望在这里不破坏兼容或者不增加额外性能开销的前提下,只能加入一个新的语法用于表示 “nonlocal” 的变量。
    FrankHB
        36
    FrankHB  
       2019-12-09 14:51:25 +08:00
    @ethego 如果不说 Python,照常理当然应该这样(捕获变量和什么时候引入声明没逻辑上的依赖,本来就该正交),但 Python 在结果上就是不按这条路来有啥办法……
    Python 不是个人作品,所以讲理由的时候还是按官方的正式文档来,况且尽管奇葩,现在的文档用的那套逻辑上确实能自圆其说。设计者另外的表示就算意图再明确,在文档修正前只能作为间接参考。(我怀疑 Py 烂摊子太多,根本就没人管得上这个。)
    ethego
        37
    ethego  
       2019-12-09 15:22:30 +08:00
    @FrankHB Python 的正式文档只是从语义上对 reference 做出了解释。如果观察一下所有引用类型( Python 文档里显然没有什么引用类型和值类型),Object list dict 啥的,对属于这些类型的 captured variable 的属性或者成员直接声明赋值都是可以的,但是无法直接写变量本身。所以切片语义( cnt[1])啥的不是根本原因,只是因为 Python 只允许读,切片语义对原变量来说是一个读操作而已。
    FrankHB
        38
    FrankHB  
       2019-12-09 15:48:24 +08:00
    @ethego 关于区分读写……如果一个语言不要求明确 identity 也不在显式类型系统上暴露(而要求用户需要保证 const correctness 之类的 type safe )的话,其实是很偏向实现细节且并不总是那么容易自然推理出来的东西。
    很多用户都已经习惯了这些设计,实现也是这样搞的,reference 或者 spec 里反而不写出来了,这种风格我只能表示呵呵。要么直接写清楚,要么都不保证,哪个都比现在这种做法干净。
    (包括 Scheme R7RS 比之前多明确某些特定的 variable 是不是具有 location,也有点设计过度了。)
    FrankHB
        39
    FrankHB  
       2019-12-09 15:54:48 +08:00
    多跑个题,考虑到语言设计者本身的理解偏差的话,真要深层挖起来这里其实是有点其它方面的意思的。
    不同 C-like 这种直接在语法上明确区分构造而不会混淆的设计,Scheme 这种全局使用近似的语法却单独对顶级(top level) 上下文约定的设计显然会把语言搞复杂。
    这样做的显然是有目的的。我的理解是,这是要同时支持 REPL 的解释党和编译党的妥协。
    Scheme 的特殊规则首先是顶级的 define 在绑定已存在时和 set! 等价,也就是一个绑定构造(binding construct) 被替换为赋值。这在 REPL 加载同一个源文件时比较方便。(考虑到 define 首先总能被当作是个 绑定构造,倒不会引起 Python 那么大的混乱。)
    与此相对,非顶级的内部定义(internal definition) 则被认为明确地只是绑定构造而不是赋值。这允许 define 被明确地被实现为 letrec* 这种可变性绑定(non immutable binding) 的语法糖。变量绑定的存在性和名称解析的错误的因果性使后者的语义中隐含一种顺序上的限制,保证内部定义能进行这样的语法变换。这实质上给允许“编译”提供一种保证。
    Python 同样有这样琐碎的问题:如何保证变量绑定的存在性能在早期被确定?不像 Scheme 这种长期两派党争下可以有简单妥协,Python 一开始基本就是 CPython 一家说了算,而其中提供 LOAD_FAST/LOAD_DEREF 之类的实现细节是否容易被静态确定以及 CPython 的“编译”倾向混在一起,客观上加剧了语言设计上的混乱。
    从语言设计的角度,这里主要的本质差异是变量绑定所在的上下文不同。
    Scheme 这样的 LISP 变体中,提供绑定的上下文数据结构称为环境(environment) ,传统上是表示变量绑定映射的关联列表(associated list) ,但实际上不要求明确内部的具体数据结构实现,只要提供能确定名称对应的实体的接口即可。
    如 Scheme 这种顶级绑定放在顶级环境和 letrec* 这样依赖的 lambda 的局部环境(local environment) 是原则上不一样的,前者是“全局”,后者是(闭包的)“局部”。
    (对应地,Python 里有不同的 namespace ; Python 的 environment 概念只是绑定的集合。)
    但区分不同环境的这种设计并非原则上必要。全局环境在实现中的唯一本质区别保证开放(open) 即不总是需要具有源代码局部就能确定有限集合的绑定,而闭包里的局部环境则不是(只要不是打算让一个翻译单元支持翻译半个闭包的语法构造)。然而谁也没说闭包环境中枚举绑定的操作就该是 deterministic 的,只要名称解析能保证 total (可终止)。因为,枚举所有变量绑定根本就不是一个环境需要支持的常规操作——至少被静态绑定的闭包的变量就不是。如果需要反射变量本身的信息,则需要其它和实现相关的辅助操作,如 MIT Scheme 的 procedure-enviornment。这种操作的存在会暴露一些脱离传统闭包语义的实现细节,可能影响用户程序的可观察行为而使语法变换不能保证语义等价,反而会阻碍编译(所以往往只是作为调试辅助接口来用)。
    另一方面,允许语言设计这样区分环境的现实是,这些语言并没有把环境暴露给用户操作的一等对象(first-class object) ,即提供一等环境(first-class environment) 。也正因为是这样,区分底层的环境不会让语言特性的复杂性失控(比如至少需要提供完全不同的两套 eval ……更别说如 R5RS 的残废 eval 本身就已经够让编译党抓狂了,以至于 Stalin 这样的实现停留在 R4RS )。但这是以用户程序的表达能力缺失为代价的。例如,Scheme 的宏不能直接和用户定义的环境交互,也无法作为一等对象像过程这样的普通函数一样被作为参数传递或作为返回值。当然,提供宏来变通这种设计,本身就是隐含了“编译优先”的思路了。这某种意义上是一种过度优化,因为一等环境原则上不阻碍对闭包代码进行编译——本质上这里只是允许“尽早”而非严格静态确定绑定(如 J.Shutt 在 Kernel 语言中提供的 $let-safe )。
    至于 Python 连宏都没有……就略过算了。
    为啥要多在乎某些语言设计者的肤浅的个别理解呢?
    FrankHB
        40
    FrankHB  
       2019-12-09 15:58:24 +08:00
    Typo:可变性绑定(non immutable binding) →非可变性绑定(immutable binding)
    (草,错位了……)
    XIVN1987
        41
    XIVN1987  
       2019-12-09 16:14:43 +08:00
    cnt += 1 等价于 cnt = cnt +1,python 中对没用 global 和 nonlocal 修饰的变量赋值,该变量就会被认定为局部变量,后面的 cnt + 1 引用了一个还没有值的局部变量,所以就报错了

    cnt[0] += 1 等价于 cnt[0] = cnt[0] + 1,注意,这里并没有给 cnt 赋值,所以查找变量时会使用 LEGB 规则,,cnt[0] + 1 引用变量 cnt,局部作用域没有它的定义,所以就跑去闭包中查找,,所以不会报错
    ethego
        42
    ethego  
       2019-12-09 16:36:44 +08:00
    @FrankHB 我想的没有你这么复杂。。不过如你所说,显式区分声明与赋值的语言不需要考虑问题。后面的语言也很少采用这种设计了。
    BTW,刚试了下 Julia 和 Ruby 的实现一样:
    https://gist.github.com/ethe/b489d96e6e92ba5e1726a990d909ce73
    XIVN1987
        43
    XIVN1987  
       2019-12-09 16:46:29 +08:00   ❤️ 1
    Python 学习手册第 17 章有说明

    ![]( )

    简单来说:python 的函数内赋值的变量,,只要没用 global 和 nonlocal 修饰,,python 都认为它是局部变量,,比如下面这段程序执行会报错

    ``` python
    s = 'lucy'

    def test():
    print(s)

    s = 'lily'

    test()
    ```

    报错消息是:UnboundLocalError: local variable 's' referenced before assignment

    就是因为 print 后面有给 s 赋值的语句,,所以 python 认定 s 是局部变量
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3468 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 11:15 · PVG 19:15 · LAX 03:15 · JFK 06:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.