@
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,但实际上为了使“块中到处可用”也不需要这样的设计,把声明改成表达式即可。)