本文由sangay(曾用名:三界、三界:天地人、xgfone)整理,转载时请注明。
笔者在学习C/C++时,本想通过自己的学习,把C/C++的全部语法通过分类、分章节的整理一下,但是,在后来的整理过程中,却发现十分复杂——语言的各个章节交错关联,很难把一个知识点简单的归为一个分类、章节下,甚至一个知识点会牵涉到几个分类下的知识,不好一一说明。(题外话:当我见到《C:参考手册》时,我对其作者特别敬佩,因为他能把这么复杂的语法关系说的这么明白)
尽管Python相对C/C++来说,学习和使用起来比较简单,但其仍有同样的交错复杂。所以,本文只是把一些Python语法要点、难点进行摘要、解析,并不打算讲述全面的Python语法,而是主要分析Python中比较难以理解的部分,比如:Python中变量名与对象的关系、Python中的变量名查找问题、Python在继承时对象的属性是如何查找的等等,以帮助一些初学者迅速理解、掌握Python语言。本文主要根据《Python学习手册(第四版)》进行整理,其中大量摘录了其中的原话,并加入了一些笔者的理解。如果想要学习Python,笔者建议看一下《Python学习手册(第四版)》和Python的官方手册(有全面的索引文档,包括初级学习手册、语法手册、各个标准库的介绍、C/C++接口等等)。在学习Python的过程中,笔者老是见到有人推荐《深入Python》或《深入Python3》,笔者看了一下,不建议初学者看这本书;如果你要问为什么,没有为什么,笔者只是通过自己的学习以及对Python的了解来感觉的,如果你非要问个为什么,我记得优快云上好像有一篇文章专门讨论了这个问题(网上现在也有一些人反对初学者去看它)或者用搜索引擎去搜索一下。
所以,本文只是辅助一些人对Python在某些方面有一些深入的了解,不建议把它作为一个手册。(当然,你实际上也不会把它作为一个手册!)另外,如果本文没有特殊说明,本文所讲的语法、知识都是基于Python官方的实现——C实现,即CPython;而且,如果没有特殊说明,本文基于的Python版本默认为3.X。
说明一下,《Python学习手册(第四版)》(中文版)的最后一部分没有,只有电子稿,要想阅读,得上网上下载;而且,这本书比较厚,除了最后没有给出的那一部分,整本书还有千把页;另外,在这本书中,作者确实有点比较啰嗦,也就是说,他的一句话会在几个章节里面老是重复出现,一句话非要说上几遍才行,这也可能是书比较厚的原因之一吧,你得适应它的这个状况。
一、基本知识
1、Python命名惯例:
(1)以单一下划线开头的变量名(_X)不会被from module import *语句导入;
(2)前后有双下划线的变量名(__X__)是系统定义的变量名,对解释器有特殊意义;
(3)以两个下划线开头,但结尾没有下划线的变量名(__X)是类的本地变量;
(4)通过交互模式运行时,只有单个下划线的变量名(_)会保存最后表达式的结果。
2、在Python中,变量名没有类型。类型属于对象,而不是变量名;变量名只是引用对象而已。
3、Python中所有的语句都是实时运行的,没有像独立的编译时间这样的流程——这是解析型编程语言的特征之一。
4、作为首要的最佳实践规则是:针对功能性文档(你的对象做什么)使用文档字符串;针对更加微观的文档(令从费解的是表达式是如何工作的)使用 # 注解。
5、sys.path的设置方法只在修改的Python会话或程序(即进程)中才会持续,在Python结束后不会保留下来。PYTHONPATH和.pth文件路径配置是保存在操作系统中,而不是执行中的Python程序。
sys.path的组成部分:(1)程序的主目录;(2)PYTHONPATH目录(如果已经进行了设置);(3)标准链接库目录;(4)任何.pth文件的内容(如果存在的话)
6、迭代器:文件迭代器(open函数返回一个迭代器)
(1)所有迭代工具内部工作起来都是在每次迭代中调用__next__,并捕捉StopIterator异常来确定何时离开。
(2)逐行读取文本文件的最佳方式就是根本不要去读取;其替代的办法就是,让for循环在每轮自动调用next从而前进到下一行。
(3)迭代器在Python中是从C语言的速度运行的,而While循环版本则是通过Python虚拟机运行Python字节码的。
二、表达式与语句
1、Python中的三元表达式:Y if X else Z,等同于C语言中的三元表达式:X?Y:Z。
2、生成器与各种解析:
(1)生成器:(expression for target in iterator)
(expression1 for target in iterator if expression2)
(expression for target1 in iterator1 for target2 in iterator2)
注:其中的for循环和if判断语句可以互相无限嵌套。生成器是单迭代器对象。
(2)列表解析:把圆括号换成方括号即可。
(3)集合解析:把圆括号换成大括号即可。
(4)字典解析:{ x:f(x) for x in items }
字典解析的语法基本上与以上类似,只不过把相应的表达式改成字典的形式即可。
3、lambda表达式详解:
(1)lambda是一个表达式,而不是一个语句。所以它有一个返回值(即函数对象)
(2)lambda的主体是一个单个的表达式,而不是单个的代码块
(3)默认参数也能够在lambda参数中使用
(4)在lambda主体中的代码像在def内的代码一样都遵循相同的作用域查找法则。lambda表达式引入的一个本地作用域更像一个嵌套的def语句,将会自动从上层函数中、模块中以及内置作用域(通过LEGB法则)查找变量名。
(5)注解只在def语句中有效,在lambda表达式中无效,因为lambda的语法已经限制了它所定义的函数工具。
(6)lambda可以看成是一个被阉割了部分功能的小函数(比如:注解、主体只能是个表达式等等),所以,它具备了函数所具有的大部分功能,正所谓“麻雀虽小,五脏俱全”。
三、函数
( 一)函数的定义
[ decorators ] def funcname([id, id=value, *id, id=value, **id]) [ -> expr ] : suite
其中,id又可写成 id: expr,这是对参数的一个注解。
函数定义并不执行函数主体,而是定义一个函数对象,并把该对象绑定到函数名上;只有当调用函数时,才执行函数的主体。
(二)函数的参数
在一个函数头部,keyword-only参数必须编写在**args任意关键字形式之前,且在*args任意位置形式之后,当二者都有的时候。无论何时,一个参数名称出现在*args之前,它可能是默认位置参数,而不是keyword-only参数。
(三)函数的返回值
函数可以返回任意类型的任意值,但是在定义函数时不需要显示声明其返回值的类型,因为Python是弱类型编程语言。
(四)函数的调用
在函数调用中,类似的排序规则也是成立的:当传递keyword-only参数的时候,它们必须出现在一个**args形式之前。keyword-only参数可以编写在*args之前或者之后,并且可能包含在**args中。
Python内部是使用以下的步骤来在赋值前进行参数匹配的:
(1)通过位置分配非关键字参数
(2)通过匹配变量名分配关键字参数
(3)其他额外的非关键字参数分配到*name元组中
(4)其他额外的关键字参数分配到*name字典中
(5)用默认值分配给在头部未得到分配的参数
在这之后,Python检测来确保每个参数只传入了一个值。如果不是这样的话,将会发生错误。当所有的匹配都完成了,Python把传递给参数名的对象赋值给它们。
(五)函数的深入理解
1、Python中的def语句实际上是一个可执行的语句:当它运行时,它创建一个新的函数对象并将其赋值给一个变量名。因为def是一个语句,所以它可以出现在任何一个语句可以出现的地方——甚至是嵌套在其他的语句中。在执行def语句时,并不会执行函数中的实际代码,它只是创建一个函数对象;只有当通过变量名进行函数调用时,才实际执行函数主体中的代码。因为函数内的变量名在函数实际执行前都不会解析,通常可以利用文件内任意地方的变量。另外,由上我们可以看出,在执行def语句时,隐含着执行了一个赋值操作,因此,我们也可以认为def语句是一个赋值语句。
2、所有的在函数内部进行赋值的变量名都默认为本地变量;所有的本地变量都会在函数调用时出现,并在函数退出时消失。每次对函数的调用都创建了一个新的本地作用域。也就是说,将会存在由那个函数创建的变量的命名空间。
3、本地变量是静态检测的。在函数中,不可能同时使用同一个简单变量名的本地变量名和全局变量名。如果希望打印全局变量,并在之后设置一个相同变量名的本地变量,可以导入上层的模块,并使用模块的属性标记来获得全局变量。
4、无法从def语句外读取函数或方法内的局部变量。局部变量对于在def内的其余代码才是可见的。而事实上,也只有函数调用或方法执行时,才会存在于内存中。
5、Python中的参数传递机制与C语言中的参数传递机制类似:
(1)不可变对象参数通过“值”进行传递(C语言中的“值传递”)
(2)可变对象参数是通过“指针”进行传递的(C语言中“地址传递”)
(3)无论是不可变对象还是可变对象,Python的参数传递都是把对象赋值给变量名(C语言中“地址传递”本质上还是“值”传递,只不过能够修改原实参的值而已)
6、参数的传递是通过自动将对象赋值给本地变量名来实现的。因为引用是以指针的形式实现的,所有的参数实际上都是通过指针进行传递的,但我们不能对该指针进行反解(即直接使用该指针所代表的地址空间)。因此,这就带来了一个结果,我们无法使用Python像C/C++那样编写一个swap函数,用来交换两个参数的值,换句话说,这在Python中是无法实现的。
7、在函数内部的参数名的赋值不会影响调用者。在函数运行时,在函数头部的参数名是一个新的、本地的变量名,这个变量名是在函数的本地作用域内的。
8、改变函数的可变对象参数的值也许会对调用者有影响。
9、默认参数是在def语句运行时评估保存的,而不是在这个函数调用时。从内部来讲,Python会将每一个默认参数保存成一个对象,附加在这个函数本身。所以,如果默认参数是一个可变对象的话,那么该函数的所有调用都共享这个可变对象。如果默认参数是可变对象,为了使该函数的每次调用时,该可变对象都互不相关,有一个技术可以实现:把该默认参数写None对象,然后在调用时检查该默认参数是否是None,如果是就自动建立一个局部该类型的可变对象;如果不是None,则说明函数调用没有使用默认参数,所以就可以直接使用它了。
10、可以通过定义具有不同参数个数的多个具有相同函数名的函数对象,可以实现函数重载。
11、在文件间进行通信的最好办法就是通过调用函数,传递参数,然后得到其返回值。
四、类
在没有讲述本节之前,先澄清一个事实:在class语句中定义方法时,我们在第一个参数位置处问题写一个self,但这不是强制的,这是一个约定(Python官方的约定),是为了告诉编写者或其他查看本代码的人——这是调用本方法的对象本身。当然,你可以把self改成任意合法的标识符,但你要记着,不管你把它改成什么样的标识符,Python在执行该方法时,都把调用该方法的对象本身传递给它,换句话说,你在使用它时,要把它当成调用该方法的对象本身,而不是其它的什么;不管你把它当成什么,反正Python语言本身会把它当成调用该方法的对象本身。但是,在调用该方法时,你不能把调用该方法的对象传递给它,因为这是Python默认在后台进行传递的,是Python的约定的任务,而不是你的任务。请注意它!
1、同def语句一样,class语句也是一个执行语句;当执行class语句时,Python将创建一个该类型的类型对象,然后把该类型对象赋值给类名,由类名来引用;当执行class语句时(不是调用该类),Python会从头至尾执行其主体内的所有语句,在这个过程中,进行的赋值运算会在这个类作用域中创建变量名,从而成为对应的类对象内的属性。注:类主体中的方法可以理解为函数;“Python会从头至尾执行其主体内的所有语句”的意思是执行那些class主体中的顶层的赋值语句和定义的方法(相当于函数,也可以认为是一个赋值语句),以便建立类对象的内置属性,而方法中的语句并不执行;对于类主体中的方法中的主体代码,只有当用该类型对象创建对象后,通过该对象调用该方法,才执行该方法中的主体代码。
2、不像C++等其他编程语言,Python中的类对象也有属性,称为类本身的属性,它是由class语句内的顶层的赋值语句(包括方法)产生的,该属性存储在类对象的命令空间当中。
3、由类对象创建的该类型的对象也有属性,它不是由类对象创建的,而是当该对象调用类中的方法时,由该方法中的self(指该对象本身)创建的。另外,对象并不保存类的属性,但可以引用类的属性(其原因请参见下面);而类对象却不能引用对象的属性。换句不太十分准确的话,类对象的属性是类对象和所有的由该类对象创建的对象所共享。
4、我们再反向说明一下:类对象的属性是在class语句中的顶层做赋值运算(包括方法)来创建的,而对象的属性是通过在方法中引用self.attribution并为其提供一个赋值操作来创建的,即在方法内对self属性做赋值运算会产生每个实例自己的属性。(对self的属性做赋值运算,会创建或修改实例内的数据,而不是类的数据。)
5、class语句内的顶层的赋值语句(不是在def之内)会产生类对象中的属性。从技术角度来讲,class语句的作用域会变成类对象的属性的命名空间。
6、每个实例对象继承类的属性并获得了自己的命名空间。由类所创建的实例对象是新命名空间。
7、不像C++等其他编程语言,在Python中,不管是类对象的属性还是对象的属性,在其创建之后,还可以动态的修改、增加、删除其属性,而且你无法对这种现象做出限制,换句话说,你没有任何办法来防止他人在使用它的过程中动态地修改、增加、删除它的属性。在Python中,默认所有的属性都是可读取的。
8、对实例的属性进行赋值运算会在该实例内创建或修改变量名,而不是在共享的类中。因此,我们无法通过对实例对象引用的属性直接进行赋值通常的情况下,继承搜索只会在属性引用时发生,而不是在赋值运算时发生:对对象属性进行赋值总是会修改该对象,除此之外,没有其他的影响。附加在类上时,变量名共享的(类似于C++中的静态成员变量);附加在实例上时,变量名是属于每个实例的数据,而不是共享的行为或数据。
9、理论的角度讲,类(和类实例)是可改变的对象。所有从类产生的实例都共享这个类的命名空间,任何在类层次所做的修改都会反映在所有实例中,除非实例拥有自己的被修改的类属性版本。
修改可变的类属性也可能产生副作用。如果一个类属性引用一个可变对象,那么从任何实例来源处修改该对象都会立刻影响到所有实例。
10、类只是独立完备的命名空间,只要有类的引用值,就可以在任何时刻增删或修改其属性。
11、命名空间对象的属性通常都是以字典的形式实现的,而类继承树(一般而言)只是连接至其他字典而已。另外,值得注意的是,任何对象(无论是内置还是自定义创建的)都有一个__dict__属性,它是一具字典,是用来保存该对象的属性及其值的,我们平常所使用的object.attribution,在底层上,Python会把先转换成object.__dict__[attribution]字典(可能的话会进行继承搜索),然后通过键引用其值。
12、因为属性实际上是Python的字典键,所以其实有两种方式可以读取并对其进行赋值:通过点号运算或者通过键索引运算。不过,这种等效关系只适用于实际中附加在实例上的属性。因为属性点号运算也会执行继承搜索,所以可以存取命名空间字典索引运算无法读取的属性。
13、内置的instance.__class__属性提供了一个从实例到创建它的类的链接。类反过来有一个__name__(就像模块一样),还有一个__base__序列,提供了超类的访问。
内置的object.__dict__属性提供了一个字典,带有一个键/值对,以便每个属性都附加到一个命名控件对象(包括模块、类和实例)。
因此,对象可以通过instance.__class__.__dict__[attribution]来引用类对象的属性,甚至可以通过instance.__class__.__base__.dict__[attribution]来引用基类对象中的属性。
14、每个object.attribute都会开启新的独立搜索。继承搜索只发生在引用时,而不是赋值时。每次使用object.attr形式的类表达式时(object是实例或类对象),Python会从头至尾搜索命名空间树,先从对象开始,接着是类对象,最后是超类对象(从左到右,递归地进行),寻找所能找到的第一个attr为止。这包括在方法中对self属性的引用。因为树中较低的定义会覆盖较高的定义,继承构成了专有化的基础。
15、搜索属性时,Python会由左至右搜索类首行中超类,直到找到相符者。从技术上来讲,因为任何超类本身可能还有一些其他的超类,对于更大的类树,这个搜索可以更复杂一点。
(1)在传统类中(默认的类,直到Python3.0),属性搜索处理对所有路径深度优先,直到继承树的顶端,然后从左到右进行。
(2)在新式类(以及Python3.x的所有类中),属性搜索处理沿着树层级、以更加广度优先的方式进行。
16、在构造时,Python会找出并且只调用一个__init__。如果要保证子类的构造函数也会执行超类构造时的逻辑,一般必须通过类明确地调用超类的__init__方法。
17、对于类中方法的调用,Python有两个方式:一个是class.method(instance, args ...),另一个是instance.method(args ...)。在底层上,Python会自动地将instance.method(args...)调用方式转换为class.method(instance, args...)调用方式。一般来说,这两种方式使用哪一种都可以;但是,这两种方式还是有区别的,前一种方式只会在本类对象的命名空间中查找该method方法,如果找不到,就宣告调用失败;而后一种方式首先在该类对象的命名空间中查找method方法,如果找不到,就继续向上、向其基类中查找,直到查到object类(所有类的父类、基类),如果还是找不到,才宣告失败;如果找到了,就调用相应的方法。
五、变量名、对象、作用域及命名空间(模块)的分析
(一)变量名与对象
1、Python自称是完全面向对象编程语言,即所有的东西都是对象,包括其他常见编程语言(如C++、Java、C#等)中的对象、类对象以及函数、类本身等等。
此时,在学习Python时会对初学者造成一定的误解,这是因为Python使用者并不直接使用这些对象,而是通过一个变量名。
2、在其他常见的编程语言(如C++),变量名代表着(或者说绑定到)一个地址空间,而且一旦绑定,以后是不能改变的,所以,如果我们把这个地址空间称为一个“对象”的话,那么这个变量名就直接可以称为“对象“了。但是,在Python中,变量名却有着不定的含义。在Python中,对象相当于C++中的变量或类对象,是可以直接操作数据存储的,但这个对象是封装在底层的,为了方便使用者使用,所以就使用一个名字,这个名字可以引用任意的对象,而这个名字就是变量名。在这,我们要明白一点,变量名是可以随意改变的,即可以在任何时候、随意地指向任何对象,例如:如果变量A指向一个整数对象B,然后可以改变A使它再次指向字符串对象C,此时对A的操作只作用于它所指向的字符串对象C,而对整数对象B不会产生任何影响。
举个例子:
A = 123
A = "abc"
在Python执行第一个语句时,Python会先通过整数类型构造函数创建一个整数对象(如果该对象不存在的话),并把它的值赋值为123,最后,用变量名A来指向这个整数对象。当执行第二条语句时,Python会先通过字符串类型构造函数创建一个字符串对象(如果该对象不存在的话),并把它的值赋值为"abc",最后,用变量名A来指向这个字符串对象,而以前的整数对象(其值为123)就不能再通过变量名A来引用了。
3、通过上例,我们可以发现一个问题,对于某些可变对象或者一些其他情况,如果我们要改变它本身的值并且原变量名仍然指向该变量时,不能简单的使用等号(=)操作符;这种情况尤其是通过from进行模块导入时,最让人容易忽略它的弊端。要想真正地解决原问题,可以直接引用原底层对象;这并没有违背我们上面所说,其中的一个原理可以参见类对象属性的添加。
4、在此,我们说明一点:虽然Python把所有的对象封装到底层,并通过一个名字来引用,但是,当操作这个名字时的任何动作都会作用到它所指向的对象,就像我们在直接操作底层对象一样。变量名没有类型,你可以把简单地理解成C/C++中的宏定义。因此,我们可以看出,变量名并没有什么实际意义。
(二)作用域与命名空间
1、Python中的变量名解析机制:LE(N)GB法则:
(1)当在函数中使用未认证的变量名时,Python会依次搜索4个作用域[本地作用域(L),之后是上一层结构中def或lambda的本地作用域(E),之后是全局作用域(G),最后是内置作用域(B)],并且在第一处能够找到这个变量名的地方停下来。如果变量名在这次搜索中没有找到,Python就会报错。
(2)当在函数中给一个变量名赋值时(而不是在一个表达式中对其进行引用),Python总是创建或改变本地作用域中的变量名,除非它已经明确地在那个函数中声明为全局变量(global)或非局部变量(nonlocal)以改变其属性。
(3)当在函数之外给一个变量名赋值时(也就是,在一个模块文件的顶层,或者是交互提示模式下),本地作用域与全局作用域(这个模块的命名空间)是相同的。
2、作用域可以做任意的嵌套(也就是说,函数可以任意层次地嵌套),但是只有内嵌的函数(而不是类)会被搜索。
3、nonlocal应用于一个嵌套的函数的作用域中的一个名称,而不是所有def之外的全局模块作用域;而且在声明nonlocal名称的时候,它必须已经存在于该嵌套函数的作用域中——它们可能只存在于一个嵌套的函数中,并且不能由一个嵌套的def中的第一次赋值创建。换句话说,nonlocal即允许对嵌套的函数作用域的名称赋值,并且把这样的名称的作用域查找限制在嵌套在def中。nonlocal使得对语句中列出的名称的查找从嵌套的def的作用域中开始,而不是从声明变量的本地作用域开始,也就是说,nonlocal也意味着“完全略过我的本地作用域”。实际上,当执行到nonlocal语句时,nonlocal中列出的名称必须在一个嵌套的def中提前定义过;否则,将会产生一个错误。
4、global与nonlocal的区别:
(1)global使得作用域查找从嵌套的模块的作用域开始,并且允许对那里的名称赋值。如果名称不存在于该模块中,作用域查找继续到内置作用域,但是,对全局名称的赋值总是在模块的作用域中创建或修改它们。
(2)nonlocal限制作用域查找只是嵌套的def,要求名称已经存在于那里,并且允许对它们赋值。作用域查找不会继续到全局或内置作用域。
5、全局作用域的作用范围仅限于单个文件。在Python中没有基于一个单个的、无所不包的情景文件的全局作用域的。
6、赋值的变量名除非被声明为全局变量或非本地变量,否则均为本地变量。在默认情况下,所有函数定义内部的变量名是位于本地作用域(与函数调用相关的)内的。如果需要给一个在函数内部都位于模块文件顶层的变量名赋值,需要在函数内部通过global语句声明。如果需要给位于一个嵌套的def中的名称赋值,从Python3.0开始可以通过在一条nonlocal语句中声明它来做到。
7、所有其他的变量名都可以归纳为本地、全局或者内置的。
8、任何情况下,一个变量的作用域(它所使用的地方)总是由在代码中被赋值的地方所决定,并且与函数调用完全没有关系。
9、各种赋值与引用的总结:
(1)无点号的简单变量名遵循函数LEGB作用域法则:
赋值语句(X=Value):使变量名成为本地变量——在当前作用域内创建或改变变量名X,除非声明它是全局变量。
引用(X):在当前作用域内搜索变量名X,之后是在任何以及所有的嵌套的函数中,然后是在当前的全局作用域中搜索,最后在内置作用域中搜索。
(2)点号的属性名指的是特定对象的属性,并且遵循模块和类的规则:
赋值语句(object.X=value):在进行点号运算的对象的命名空间内创建或修改属性名X,并没有其他作用。继承树的搜索只发生在属性引用时,而不是属性的赋值运算时。
引用(object.X):就基于类的对象而言,会在对象内搜索属性名X,然后是其上所有可读取的类(使用继承搜索流程)。对于不是基于类的对象而言(例如:模块),则是从对象中直接读取X。
10、当在一个程序中使用变量名时,Python创建、改变或查找变量名都是在所谓的命名空间(一个保存变量名的地方)中进行的。
11、在代码中给一个变量赋值的地方决定了这个变量将存在于哪个命名空间,也就是它可见的范围。
12、除打包代码之外,函数还为程序增加了一个额外的命名空间层:在默认情况下,一个函数的所有变量名都是与函数的命名空间相关联的。这意味着:
(1)一个在def内定义的变量名能够被def内的代码使用,不能在函数的外部引用这样的变量名;
(2)def之内的变量名与def之外的变量名并不冲突,即使是使用在别处的相同的变量名。
(三)模块、包的导入
1、一个模块文件的全局变量一旦被导入就成为了这个模块对象的一个属性:导入者自动得到了这个被导入的模块文件的所有全局变量的访问权。所以,在一个文件被导入后,它的全局作用域实际上就构成了一个对象的属性。
2、导入只发生一次。
(1)当一个模块被导入时,Python会把内部模块名映射到外部文件名,也就是通过把模块搜索路径中的目录路径加在前边,而.py或其他后缀名添加在后边。
(2)import会读取整个模块,所以必须进行定义后才能读取它的变量名;from将获取(或者说是复制)模块特定的变量名。
(3)人技术角度来说,import和from语句都会使用相同的导入操作。from *形式只是多加个步骤,把模块中所有变量名复制到了进行导入的作用域之内。从根本上说,就是把一个模块的命名空间融入另一个模块之中;同样地,实际效果就是可以让我们少输入一些。
(4)import和from都是隐性的赋值语句。import将整个模块对象赋值给一个变量名;from将一个或多个变量名赋值给另一个模块中同名的对象。
(5)以from复制而来的变量名和其来源的文件之间并没有联系。为了实际修改另一个文件中的全局变量名,必须使用import。
(6)from只是把变量名从一个模块复制到另一个模块,并不会对模块名本身进行赋值。从概念上来说,以下两种语句等效:
from module import name1, name2
等价于:
import module
name1 = module.name1
name2 = module.name2
del module
(7)from语句有破坏命名空间的潜质,理论上是这样的,简单模块一般倾向于使用import,而不是from。
(8)当你必须使用两个不同模块定义的相同变量名的变量时,才真的必须使用import,这种情况下不能使用from。
3、from复制变量名,而不是连接。from语句其实是在导入者的作用域内对变量名的赋值语句,也就是变量名拷贝运算,而不是变量名的别名机制。它的实现和Python所有赋值运算都一样,但是其微妙之处在于,共享对象的代码存在于不同的文件中。
4、模块就是命名空间(变量名建立所在的场所),而存在于模块之内的顶层的变量名就是模块对象的属性。
5、模块语句会在首次导入时进行。系统中,模块在第一次导入时,无论在什么地方,Python都会建立空的模块对象,并逐一执行该模块文件内的语句,依照文件从头到尾的顺序;而所有的赋值语句都会建立本模块的属性。
6、在Python2.6中,包的代码中的常规导入(没有前面的点号),且前默认为一种先相对再绝对的搜索路径顺序,也就是说,它们首先搜索包自己的路径。然而,在Python3.0中,在一个包中导入默认是绝对的——在缺少任何特殊的点语法的时候,导入忽略了包含自身并在sys.path搜索路径上的某处查找。
7、相对导入的作用域:
(1)相对导入只适用于在包内导入
(2)相对导入只是用于from语句
8、使用包含导入和相对导入,Python3.0中的模块查找可以完整地概括为如下几条:
(1)简单模块(例如A)通过搜索sys.path路径列表上的每个目录来查找,从左到右进行。这个列表由系统默认设置和用户配置设置组成。
(2)包是带有一个特殊的__init__.py文件的Python模块的直接目录,这使得一个导入中可以使用A.B.C目录路径语法。在A.B.C的一条导入中,名为A的目录位于相对于sys.path的常规模块导入搜索,B是A中的另一个包子目录,C是一个模块或B中的其他可导入项。
(3)在一个包文件中,常规的import语句使用和其他地方的导入一样的sys.path搜索规则。包中的导入使用from语句以及前面的点号,然而,它是相对包的;也就是说,只检查包目录,并且不使用常规的sys.path查找。例如,在from . import A中,模块搜索限制在包含了该语句中的出现的文件的目录之中。
9、导入操作不会赋予被导入文件中的代码对上层代码的可见度:被导入文件无法看见进行导入的文件内的变量名。
10、reload不会影响from导入;递归形式的from导入无法工作。
11、每个模块都有一个名为__name__的内置属性,Python会自动设置该属性:
(1)如果文件是以顶层程序文件执行的,在启动时,__name__就会设置为字符串"__main__"
(2)如果文件被导入,__name__就会成为设成客户端所了解的模块名。
12、包导入:
(1)当我们使用 from package import item语法时,item要么是个包package的子模块(或子包),要么是在包package中定义的其他名字,如:函数、类或变量。import语句首先测试item是否已经在包package中定义,如果还没有,它假定它是一个模块并试图导入它;如果找不到这个模块,一个ImportError异常将被抛出。
(2)相应地,当使用像import item.subitem.subsubitem语法时,除了最后一个item外,每个item都必须是一个包。最后一个item可以是一个模块或者一个包,但不能是一个在前一个item中定义的类、函数或变量;另外,要想引用它,还必须通过它的全名,如:“item.subitem.subitem.attribution”。
(3)当使用语法“from package import *”时,如果在一个包中的__init__.py文件中定义了一个名为__all__的列表,那么只有该列表中列出的名字才能被导入;如果__all__没有被定义,语句“from package import *”并不从包packae中导入所有的子模块到当前命名空间,它只保证包package已经被导入(可能运行__init__.py中的任何初始化代码),并且导入所有在该包package中被定义的名字(这包括任何被__init__.py定义的名字,也包括被上一个import语句显示导入的包的任何子模块)。
五、异常
1、经常会失败的运算一般都应该包装在try语句内,除非你希望这类运算失败时终止程序,而不是被捕捉或是忽略。如果是一个重大的错误更是如此。
2、应该在try/finally中实现终止动作,从而保证它们的执行,除非环境管理器作为一个with/as选项可用。
3、偶尔,把对大型函数的调用包装在单个try语句内,而不是让函数本身零散着放入若干try语句中,这样会更方便。
六、高级应用
写在前面
笔者在学习C/C++时,本想通过自己的学习,把C/C++的全部语法通过分类、分章节的整理一下,但是,在后来的整理过程中,却发现十分复杂——语言的各个章节交错关联,很难把一个知识点简单的归为一个分类、章节下,甚至一个知识点会牵涉到几个分类下的知识,不好一一说明。(题外话:当我见到《C:参考手册》时,我对其作者特别敬佩,因为他能把这么复杂的语法关系说的这么明白)
尽管Python相对C/C++来说,学习和使用起来比较简单,但其仍有同样的交错复杂。所以,本文只是把一些Python语法要点、难点进行摘要、解析,并不打算讲述全面的Python语法,而是主要分析Python中比较难以理解的部分,比如:Python中变量名与对象的关系、Python中的变量名查找问题、Python在继承时对象的属性是如何查找的等等,以帮助一些初学者迅速理解、掌握Python语言。本文主要根据《Python学习手册(第四版)》进行整理,其中大量摘录了其中的原话,并加入了一些笔者的理解。如果想要学习Python,笔者建议看一下《Python学习手册(第四版)》和Python的官方手册(有全面的索引文档,包括初级学习手册、语法手册、各个标准库的介绍、C/C++接口等等)。在学习Python的过程中,笔者老是见到有人推荐《深入Python》或《深入Python3》,笔者看了一下,不建议初学者看这本书;如果你要问为什么,没有为什么,笔者只是通过自己的学习以及对Python的了解来感觉的,如果你非要问个为什么,我记得优快云上好像有一篇文章专门讨论了这个问题(网上现在也有一些人反对初学者去看它)或者用搜索引擎去搜索一下。
所以,本文只是辅助一些人对Python在某些方面有一些深入的了解,不建议把它作为一个手册。(当然,你实际上也不会把它作为一个手册!)另外,如果本文没有特殊说明,本文所讲的语法、知识都是基于Python官方的实现——C实现,即CPython;而且,如果没有特殊说明,本文基于的Python版本默认为3.X。
说明一下,《Python学习手册(第四版)》(中文版)的最后一部分没有,只有电子稿,要想阅读,得上网上下载;而且,这本书比较厚,除了最后没有给出的那一部分,整本书还有千把页;另外,在这本书中,作者确实有点比较啰嗦,也就是说,他的一句话会在几个章节里面老是重复出现,一句话非要说上几遍才行,这也可能是书比较厚的原因之一吧,你得适应它的这个状况。
一、基本知识
1、Python命名惯例:
(1)以单一下划线开头的变量名(_X)不会被from module import *语句导入;
(2)前后有双下划线的变量名(__X__)是系统定义的变量名,对解释器有特殊意义;
(3)以两个下划线开头,但结尾没有下划线的变量名(__X)是类的本地变量;
(4)通过交互模式运行时,只有单个下划线的变量名(_)会保存最后表达式的结果。
2、在Python中,变量名没有类型。类型属于对象,而不是变量名;变量名只是引用对象而已。
3、Python中所有的语句都是实时运行的,没有像独立的编译时间这样的流程——这是解析型编程语言的特征之一。
4、作为首要的最佳实践规则是:针对功能性文档(你的对象做什么)使用文档字符串;针对更加微观的文档(令从费解的是表达式是如何工作的)使用 # 注解。
5、sys.path的设置方法只在修改的Python会话或程序(即进程)中才会持续,在Python结束后不会保留下来。PYTHONPATH和.pth文件路径配置是保存在操作系统中,而不是执行中的Python程序。
sys.path的组成部分:(1)程序的主目录;(2)PYTHONPATH目录(如果已经进行了设置);(3)标准链接库目录;(4)任何.pth文件的内容(如果存在的话)
6、迭代器:文件迭代器(open函数返回一个迭代器)
(1)所有迭代工具内部工作起来都是在每次迭代中调用__next__,并捕捉StopIterator异常来确定何时离开。
(2)逐行读取文本文件的最佳方式就是根本不要去读取;其替代的办法就是,让for循环在每轮自动调用next从而前进到下一行。
(3)迭代器在Python中是从C语言的速度运行的,而While循环版本则是通过Python虚拟机运行Python字节码的。
二、表达式与语句
1、Python中的三元表达式:Y if X else Z,等同于C语言中的三元表达式:X?Y:Z。
2、生成器与各种解析:
(1)生成器:(expression for target in iterator)
(expression1 for target in iterator if expression2)
(expression for target1 in iterator1 for target2 in iterator2)
注:其中的for循环和if判断语句可以互相无限嵌套。生成器是单迭代器对象。
(2)列表解析:把圆括号换成方括号即可。
(3)集合解析:把圆括号换成大括号即可。
(4)字典解析:{ x:f(x) for x in items }
字典解析的语法基本上与以上类似,只不过把相应的表达式改成字典的形式即可。
3、lambda表达式详解:
(1)lambda是一个表达式,而不是一个语句。所以它有一个返回值(即函数对象)
(2)lambda的主体是一个单个的表达式,而不是单个的代码块
(3)默认参数也能够在lambda参数中使用
(4)在lambda主体中的代码像在def内的代码一样都遵循相同的作用域查找法则。lambda表达式引入的一个本地作用域更像一个嵌套的def语句,将会自动从上层函数中、模块中以及内置作用域(通过LEGB法则)查找变量名。
(5)注解只在def语句中有效,在lambda表达式中无效,因为lambda的语法已经限制了它所定义的函数工具。
(6)lambda可以看成是一个被阉割了部分功能的小函数(比如:注解、主体只能是个表达式等等),所以,它具备了函数所具有的大部分功能,正所谓“麻雀虽小,五脏俱全”。
三、函数
( 一)函数的定义
[ decorators ] def funcname([id, id=value, *id, id=value, **id]) [ -> expr ] : suite
其中,id又可写成 id: expr,这是对参数的一个注解。
函数定义并不执行函数主体,而是定义一个函数对象,并把该对象绑定到函数名上;只有当调用函数时,才执行函数的主体。
(二)函数的参数
在一个函数头部,keyword-only参数必须编写在**args任意关键字形式之前,且在*args任意位置形式之后,当二者都有的时候。无论何时,一个参数名称出现在*args之前,它可能是默认位置参数,而不是keyword-only参数。
(三)函数的返回值
函数可以返回任意类型的任意值,但是在定义函数时不需要显示声明其返回值的类型,因为Python是弱类型编程语言。
(四)函数的调用
在函数调用中,类似的排序规则也是成立的:当传递keyword-only参数的时候,它们必须出现在一个**args形式之前。keyword-only参数可以编写在*args之前或者之后,并且可能包含在**args中。
Python内部是使用以下的步骤来在赋值前进行参数匹配的:
(1)通过位置分配非关键字参数
(2)通过匹配变量名分配关键字参数
(3)其他额外的非关键字参数分配到*name元组中
(4)其他额外的关键字参数分配到*name字典中
(5)用默认值分配给在头部未得到分配的参数
在这之后,Python检测来确保每个参数只传入了一个值。如果不是这样的话,将会发生错误。当所有的匹配都完成了,Python把传递给参数名的对象赋值给它们。
(五)函数的深入理解
1、Python中的def语句实际上是一个可执行的语句:当它运行时,它创建一个新的函数对象并将其赋值给一个变量名。因为def是一个语句,所以它可以出现在任何一个语句可以出现的地方——甚至是嵌套在其他的语句中。在执行def语句时,并不会执行函数中的实际代码,它只是创建一个函数对象;只有当通过变量名进行函数调用时,才实际执行函数主体中的代码。因为函数内的变量名在函数实际执行前都不会解析,通常可以利用文件内任意地方的变量。另外,由上我们可以看出,在执行def语句时,隐含着执行了一个赋值操作,因此,我们也可以认为def语句是一个赋值语句。
2、所有的在函数内部进行赋值的变量名都默认为本地变量;所有的本地变量都会在函数调用时出现,并在函数退出时消失。每次对函数的调用都创建了一个新的本地作用域。也就是说,将会存在由那个函数创建的变量的命名空间。
3、本地变量是静态检测的。在函数中,不可能同时使用同一个简单变量名的本地变量名和全局变量名。如果希望打印全局变量,并在之后设置一个相同变量名的本地变量,可以导入上层的模块,并使用模块的属性标记来获得全局变量。
4、无法从def语句外读取函数或方法内的局部变量。局部变量对于在def内的其余代码才是可见的。而事实上,也只有函数调用或方法执行时,才会存在于内存中。
5、Python中的参数传递机制与C语言中的参数传递机制类似:
(1)不可变对象参数通过“值”进行传递(C语言中的“值传递”)
(2)可变对象参数是通过“指针”进行传递的(C语言中“地址传递”)
(3)无论是不可变对象还是可变对象,Python的参数传递都是把对象赋值给变量名(C语言中“地址传递”本质上还是“值”传递,只不过能够修改原实参的值而已)
6、参数的传递是通过自动将对象赋值给本地变量名来实现的。因为引用是以指针的形式实现的,所有的参数实际上都是通过指针进行传递的,但我们不能对该指针进行反解(即直接使用该指针所代表的地址空间)。因此,这就带来了一个结果,我们无法使用Python像C/C++那样编写一个swap函数,用来交换两个参数的值,换句话说,这在Python中是无法实现的。
7、在函数内部的参数名的赋值不会影响调用者。在函数运行时,在函数头部的参数名是一个新的、本地的变量名,这个变量名是在函数的本地作用域内的。
8、改变函数的可变对象参数的值也许会对调用者有影响。
9、默认参数是在def语句运行时评估保存的,而不是在这个函数调用时。从内部来讲,Python会将每一个默认参数保存成一个对象,附加在这个函数本身。所以,如果默认参数是一个可变对象的话,那么该函数的所有调用都共享这个可变对象。如果默认参数是可变对象,为了使该函数的每次调用时,该可变对象都互不相关,有一个技术可以实现:把该默认参数写None对象,然后在调用时检查该默认参数是否是None,如果是就自动建立一个局部该类型的可变对象;如果不是None,则说明函数调用没有使用默认参数,所以就可以直接使用它了。
10、可以通过定义具有不同参数个数的多个具有相同函数名的函数对象,可以实现函数重载。
11、在文件间进行通信的最好办法就是通过调用函数,传递参数,然后得到其返回值。
四、类
在没有讲述本节之前,先澄清一个事实:在class语句中定义方法时,我们在第一个参数位置处问题写一个self,但这不是强制的,这是一个约定(Python官方的约定),是为了告诉编写者或其他查看本代码的人——这是调用本方法的对象本身。当然,你可以把self改成任意合法的标识符,但你要记着,不管你把它改成什么样的标识符,Python在执行该方法时,都把调用该方法的对象本身传递给它,换句话说,你在使用它时,要把它当成调用该方法的对象本身,而不是其它的什么;不管你把它当成什么,反正Python语言本身会把它当成调用该方法的对象本身。但是,在调用该方法时,你不能把调用该方法的对象传递给它,因为这是Python默认在后台进行传递的,是Python的约定的任务,而不是你的任务。请注意它!
1、同def语句一样,class语句也是一个执行语句;当执行class语句时,Python将创建一个该类型的类型对象,然后把该类型对象赋值给类名,由类名来引用;当执行class语句时(不是调用该类),Python会从头至尾执行其主体内的所有语句,在这个过程中,进行的赋值运算会在这个类作用域中创建变量名,从而成为对应的类对象内的属性。注:类主体中的方法可以理解为函数;“Python会从头至尾执行其主体内的所有语句”的意思是执行那些class主体中的顶层的赋值语句和定义的方法(相当于函数,也可以认为是一个赋值语句),以便建立类对象的内置属性,而方法中的语句并不执行;对于类主体中的方法中的主体代码,只有当用该类型对象创建对象后,通过该对象调用该方法,才执行该方法中的主体代码。
2、不像C++等其他编程语言,Python中的类对象也有属性,称为类本身的属性,它是由class语句内的顶层的赋值语句(包括方法)产生的,该属性存储在类对象的命令空间当中。
3、由类对象创建的该类型的对象也有属性,它不是由类对象创建的,而是当该对象调用类中的方法时,由该方法中的self(指该对象本身)创建的。另外,对象并不保存类的属性,但可以引用类的属性(其原因请参见下面);而类对象却不能引用对象的属性。换句不太十分准确的话,类对象的属性是类对象和所有的由该类对象创建的对象所共享。
4、我们再反向说明一下:类对象的属性是在class语句中的顶层做赋值运算(包括方法)来创建的,而对象的属性是通过在方法中引用self.attribution并为其提供一个赋值操作来创建的,即在方法内对self属性做赋值运算会产生每个实例自己的属性。(对self的属性做赋值运算,会创建或修改实例内的数据,而不是类的数据。)
5、class语句内的顶层的赋值语句(不是在def之内)会产生类对象中的属性。从技术角度来讲,class语句的作用域会变成类对象的属性的命名空间。
6、每个实例对象继承类的属性并获得了自己的命名空间。由类所创建的实例对象是新命名空间。
7、不像C++等其他编程语言,在Python中,不管是类对象的属性还是对象的属性,在其创建之后,还可以动态的修改、增加、删除其属性,而且你无法对这种现象做出限制,换句话说,你没有任何办法来防止他人在使用它的过程中动态地修改、增加、删除它的属性。在Python中,默认所有的属性都是可读取的。
8、对实例的属性进行赋值运算会在该实例内创建或修改变量名,而不是在共享的类中。因此,我们无法通过对实例对象引用的属性直接进行赋值通常的情况下,继承搜索只会在属性引用时发生,而不是在赋值运算时发生:对对象属性进行赋值总是会修改该对象,除此之外,没有其他的影响。附加在类上时,变量名共享的(类似于C++中的静态成员变量);附加在实例上时,变量名是属于每个实例的数据,而不是共享的行为或数据。
9、理论的角度讲,类(和类实例)是可改变的对象。所有从类产生的实例都共享这个类的命名空间,任何在类层次所做的修改都会反映在所有实例中,除非实例拥有自己的被修改的类属性版本。
修改可变的类属性也可能产生副作用。如果一个类属性引用一个可变对象,那么从任何实例来源处修改该对象都会立刻影响到所有实例。
10、类只是独立完备的命名空间,只要有类的引用值,就可以在任何时刻增删或修改其属性。
11、命名空间对象的属性通常都是以字典的形式实现的,而类继承树(一般而言)只是连接至其他字典而已。另外,值得注意的是,任何对象(无论是内置还是自定义创建的)都有一个__dict__属性,它是一具字典,是用来保存该对象的属性及其值的,我们平常所使用的object.attribution,在底层上,Python会把先转换成object.__dict__[attribution]字典(可能的话会进行继承搜索),然后通过键引用其值。
12、因为属性实际上是Python的字典键,所以其实有两种方式可以读取并对其进行赋值:通过点号运算或者通过键索引运算。不过,这种等效关系只适用于实际中附加在实例上的属性。因为属性点号运算也会执行继承搜索,所以可以存取命名空间字典索引运算无法读取的属性。
13、内置的instance.__class__属性提供了一个从实例到创建它的类的链接。类反过来有一个__name__(就像模块一样),还有一个__base__序列,提供了超类的访问。
内置的object.__dict__属性提供了一个字典,带有一个键/值对,以便每个属性都附加到一个命名控件对象(包括模块、类和实例)。
因此,对象可以通过instance.__class__.__dict__[attribution]来引用类对象的属性,甚至可以通过instance.__class__.__base__.dict__[attribution]来引用基类对象中的属性。
14、每个object.attribute都会开启新的独立搜索。继承搜索只发生在引用时,而不是赋值时。每次使用object.attr形式的类表达式时(object是实例或类对象),Python会从头至尾搜索命名空间树,先从对象开始,接着是类对象,最后是超类对象(从左到右,递归地进行),寻找所能找到的第一个attr为止。这包括在方法中对self属性的引用。因为树中较低的定义会覆盖较高的定义,继承构成了专有化的基础。
15、搜索属性时,Python会由左至右搜索类首行中超类,直到找到相符者。从技术上来讲,因为任何超类本身可能还有一些其他的超类,对于更大的类树,这个搜索可以更复杂一点。
(1)在传统类中(默认的类,直到Python3.0),属性搜索处理对所有路径深度优先,直到继承树的顶端,然后从左到右进行。
(2)在新式类(以及Python3.x的所有类中),属性搜索处理沿着树层级、以更加广度优先的方式进行。
16、在构造时,Python会找出并且只调用一个__init__。如果要保证子类的构造函数也会执行超类构造时的逻辑,一般必须通过类明确地调用超类的__init__方法。
17、对于类中方法的调用,Python有两个方式:一个是class.method(instance, args ...),另一个是instance.method(args ...)。在底层上,Python会自动地将instance.method(args...)调用方式转换为class.method(instance, args...)调用方式。一般来说,这两种方式使用哪一种都可以;但是,这两种方式还是有区别的,前一种方式只会在本类对象的命名空间中查找该method方法,如果找不到,就宣告调用失败;而后一种方式首先在该类对象的命名空间中查找method方法,如果找不到,就继续向上、向其基类中查找,直到查到object类(所有类的父类、基类),如果还是找不到,才宣告失败;如果找到了,就调用相应的方法。
五、变量名、对象、作用域及命名空间(模块)的分析
(一)变量名与对象
1、Python自称是完全面向对象编程语言,即所有的东西都是对象,包括其他常见编程语言(如C++、Java、C#等)中的对象、类对象以及函数、类本身等等。
此时,在学习Python时会对初学者造成一定的误解,这是因为Python使用者并不直接使用这些对象,而是通过一个变量名。
2、在其他常见的编程语言(如C++),变量名代表着(或者说绑定到)一个地址空间,而且一旦绑定,以后是不能改变的,所以,如果我们把这个地址空间称为一个“对象”的话,那么这个变量名就直接可以称为“对象“了。但是,在Python中,变量名却有着不定的含义。在Python中,对象相当于C++中的变量或类对象,是可以直接操作数据存储的,但这个对象是封装在底层的,为了方便使用者使用,所以就使用一个名字,这个名字可以引用任意的对象,而这个名字就是变量名。在这,我们要明白一点,变量名是可以随意改变的,即可以在任何时候、随意地指向任何对象,例如:如果变量A指向一个整数对象B,然后可以改变A使它再次指向字符串对象C,此时对A的操作只作用于它所指向的字符串对象C,而对整数对象B不会产生任何影响。
举个例子:
A = 123
A = "abc"
在Python执行第一个语句时,Python会先通过整数类型构造函数创建一个整数对象(如果该对象不存在的话),并把它的值赋值为123,最后,用变量名A来指向这个整数对象。当执行第二条语句时,Python会先通过字符串类型构造函数创建一个字符串对象(如果该对象不存在的话),并把它的值赋值为"abc",最后,用变量名A来指向这个字符串对象,而以前的整数对象(其值为123)就不能再通过变量名A来引用了。
3、通过上例,我们可以发现一个问题,对于某些可变对象或者一些其他情况,如果我们要改变它本身的值并且原变量名仍然指向该变量时,不能简单的使用等号(=)操作符;这种情况尤其是通过from进行模块导入时,最让人容易忽略它的弊端。要想真正地解决原问题,可以直接引用原底层对象;这并没有违背我们上面所说,其中的一个原理可以参见类对象属性的添加。
4、在此,我们说明一点:虽然Python把所有的对象封装到底层,并通过一个名字来引用,但是,当操作这个名字时的任何动作都会作用到它所指向的对象,就像我们在直接操作底层对象一样。变量名没有类型,你可以把简单地理解成C/C++中的宏定义。因此,我们可以看出,变量名并没有什么实际意义。
(二)作用域与命名空间
1、Python中的变量名解析机制:LE(N)GB法则:
(1)当在函数中使用未认证的变量名时,Python会依次搜索4个作用域[本地作用域(L),之后是上一层结构中def或lambda的本地作用域(E),之后是全局作用域(G),最后是内置作用域(B)],并且在第一处能够找到这个变量名的地方停下来。如果变量名在这次搜索中没有找到,Python就会报错。
(2)当在函数中给一个变量名赋值时(而不是在一个表达式中对其进行引用),Python总是创建或改变本地作用域中的变量名,除非它已经明确地在那个函数中声明为全局变量(global)或非局部变量(nonlocal)以改变其属性。
(3)当在函数之外给一个变量名赋值时(也就是,在一个模块文件的顶层,或者是交互提示模式下),本地作用域与全局作用域(这个模块的命名空间)是相同的。
2、作用域可以做任意的嵌套(也就是说,函数可以任意层次地嵌套),但是只有内嵌的函数(而不是类)会被搜索。
3、nonlocal应用于一个嵌套的函数的作用域中的一个名称,而不是所有def之外的全局模块作用域;而且在声明nonlocal名称的时候,它必须已经存在于该嵌套函数的作用域中——它们可能只存在于一个嵌套的函数中,并且不能由一个嵌套的def中的第一次赋值创建。换句话说,nonlocal即允许对嵌套的函数作用域的名称赋值,并且把这样的名称的作用域查找限制在嵌套在def中。nonlocal使得对语句中列出的名称的查找从嵌套的def的作用域中开始,而不是从声明变量的本地作用域开始,也就是说,nonlocal也意味着“完全略过我的本地作用域”。实际上,当执行到nonlocal语句时,nonlocal中列出的名称必须在一个嵌套的def中提前定义过;否则,将会产生一个错误。
4、global与nonlocal的区别:
(1)global使得作用域查找从嵌套的模块的作用域开始,并且允许对那里的名称赋值。如果名称不存在于该模块中,作用域查找继续到内置作用域,但是,对全局名称的赋值总是在模块的作用域中创建或修改它们。
(2)nonlocal限制作用域查找只是嵌套的def,要求名称已经存在于那里,并且允许对它们赋值。作用域查找不会继续到全局或内置作用域。
5、全局作用域的作用范围仅限于单个文件。在Python中没有基于一个单个的、无所不包的情景文件的全局作用域的。
6、赋值的变量名除非被声明为全局变量或非本地变量,否则均为本地变量。在默认情况下,所有函数定义内部的变量名是位于本地作用域(与函数调用相关的)内的。如果需要给一个在函数内部都位于模块文件顶层的变量名赋值,需要在函数内部通过global语句声明。如果需要给位于一个嵌套的def中的名称赋值,从Python3.0开始可以通过在一条nonlocal语句中声明它来做到。
7、所有其他的变量名都可以归纳为本地、全局或者内置的。
8、任何情况下,一个变量的作用域(它所使用的地方)总是由在代码中被赋值的地方所决定,并且与函数调用完全没有关系。
9、各种赋值与引用的总结:
(1)无点号的简单变量名遵循函数LEGB作用域法则:
赋值语句(X=Value):使变量名成为本地变量——在当前作用域内创建或改变变量名X,除非声明它是全局变量。
引用(X):在当前作用域内搜索变量名X,之后是在任何以及所有的嵌套的函数中,然后是在当前的全局作用域中搜索,最后在内置作用域中搜索。
(2)点号的属性名指的是特定对象的属性,并且遵循模块和类的规则:
赋值语句(object.X=value):在进行点号运算的对象的命名空间内创建或修改属性名X,并没有其他作用。继承树的搜索只发生在属性引用时,而不是属性的赋值运算时。
引用(object.X):就基于类的对象而言,会在对象内搜索属性名X,然后是其上所有可读取的类(使用继承搜索流程)。对于不是基于类的对象而言(例如:模块),则是从对象中直接读取X。
10、当在一个程序中使用变量名时,Python创建、改变或查找变量名都是在所谓的命名空间(一个保存变量名的地方)中进行的。
11、在代码中给一个变量赋值的地方决定了这个变量将存在于哪个命名空间,也就是它可见的范围。
12、除打包代码之外,函数还为程序增加了一个额外的命名空间层:在默认情况下,一个函数的所有变量名都是与函数的命名空间相关联的。这意味着:
(1)一个在def内定义的变量名能够被def内的代码使用,不能在函数的外部引用这样的变量名;
(2)def之内的变量名与def之外的变量名并不冲突,即使是使用在别处的相同的变量名。
(三)模块、包的导入
1、一个模块文件的全局变量一旦被导入就成为了这个模块对象的一个属性:导入者自动得到了这个被导入的模块文件的所有全局变量的访问权。所以,在一个文件被导入后,它的全局作用域实际上就构成了一个对象的属性。
2、导入只发生一次。
(1)当一个模块被导入时,Python会把内部模块名映射到外部文件名,也就是通过把模块搜索路径中的目录路径加在前边,而.py或其他后缀名添加在后边。
(2)import会读取整个模块,所以必须进行定义后才能读取它的变量名;from将获取(或者说是复制)模块特定的变量名。
(3)人技术角度来说,import和from语句都会使用相同的导入操作。from *形式只是多加个步骤,把模块中所有变量名复制到了进行导入的作用域之内。从根本上说,就是把一个模块的命名空间融入另一个模块之中;同样地,实际效果就是可以让我们少输入一些。
(4)import和from都是隐性的赋值语句。import将整个模块对象赋值给一个变量名;from将一个或多个变量名赋值给另一个模块中同名的对象。
(5)以from复制而来的变量名和其来源的文件之间并没有联系。为了实际修改另一个文件中的全局变量名,必须使用import。
(6)from只是把变量名从一个模块复制到另一个模块,并不会对模块名本身进行赋值。从概念上来说,以下两种语句等效:
from module import name1, name2
等价于:
import module
name1 = module.name1
name2 = module.name2
del module
(7)from语句有破坏命名空间的潜质,理论上是这样的,简单模块一般倾向于使用import,而不是from。
(8)当你必须使用两个不同模块定义的相同变量名的变量时,才真的必须使用import,这种情况下不能使用from。
3、from复制变量名,而不是连接。from语句其实是在导入者的作用域内对变量名的赋值语句,也就是变量名拷贝运算,而不是变量名的别名机制。它的实现和Python所有赋值运算都一样,但是其微妙之处在于,共享对象的代码存在于不同的文件中。
4、模块就是命名空间(变量名建立所在的场所),而存在于模块之内的顶层的变量名就是模块对象的属性。
5、模块语句会在首次导入时进行。系统中,模块在第一次导入时,无论在什么地方,Python都会建立空的模块对象,并逐一执行该模块文件内的语句,依照文件从头到尾的顺序;而所有的赋值语句都会建立本模块的属性。
6、在Python2.6中,包的代码中的常规导入(没有前面的点号),且前默认为一种先相对再绝对的搜索路径顺序,也就是说,它们首先搜索包自己的路径。然而,在Python3.0中,在一个包中导入默认是绝对的——在缺少任何特殊的点语法的时候,导入忽略了包含自身并在sys.path搜索路径上的某处查找。
7、相对导入的作用域:
(1)相对导入只适用于在包内导入
(2)相对导入只是用于from语句
8、使用包含导入和相对导入,Python3.0中的模块查找可以完整地概括为如下几条:
(1)简单模块(例如A)通过搜索sys.path路径列表上的每个目录来查找,从左到右进行。这个列表由系统默认设置和用户配置设置组成。
(2)包是带有一个特殊的__init__.py文件的Python模块的直接目录,这使得一个导入中可以使用A.B.C目录路径语法。在A.B.C的一条导入中,名为A的目录位于相对于sys.path的常规模块导入搜索,B是A中的另一个包子目录,C是一个模块或B中的其他可导入项。
(3)在一个包文件中,常规的import语句使用和其他地方的导入一样的sys.path搜索规则。包中的导入使用from语句以及前面的点号,然而,它是相对包的;也就是说,只检查包目录,并且不使用常规的sys.path查找。例如,在from . import A中,模块搜索限制在包含了该语句中的出现的文件的目录之中。
9、导入操作不会赋予被导入文件中的代码对上层代码的可见度:被导入文件无法看见进行导入的文件内的变量名。
10、reload不会影响from导入;递归形式的from导入无法工作。
11、每个模块都有一个名为__name__的内置属性,Python会自动设置该属性:
(1)如果文件是以顶层程序文件执行的,在启动时,__name__就会设置为字符串"__main__"
(2)如果文件被导入,__name__就会成为设成客户端所了解的模块名。
12、包导入:
(1)当我们使用 from package import item语法时,item要么是个包package的子模块(或子包),要么是在包package中定义的其他名字,如:函数、类或变量。import语句首先测试item是否已经在包package中定义,如果还没有,它假定它是一个模块并试图导入它;如果找不到这个模块,一个ImportError异常将被抛出。
(2)相应地,当使用像import item.subitem.subsubitem语法时,除了最后一个item外,每个item都必须是一个包。最后一个item可以是一个模块或者一个包,但不能是一个在前一个item中定义的类、函数或变量;另外,要想引用它,还必须通过它的全名,如:“item.subitem.subitem.attribution”。
(3)当使用语法“from package import *”时,如果在一个包中的__init__.py文件中定义了一个名为__all__的列表,那么只有该列表中列出的名字才能被导入;如果__all__没有被定义,语句“from package import *”并不从包packae中导入所有的子模块到当前命名空间,它只保证包package已经被导入(可能运行__init__.py中的任何初始化代码),并且导入所有在该包package中被定义的名字(这包括任何被__init__.py定义的名字,也包括被上一个import语句显示导入的包的任何子模块)。
五、异常
1、经常会失败的运算一般都应该包装在try语句内,除非你希望这类运算失败时终止程序,而不是被捕捉或是忽略。如果是一个重大的错误更是如此。
2、应该在try/finally中实现终止动作,从而保证它们的执行,除非环境管理器作为一个with/as选项可用。
3、偶尔,把对大型函数的调用包装在单个try语句内,而不是让函数本身零散着放入若干try语句中,这样会更方便。
六、高级应用
1、Twisted是一个完全事件驱动的网络框架,允许你使用和开发完全异步的网络应用程序和协议。系统中有:网络协议、线程、安全和认证、聊天/即时通讯、数据库管理、关系数据库集成、Web/Internet、电子邮件、命令行参数、图形界面集成等等。