Python2.4-原理之函数
此节来自于《Python学习手册第四版》第四部分
一、函数基础
函数的作用在每个编程语言中都是大同小异的,,这个表是函数的相关语句和表达式。
1、编写函数,a、def是可执行代码,python中的函数是由一个新的语句编写的,即def,不像c这样的编译语言,def是一个可执行的语句---函数并不存在,知道python运行了def后才存在。事实上,在if、while甚至在其他的def中嵌套都是合法的,在典型的操作中,def语句在模块文件中编写,并自然而然的在模块文件第一次被导入的时候生成定义的函数;
b、def创建一个对象并将其赋值给某一变量名,当python运行到def语句时,它将会生成一个新的函数对象并将其赋值给这个函数名,就像所有的赋值一样,函数名变成某一个函数的引用,就如看到的,函数对象可以赋值给其他的变量名,保持在列表之中,函数也可以通过lambda表达式来创建;
c、lambda创建一个对象当将其作为结果返回,lambda表达式可以创建函数,这一功能允许我们把函数定义内联到语法上一条def语句不能工作的地方;
d、return将一个结果对象发送给调用者;
e、yield向调用者发回一个结果对象,但记住它离开的地方,像生成器这样的函数也可以通过yield语句来返回值,并挂起他们的状态以便稍后能够恢复状态;
f、global声明了一个模块级的变量并被赋值,在默认情况下,所有在一个函数中被赋值的对象,是这个函数的本地变量,并且仅在这个函数运行的过程中存在,为了分配一个可以在整个模块中都可以使用的变量名,函数需要在golbal语句中将它列举踹,通常情况,变量名往往需要关注它的作用域,并且通过赋值语句将变量名绑定到作用域;
g、noncal声明了将要赋值的一个封闭的函数变量,类似的,3.0中添加的nonlocal语句允许一个函数来赋值一条语法封闭的def语句的作用域中已有的名称,这就允许封闭的函数作为保留状态的一个地方---当一个函数调用的时候,信息被记住,而不必使用共享的全局名称。
h、函数是通过赋值(对象引用)传递的,改变函数中的参数名并不会改变调用者中的变量名,但是改变传递的可变对象可以改变调用者共享的那个对象。
i、参数、返回值以及变量并不是声明,函数中并没有类型约束,实际上,函数不需要声明:可以传递任意类型的参数给函数,函数也可以返回任意类型的对象,其结果就是函数常常可以用在很多类型的对象身上,任意支持兼容接口(方法和表达式)的对象都能使用,无论他们是什么类型。
2、def语句,创建一个函数对象并将其赋值给一个变量名,格式:,def包含了首行,并有一个代码块跟随在后面,这个代码块通常也会缩进(或者就在冒号后面简单的一句)。这个代码块就是函数的主体,也就是每当调用函数时python所执行的语句;首行定义了函数名,赋值给了函数对象,并在括号中包含了0个或以上的参数(有时候称为形参)。在函数调用的时候,在首行的参数名赋值给括号中的传递来的对象,函数主题往往包含一条return语句:,return可以在函数主体中任何地方出现。表示函数调用的结束,并将结果值返回至函数调用出,return语句包含一个对象表达式,这个对象给出的函数结果。return是可选的,如果没有,那么将会在控制流执行完函数主体时结束,函数也许会有yield语句,会在每次都产生一系列之时被用到。
3、def语句是实施执行的,def语句是一个可执行语句,当他运行时,创建一个新的函数对象并将其赋值给一个变量名。(python中所有的语句都是实时运行的,没有像独立的编译时间这样的流程),因为它是一个语句,一个def可以出现在任一语句可以出现的地方,甚至是嵌套在其他的语句中:这是合法的。def在运行时才进行评估,而在def之中的代码在函数调用后才会评估,因为函数定义是实时发送的,所以对函数名没什么特别的地方,关键在于函数名引用的那个对象:这是将函数赋值给一个不同的变量名,并通过新的变量名进行调用。除了调用之外,函数允许任意的属性附加到记录信息以供随后使用:。
4、 函数描绘了两个方面:定义(def创建了一个函数)以及调用(表达式告诉python去运行函数主体):a、定义,这个例子是在交互模式下的,定义一个名为times的函数,该函数返回两个参数的乘积:,当python运行到这里并执行了def语句时,它将会创建一个新的函数对象,封装这个函数的代码并将这个对象赋值给变量名times。典型的情况是,这样一个语句编写在一个模块文件中,当这个文件导入的时候运行;b、在def运行之后,可以在程序中通过在函数名后面增加括号调用(运行)这个函数。括号中可以包含一个或多个对象参数,这些参数将会传递(赋值)给函数头部的参数名,例如:,
5、python中的多态,如上面的times的函数,通过传递的参数的不同来实现不同的行为,这种依赖类型的行为叫做多态,含义就是一个操作的意义取决于被操作对象的类型。因为python是动态类型语言,所以多态在python中随处可见。实际上,在python中每个操作都是多态的操作:print、index、*操作符,还有很多。这种行为是有意为之,作为函数例如,它可以自动的适用于所有类别的对象类型,只要对象支持所预期的接口,那么函数就能处理,也就是说,如果对象传给函数的对象有预期的方法和表达式操作符,那么他们对于函数的逻辑来说,就是有着即插即用的兼容性的。除此之外,如果传递的对象不支持这种预期的接口,python将会在*表达式运行时检测到错误,并自动跑出一个异常,因此编写代码错误进行检查是没意义的,实际上,这样做会限制函数的功能,因为这会让函数限制在测试过的那些类型上才有效。
6、接5,在python中,代码不应该关心特定的数据类型,如果不这样,那么代码将只对编写时所关心的那些类型有效,对以后的那些可能会编写的兼容对象类型并不支持,这样会打乱代码的灵活性,大体来说,我们在python中尉对象编写接口,而不是数据类型。
7、将for循环封装在一个函数之中,这样做的好处在于:a、把代码放在函数中让他能够成为一个可以运行多次的工具;b、调用者可以传递任意类型的参数;c、当逻辑由一个函数进行封装时候,一旦需要修改重复性的任务,只需要在函数里进行修改即可;d、在模块文件中编写函数意味着它可以被计算机中的任意程序来导入和重用。;例子:这里将第13章的一个loop循环,搜索两个字符串公共元素作为例子,将代码封装在函数中,使他成为一个通用搜索交集的工具:这里就是直接将原先的逻辑编写在def头部之后,并且让被操作的对象编程被传递进入的参数。为了实现这个函数的功能,多加了一条return;调用,在调用之前,需要创建它,对于上述这个函数,我们调用的形式是:,这里只是做了个例子,其实该问题可以通过下面的列表解析表达式完成,:。
8、和所有其他函数一样,intersect是多态的,也就是说他支持多种类型,只要其支持扩展对象接口:,这里传递了不同的类型对象(一个列表和一个元组),但是还是选出了共有的元素。如果传入了不支持这些接口的对象(比如数字),python会自动检测出不匹配,并抛出一个异常。
9、本地变量:intersect函数中res变量叫做本地变量,这个变量只在def内涵是可见,并且仅在函数运行时存在,实际上,所有在函数内部进行赋值的变量名都默认为本地变量,所以intersect函数内所有的变量均为本地变量:a、res是明显被赋值过的,所以它是本地变量;b、参数也是通过赋值被传入,所以seq1和seq2也是本地变量;c、for循环将元素赋给一个变量,所以变量x也是本地变量。所有本地变量在函数调用时出现,在函数退出时消失----return语句返回结果对象,但是变量res却消失了。
二、作用域
当在一个程序中使用变量名时,python创建、改变或查找变量名都是在所谓的命名空间中进行的,当我们谈论到搜索变量名对应于代码的值的时候,作用域这个术语指的就是命名空间。除了打包代码之外,函数还为程序增加了一个额外的命名空间层:在默认的情况下,一个函数的所有变量名都是与函数的命名空间关联的:a、一个在def内定义的变量名能够被def内的代码使用,不能在函数外部引用这个变量名;b、def之中的变量名与def之外的变量名不冲突,两个作用域不同。
1、变量名可以在三个不同的地方分配,分别对应3种不同的作用域:a、如果一个变量在def内赋值,它被定位在这个函数之内;b、如果一个变量在一个嵌套的def中赋值,对于嵌套的函数来说,它是非本地的;c、如果在def之外赋值,他就是整个文件全局的。这里将其称为语义作用域,因为变量的作用域完全是由变量在程序中源代码的位置而决定的。
2、之前我们写的代码都是位于一个模块的顶层(并不是嵌套在def之内的0,所以要么存在于模块文件本身,要么是python内置预定义的,函数提供了嵌套的命名空间(作用域),使其内部使用的变量名本地化,以便函数内部使用的变量名不会与函数外冲突:函数定义本地作用域,模块定义全局作用域:
a、内嵌的模块是全局作用域,每个模块都是全局作用域 ,对于外部的全局变量就成为一个模块对象的属性,但是在一个模块中却能够和简单变量一样使用;
b、全局作用域的作用范围仅限于单个文件,在python中是么有基于一个单个的、无所不包的情景文件的全局作用域的,解决方法就是变量名由模块文件隔开,必须精确导入一个模块文件才能使用这个文件中定义的变量名;
c、每次对函数的调用都创建了一个新的本地作用域,也就是说,将会存在由那个函数创建的变量的命名空间。
d、赋值的变量名除非声明为全局变量或非本地变量,否则均为本地变量,默认情况下,所有函数定义内部的变量名是位于本地作用域内的,如果需要给一个在函数内部却位于模块文件顶层的变量名赋值,需要在函数内部通过global声明,如果需要给位于一个嵌套的def中的名称赋值,在3.0中可以通过nonlocal语句来声明;
e、所有其他的变量名都可以归纳为本地、全局或内置的,在函数定义内部的尚未赋值的变量名是一个在一定范围内(在这个def内部)的本地变量、全局(在一个模块的命名空间按内部)或内置(由于定义__builtin__模块提供)变量。
3、交互命令模式下的代码也遵循上述的规则,因为交互模式下的代码实际上是输入到一个叫做__main__的内置模块中的,这个模块就像一个模块文件一样工作,但是结果是随着输入而反馈的。所以交互模式下的变量名对于会话来说是全局的。
4、一个函数内部的任何类型的赋值都会把一个名称划定为本地的,这包括=语句、import中的模块名称、def中的函数名称、函数参数名称等,如果在一个def中以任何方式赋值一个名称,它都将对于该函数称为本地的。ps:注意原处改变对象并不会把变量划分为本地变量,实际上只有对变量名赋值才可以,例如,如果L在模块的顶层被赋值为一个列表,在函数内部的像L.append(X) 并不会将L划分为本地变量,不过L=X却会。
5、变量名解析:LEGB原则,对于一个def语句:a、变量名引用分为三个作用域进行查找:首先是本地,之后是函数内(如果有的话),之后是全局,最后是内置;b、在默认情况下,变量名赋值会创建或改变本地变量;c、全局声明和非本地声明将赋值的变量名映射到模块文件内部的作用域。ps:函数能够在函数内部以及全局作用域(物理上的)直接使用变量名,但是必须声明为非本地变量和全局变量去改变其属性。
6、LEGB法则:a、搜索作用域:本地作用域(L)、上一层def或lambda的本地作用域(E)、全局作用域(G)、内置作用域(B):,nonlocal声明可以迫使名称映射到函数内部的作用域中,而不管是否对其赋值。ps:这些规则只对简单的变量名有效,在第五、六部分会介绍被验证的属性变量名(比如 object.spam)会存在于特定的对象中,并遵循一种完全不同的查找规则,而不止这里提到的作用域概念。属性引用(变量名跟着点号)搜索一个或多个对象,而不是作用域,并且会涉及到“继承”的概念
7、例子:
8、内置作用域很简单,实际上,内置作用域仅仅是一个名为__builtin__的内置模块,但是必须要import __builtin__之后才能使用,因为变量名builtin本身并没有预先内置:,得到的列表中的变量名组成了python中的内置作用域,概括的说,前一半是内置的异常,后一半是内置函数。因为LEGB法则,python会在最后自动得到这个列表所有的变量名,所以你可以使用这个变量名而不需要导入。所以要么手动导入,要么等着LEGB法则搜索。如果在def内不增加global(或nonlocal)声明的话,是没有办法在函数内改变函数外部的变量的。
9、global语句是python中唯一看起来有些像声明语句的语句,但是他不是一个类型或大小的声明,它是一个命名空间的声明,告诉python函数打算生成一个或多个全局变量名,也就是说,存在于整个模块内部作用域(命名空间)的变量名:a、全局变量位于模块文件内部的顶层的变量名;b、全局变量如果在函数内部被赋值,必须经过声明;c、全局变量名在函数的内部不经过声明也可以被引用。
10、global允许我们修改一个模块文件中顶层的一个def之外的名称,nonlocal语句也几乎相同,但它应用于嵌套的def的本地作用域内的名称,而不是嵌套的模块中的名称。当在函数主体被赋值或引用时,所有列出来的变量名将被映射到整个模块的作用域内:
这个例子中,我们增加了一个global声明,以便在def之内能够引用def之外的X,这次他们有一样的值,还有个global使用的值:这里x,y和z都是all_global函数内的全局变量,y和z是全局变量,而且x也被告知是全局变量。
11、在python中使用多线程进行并行计算程序实际上是要依靠全局变量的。因为全局变量在并行线程中在不同函数之间成为了共享内存,所以扮演了通信工具的角色。,第一个模块定义X,并在第二个文件中通过赋值修改了,一个模块文件的全局变量一旦被导入就成了这个模块对象的一个属性:导入者自动得到了这个被导入的模块文件的所有全局变量的访问权,所以在一个文件被导入后,它的全局作用域实际上就构成了一个对象的属性。
12、但是如11后面说的,这样会让维护原有的那个X的很为难,因为他不知道在哪个地方会有文件会用到X,所以最好的还是通过调用函数,传递参数,的方法,在这个特定的情况下,我们最好使用accessor函数管理这种变化:,这样虽然会多一些代码,但是很明显的这样可以最小化文件间变量的修改,防止第一个文件X会被其他文件使用。
13、由于全局变量构成了一个被导入的对象的属性,我们能够通过使用导入嵌入的模块并对其属性进行赋值来仿造出一个global语句,就像下边这个模块文件的例子一样。这个文件中的代码先通过变量名然后通过索引sys.modules导入了嵌套的模块,其中包含了已载入的表:上述表面全局变量和模块的属性是等效的。
14、为了介绍nonlocal,先来理解下嵌套函数,也就是LEGB中的E的部分,嵌套作用域有时候也叫做静态嵌套作用域,实际上,嵌套是一个语法上嵌套的作用域,它是对应于程序源代码的物理结构上的嵌套结构。在增加了嵌套的函数作用域后,变量的查找法则就稍微有些复杂了,对于一个函数:a、一个引用(X)首先在本地(函数内)作用域查找变量名x;之后会在代码的语法上嵌套了的函数中的本地作用域,从内到外查找;之后查找当前的全局作用域(模块文件);最后再内置作用域内(模块_uiltin_)。全局声明将会直接从全局(模块文件)作用域进行搜索;b、在默认情况下,一个赋值(X= value)创建惑改变了变量名X的当前作用域。如果X在函数内部声明为全局变量,它将会创建惑改变变量名X为整个模块的作用域。另一方面,如果X在函数内声明为nonlocal,赋值会修改最近的嵌套函数的本地作用域中的名称X。ps:全局声明将会将变量映射至整个模块,当嵌套函数存在时,嵌套函数中的变量也许仅仅是引用,但它需要nonlocal声明才能修改。
15、上述14原理的例子,:,这个就是按照c规矩,x是上一层的f1中的,下面的例子:,在这个代码中,我们命名为f2的函数的调用动作的运行是在f1运行后发生的。f2记住了在f1中嵌套作用域中的x,尽管f1已经不处于激活状态了。
16、在上面15中第二个例子中这种行为有时候也叫做闭合或者工厂函数,能够记住嵌套作用域的变量值的函数,尽管那个作用域或许已经不存在了。工厂函数有时用于需要及时生成事件处理,实时对不同情况进行反馈的程序中(用户的输入是无法预测的),作为例子,下面的函数:,这定义了一个外部的函数,这个函数简单的生成并返回一个嵌套的函数,却并不调用这个内嵌的函数,如果我们调用外部的函数:,我们得到的是生成的内嵌函数的一个引用。这个内嵌函数是通过运行内嵌的def而创建的,如果现在调用从外部得到的这个函数:,它将会调用内嵌的函数,也就是说maker函数内部名为action的函数,,也就是内嵌函数记住了整数2,及maker函数内部的变量N的值,尽管在调用执行f是maker已经返回了值并推出,实际上,在本地作用域内的N被作为执行的状态信息被保留了下来,我们返回其参数的平方运算。:这是一种相当高级的计数,另一方面,嵌套的作用域常常被lambda函数创建表达式使用,因为他们是表达式,他们几乎总是嵌套在一个def中。此外,函数嵌套通常用作装饰其(第38章介绍),在某些情况下,它是最为合理的编码模式。
17、在较早的python中,第16中的代码会失效,因为嵌套的def与作用域没一点关系,因为那时候没有E规则,只有LGB规则,这时候程序员会将默认参数值传递给一个内嵌作用域内的对象:
18、尽管对于def本身来说,嵌套作用域很少使用,但是当开始边写;ambda表达式时,就要注意了,在19章也就是第四部分,才会主要学习lambda,这里简单的说,它就是个表达式,将会生成后面调用的一个新的函数,与def语句很相似,由于他是一个表达hi,尽管能够使用在def中不能使用的地方,例如在一个列表或字典常量中。像def一样,lambda表达式引入了新的本地作用域,多亏了嵌套作用域查找层,lambda能够看到所有在所编写的函数中可用的变量,因此下面的代码现在能够运行,但仅仅是因为现在有了E搜索法则了:,和之前对嵌套作用域的介绍,程序员需要使用默认参数从上层作用域传递值给lambda,就像为def做过的那样。下面的代码对所有的版本都可以工作:,因为lambda是表达式,所以可以嵌套在def中,而且在大多数情况下,给lambda函数通过默认参数传递值也就没必要了,在3.0中。
19、作用域与带有循环变量的默认参数相比较:在给出的法则中有个特例:如果lambda或者def在函数中定义,嵌套在一个循环之中,并且嵌套的函数引用了一个上层作用域的变量,该变量被循环所改变,所有在这个循环中产生的函数将会有相同的值,在最后一次循环中完成时被引用变量的值:,因为嵌套作用域中的变量在嵌套的函数被调用时才进行查找,所以它们实际上记住的是同样的值(在最后一次循环迭代中循环变量的值),也就是说我们将从列表中的每个函数得到4的平方的函数,因为i 对于在每个列表中的函数都是相同的值4:,这是在嵌套作用域的值和默认参数方面遗留的问题,而不是引用所在的嵌套作用域的值,也就是说,解决这个问题的方法就是使用默认参数把当前的值传递给嵌套作用域的变量,因为默认参数是在嵌套函数创建时评估的(而不是在其稍后的调用时),每个函数记住了自己的变量i的值:
20、任意作用域的嵌套:作用域可以任意的嵌套,但是只有内嵌的函数会被搜索:,python将会在所有的内嵌的def中搜索本地作用域,从内至外,在引用过函数的本地作用域之后,并在搜索模块的全局作用域之前进行这一过程,尽管如此,这种代码不可能在实际中使用。所以经可能的少定义嵌套函数。
21、我们可以修改在嵌套作用域中的变量,只要用nonloca语句声明就行,使用这条语句,嵌套的def可以对嵌套函数中的名称进行读取和写入访问。和global不同的在于,nonloca应用于一个嵌套的函数的作用域中的一个名称,而不是所有def之外的全局模块作用域;而且在声明nonlocal名称的时候,它必须已经存在于该嵌套函数的作用域中---他们可能只存在于一个嵌套的函数中,并且不能由一个嵌套的def中的第一次赋值创建。也就是说,nonlocal允许对嵌套的函数作用域中的名称值,并且把这样的名称的作用域查询限制在嵌套的def,
22、nonlocal语句,它只在一个函数内有意义:这条语句允许一个嵌套函数来修改在一个语法嵌套函数的作用域中定义的一个惑多个名称,在3.0中,一条nonloca语句中声明嵌套的作用域,使得嵌套的函数能够赋值,并且由此能够修改这样的名称;除了允许修改嵌套的def中的名称,nonlocal语句还加快了引用---和global一样,nonlocal使得对该语句中列出的名称的查找从嵌套的def的作用域开始,而不是从声明函数的本地作用域开始,也就是说,nonlocal意味着“完全略过我的本地作用域”。
23、nonlocal 一个比global更严格的地方在于,作用域查找只限定在嵌套的def,nonlocal名称只能出现在嵌套的def中,而不能在模块的全局作用域中或def之外的内置作用域中。nonlocal主要作用是允许嵌套的作用域中的名称被修改,而不只是被引用,然而,当在一个函数中使用的时候,global和nonlocal语句都在某种程度上限制了查找规则:a、global使得作用域查找从嵌套的模块的作用域开始,并且允许对那里的名称进行赋值。如果不在,就到内置作用域中,但是对全局名称的赋值总是在模块的作用域中创建或修改它们;b、nonlocal限制作用域查找只是嵌套的def,要求名称已经存在于那里,并且允许对它们赋值,作用域查找不会继续到全局或内置作用域。
24、默认情况下如下图:,而且通常使用嵌套作用域引用时,我们可以多次调用tester工厂函数,以便在内存中获得其状态的多个副本,嵌套作用域中的state对象基本上附加到了返回的nested函数对象,每次调用都产生一个新的、独特的state对象,以至于更新一个函数的state不会影响到其他的.
25、在使用nonlocal之前。必须有在一个嵌套的def作用域中赋值过,否则会错误----不能通过在嵌套的作用域中赋给它们一个新值来创建他们:,,后面的global会创建个新的变量即使之前的state不存在,但是前面的代码就不行。nonlocal限制作用域查找仅为嵌套的def,nonlocal不会在嵌套的模块的全局作用域惑所有def之外的内置作用域中查找,即时已经有了这些作用域
26、nonlocal可以让在内存中保持可变状态的多个副本,并解决了在类无法保证的情况下的简单的状态保持,如下的代码允许在一个嵌套作用域中保持和修改状态,对tester的每次调用都创建了可变信息的一个小小的自包含包,可变信息的名称不会与程序的其他部分产生冲突:
27、上述可以通过两个global声明来实现,但是会引起全局作用域中的名称冲突,而使用nonlocal可以让每次顶层调用都记得不同调用自己对象的独特副本。这里先预览下后面要涉及到的类,作为类来说,一个类的每个实例都得到状态信息的一个新副本,,这里用前面使用过的tester/nested函数作为类来重新实现----state在对象创建的时候显式的保存在对象中。为了让代码有意义,我们需要知道像这样的一个类中的def与一个类外的def完全一样的工作,除非函数的self参数自动接收隐式的调用主体(通过类自身创建的一个实例对象):从这里可以看出F= tester(0)是创建一个类对象并赋予初始值,后续会有更详细的介绍,这里简单说说,这里还可以使用运算符重载让类看上去像是一个可调用函数.__call__获取了一个实例上的直接调用,因此,我们不需要调用一个指定的方法:,这里不解释这段代码,只是给个初印象,这里的关键是类可以让状态信息更明显,通过利用显示属性赋值而不是作用域查找。
28、使用函数属性的状态:作为最后一种状态保持选项,我们有时候可以使用函数属性实现与nonlocal相同的效果---用户定义的名称直接附给函数,下面的代码是用附加给嵌套的函数的一个属性替代了nonlocal。也许这种方法可能对某些人来说不那么容易理解,但它允许从嵌套的函数之外访问状态变量(使用nonlocal,只能在嵌套的def内部看到状态变量):(这里第二个def后面没有缩进,暂时发现该代码无法运行!!!)这里有个前提:函数名nested是包围nested的tester作用域中的一个本地变量:同样,它可以在nested内自由的引用;本地修改一个对象并不是给一个名称赋值:当他自增nested.state。他是在修改对象nested引用的一部分,而不是指定nested本身,由于我们不是要真的在嵌套作用域内给一个名称赋值,所以不需要nonlocal。
三、参数
参数传递,更多的与对象引用相关,而不是与变量作用域相关:a、参数的传递是通过自动将对象赋值给本地变量名来实现的,因为引用是以指针的形式实现的,所有的参数实际上都是通过指针进行传递的,作为参数被传递的对象从来不自动拷贝;b、在函数内部的参数名的赋值不会影响调用者,在函数运行时,在函数头部的参数名是一个新的、本地的变量名,这个变量名是在函数的本地作用域内的。函数参数名和调用者作用域中的变量名是没有别名的;c、改变函数的可变对象参数的值也许会对调用者有影响,因为参数是简单的赋值给传入的对象,函数能够就地改变传入的可变对象,因此其结果会影响调用者,可变参数对于函数来说是可以做输入和输出的。
1、python通过赋值进行传递与cpp中引用参数选项并不完全相同,但在实际中,它与c语言的参数传递很像:a、不可变参数“通过值”进行传递,像整数和字符串这样的对象是通过对象引用而不是拷贝进行传递的,因为无法进行原处改变不可变对象,所以效果看上去就像是拷贝;b、可变对象是通过“指针”进行传递的,列表和字典这样的对象也是通过对象引用进行传递的,这一点与c语言使用指针传递数组很相似,可变对象可以在函数内部原处进行改变。
2、避免可变参数的修改,在python中,默认通过引用(也就是指针)进行函数的参数传递,是因为这通常是我们所想要的,不过正如后面第六部分要说到的,python类模型依赖于原处修改一个传入的“self”参数来更新对象状态,如果不想要函数内部的原处修改影响传递给他的对象,那么就简单创建一个明确的可变对象的拷贝,:,如果不想改变传入的对象,那么同样可以在函数内部进行拷贝:,这两种方法都是iweile防止改变会影响调用者,不过为了真正意义上的防止某些参数的改变,可以将可变对象转换为不可变对象来杜绝这种问题,例如元组:,不过这有时候有些太极端了。
3、因为return能够返回任意种类的对象,所以它也能够返回多个值,不过这需要将这些值封装进一个元组惑其他的集合类型中,虽然python不支持其他语言的“通过引用进行调用”的参数传递,通常还是能够通过返回元组并将结果赋值给最初的调用者的参数变量名来进行模拟:,这里返回值返回的是一个元组,还记得逗号没有括号表示元组。ps:在2.X中解包参数:上面的例子用元组赋值解包了函数返回的一个元组,在2.6中,可能在传递给函数的参数中自动解包元组,在2.6中,通过如下头部定义的一个函数:,可以通过与期望的结构匹配的元组来调用:f((1,(2,3)))分别给a、b、c赋值为1,2,3自然,传递的元组可以是调用(f(T))前创建的一个对象,不过这个语法在3.0中不再支持,而是用这种格式:以便在一条显式赋值语句中解包。这种显式形式在3.0和2.6中都有效。元组解包参数语法在3.0的lambda函数参数列表中也是不允许的:第20章有解释,有些不对称的是,元组解包赋值在3.0中仍然是自动化的,以便实现循环。
4、特定的参数匹配模型:在模型的上层,python提供了额外的工具,该工具改变了调用过程中赋值时参数对象匹配在头部参数名的优先级。这些工具是可选的,不过却允许你边写支持更复杂的调用模式的函数。默认情况下,参数是通过位置进行匹配的,从左至右,而且必须精确的传递和函数头部参数名一样多的参数,还能通过定义变量名进行匹配,默认参数值,以及对于额外参数的容器。
5、这些特定的模型是可选的,并且必须要根据变量名匹配对象,匹配完成之后在传递机制的底层依然是赋值。a、位置:从左至右进行匹配;b、关键字参数:通过参数名进行匹配,调用者可以定义哪一个函数接受这个值,通过在调用时使用参数的变量名,使用name=value这种语法;c、默认参数:为没有传入值的参数定义参数值,如果调用时传入的值过少,函数能够为参数定义接受的默认值,再次使用name=value;d、可变参数:收集任意多基于位置或关键字的参数,函数能够使用特定的参数,以*开头,收集任意多的额外参数(这可以叫做可变参数,也支持可变长度参数的列表);e、可变参数解包:传递任意多的基于位置惑关键字的参数,调用者可以用*语法将参数集合打散,分成参数,这个“*”与函数头部的“*”恰恰相反:在函数头部意味着收集任意多的参数,而这里意味着传递任意多的参数;f、Keyword-only参数:参数必须按照名称传递,在3.0中,函数也可以指定参数,参数必须用带有关键参数的名字(而不是位置)来传递。这样的参数通常用来定义实际参数意外的配置选项。
6、匹配语法:,a、在函数的调用中(上表前四行),简单的通过变量名位置进行匹配,但是使用name=value形式告诉python按照变量名进行匹配,这些叫做关键字参数。在调用中使用*sequence或者**dict允许我们在一个序列惑字典中相应的封装任意多的位置相关或者关键字的对象,并且在将他们传递给函数的时候,将它们解包为分开的、单个的参数;b、在函数的头部,一个简单的变量名是通过位置或变量名进行匹配的(取决于调用者是如何传递给它参数的),但是name=value的形式定义了默认的参数值。*name的形式收集了任意的额外不匹配的参数到元组中,并且**name的形式将会收集额外的关键字参数到字典中,在3.0极其以后的版本中,跟在*name或一个单独的*之后的、任何正式的惑默认的参数名称,都是keyword-only参数,并且必须在调用中按照关键字传递。
7、关键字参数和默认参数也许是在python代码中最常见的,a、我们已经使用关键字来指定3.0的print函数的选项,但是他们更广泛的用途---关键字允许使用其变量名去标记参数,让调用变得更有意义;b、默认参数作为一种从内嵌函数作用域传递值的办法,但是他们实际上更通用:它们允许创建任意可选的参数,并且在函数定义中提供默认值。简单来说,特定的参数匹配模式可以自由的确认有多少参数是必须传递给函数的,如果函数定义了默认参数,如果传递的参数太少,他们就会被用,如果函数使用*可变参数列表的形式,就能够传递任意多的参数;*变量名将会额外的参数收集到一个数据结构中去。
8、如果使用混合特定的参数匹配模型,python将会遵循下面有关顺序的法则:a、在函数调用中,参数必须以此顺序出现:任何位置参数(value),后面跟着任何关键字参数(name=value)和*sequence形式的组合,后面跟着**dict形式;b、在函数头部,参数必须以此顺序出现:任何一般参数(name),紧跟着任何默认参数(name=value),如果有的话,后面是*name(3.0中是*)的形式,后面跟着任何name惑name=value keyword-only参数(3.0中),后面跟着**name。
9、在调用和函数头部中,如果出现**arg形式,都必须出现在最后,如果使用任何其他的顺序混合了参数,将会得到一个语法错误,因为其他顺序的混合含义会产生起义,python内部使用以下的步骤来在赋值前进行参数匹配:a、通过位置分配非关键字参数;b、通过匹配变量名分配关键字参数;c、其他额外的非关键字参数分配到*name元组中;d、其他额外的关键字参数分配到**name字典中;e、用默认值分配给头部未得到分配的参数。
10、在3.0中,函数头部中的参数名称也可以有一个注解值,特定形式如name:value(或者给出默认值的name:value=default形式),这只是参数的一个额外语法,不会增加惑修改这里介绍的参数顺序发展。函数自身也可以有一个注解值,以def f()->value的形式给出。
11、例子:位置匹配:;关键字参数:,当使用关键字参数的时候,从左到右的关系就不再重要了,因为参数是通过变量名进行传递的,而不是位置。或者这两种的混合:,这种情况下,先按照从左至右的顺序匹配,然后在进行变量名的关键字匹配。,这种直接的关键字匹配可以更明显直观,而且起到数据标签的作用。
12、像这种默认参数,如果没有传入值的话,在函数运行之前,参数就被赋予了默认值,这种情况,必须给a值,其他两个可以省略f(1)或者f(a=1)。在给个稍微复杂的例子:
13、任意参数的实例:*和**是让函数支持接受任意数目的参数的,他们都可以出现在函数定义或是函数调用中,并且他们在两种场合下有着相关的目的:a、收集参数,在函数定义中,在元组中收集不匹配的位置参数:,当这个函数调用时,python将所有位置相关的参数收集到一个新的元组中,并将这个元组赋值给变量args。因为它是一个一般的元组对象,能够索引惑在一个for循环中进行步进:;**特性类似,不过它只对关键字参数有效,将这些关键字参数传递给一个新的字典,这个字典之后将能够通过一般的字典工具进行处理,在这种情况下,**允许将关键字参数转换为字典,能够在之后使用键调用进行步进惑字典迭代:。当第三种情况:函数头部混合一般参数、*参数以及*8来实现更灵活的调用方式。
14、解包参数,在调用函数时能够使用*语法,这种情况下,它与函数定义的意思相反,他会解包参数的集合,而不是创建参数的集合:;**会以键/值对的形式解包一个字典,使其成为独立的关键字参数。他们的混合例子:(结果1,2,3,4)。正如之前在14章看到的调用中的*pargs形式是一个迭代环境,技术上,它接受任何可迭代对象,而不仅像这里的示例所示的元组惑其他序列,例如一个文件对象在*之后工作,并将其行解包为单个的参数(func(*open(‘fname’)));
14、这里的“varargs”调用的强大之处在于,编写一段脚本之前不需要知道一个函数调用需要多少参数:,,由于这里的参数列表是作为元组导入的,程序可以在运行时构建它。在3.0中已经废弃了apply内置函数。
15、3.0把函数头部的排序规则通用化了,允许我们指定keyword-only参数---即必须只按照关键字传递并且不会由一个位置参数来填充的参数。从语法上说,keyword-only参数编码为命名的参数,出现在参数列表中的×args之后,所有这些参数都必须在调用中使用关键字语法来传递,例如下面的代码,a可能按照名称或者位置传递,b收集任何额外的位置参数,c必须只按照关键字传递:,下面这个图是在参数列表中使用一个*字符,来表示一个函数不会接受一个变量长度的参数列表,而是仍然期待在×后面的所有参数都作为关键字传递,下一个函数中,a可以名称或位置,可是b、c必须按照关键字传递,不允许额外的位置传递:;不过仍然可以使用默认值,下面的代码中a可以名称或位置,b、c是可选的,不过如果使用的话,必须关键字传递:
16、keyword-only的排序规则:keyword-only参数必须在一个单个星号后面指定,而不是两个星号---命名的参数不能出现在××args任意关键字形式的后面,并且一个**不能独自出现在参数列表中,不然会有语法错误:。所以如上所述,在函数头部中,keyword-only参数必须写在××args任意关键字形式之前,且在*args任意位置形式之后,才算是keyword-only参数。,在函数调用中,类似的排序规则也是成立的:当传递keyword-only参数的时候,它们必须出现在一个**args形式之前。keyword-only参数可以编写在*args之前或者之后,并且可能包含在**args中:
17、使用keyword-only参数是因为它们使得很容易允许一个函数即接受任意多个要处理的位置参数,也接受作为关键字传递的配置选项。尽管它们的使用是可选的,不过没有这样的参数的话,要为这样的选项提供默认值并验证没有传递多余的关键字则需要额外的工作,如下面的代码:,传入一组对象,并允许传递一个跟踪标志,如果没有keyword-only的话,必须使用*args和**args,并且手动检查关键字,如下面的语句通过notify保证不会有位置参数错误的匹配,并且要求它如果传递则作为关键字传递:
18、举个例子,想要编写一个函数,可以计算任意参数集合和任意对象数据类型集合中的最小值,也就是从空到任何参数都有,下面三个方法:a、第一个函数获取第一个参数(args是一个元组),并且使用分片去掉第一个得到剩下的参数;b、让python自动获取第一个参数及其余的参数,避免索引和分片;c、通过内置list的调用让一个元组转换为一个列表,之后调用list内置的sort方法来实现比较。这其中sort方法是c语言实现的,所以会比其他程序运行的快,而头两种方法的线性搜索将会让它们在绝大多数时间都要快。文件mins.py包含了所有三种解决方法:,,,,这三种解决方法,如果没有参数传入的话,python都会自动的抛出一个异常。当尝试获取元素0时,第一个方法会发生异常;当检测参数列表不匹配时,第二种会发生异常;在尝试最后返回元素0时,第三种会发生异常。这里顺带列个max的:,这种版本更通用,因为只需要修改额外的一个函数即可,逻辑上简单易懂。
19、在实际中常用的是用特定参数匹配模式,这里接着用上一章的intersect函数,也就是用来找不同的任意数目的序列的公共部分的选择(也就是求公子集)。这里增加一个union联合函数实现或集合操作:,将函数封装在名为inter2.py的模块文件中,就可以如下进行导入并调用:
20、这里书本上有个用2.6或更早版本来模拟3.0print函数的例子,略
21、使用keyword-only参数来改写上述20的例子,:,任何额外的keyword-only参数都会默默的忽略掉,如下的一个调用将会对keyword-only参数形式产生一个异常:,要手动检测多余的关键字,可以使用dict.pop()删除收到的条目,并检查字典是否为空,这里是keyword-only参数版本的一个对等形式:,它和前面一样有效,但是它会捕捉外部的关键字参数:这个版本的函数可以在2.6下运行,但是它比keyword-only参数版本需要额外的4行代码,不过keyword-only只能在3.0中运行,在编写来运行于3.0的程序中,keyword-only参数可以简化一类既接受参数又接受选项的函数。
22、高级参数匹配模式可能更复杂,他们也完全是可选的,我们可以只使用简单的位置匹配,并且,作为新手,这是一个好方法。例如:关键字参数在tkinter中扮演很重要的角色,Tkinter是python中的标准GUI API (在2.6中叫做Tkinter)。关键字参数设置配置选项,例如:一种调用形式:,创建了一个新的按钮并定义了它的文字以及回调函数,使用了text和command关键字参数。
四、函数的高级话题
这一部分将会介绍一系列更高级的与函数相关的话题:递归函数、函数属性和注解、lambda表达式、如map和filter这样的函数式编程工具。当开始使用函数时,就开始面对如何将组件聚合在一起的选择了。例如如何将任务分解成更有针对性的函数(导致了聚合性)、函数将如何通信(耦合性)等。
1、耦合性:对于输入使用参数并且对于输出使用return语句,一般来说,需要力求让函数独立于它外部的东西,参数和return语句通常就是隔离代码中少数醒目位置的外部的依赖关系的最好办法;b、耦合性:只有在真正必要的情况下使用全局变量,全局变量通常是一种蹩脚的函数间进行通信的办法。他们引发了依赖关系和计时问题,会导致程序调试和修改的困难;c、耦合性:不要改变可变类型的参数,除非有意为之;d、聚合性:每一个函数都应该有一个单一的、统一的目标,在设计完美的情况下,每个函数都应该只做一件事;e、大小:每个函数应该相对较小,保持简单,保持简短;f、耦合:避免直接改变在另一个模块文件中的变量,在文件间改变变量会导致模块文件间的耦合性,就像全局变量产生了函数间的耦合一样:模块难于理解和重用。
2、通常来说,我们应该竭力使用函数和其他编程组件中的外部依赖性最小化,函数的自包性越好,它越容易被理解、复用和修改:这是函数执行环境。函数可以通过多种方法获得输入产生输出,尽管使用参数作为输入,return并配合可变参数的改变作为输出时,函数往往更容易理解和维护。在3.0中,输出也可能采取存在于一个封闭的函数作用域中的声明的nonlocal名称的形式。
3、python支持递归函数----直接或间接的调用自身以进行循环的函数,递归是颇为高级的话题,并且它在python中相对少见。但是它允许程序遍历拥有任意的、不可预知的形状的结构。递归甚至是简单循环和迭代的替换,虽然它不是最简单的或最高效的。比如下面是用递归实现的一个定制求和函数的示例:
4、我们可以使用三元if/else表达式在这里保存某些代码资产。并且使用3.0的扩展序列赋值来使得第一个/其他解包更简单:,这些例子中的后两个由于空的列表而失败,但是考虑到支持+的任何对象类型的序列,而不只是数字:
,如果观察这三个变体,就可发现,后两个在一个单个字符参数上也有效,因为字符串是一字符的字符串的序列;第三种遍体在任意可迭代对象上都有效,包括打开的输入文件,但是其他的两种不会有效,因为他们索引;并且函数头部def mysum(first,*rest)尽管类似于第三种变体,但是没法工作,因为它期待单个参数,而不是一个单独的可迭代对象。不过也可以进行间接的递归调用:
5、虽然递归很好,可是python中不推荐递归,它不像Prolog或Lisp中有效,python倾向于循环这样的简单的过程式语句,循环语句通常更好。比如下面的while:。更好的情况就是for循环为我们自动迭代,使得在大多数情况下不需要递归(递归在时间和空间上效率都不好):,有了循环语句,就不需要在调用堆栈上针对每次迭代都有一个本地作用域的副本,并且避免了一般会与函数 调用相关的速度成本。
6、处理任意结构。一方面,递归可以要求遍历任意形状的结构,比如下面这个嵌套的子列表结构:,简单的循环没法起作用因为这不是一个线性迭代,这种情况下递归确实很容易:虽然这个例子是简单的,但是它是一类更大的程序的代表,例如,继承树和模块导入链可以展示类似的通用结构。在本书后面有更为实用的示例:a、在第24章的reloadall.py,用来遍历导入链;b、在第28章的classtree.py中,用来遍历类继承树;c、第30章的lister.py中,再次用来遍历类继承树。不过还需要注意在程序中无意的递归的潜在性,比如类中的一些运算符重载方法,例如:_setattr_和_getattribute_,如果使用不正确就会潜在的引起递归循环。
7、函数对象:属性和注解。python中的函数比一个编译器额代码生成规范还要多---python函数是俯拾皆是的对象,自身全部存储在内存快中,同样他们可以跨程序自由的传递和间接调用,他们也支持与调用根本无关的操作----属性存储和注解。
8、间接函数调用,因为python中函数也是对象,所以可以编写通用的处理他们的程序,函数对象可以赋值给其他的名字、传递给其他函数、嵌入到数据结构、从一个函数返回给另一个函数,等等,就好像他们是简单的数字或字符串。函数对象还恰好有个特殊操作:他们可以由一个函数表达式后面的括号中的列表参数调用。然而,函数和其他对象一样,属于通用的领域:由于参数通过赋值对象来传递,这就像是把函数作为参数传递给其他函数一样容易。然后被调用者可能通过把参数添加到括号中来调用传入的函数:,甚至可以将函数对象的内容填入到数据结构中,就好像他们是整数或字符串一样。下面的例子就是将函数两次嵌套到一个元组列表中,作为一种动作表:,这段代码是遍历schedule列表,每次遍历的时候使用一个参数来调用echo函数。不过函数也可以创建并返回以便之后使用:
9、函数内省:因为函数也是对象,所以用常规的对象工具来处理函数,实际上,函数比我们想的更灵活。例如一旦创建,就可以直接调用,但是调用表达式只是定义来在函数对象上工作的一个操作。我们也可以通用的检查他们的属性:>>>func.__name__; >>>func.__code__;>>>dir(func.__code__);>>>func.__code__.co_varnames;>>>func.__code__.co_argcount;
10、函数属性:函数对象不仅限于前面小节中列出的系统定义的属性,也可能向函数附加任意的用户定义的属性:,这样的属性可以用来直接把状态信息附加到函数对象,而不必使用全局、非本地和类等技术。这样的属性可以在函数自身的任何地方访问。某种角度说,这也是模拟其他语言的“静态本地变量”的一种方式。
11、在3.0中,可以给函数对象附加注解信息---与函数的参数和结果相关的任意的用户定义的数据。python为声明注解提供了特殊的语法,但是自身不做任何事情,注解完全是可选的。并且,出现的时候只是直接附加到函数对象的__annotations__属性以供其他用户使用。注解进一步使函数头部语法通用化。从语法上说,注解编写在def头部行,就像与参数和返回值相关的任意表达式一样。对于参数,他们出现在紧随参数名之后的冒号之后;对于返回值,它们编写于紧跟在参数列表之后的一个->之后:
,调用一个注解过的函数,像以前一样,不过当注解出现的时候ipython将它们收集到的字典中并且将它们附加给函数对象本身。参数名成了键,如果编写了返回值注解的话,它存储在键“return”下,而注解键的值则赋给了注解表达式的结果:。由于注解只是附加到一个python对象的python对象,注解可以直接处理。下面的例子只是注解了3个参数中的两个,并且通用的遍历附加的注解:,这里注意两点:a、如果编写了注解的话,仍然可以对参数使用默认值----注解(及其:字符)出现在默认值(及其=字符)之前。例如下面的a:‘spam’ = 4意味着参数a的默认值为4,并用字符串‘spam’注解它:,注意到前面例子中的空格是可选的---可以在函数头部的各部分之间使用空格,或者不用,但省略它们对某些读者来说可能会提高可读性:,注解是3.0中的新功能,并且还有一些潜在的用途。注解可以用作参数类型或值的特定限制,并且较大的API可能使用这一功能作为注册函数接口信息的方式。在38章就会看到注解作为函数装饰器参数(更为通用的概念)的一种代替方法。和python一样,注解是一种功能岁想象变化的工具。ps:注解值在def语句中有效,在lambda中无效。
12、除了def语句外,python提供了一种生存函数对象的表达式形式,因为与LISP语言中的一个工具类似,所以称之为lambda。这个表达式创建了一个之后能够调用的函数,但是他返回一个函数而不是将这个函数赋值给一个变量名。这也是lambda有时候叫做匿名函数的原因。实际上,他们常常以一种行内进行函数定义的形式使用,或者用作推迟执行一些代码。
13、lambda表达式的一般形式是关键字lambda,之后跟一个或多个参数(与def头部内用括号中的参数列表相似),紧跟是一个冒号,之后是一个表达式:,其返回额函数对象与def创建并赋值后的函数对象工作起来完全一样,不过还是有些不同:a、lambda是个表达式,而不是语句,所以它能出现在def无法出现的地方,例如在一个列表常量中或者函数调用的参数中。而且其返回的值可以选择性的赋值给一个变量名。相反,def语句中是得在头部将一个新的函数赋值给一个变量名,而不是将函数作为结果返回;b、lambda的主体是一个单个的表达式,而不是一个代码块,这个lambda的主体简单的就好像放在def主题的return语句中的代码一样。因为它仅限于表达式,lambda通常要比def功能药效:也就是仅能够在lambda主题中封装有限的逻辑进去,if这样的语句都无法使用,这是有意的,它限制了程序的嵌套:lambda是一个为编写简单的函数而设计的,而def用来处理更大的任务。:,,这里的f被赋值给了一个lambda表达式创建的函数对象。这也就是def所完成的任务,只不过def是自动进行赋值的。默认参数也能够在lambda参数中使用,就像def中一样:,在lambda主体中的代码想在def内的代码一样都遵循相同的作用域查找法则。lambda表达式引入的一个本地作用域更像一个嵌套的def语句,将会自动从上层函数中、模块中以及内置作用域中(LEGB)查找变量名:。
14、通常来说,lambda起到了一种函数速写的作用,允许在使用的代码内嵌入一个函数的定义。他们完全是可选的(总是能够使用def来替代它们),但是在仅需要嵌入小段可执行代码的情况下它们会带来更简洁的代码结构。例如后面介绍的回调函数,常常在一个注册调用的参数列表中编写成单行的lambda表达式,而不是使用在文件其他地方的一个def来定义,之后引用那个变量名。lambda通常用来编写跳转表(jump table),也就是行为的列表或字典,能够按照需要执行相应的动作。如下段代码:,当需要把小段的可执行代码编写进def语句从语法上不能编写进的地方时,lambda表达式作为def的一种速写是最有用的。例如,这种代码片段,可以通过在列表常量中嵌入lambda表达式创建一个含有三个函数的列表。一个def是不会在列表常量中工作的,因为它是一个语句,而不是一个表达式。对等的def代码可能需要在想要使用的环境之外有临时性函数名称和函数定义。,实际上,我们可以用python中的字典或者其他数据结构来构建更多种类的行为表,从而做同样的事情:,当创建这个字典的时候,每个嵌套的lambda都生成并留下了一个在之后能够调用的函数。通过键索引来取回其中一个函数,而括号使取出的函数被调用。如果不是用lambda,需要使用三个文件中其他地方出现过的def语句来代替,:
,相比较于def格式要求为这些小函数创建变量名,这些变量名也许会与这个文件中的其他变量名发生冲突,lambda在函数调用参数里作为行内临时函数的定义,并且该函数在程序中不再其他地方使用时也是很方便的。
15、如何让python代码变得简单易懂:lambda的主体必须是单个表达式,所以要作为基于表达式等效的写法编写足够多的语句。如果希望在lambda函数中进行print,直接编写sys.stdout.write(str(x)+'\n')这个表达式,而不是使用print(X);类似的,要在一个lambda中嵌套逻辑,可以使用if/else 三元表达式,或者对等的当需要些技巧的and/or组合:,这样类似的表达式能够放在lambda中,所以它们能够在lambda函数中来实现选择逻辑。,如果需要在lambda函数中执行循环,能够嵌入map调用或列表解析表达式这样的工具来实现:这些技巧只能在没有其他代替方法的时候才使用,因为这种很容易导致不可读,简洁优于复杂,明确优于晦涩,而且一个完整的语句要比神秘的表达式要好,这就是为什么lambda仅限于表达式。
16、lambda最可以用于嵌套函数作用域查找,例如下面的例子中,lambda出现在def中,并且在上层函数调用的时候,嵌套的lambda能够获取到在上层函数作用域中的变量名x的值。
,将def换成lambda:,这里嵌套的lambda结构让函数在调用时创建了一个函数。嵌套的lambda代码都能够获取上层lambda函数中的变量x,不过对于可读性来说,通常最好避免使用嵌套的lambda。
17、这部分有一个关于python自带的tkinter gui api的回调的例子,略,有关lambda的回调部分 见另一本书。
18、在序列中映射函数:map。程序对列表和其他序列常常要做的事情就是对每个元素进行一次操作并把结果集合起来,例如在一个列表counter中更新所有的数字,可以简单的通过一个for循环来实现:,map函数会对一个序列对象中的每一个元素应用被传入的函数,并且返回一个包含了所有函数调用结果的一个列表:,在第13和14章简短的介绍过map,它对一个可迭代对象中的项应用一个内置函数,这里,传入一个用户定义的函数来对它进行利用,从而可以对列表中的每一个元素应用该函数:map对每个列表中的元素都调用了inc函数,并将所有的返回值收集到一个新的列表中,在3.0中map是一个可迭代对象,因此,在这里,一个列表调用用来迫使它生成所有的结果以显示。由于map期待传入一个函数,它恰好是lambda通常出现的地方之一:,这里函数是将为counters列表每个元素加3.因为这个函数不会在其他地方用到,所以将它写成遗憾lambda。因为这样使用map和for循环是等效的,在多编写一些代码后,就能自己编写一个一般的映射工具了:,假设函数inc人人想前面出现的那样,那么用内置函数或自己的对等形式将其映射到一个序列:,因为map是内置函数,所以比自己编写的for循环要快,此外,map还有更高级的方法,比如,提供多个序列作为参数,能够并行返回分别以每个序列中的元素作为函数对应参数得到的结果的列表。,对于多个序列,map期待一个N参数的函数用于N序列。这里,pow函数在每次调用中都是用了两个参数:每个传入map序列中都取一个。ps:map调用与之前学过的列表解析很相似,但是map对每个元素都应用了函数调用而不是任意的表达式。从某种意义上来说,它成为了不太通用的工具,不过在某些情况下,目前map比列表解析运行起来更快(当映射一个内置函数时),并且它所编写的代码也较少。
19、在内置函数中,map函数是用来进行函数式编程的这类工具中最简单的内置函数代表:函数式编程的意思就是对序列应用一些函数的工具。例如,基于某一测试函数过滤出一些元素(filter),以及对每对元素都应用函数并运行到最后的结果(reduce)。优于range和filter都返回可迭代对象。在3.0中,它们需要list调用来显示其所有结果。例如,下面这个filter的调用实现了从一个序列中挑选出大于0的元素:,序列中的元素如果返回值为真,则被键入到结果的列表中,就像map。这个函数也能够概括的用一个for来等效:,reduce在2.6中是一个简单的内置函数,在3.0中则位于functools模块中,它接受一个迭代器来处理,但是它本身不是一个迭代器,他发挥一个单个的结果。这里是lianggereduce调用,计算了在一个列表中所有元素加起来的和以及乘起来的积:,每一步,reduce传递了当前的和或乘积以及列表中下一个元素,传给列出的lambda函数。默认,序列中的第一个元素初始化了起始值。这里是一个对第一个调用的for循环的等效:,编写自己的reduce版本也是相当直接的这个内置的reduce还允许一个可选的第三个参数放置于序列的各项之前,从而当序列为空时充当一个默认的结果;
20、萌妹纸的operator模块,其中提供了内置表达式对应的函数,并且对于函数式工具来说,它使用起来很方便:,与map一样,filter和reduce支持强大的函数式编程的技术。
五、迭代和解析,第二部分
python中的迭代也包括用户定义的类,但是会在第6部分来介绍,这部分将概括目前为止所遇到的各种工具,并且计算他们中的一些相对性能。
1、回顾列表解析:函数式编程工具。列表解析把任意一个表达式而不是一个函数应用于一个迭代对象中的元素。同样,它可以是成为一个比map和filter更通用的工具,有时候通过基于函数的另类视角进行分析,有助于深入理解它。
2、列表解析与map。举个例子,python的内置ord函数会返回一个单个字符的ascii整数编码(chr是它的逆过程,将ascii转换成字符):>>ord("s");115;如果整串字符串的ascii,可以使用for或者mao:,或者通过列表解析表达式得到一样的结果,:map是将函数映射遍整个序列,而列表解析是将一个表达式映射遍整个序列:,列表解析是将序列的值应用于一个任意表达式,然后收集新的列表,使用方括号封装起来。
3、列表解析甚至可以更通用,例如,在for之后编写一个if分支,用来增加选择逻辑,使用if分支的列表解析能够当成一种与上一部分讨论过的内置的filter类似的工具,他们会在分支不是真的情况下跳过一些序列的元素。这里的filter版本创建了一个小的lambda函数。,,,再举个例子:,列表解析可以更通用,在一个列表解析中编写任意数量的嵌套的for循环,并且每个关联if测试:,或者进行列表解析中的for嵌套:,这段代码与下面一段是等效的:。
4、使用python编写矩阵的一个基本方法就是使用嵌套的列表结构:,,,对矩阵进行列表解析可以像下面哪一通过在行内的位置进行迭代:,,下面的是提取矩阵的对角线:。接着介绍一个使用列表解析来混合多个矩阵。下面的代码首行创建了一个单层的列表,其中包含了矩阵对元素的乘积,然后通过嵌套的列表解析来构建具有相同值的一个嵌套列表结构:,第二个有效是因为row迭代是外层的循环,对于每个row,它允许嵌套的列的迭代来创建矩阵每一行的结果。它等同于如下的简单语句:,列表解析对于大型矩阵来说,运行相当快。
5、因为有通用性,所以对于初学者,列表解析变得难以理解,所以开始推荐for循环,或者map调用。不过基于对当前额外的复杂度来说:map调用比等效的for循环快两倍,而列表解析往往比map调用快一些。map和列表解析在解释器中以c语言速度来运行的,比python的for循环代码在pvm中步进速度要快的多。
6、这里举个之前文件部分的readlines方法的不同替代;,目的是除去后面的换行符:,,,一般来说列表解析比for循环快,甚至比map快(不过,对于每只函数来说,map还是更好)。列表解析还能作为一种列选择操作来使用。python的标准sql数据库api将返回查询结果保存为与下面类似的元组的列表:列表就是表,而元组为行,元组中的元素就是列的值:。一个for循环能够手动从选定的列中提取出所有的值,但是map和列表解析能够一步就做到,并且更快:,,第一种方法是用元组赋值来解包列表中的行元组,第二种方法使用索引。在python2.6中,map也可以对其参数使用元组解包。在3.0中map和列表解析最大的区别在于:map是一个迭代器,根据需求产生结果;为了同样的实现内存节省,列表解析必须编码为生成器表达式。
7、重返迭代器:生成器,python提供了工具在需要的时候才产生结果,而不是立即产生结果。特别的,有两种语言结构尽可能的延迟结果创建:a、生成器函数:编写为常规的def语句,但是使用yield语句一次返回一个结果,在每个结果之间挂起和继续他们的状态;b、生成器表达式类似于上一小节的列表解析,但是,他们返回按需产生结果的一个对象,而不是构建一个结果列表。因为这两种都不会一次性构建整个列表,可以节省空间,并且允许将计算时间分散到各个结果请求。
8、生成器函数:yield vs return。之前的是编写接收输入参数并立即送回单个结果的常规函数。这里介绍编写可以送回一个值并随后从其退出的地方继续的函数。这样的函数叫做生成器函数。因为它们随着时间产生值的一个序列。通常来说,生成器函数和常规函数一样,并且,实际上也是用常规的def语句编写的。不过在创建的时候,他们自动实现迭代协议,以便可以出现在迭代背景中。这里介绍下迭代器是如何与生成器相关的:
9、接8的:a、状态挂起,和传统的返回一个值然后退出的常规函数不同,生成器函数自动在生成值的时刻挂起并继续函数的执行。因此,它们对于提前计算整个一系列值以及在类中手动保存或恢复状态都很有用。由于生成器函数在挂起的时候保存的状态包含他们的整个本地作用域,当函数恢复时,他们的本地变量保持了信息并且使其可用。生成器函数与常规函数之间的主要代码上的区别在于:生成器yields一个值,而不是返回一个值,yield语句挂起该函数并向调用者发送一个值,但是,保留足够的状态以使得函数能够从它离开的地方继续。当继续时,函数在上一个yield返回后立即继续执行。从函数的角度来看,这允许其代码随着时间产生一系列的值,而不是一次计算它们并在诸如列表的内容中送回它们。
10、接8的:b、迭代协议整合。要真正理解生成器函数,我们需要知道它们与python中的迭代协议的概念密切相关。可迭代的对象定义了一个__next__方法,他要么返回迭代中的下一项,或者引发一个特殊的StopIteration异常来终止迭代。一个对象的迭代器用iter内置函数接收。如果支持该协议的话,for循环以及其他的迭代背景,使用这种迭代协议来遍历一个序列或值生成器;如果不支持,迭代返回去重复索引序列。要支持这一协议,函数包含一条yield语句,该语句特别编译为生成器。当调用时,它们返回一个迭代器对象,该对象支持用一个名为__next__的自动创建的方法来继续执行的接口。生成器函数也可能有一条return语句,总是在def语句块的末尾,直接终止值的生成。从技术上说,可以在任何常规函数退出执行之后,引发一个StopIterationyichang来实现。从调用者的角度来看,生成器的__next__方法继续函数并且运行到下一个yield结果返回或引发一个StopIteration异常。直接效果就是生成器函数,编写为包含yield语句的def语句,自动的支持迭代协议,并且由此可能在任何迭代环境中以随着时间并根据需要产生结果。
11、生成器函数应用。下面的代码定义了一个生成器函数:,该函数在每次循环时都会产生一个值,之后将其返还给它的调用者。当被暂停时,它的上一个状态保存了下来,并在yield语句之后控制器马上被回收。例如下面的for循环中,每次完成函数的yield语句之后,控制权都会返回给函数:为了终止生成值,函数可以使用一个无值的返回语句,或者在函数主题最后简单的让控制器脱离。如果想要看for里面发生了什么,直接调用一个生成器函数:,得到的是一个生成器对象,其有一个__next__方法,为了方便这里用next(x)来作为调用X.__next__()方法:接着调用会出现StopIteration异常。for循环以同样的方式与生成器一起工作:通过重复调用__next__方法,直到捕获一个异常。如果一个不支持这种协议的对象进行这样的迭代,for循环会使用索引协议进行迭代。下面:,,,,可是相比较来说生成器在内存和性能上都更好,它们允许函数避免临时再做所有的工作。当结果的列表很大或者在处理每一个结果都需要很多时间时,这点特别有用。在更多高级的应用中,它们提供了一个更简单的替代方案来手动将类的对象保存到迭代中的状态。
12、扩展生成器函数协议:send和next。在2.5中,生成器函数协议中增加了一个send的方法。send方法生成一系列结果的下一个元素,而且它提供了一种调用者与生成器之间进行通信的方法,从而能够影响它的操作。yield是一个表达式形式,可以返回传入的元素来发送,而不是一个局域(哪种叫法都行:yield X 或者A = (yield X))。表达式必须包含在括号中,除非他是赋值语句右边的唯一一项,例如X = yield Y 这样。不然X = (yield Y)+42.
13、接上12,当使用这额外的协议时,值可以通过调用G.send(value)发送给一个生成器G。之后恢复生成器的代码,并且生成器中的yield表达式返回了为了发送而传入的值。如果提前调用了正常的G.__next__()方法,yield返回none:,用send方法,边写一个能够被它的调用者终止的生成器。此外,在2.5中生成器还支持throw(type)方法,它将在生成器内部最后一个yield时产生一个异常以及一个close方法,在生成器内部产生一个终止迭代的新的GeneratiorExit异常。尽管3.0提供了一个next(X)内置函数,但是其他的生成器方法例如send,必须直接作为生成器对象的方法来调用(G.send(X)).额外的方法只是在内置的生成器对象上实现,而__next__方法应用于所有的可迭代对象。
14、生成器表达式:迭代器遇到列表解析。在最新的python中,迭代器和列表解析的概念形成了一种新特性,生成器表达式。生成器表达式就像一般的列表解析一样,但是它们括在圆括号中而不是方括号中:。至少在一个函数的基础上,编写一个列表解析基本上等同于在一个list内置调用中包含一个生成器表达式以迫使其一次生成列表中所有的结果:,但是从执行过程上看,生成器表达式很不同:不是在内存中构建结果,而是返回一个生成器对象,这个对象将会支持迭代协议并在任意的迭代语境的操作中:,不过通常不会使用next迭代其,因为for循环会自动触发,不过每一个迭代的语境都这样,包括sum、map和sorted等内置函数,还有any、all和list内置函数等。ps:如果生成器表达式在其他括号中,就像在函数调用中,这种情况下,生成器自身的括号就不是必须的了,不过在下面的第二个sorted调用中,还是需要额外的括号的:,,,,生成器表达式可以认为是对内存空间的优化,它们不需要像方括号的列表解析一样,一次构造出整个结果列表。它们在实际中运行起来可能稍微慢一点,所以对于非常大的结果集合的运算来说才有优势体现。
15、生成器函数VS生成器表达式:看下面的例子:,,上面的生成器表达式等价下面的生成器函数。
16、生成器是单迭代对象:生成器函数和表达式自身都是迭代其,并且只支持一次活跃迭代---也就是没法在结果集中不同位置有多个迭代器:。一旦任何迭代器运行到完成,所有的迭代器都会用尽,必须产生一个新的生成器以再次开始:,,对于生成器函数来说也是这样,下面基于语句的def等价形式只支持一个活跃的生成器并且在一次迭代之后用尽:,某些内置类型支持多个迭代其并且在一个活动迭代器中传递并反映它们的原处修改:。
17、用迭代工具模拟zip和map:内置的zip和map函数可以组合可迭代对象和映射函数,使用多个序列参数,map以与zip配对元素相同的方式,把函数映射到取自每个序列的元素:。
18、zip结果和map函数参数之间有种关系:举例,在上一章中一个函数针对单个的序列参数来模拟map内置函数。针对多个序列的时候,也比较简单:,这里很大程度上依赖于特殊的*args参数传递语法。它收集多个序列(可迭代对象)参数,将其作为zip的参数解包以便组合,然后成对的zip结果解包作为参数以便传入到函数。也就是说,我们基于这样一个基础:zip是map中的一个基本的嵌套操作。这是经典的列表解析模式,在一个for循环中构建操作结果的一个列表。可以更精简的写自己的map,作为单行列表解析的对等体:。虽然这段代码结果与前面相同,但是更精炼而且运行的更快,之前的mymap一次性构建结果列表,对于较大的列表来说,浪费内存,这里可以通过生成器函数和表达式 进行重新编码来解决:,第一个版本每次yield一个结果;第二个版本返回一个生成器表达式的结果来做同样的事情。如果将它们包含到一个list调用中迫使它们一次生成所有的值,它们会产生同样的结果:。当list调用的时候会迫使生成器运行,通过激活迭代协议而进行。生成器由这些函数自身返回。也由它们所使用的3.0中zip内置函数返回,根据需要产生结果。
19、编写自己的zip(...)和map(None,...):在2.X中带有一个None参数的map如下图:,使用迭代工具,可以边写近似版本来模拟截断的zip和2.6中的map:。这里的代码可以在任何类型的可迭代对象上运行,是因为是通过list内置函数来运行自己的参数以使得结果生成,记得注意这里的all和any内置函数的使用,如果一个可迭代对象中所有惑任何元素为True,它们分别返回True,当列表中任何惑所有参数在删除后变成了空,这些内置函数将用来停止循环。;注意到3.0的keyword-only参数pad和2.6中的map不同,当这些函数运行时,有如下结果一个zip和两个补充的map:,这里的函数不能用来列表解析,因为太具体不够动态,用yield将它们转换为生成器以便它们每个都每次返回结果中的一项,不过还是需要使用list来使得生成器产生其值以供显示:2,3,。下面的是zip和map模拟器的替代实现---下面的不是使用pop从列表中删除参数,而是通过计算最小和最大参数长度来完成工作,这样很容易编写嵌套的列表解析来遍历参数索引范围:,这里假设参数是序列惑类似的,而不是任意的尅迭代对象。;外围的解析遍历参数索引范围,内部的解析(传递到元组)遍历传入的序列以并列的提取参数。当运行的时候,结果一样。这里注意到生成器和迭代其似乎有些泛滥,传递给min和max的参数是生成器表达式,它在嵌套的解析开始迭代之前运行完成。此外,嵌套的列表解析使用了两个层级的延迟计算---3.0的range内置函数是一个可迭代对象,就像生成器表达式参数对元组。当列表解析的方括号要求放入到结果列表中的值---迫使解析和生成器运行,才产生了结果。将这些函数自身转换为生成器而不是列表构建器,使用圆括号而不是方括号:,。这里用一个list调用来激活生成器和迭代其以产生结果。
20、需要留意单次迭代:之前说一些内置函数(比如map)如何只支持一个单个的便利,并且在发生之后为空。这里先给出一个例子:,由于这段代码使用iter和next,它对任何类型的可迭代对象都有效。当这个参数的迭代器之一用尽时,不能去捕获由这个解析内的next(it)来引发的StopIteration--允许它传递就会终止这个生成器函数,并且与一条return语句具有相同的效果。如果至少传递了一个参数的话,while iters:对于循环来说足够了,并且,避免了无限循环(列表解析将总是返回一个空的列表):,这段代码在2.6中效果很好;可是在3.0找那个会陷入一个无限循环中并失效。因为3.0中map返回一个单次可迭代对象,而不是像2.6那样的一个立碑。在3.0中只要在循环中运行了一次列表解析,iters将会永远为空(并且res将会是【】).为了在3.0中正常工作,需要使用list内置函数来创建一个支持多次迭代的对象:,在3.0中把map调用放入到list调用中不仅是为了显示。
21、内置类型和类中值生成:很多内置的类型在每次迭代中都会产生键的迭代器,和手动编写的生成器所产生的值一样,字典键也可以手动迭代,或者使用包括for循环、api调用、列表解析和我们之前在14章介绍的一些自动迭代工具。可以通过遵守迭代协议的类来实现任意的用户定义的生成器对象,这样类定义了一个特别的__iter__方法,它由内置的iter函数调用,将返回一个对象,该对象有一个__next__方法,该方法由next内置函数调用(一个__getitem__索引方法作为迭代的退而求其次的选项)。
22、在3.0中还有两种可用的解析表达式形式:集合解析和字典解析:a、对于集合,新的常量形式{1,2,3}等同于set(【1,3,2】),并且新的集合解析语法{f(x) for x in S if P(x)}就像是生成器表达式set(f(x) for x in S if P(x))其中f(x)是一个任意的表达式;b、对于字典,新的字典解析语法{key:val for (key,val)in zip(keys,vals)}像dict(zip(keys,vals))形式一样工作,并且{x:f(x) for x in items}像生成器表达式dict((x,f(x))for x in items)一样工作。下面是3.0中的所有解析替代方式的总结,最后两种是新的,并且在2.6中不可用:
23、解析集合和字典解析:从某种意义上说,集合解析和字典解析只是把生成器表达式传递给类型名的语法糖。因此,二者都接受任何的可迭代对象,一个生成器在这里工作的很好:。对于列表解析来说,总是可以用手动代码来构架结果对象。下面是最后两个解析的基于语句的等价形式:,尽管这两种形式都接受迭代器,它们没有根据需要产生结果的概念--两种形式都是一次构建所有对象,如果想要根据需求产生键和值,生成器表达式更合适:。
24、针对集合和字典的扩展的解析语法:和列表解析及生成器表达式一样,集合和字典解析都支持嵌套相关的if子句从结果中过滤掉元素---下面的代码收集一个范围内的每个元素的平方:,嵌套的for循环也有效,尽管两种类型的对象无序和无副本的特性可能会使得结果看上去缺乏直接性:,和列表解析一样,集合解析和字典解析也可以在任何类型的可迭代对象上迭代---列表、字符串、文件、范围以及支持迭代协议的任何其他类型: 。
25、对迭代的各种方法进行计时:列表解析要比for循环语句有速度上的优势,而且map会依据调用方法的不同表现出更好惑更差的性能,在上一节中生成器表达式看起来比列表解析速度更慢一些,但是在内存需求上却降到了最小。
26、对模块进行计时:python可以让代码计时变得更容易,这里我们来介绍迭代选项是如何叠加起来的,让我们从编写到一个模块文件中的简单但通用的计时器工具函数开始,从而使其可以用于各类程序中:,这个模块通过获取开始时间,调用函数固定的次数并且用开始时间减去停止时间,从而对使用任何位置和关键字参数调用任意函数进行计时,注意以下几点:a、time模块允许访问当前时间,精度随着每个平台而有所不同。在windows上,这个调用号称能够达到微秒的精度,已经相当准确了;b、range调用放到了计时循环之外,因此,它的构建成本不会计算到2.6的计时函数中,在3.0的range是一个迭代器,因此这个步骤是不需要的;c、reps计数是一个全局变量,如果需要的话,导入者可以修改它:mytimer.reps = N.当这些完成后,所有调用的总的使用时间在一个元组中返回,还带有被计时的函数的最终返回值,以便调用者可以验证其操作。当这些完成后,所有调用的总的使用时间在一个元组中返回,还带有被计时的函数的最终返回值,以便调用者可以验证其操作。
27、计时脚本:假设要计时迭代工具的速度,就可以运行下面的代码:,这段代码测试了五种构建结果列表的替代方法,并且,每种方法都执行了一千万次级别的步骤,也就是说,五个测试中的每一个都构建了拥有1w个元素的列表1k次。我们必须通过内置的list调用来运行生成器表达式和函数结果,从而迫使它们生成其所有的值;不然可能就得不到真正工作的生成器。在3.0中,我们必须对map结果做同样的事情,因为它现在也是一个可迭代对象。而且底部的代码如何遍历4个函数对象的一个元组并打印出每一个的__name__。这是一个内置的属性,给出函数的名称。
28、计时结果:作者实验了上面的代码,在3.0中,得到的结果:map比列表解析略微快点,但二者都比for循环要快的多,并且,生成器表达式和函数速度居中:如果仔细研究代码,可以发现生成器表达式比列表解析运行的慢。尽管把一个生成器表达式包装到一个list调用中,会使得其功能等同于一个带有方括号的列表解析,两种表达式的内部实现看上去有所不同:,这是在visita系统上运行的,当在xp上运行2.5版本的时候,列表解析几乎比对等的for循环快一倍,并且当映射abs这样的一个内置函数的时候,map比列表解析略快:如果从map测试中移除list调用以避免两次创建结果列表的话,这段代码所有的2.6结果都会比同样机器的3.0要快。如果修改这段代码,执行的不是abs而是加法操作:,如果需要针对map调用来调用一个用户定义的函数,会使它比for循环要慢,经管循环语句需要的代码量更多。下面是3.0的测试结果: ,在另一台较慢的机器上,使用2.5得到的结果:,总的来说,对代码进行性能分析是非常需要技术的,所以最好就是在自己电脑上进行实验,不过可以肯定的就是在python版本下,在map调用中使用一个用户定义的函数至少会因为两种因素中的一种而执行的比较慢,并且列表解析对于这一测试运行是最快的。
29、计时模块替代方案:前面的计时模块的缺陷:a、总是使用time.clock调用计时代码,尽管该选项是windows上最好的,time.time在某些unix平台上可能提供更好的解析;b、调整重复的次数需要修改模块级别的全局变量---如果要使用timer函数并且有多个导入者共享的话,这是不太理想的安排;c、计时器必须通过运行测试函数很多次才能工作,需要考虑随机的系统载入的波动,在所有的测试中选择最好的时间,而不是总的时间。下面的代码:根据平台选择一个计时器调用,允许重复计数作为一个名为_reps的关键字参数传入,并且提供N中最好的一个替代计时函数:,位于文件顶部的这个模块的文档字符串描述了模块的目标用途,它使用字典的pop操作,从用于测试函数的参数中删除_reps参数并为其提供了一个默认值,并且如果将器trace函数修改为print的话,会在卡发过程中跟踪参数。如果要在3.0或者2.6上测试新的计时器模块,把计时器脚本做如下修改:,结果就不贴了,反正当使用内置函数时,map表现更好。
30、在3.0中使用keyword-only参数:这里使用keyword-only来简化计时器模块代码。代码如下“:,可以通过使用直接shell模式,或者使用交互模式:,或,为了使得计算更具有意义,需要加大计算时间:
31、函数陷阱:a、本地变量是静态检测的;b、默认和可变对象;c、没有return语句的函数;d、嵌套作用域的循环变量。
32、本地变量是静态检测的:python是当编译def代码时静态检测python的本地变量的,不是通过发现赋值语句在运行时进行检测的。一般来说,没有在函数中赋值的变量名会在整个模块文件中查找:也就是在编译的时候,python看到了对x的赋值语句,那么就会决定对x为函数中任意地方都是本地变量名,所以如果前面有x,而赋值的在后面,那么就显示出错。,这种情况下X被视为修改了全局变量。没法同时使用同一个变量的本地变量和全局变量。如果真的希望打印全局变量,然后之后设置一个有着相同变量名的额本地变量,那么就导入上层的模块,并使用模块的属性标记来获得其全局变量:,在交互模式下的命名空间为__main__。
33、默认和可变对象,默认参数是在def语句运行时评估保存的,而不是在这个函数调用时候,从内部来说,python会将每一个默认参数保存成一个对象,附加在这个函数本身,因为默认参数在调用之间都保存了一个对象,必须对修改可变的默认参数十分小心:,可变类型的默认参数在函数调用之间保存了它们的状态,从某种意义上讲它们能够充当c语言中的静态本地函数变量的角色。一定程度上,它们工作起来就像全局变量,但是对于函数来说就是本地变量,而且不会与程序中其他变量名冲突。不过这对于大多数来说就是陷阱,它们的值取决于默认对象构造的时间,比如上面那个例子,其中只有一个列表对象作为默认值,这个列表对象是在def语句执行时被创建的。不会每次函数调用时都得到一个新的列表,所以每次新的元素加入后,列表会变大,对于每次调用,它都没有重置为空列表。如果不想要这种行为,那么在函数主体的开始对默认参数进行简单的拷贝,或者将默认参数值的表达式移至函数体内部。只要值是存在于代码中,而这部分代码在函数每次运行时都会执行的话,你就会每次都得到一个新的对象:,这个例子中的if语句可以被赋值语句x = x or 【】 来代替,这个赋值语句会利用python的or语句返回操作符对象中的一个特性:如果没有参数传入的话,x将会默认为None,所以or将会返回右边的空列表。如果是一个空列表被传入,or表达式将会导致函数扩展并返回一个新创建的列表,而不是像if那样扩展并返回传入的列表。或者使用19章讨论的函数属性来实现可变默认值效果的另一种方式:,该函数的名称对于函数自身来说是全局的,但是,它不需要声明,因为它在函数内部是不会直接修改的。这并不是总是以完全相同的方式使用,但是这样编写代码的时候,一个对象到函数的附加总是更加明确。
34、没有return语句的函数:如果没有return语句,函数将会自动返回None对象。