简介:蟒猫是猫来客,它热爱地球上的一切,尤其是优雅万能的蟒蛇。我是它的人类朋友,豌豆花下的猫,授权我润色和发布它的文章。如果你是第一次阅读本系列文章,那么我强烈建议你阅读它写的前几篇文章(见文末链接),相信你会爱上这种神秘的哲学+极客猫。话不多说,一起来享受今天的“思想盛宴”吧!喵,朋友们,好久不见。刚吃完一顿美餐,心里好满足。自从习惯了地球上的食物,我的胃里就出现了一些莫名其妙的反应。从最近的新陈代谢可以感觉到,我的母胎习惯在逐渐消退。人类的食物正在改变我,或者说重塑我。也许有一天,我会变成白菜,变成鱼……呸呸呸。我还是想当一只猫。喵喵的生命太短暂了,要抓紧更新正文了。最近,看到两件我觉得非常有趣的事情,那就从那说起吧。首先是某知名影视明星,因为名不副实的学术精英身份,被讽刺的打假系统批评;质朴和俏皮赢得了猫屎的称号。身份是一个如此神奇的话题。看着他们的身份错位,我总是想起自己的处境。我(也许)知道过去时的我是谁,但我越来越不确定现在时的我是谁,更不用说未来的我了。在人间应该怎样对待自己?我该如何与你相处?想了半天,也没有答案。头疼,尾巴疼。想都别想喵。下面继续和大家聊聊Python。上次我们讲了对象的边界问题。无论是固定边界还是弹性边界,无非是修身的两种诉求。有的对象自足快乐,有的对象相容包容理想。然而,边界问题还没有结束。正如儒家经典所说:修身——齐家——治国——平天下。内层的势能扩散出去,进入更广阔的空间。Python对象的边界并不局限于自身。这里有一个巧妙的映射关系:对象(body)-函数(home)-模块(country)-package(world)。个人被归入不同的命名空间并生活在分层范围内。(当然,万幸的是,他们不会受到道德和礼仪的严格压迫~__~)1.你的名字我们先来看模块。这是一个合适的尺度,从中可以平滑地连接功能和包。什么是模块?任何以.py后缀结尾的文件都是一个模块。模块有什么好处?首先,不同功能的代码容易拆分,单个功能的代码量小更容易维护;第二,易于组装和重用,Python以丰富的第三方模块着称;最后,模块创建一个私有命名空间,可以有效地管理各种对象的命名。可以说,模块是Python世界中最小的自洽生态系统——除了在控制台直接运行命令的情况,模块是最小的可执行单元。前面我把模块比作一个国家,当然不伦不类,因为很难想象在现实世界中,会有成千上万个完全不同的国家(我指的是到地球,但喵星人不同,我会在后面详细说明)。类比可以帮助我们运用我们的思维,所以我们只做这个假设。这样一来,思考模块之间的相互引用就这么有趣了。这不是国家之间的战争入侵,而是人道主义援助。至于市民的流动和迁徙,可能会成为冒险之旅的谈资。我也对模块的身份角色感兴趣。碰巧发现,他们在用名字的时候,玩了个双姓把戏。观看下面的表演。首先创建两个模块A.py和B.py,它们的内容如下:#模块A的内容:print("moduleA:",__name__)#模块B的内容:importAprint("moduleB:",__name__)其中__name__是指当前模块的名称。代码的逻辑是:模块A会打印这个模块的名字,模块B会先打印模块A的名字再打印这个模块的名字,因为它引入了模块A。那么结果是什么样的呢?A.py的执行结果:moduleA:__main__B.py的执行结果:moduleA:testmoduleB:__main__可以看到问题了!模块A前后其实有两个不同的名字,这两个名字是什么意思,为什么会有这样的区别?我觉得这体现了名字的本质——对我自己来说,我就是我,不需要名字来标示;但对于其他人来说,ta是众生之一,只有命名才能区分。因此,当一个模块调用自己时(即自己执行时),就是“__main__”,当被别人调用时(即被引用时),就会是模块的真实名称。这真是一个巧妙的设置。由于模块名称的双重性,我们可以添加一个判断来隐藏一个不属于外部的模块的内容。#ContentsofmoduleA:print("moduleA:",__name__)if__name__=="__main__":print("privateinfo.")在上面的代码中,只有当模块A本身是executed”,当导入到其他模块时,这部分内容将不会被执行。2.名称的时间和空间对于生物,我们有各种属性,比如名称,性别,年龄等等。和Python对象一样,它们也有各种属性,一个模块就是一个对象,“__name__”是它的一个属性。此外,模块还有以下最基本的属性:>>>importA>>>print(dir(A))['__builtins__','__cached__','__doc__','__file__','__loader__','__name__','__package__','__spec__']在模块的全局空间中,一些属性作用于全局,其中Python称为全局变量,而其他作用于局部的属性称为局部变量。一个变量对应一个属性的名称,它与一个特定的值相关联。通过globals()和locals(),“名称-值对”"的变量可以被打印出来。x=1deffoo():y=2print("Globalvariables:",globals())print("Localvariables:",locals())foo()在IDE中执行上面的代码,结果:Globalvariables:{'__name__':'__main__','__doc__':None,'__package__':None,'__loader__':<_frozen_importlib_external.SourceFileLoaderobjectat0x000001AC1EB7A400>,'__spec__':None,'__annotations__':{},'__builtins__':,'__file__':'C:/pythoncat/A.py','__cached__':None,'x':1,'foo':}Localvariable:{'y':2}可以看出,x是全局变量,对应值为1,y是局部变量,对应值为2,两个变量作用域不同:局部变量的作用是在函数内部,不能在外部直接使用;全局变量作用于整个世界,但只能在函数内部访问,不能修改。与Java、C++等语言不同,Python并没有屈服于解析的便利,也没有使用呆板的花括号来组织作用域,而是采用了轻巧简洁的缩进方式。但是,所有的编程语言在区分变量类型和区分范围的目的上都是相似的:控制访问权限和管理变量命名。关于访问控制,在上面的例子中,局部变量y的作用域仅限于foo方法。如果直接在外面使用,会报错“NameError:name'y'isnotdefined”。关于变量命名的管理,不同的作用域管理自己独立的名册。一个作用域中的名称指的是唯一的对象,而不同作用域中的对象可以具有相同的名称。修改上面的例子:x=1y=1deffoo():y=2x=2print("insidefoo:x="+str(x)+",y="+str(y))foo()print("outsidefoo:x="+str(x)+",y="+str(y))在全局作用域和局部作用域命名同一个变量,那么打印出来的结果是什么呢?insidefoo:x=2,y=2outsidefoo:x=1,y=1可见同名可以出现在不同作用域,互不干扰。那么,如何判断一个变量在哪个作用域呢?对于跨域分布的嵌套作用域和变量名,应该采用什么样的搜索策略?Python设计了命名空间(namespace)机制,命名空间本质上是一个字典,一个花名册,里面注册了所有变量的名称和对应的值。根据记录的内容不同,可以分为四类:局部命名空间(localnamespace),记录了函数的变量,包括函数的参数和局部定义的变量。可以通过内置函数locals()查看。调用函数时创建,函数退出时删除。全局命名空间(globalnamespace)记录了模块的变量,包括函数、类、其他导入的模块、模块级变量和常量。可以通过内置函数globals()查看。在加载模块时创建并始终存在。内置命名空间(built-innamespace)记录了所有模块共享的变量,包括一些内置函数和异常。在解释器启动时创建并持续存在。命名空间包(namespacepackages),一个包级的命名空间,跨包对模块进行分组和管理。命名空间总是存在于特定的范围内,并且范围具有优先级。查找变量的顺序是:local/localscope-->global/module/packagescope-->built-inscope。命名空间充当变量和范围之间的桥梁,承担管理命名、记录名称-值对和检索变量的任务。难怪《Python之禅》(TheZenofPython)在最后一句话中说:Namespacesisonehoninggreatidea--let'sdomoreofthose!——译文:命名空间是一个很棒的想法,应该更多地使用!3.不可见客名(变量)是身份问题,空格(作用域)是边界问题,命名空间都是。这两个问题恰恰是困扰众生的两个核心问题。它们的特点是:无处不在,生生不息,就像一个超大的乱七八糟的毛线团。Python就是继承了这些人类烦恼(不可避免)的神器,幸好这个简化版的烦恼可以解决。(当然现在可以解决,但是如果人工智能高度发达呢?我觉得不会,喵喵,好像让我想起了一个痛苦的梦,打住。)这里有几个问题(注:每个例子是相互独立的):#示例1:x=x+1#示例2:x=1deffoo():x=x+1foo()#示例3:x=1deffoo():print(x)x=2foo()#Example4:deffoo():ifFalse:x=3print(x)foo()#Example5:ifFalse:x=3print(x)下面给出了几个选项,请大家想一想选择每个示例的答案:1.没有报告错误。2.报错:name'x'isnotdefined。3、报错:赋值前引用了局部变量'x'。报一类错误,即没有定义的变量不能使用,其他例子是第二类错误,即已经定义但未赋值的变量不能使用。为什么会出现错误?为什么报告的错误不同?下面一一解释。例1是定义一个变量的过程。本身定义还没有完成,但是等号右边要用到变量x,所以报变量undefined。在示例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方法解析成一棵抽象语法树(abstractsyntaxtree),然后扫描树上的名称(name)节点,然后,所有扫描到的变量名都会作为局部函数的字段变量名存储在内存中(堆栈?)。编译期结束后,局部范围内的变量名已经确定,但还没有赋值。在后续的解释期(也就是代码执行期),如果有赋值过程,变量名和值会存储在本地命名空间中,可以通过locals()查看。只有存储在命名空间中,变量才能真正定义(声明+赋值)。上面三个例子报错的原因是变量名已经被解析成局部变量,但是还没有赋值。可以推断,在局部范围内查找变量实际上分为两步:检查内存和检查命名空间。另外,如果要在局部范围内修改全局变量,需要在范围内写上“globalx”。例5作为例4的对比,也是对其原理的补充。它们的区别在于,一个不在函数中,一个在函数中,但错误完全不同。Example4分析背后的原理是编译过程和抽象语法树。如果这个原理对例5也有效,那么两者的报错应该是一样的。现在有区别了,为什么?我不得不承认,这触及了我的知识盲点。我们可以推测,例5的编译过程是不同的,它没有解析抽象语法树的步骤。不过,继续追问,为什么不一样,为什么没有解析语法树的步骤呢?如果是出于解析函数和解析模块成本的考虑,或者其他的考虑,那么新的问题是,编译和解析的底层原理是什么,如果还有其他的考虑,又是什么呢?这些都不是回答其中任何一个的可爱问题。然而,一步步思考探索到这个层次又能怪谁呢?回到我前面说的,命名空间是一个身份和边界的整合问题,它和作用域密切相关。现在看来,编译器会介入,让这些问题变得更加复杂。本来问的是Python中的边界问题,结果触及了自己的知识边界。多么讽刺。(这次寻找人工造物身份的旅程会不会像走迷宫,陷入自己身份的两难境地?)4.边界内外边界让我们暂时抛开那些不讨人喜欢的问题,继续说说修身齐家,国泰天下。要想把国家治好,就要面对更多的国内和国际问题。给大家看一个问题:defmake_averager():count=0total=0defaverager(new_value):nonlocalcount,totalcount+=1total+=new_valuereturntotal/countreturnaverageraverager=make_averager()print(averager(10))print(averager(11))###输出结果:10.010.5这里存在嵌套函数,即函数中包含其他函数。外部-内部函数关系类似于模块-外部函数关系。同样,它们的作用域关系也类似:外部函数作用域-内部函数作用域,模块全局作用域-外部函数作用域。在内层作用域中,可以访问外层作用域的变量,但不能直接修改,除非使用nonlocal进行转换。Python3引入了nonlocal关键字来标识外部函数的作用域,介于全局作用域和局部作用域之间,即global--nonlocal--local。也就是说,国家-每个人-小家庭。上面的例子中,nonlocal关键字使小加(内部函数)可以修改everyone(外部函数)的变量,但是变量并不是在小加中创建的。当小家的函数执行完毕后,它没有权限清理这些变量。nonlocal只带修改权限,不带回收清理权限,导致外部函数的变量突破原来的生命周期,变成自由变量。上面的例子是一个平均函数。由于自由变量的存在,每次调用时,新传入的参数都会和自由变量一起计算。在计算机科学中,引用自由变量的函数称为闭包。本质上,闭包就是突破局部界限的东西,所谓“跳出三界,不在五行”。每次调用闭包函数,都可以继续使用上次调用的结果。这不就像一个转世的人(按照某种宗教的说法),还带着前世的记忆和技能吗?打破界限必然会带来新的身份认同问题,这一点就是明证。然而,人类并不打算修复它,因为他们发现这种身份异化特性可以在很多场合发挥作用,比如装饰器和函数式编程。适应身份异化并从中获益,是地球人类的天赋。说完了家破人亡的话题,让我们睁眼看一看世事。计算机语言中的包(package)其实就是一个目录结构,以文件夹的形式打包组织起来,内容可以包括各种模块(py文件)、配置文件、静态资源文件等,话题很多与包相关的,比如内置包、第三方包、包仓库、如何打包、如何使用包、虚拟环境等等。这是可以理解的,更大的边界意味着更多的关系,更大的边界也意味着更多的知识和未知。在这里,我想谈谈Python3.3中引入的命名空间包,因为它是前面讨论的所有主题的延续。然而,关于其背景、实现方式和用途的细节并不重要。我敏感而发散的思维突然捕捉到一个相似的结构,似乎更值得一提。通过命名空间包的设计,不同包中的同一个命名空间可以共同使用,从而将不同目录下的代码归纳为一个通用的命名空间。也就是说,多个原本相对独立的包,在同名命名空间的帮助下,实现了远距离的即时连接,简直令人惊叹。我想到了空间折叠,一个说不上很深的技术,但确实帮助我从猫穿越到地球。两个包裹,两个世界,两个宇宙,它们的距离和突破边界的方式是何等的相似!我对这种类似的结构着迷。在不同的事物中,相似性的出现意味着更高维规律的存在,而在不同的规律中,新的相似性意味着更抽象的规律。学完Python,想通过调查来回答关于自己的类似问题……啊,不知不觉写了这么久,该死的皮又在叫嚣了——地球网上的菜真够抠门的,而且不知道你们人类怎么受得了这几百万年的驯化过程……我就不写了,去觅食吧。各位读者,后面会有期~~~Python猫前作:有了Python,我可以给所有的猫起名字Python对象的身份神话:从所有公民到万物皆可数Python对象的空间边界:孤独、开放、包容附录:局部变量编译原理:https://dwz.cn/ipj6FluJ命名空间包:https://www.tuicool.com/artic...公众号【Python猫】,专注于Python技术和数据科学以及深度学习,试图打造一个有趣好用的学习分享平台。本期连载系列精品文章,包括喵星哲学猫系列、Python进阶系列、好书推荐系列、优质英文推荐与翻译等,欢迎关注。PS:后台回复“爱学习”,即可免费获得学习大礼包。