Python 与家国天下

1、你的名字
.py
后缀结尾的文件就是一个模块(module)。# A 模块的内容:
print("module A : ", __name__)
# B 模块的内容:
import A
print("module B : ", __name__)
__name__
指的是当前模块的名字。代码的逻辑是:A 模块会打印本模块的名字,B 模块由于引入了 A 模块,因此会先打印 A 模块的名字,再打印本模块的名字。module A : __main__
module A : test module B : __main__
# A 模块的内容:
print("module A : ", __name__)
if __name__ == "__main__":
print("private info.")
2、名字的时空
>>> import A
>>> print(dir(A))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
globals()
和 locals()
,可以将变量的“名值对”打印出来。x = 1
def foo():
y = 2
print("全局变量:", globals())
print("局部变量:", locals())
foo()
全局变量: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001AC1EB7A400>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'C:/pythoncat/A.py', '__cached__': None, 'x': 1, 'foo': <function foo at 0x000001AC1EA73E18>}
局部变量: {'y': 2}
x = 1
y = 1
def foo():
y = 2
x = 2
print("inside foo : x = " + str(x) + ", y = " + str(y))
foo()
print("outside foo : x = " + str(x) + ", y = " + str(y))
inside foo : x = 2, y = 2 outside foo : x = 1, y = 1
- 局部命名空间(local namespace),记录了函数的变量,包括函数的参数和局部定义的变量。可通过内置函数 locals() 查看。在函数被调用时创建,在函数退出时删除。
- 全局命名空间(global namespace),记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。可通过内置函数 globals() 查看。在模块加载时创建,一直存在。
- 内置命名空间(build-in namespace),记录了所有模块共用的变量,包括一些内置的函数和异常。在解释器启动时创建,一直存在。
- 命名空间包(namespace packages),包级别的命名空间,进行跨包的模块分组与管理。
Namespaces are one honking great idea — let’s do more of those!
——译:命名空间是个牛bi哄哄的主意,应该多加运用!
3、看不见的客人
# 例1:
x = x + 1
# 例2:
x = 1
def foo():
x = x + 1
foo()
# 例3:
x = 1
def foo():
print(x)
x = 2
foo()
# 例4:
def foo():
if False:
x = 3
print(x)
foo()
# 例5:
if False:
x = 3
print(x)
1、没有报错
2、报错:name ‘x’ is not defined
3、报错:local variable ‘x’ referenced before assignment
-
例 1 是一个定义变量的过程,本身未完成定义,而等号右侧就想使用变量 x,因此报变量未定义。
-
例 2 和例 3 中,已经定义了全局变量 x,如果只在 foo 函数中引用全局变量 x 或者只是定义新的局部变量 x 的话,都不会报错,但现在既有引用又有重名定义,这引发了一个新的问题。请看下例的解释。
-
例 4 中,if 语句判断失效,因此不会执行到 “x=3” 这句,照理来说 x 是未被定义。这时候,在 locals() 局部命名空间中也是没有内容的(读者可以试一下)。但是 print 方法却报找到了一个未赋值的变量 x ,这是为什么呢?
使用 dis 模块查看 foo 函数的字节码:
LOAD_FAST 这句说明它在局部作用域中找到了变量名 x。既然此时在 locals() 局部命名空间中没有内容,那局部作用域中找到的 x 是来自哪里的呢?(20190513update:有错,修改)
实际上,Python 虽然是所谓的解释型语言,但它也有编译的过程 (跟 Java 等语言的编译过程不同)。在例 2-4 中,编译器先将 foo 方法解析成一个抽象语法树(abstract syntax tree),然后扫描树上的名字(name)节点,接着,所有被扫描出来的变量名,都会作为局部作用域的变量名存入内存(栈?)中。
在编译期之后,局部作用域内的变量名已经确定了,只是没有赋值。在随后的解释期(即代码执行期),如果有赋值过程,则变量名与值才会被存入局部命名空间中,可通过 locals() 查看。只有存入了命名空间,变量才算真正地完成了定义(声明+赋值)。
而上述 3 个例子之所以会报错,原因就是变量名已经被解析成局部变量,但是却未曾被赋值。
可以推论:在局部作用域中查找变量,实际上是分查内存与查命名空间两步的。 另外,若想在局部作用域内修改全局变量,需要在作用域中写上 “global x”。
-
例 5 是作为例 4 的比对,也是对它的原理的补充。它们的区别是,一个不在函数内,一个在函数内,但是报错完全不同。前面分析了例 4 的背后原理是编译过程和抽象语法树,如果这个原理对例 5 也生效,那两者的报错应该是一样的。现在出现了差异,为什么呢?
我得承认,这触及了我的知识盲区。我们可以推测,说例 5 的编译过程不同,它没有解析抽象语法树的步骤,但是,继续追问下去,为什么不同,为什么没有解析语法树的步骤呢?如果说是出于对解析函数与解析模块的代价考虑,或者其它考虑,那么新的问题是,编译与解析的底层原理是什么,如果有其它考虑,会是什么?
这些问题真不可爱,一个都答不上。但是,自己一步一步地思考探寻到这一层,又能怪谁呢?
4、边界内外的边界
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
averager = make_averager()
print(averager(10))
print(averager(11))
### 输出结果:
10.0
10.5
命名空间包
,因为它是对前面谈论的所有话题的延续。然而,关于它的背景、实现手段与使用细节,都不重要,我那敏感而发散的思维突然捕捉到了一种相似结构,似乎这才更值得说。