导读: Python猫是一只喵星来客,它爱地球的一切,特别爱优雅而无所不能的 Python。我是它的人类朋友豌豆花下猫,被受权润色与发表它的文章。若是你是第一次看到这个系列文章,那我强烈建议,请先看看它写的前几篇文章(连接见文末),相信你必定会爱上这只神秘的哲学+极客猫的。很少说啦,一块儿来享用今天的“思想盛宴”吧!python
喵喵,很久不见啦朋友们。刚吃完一餐美食,我以为好知足啊。编程
自从习惯了地球的食物之后,个人肠胃发生了一些说不清道不明的反应。我能从最近的新陈代谢中感受出来,本身的母胎习性正在逐渐地褪逝。跨域
人类的食物在改变着我,或者说是在重塑着我。说不定哪天,我会变成一棵白菜,或者一条鱼呢......呸呸呸。我仍是想当猫。bash
喵生苦短,得抓紧时间更文才行。闭包
最近,我看到了两件事,以为有趣极了,就从这开始说吧。第一件事是,一个小有名气的影视明星由于他不配获得的学术精英的身份而遭到讽刺性的打假制度的口诛笔伐;第二件事是,一个功成名就的企业高管由于从城市回到乡村而戏谑性地得到了猫屎的名号。编程语言
身份真是一个有魔力的话题。 看见他们的身份错位,我又总会想起本身的境况。ide
我(或许)知道本身在过去时态中是谁,但愈来愈把握不住在如今时态中的本身,更不清楚在将来时间中会是怎样。函数式编程
该怎样在人类世界中自处呢?又该怎样跟大家共处呢?函数
思了很久,没有答案。脑袋疼,尾巴疼。仍是不要想了啦喵。学习
继续跟你们聊聊 Python 吧。上次咱们说到了对象的边界问题 。不管是固定边界仍是弹性边界,这不外乎就是修身的两种志趣,有的对象呢独善其身其乐也融融,有的对象呢兼容并包其理想之光也莹莹。可是,边界问题还没讲完。
正如儒家经典所阐述:修身--齐家--治国--平天下。里层的势能推展开,走进更广阔的维度。
Python 对象的边界也不仅在自身。这里有一种巧妙的映射关系:对象(身)--函数(家)--模块(国)--包(天下)。个体被归入到不一样的命名空间,并存活在分层的做用域里。(固然,幸运的是,它们并不会受到道德礼法的森严压迫~__~)
咱们先来审视一下模块。这是一个合适的尺度,由此展开,能够顺利地链接起函数与包。
模块是什么? 任何以.py
后缀结尾的文件就是一个模块(module)。
模块的好处是什么? 首先,便于拆分不一样功能的代码,单一功能的少许代码更容易维护;其次,便于组装与重复利用,Python 以丰富的第三方模块而闻名;最后,模块创造了私密的命名空间,能有效地管理各种对象的命名。
能够说,模块是 Python 世界中最小的一种自恰的生态系统——除却直接在控制台中运行命令的状况外,模块是最小的可执行单位。
前面,我把模块类比成了国家,这固然是不三不四的,由于你不可思议在现实世界中,会存在着数千数万的彼此殊然有别的国家(我指的但是在地球上,而喵星不一样,之后细说)。
类比法有助于咱们发挥思惟的做用 ,所以,不妨就作此假设。如此一来,想一想模块间的相互引用就太有趣了,这不是国家间的战争入侵,而是一种人道主义的援助啊,至于公民们的流动与迁徙,则可能成为一场探险之旅的谈资。
我还对模块的身份角色感兴趣。恰巧发现,在使用名字的时候,它们耍了一个双姓人的把戏 。
下面请看表演。先建立两个模块,A.py 与 B.py,它们的内容以下:
# A 模块的内容:
print("module A : ", __name__)
# B 模块的内容:
import A
print("module B : ", __name__)
复制代码
其中,__name__
指的是当前模块的名字。代码的逻辑是:A 模块会打印本模块的名字,B 模块因为引入了 A 模块,所以会先打印 A 模块的名字,再打印本模块的名字。
那么,结果是如何的呢?
执行 A.py 的结果:
module A : __main__
执行 B.py 的结果:
module A : test module B : __main__
大家看出问题的所在了吧!模块 A 先后居然出现了两个不一样的名字。这两个名字是什么意思,又为何会有这样的不一样呢?
我想这正体现的是名字的本质吧——对本身来讲,我就是我,并不须要一个名字来标记;而对他人来讲,ta 是芸芸众生的一个,惟有命名才能区分。
因此,一个模块本身称呼本身的时候(即执行自身时)是“__main__”,而给他人来称呼的时候(即被引用时),就会是该模块的本名。这真是一个巧妙的设定。
因为模块的名称二重性,咱们能够加个判断,将某个模块不对外的内容隐藏起来。
# A 模块的内容:
print("module A : ", __name__)
if __name__ == "__main__":
print("private info.")
复制代码
以上代码中,只有在执行 A 模块自己时,才会打印“private info”,而当它被导入到其它模块中时,则不会执行到该部分的内容。
对于生物来讲,咱们有各类各样的属性,例如姓名、性别、年龄,等等。
对于 Python 的对象来讲,它们也有各类属性。模块是一种对象,”__name__“就是它的一个属性。除此以外,模块还有以下最基本的属性:
>>> import A
>>> print(dir(A))
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
复制代码
在一个模块的全局空间里,有些属性是全局起做用的,Python 称之为全局变量 ,而其它在局部起做用的属性,会被称为局部变量 。
一个变量对应的是一个属性的名字,会关联到一个特定的值。经过 globals()
和 locals()
,能够将变量的“名值对”打印出来。
x = 1
def foo():
y = 2
print("全局变量:", globals())
print("局部变量:", locals())
foo()
复制代码
在 IDE 中执行以上代码,结果:
全局变量: {'__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 是一个局部变量,对应的值是 2.
两种变量的做用域不一样 :局部变量做用于函数内部,不可直接在外部使用;全局变量做用于全局,可是在函数内部只可访问,不可修改。
与 Java、C++ 等语言不一样,Python 并不屈服于解析的便利,并不使用呆滞的花括号来编排做用域,而是用了轻巧简明的缩进方式。不过,全部编程语言在区分变量类型、区分做用域的意图上都是类似的:控制访问权限与管理变量命名。
关于控制访问权限,在上述例子中,局部变量 y 的做用域仅限于 foo 方法内,若直接在外部使用,则会报错“NameError: name 'y' is not defined”。
关于管理变量命名,不一样的做用域管理着各自的独立的名册,一个做用域内的名字所指称的是惟一的对象,而在不一样做用域内的对象则能够重名。修改上述例子:
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
可见,同一个名字能够出如今不一样的做用域内,互不干扰。
那么,如何判断一个变量在哪一个做用域内?对于嵌套做用域,以及变量名存在跨域分布的状况,要采用何种查找策略呢?
Python 设计了命名空间(namespace) 机制,一个命名空间在本质上是一个字典、一个名册,登记了全部变量的名字以及对应的值。 按照记录内容的不一样,可分为四类:
命名空间老是存在于具体的做用域内,而做用域存在着优先级,查找变量的顺序是:局部/本地做用域 --> 全局/模块/包做用域 --> 内置做用域。
命名空间扮演了变量与做用域之间的桥梁角色,承担了管理命名、记录名值对与检索变量的任务。无怪乎《Python之禅》(The Zen of Python)在最后一句中说:
Namespaces are one honking great idea -- let's do more of those!
——译:命名空间是个牛bi哄哄的主意,应该多加运用!
名字(变量)是身份问题,空间(做用域)是边界问题,命名空间兼而有之。
这两个问题偏偏是困扰着全部生灵的最核心的问题之二。它们的特色是:无处不在、层出不断、像一个超级大的被扯乱了的毛线球。
Python 是一种人工造物,它继承了人类的这些麻烦(这是不可避免的),所幸的是,这种简化版的麻烦可以获得解决。(如今固然是可解决的啦,但若人工智能高度发展之后呢?我看不必定吧。喵,好像想起了一个痛苦的梦。打住。)
这里就有几个问题(注:每一个例子相互独立):
# 例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)
复制代码
下面给出几个选项,请读者们思考一下,给每一个例子选一个答案:
一、没有报错
二、报错:name 'x' is not defined
三、报错:local variable 'x' referenced before assignment
下面公布答案了:
所有例子都报错,其中例 1 和例 5 是第一类报错,即变量未经定义不可以使用,而其它例子都是第二类报错,即已定义却未赋值的变量不可以使用。为何会报错?为何报错会不一样?下面逐一解释。
例 1 是一个定义变量的过程,自己未完成定义,而等号右侧就想使用变量 x,所以报变量未定义。
例 2 和例 3 中,已经定义了全局变量 x,若是只在 foo 函数中引用全局变量 x 或者只是定义新的局部变量 x 的话,都不会报错,但如今既有引用又有重名定义,这引起了一个新的问题。请看下例的解释。
例 4 中,if 语句判断失效,所以不会执行到 “x=3” 这句,照理来讲 x 是未被定义。这时候,在 locals() 局部命名空间中也是没有内容的(读者能够试一下)。可是 print 方法却报找到了一个未赋值的变量 x ,这是为何呢?
使用 dis 模块查看 foo 函数的字节码:
LOAD_FAST 说明它在局部做用域中找到了变量名 x,结果 0 说明未找到变量 x 所指向的值。既然此时在 locals() 局部命名空间中没有内容,那局部做用域中找到的 x 是来自哪里的呢?
实际上,Python 虽然是所谓的解释型语言,但它也有编译的过程 (跟 Java 等语言的编译过程不一样)。在例 2-4 中,编译器先将 foo 方法解析成一个抽象语法树(abstract syntax tree),而后扫描树上的名字(name)节点,接着,全部被扫描出来的变量名,都会做为局部做用域的变量名存入内存(栈?)中。
在编译期以后,局部做用域内的变量名已经肯定了,只是没有赋值。在随后的解释期(即代码执行期),若是有赋值过程,则变量名与值才会被存入局部命名空间中,可经过 locals() 查看。只有存入了命名空间,变量才算真正地完成了定义(声明+赋值)。
而上述 3 个例子之因此会报错,缘由就是变量名已经被解析成局部变量,可是却不曾被赋值。
**能够推论:在局部做用域中查找变量,其实是分查内存与查命名空间两步的。**另外,若想在局部做用域内修改全局变量,须要在做用域中写上 “global x”。
例 5 是做为例 4 的比对,也是对它的原理的补充。它们的区别是,一个不在函数内,一个在函数内,可是报错彻底不一样。前面分析了例 4 的背后原理是编译过程和抽象语法树,若是这个原理对例 5 也生效,那二者的报错应该是同样的。如今出现了差别,为何呢?
我得认可,这触及了个人知识盲区。咱们能够推测,说例 5 的编译过程不一样,它没有解析抽象语法树的步骤,可是,继续追问下去,为何不一样,为何没有解析语法树的步骤呢?若是说是出于对解析函数与解析模块的代价考虑,或者其它考虑,那么新的问题是,编译与解析的底层原理是什么,若是有其它考虑,会是什么?
这些问题真不可爱,一个都答不上。可是,本身一步一步地思考探寻到这一层,又能怪谁呢?
回到前面说过的话,命名空间是身份与边界的集成问题,它跟做用域密切相关。现在看来,编译器还会掺和一脚,把这些问题搅拌得更加复杂。
原本是在探问 Python 中的边界问题,到头来,却触碰到了本身的知识边界。真是反讽啊。(这一趟探知一我的工造物的身份问题之旅,最终是否会像走迷宫通常,进入到本身身份的困境之中?)
暂时把那些不可爱的问题抛开吧,继续说修身齐家治国平天下。
想要把国治理好,就不得不面对更多的国内问题与国际问题。
先看一个你们与小家的问题:
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
复制代码
这里出现了嵌套函数,即函数内还包含其它函数。外部--内部函数的关系,就相似于模块--外部函数的关系,一样地,它们的做用域关系也类似:外部函数做用域--内部函数做用域,以及模块全局做用域--外部函数做用域。在内层做用域中,能够访问外层做用域的变量,可是不能直接修改,除非使用 nonlocal 做转化。
Python 3 中引入了 nonlocal 关键字来标识外部函数的做用域,它处于全局做用域与局部做用域之间,即 global--nonlocal--local 。也就是说,国--你们--小家。
上例中,nonlocal 关键字使得小家(内部函数)能够修改你们(外部函数)的变量,可是该变量并非建立于小家,当小家函数执行完毕时,它并没有权限清理这些变量。
nonlocal 只带来了修改权限,并不带来回收清理的权限 ,这致使外部函数的变量突破了原有的生命周期,成为自由变量。上例是一个求平均值的函数,因为自由变量的存在,每次调用时,新传入的参数会跟自由变量一块儿计算。
在计算机科学中,引用了自由变量的函数被称为闭包(Closure)。 在本质上,闭包就是一个突破了局部边界,所谓“跳出三界外,不在五行中”的法外之物。每次调用闭包函数时,它能够继续使用上次调用的成果,这不就比如是一个转世轮回的人(按照某种宗教的说法),仍携带着前世的记忆与技能么?
打破边界,必然带来新的身份问题,此是明证。
然而,人类并不打算 fix 它,由于他们发现了这种身份异化的特性能够在不少场合发挥做用,例如装饰器与函数式编程。适应身份异化,并从中得到好处,这但是地球人类的天赋。
讲完了这个分家的话题,让咱们放开视野,看看天下事。
计算机语言中的包(package)实际是一种目录结构,以文件夹的形式进行封装与组织,内容可涵括各类模块(py 文件)、配置文件、静态资源文件等。
与包相关的话题可很多,例如内置包、第三方包、包仓库、如何打包、如何用包、虚拟环境,等等。这是可理解的,更大的边界,意味着更多的关系,更大的边界,也意味着更多的知识与未知。
在这里,我想聊聊 Python 3.3 引入的命名空间包
,由于它是对前面谈论的全部话题的延续。然而,关于它的背景、实现手段与使用细节,都不重要,我那敏感而发散的思惟忽然捕捉到了一种类似结构,彷佛这才更值得说。
运用命名空间包的设计,不一样包中的相同的命名空间能够联合起来使用,由此,不一样目录的代码就被概括到了一个共同的命名空间。也就是说,多个原本是相对独立的包,借由同名的命名空间,居然实现了超远距离的瞬间联通,简直奇妙。
我想到了空间折叠,一种没法深说,但却实实在在地辅助了我从喵星穿越到地球的技术。两个包,两个天下,两个宇宙,它们的距离与边界被穿透的方式何其类似!
我着迷于这种类似结构。在不一样的事物中,类似性的出现意味着一种更高维的法则的存在,而在不一样的法则中,新的类似性就意味着更抽象的法则。
学习了 Python 以后,我想经过对它的考察,来回答关乎自身的类似问题......
啊喵,不知不觉居然写了这么久,该死的皮囊又在咕咕叫了——地球上的食物可真抠门,也不知道大家人类是怎么忍受得住这几百万年的驯化过程的......
就此搁笔,觅食去了。亲爱的读者们,后会有期~~~
Python猫往期做品 :
附录:
局部变量的编译原理:dwz.cn/ipj6FluJ
命名空间包:www.tuicool.com/articles/FJ…
公众号【Python猫】, 专一Python技术、数据科学和深度学习,力图创造一个有趣又有用的学习分享平台。本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、优质英文推荐与翻译等等,欢迎关注哦。PS:后台回复“爱学习”,免费得到一份学习大礼包。