Python 第四部分 函数
目录
内置函数apply (将在python3.0中消失). 22
第15章 函数基础
函数作用
最大化的代码重用和最小化代码冗余
流程的分解
Def语句是实时执行的
Python中所有的语句都是实时运行的,没有的独立的编译时间,
Def是一个语句 ,一个def可以出现在任一语句可以出现的地方—甚至是嵌套在其他的语句中,尽管def往往是包含在模块文件中,并在 模块导入时运行,函数还是可以通过嵌套在if语句中去实现不同的函数定义,这是完全合法的。
If test: Def func(): … Else: Def func(): … … Func() |
函数赋值给一个不同的变量名, 并通过新的变量名进行了调用。就像python中其他语句的一样,函数仅仅是对象,在程序执行时它清楚 地记录在内存之中。
>>> def times(x, y):
returnx*y
>>> times(2,3)
6
>>> times('ab', 3)
'ababab'
>>>
Python中的多态
如果传递的对象不支持这种预期的接口,python将会在*表达式运行时检测到错误,并自动抛出一个异常。
因此编写代码错误进行检查是没有意义的。
在python中代码不应该关心特定的数据类型。
第二个例子,寻找序列的交集
>>>def intersect(seq1, seq2):
res = []
for x in seq1:
if x in seq2:
res.append(x)
return res
>>>s1, s2 = 'SPAM', 'SCAM'
>>>intersect(s1, s2)
['S','A', 'M']
>>>x = intersect([1, 2, 3], (1, 4)) #函数intersect传递了不同类型的对象,一个列表和一个元组(混合类型)
>>>x
[1]
对于intersect函数,这意味着第一个参数必须支持for循环,并且第二个参数支持成员测试。
需要强调的是:如果我们传入了不支持这些接口的对象(例如,数字), python将会自动检测 出不匹配,并抛出一个异常—这正是我们所想要的,如果我们希望明确地编写类型检测的话,我们利用它来自己实现.通过 不编写类型测试,并且允许python检测不匹配,我们都减少了自己动手编写代码的数量,并且增强了代码的灵活性.
Intersect函数中的res变量在python中称为本场变量,所有的本地变量都 会在函数调用时出现,并在函数 退出时消失.
Def语句是实时 创建函数对象的可执行代码. 当一个函数稍后被调用 时,对象通过赋值被传递给函数.
什么时候python将会创建函数?
当python运行到并执行def语句时,函数就会被创建.这个语句 会创建 函数对象, 并将其赋值给函数名. 当函数所在模块文件被 另一个模块导入时,通常就会发生这种事.(导入操作会从头到尾运行文件中的代码,包括任何的def),但是,当def通过交互模式输入,或者嵌套在其他语句中时(例如if),也会发生这件事.
检查传入函数的对象类型有什么错误?
检查传入函数的对象类型,实质上就是破坏函数的灵活性,把函数限制在特定的类型上,没有这类检查时,函数可能可以处理所有 的对象 类型,任何支持函数 所预期的接口的对象都能用(接口一词是指函数所执行的一组方法和表达工运算符).
第16章 作用域和参数
作用域法则
当我们谈论搜索变量名对应于代码的值 时时候,作用域这个术语指的就是命名空间.也就是说, 在代码中变量名被 赋值的位置决定了这个变量名能被 访问的范围.
所有变量名,包括作用域的定义在内, 都是在python赋值的时候生成的,正如我们所知,python中的变量名在第一次被赋值时已经 创建,并且必须经过赋值后才能够使用. 由于变量名最初没有声明, python将一个变量名被赋值的地点关联为 一个特定的命名空间,换名话说, 在代码中给一个变量赋值的地方 决定 了这个变量将存在那个命名空间,也就是它可见的范围.
如果一个变量在def内被赋值,它被定位 在这个函数 内,如果在def之 外被 赋值,它就是整个文件全局的.
函数作用域
函数定义了本地作用域,面模块定的是全局作用域.这两个作用域有如下的关系:
l 内嵌的模块是全局作用域.: 它对于外部的全局变量就灰飞烟灭一个模块对象的属性.
l 全局作用域的作用范围仅限于单个文件.
l 每次对函数的调用都创建了一个新的本地作用域.
l 赋值的变量名除非声明为全局变量,否则均为本地变量.
l 所有的变量名都可以归纳为本地,全局或者内置的.
在函数内部定义的任意的赋值操作定义的变量名都 将成为本地变量:=语句, import语句,def语句,参数传递。
变量名解析:LEGB原则
对于一个def语句:
变量名引用分为三个作用域进行查找:首先是本地,之后是函数内(如果有的话),之后全局,最后是内置。
在默认情况下,变量名赋值会创建或者改变本地变量。
全局声明将赋值变量名映射到模块文件内部的作用域。
>>>x = 99 #全局变量名x和func
>>>def func(Y):
Z = x + Y #本地变量名Y, Z
return Z
>>>func(1)
100
当在函数中使用未认证的变量名时,python搜索4个作用域[本地作用域(L), 之后是上一层结构中def或lambda的本地作用域(E), 之后是全局作用域(G), 最后是内置作用域(B)]并且在第一处能够找到这个变量名的地方 停下来. 如果变量名在这次搜索 中没有找到, python会报错.
当在函数之外给一个变量名赋值时, 本地作用域与全局作用域(这个模块的命名空间)是相同的
内置作用域
内置作用域仅仅是一个名为__builtin__的内置模块, 但是必须要import __builtin__ 之后才能使用内置作用域, 因为变量名builtin本身并没有预先内置.
>>>import __builtin__
>>>dir(__builtin__)
['ArithmeticError','AssertionError', 'AttributeError', 'BaseException', 'BufferError','BytesWarning', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError','Exception', 'False', 'FloatingPointError', 'FutureWarning', 'GeneratorExit','IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError','KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'NameError','None', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError','PendingDeprecationWarning', 'ReferenceError', 'RuntimeError','RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError','SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True', 'TypeError','UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError','UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning','ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '_', '__debug__','__doc__', '__import__', '__name__', '__package__', 'abs', 'all', 'any','apply', 'basestring', 'bin', 'bool', 'buffer', 'bytearray', 'bytes','callable', 'chr', 'classmethod', 'cmp', 'coerce', 'compile', 'complex','copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate','eval', 'execfile', 'exit', 'file', 'filter', 'float', 'format', 'frozenset','getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int','intern', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals','long', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open','ord', 'pow', 'print', 'property', 'quit', 'range', 'raw_input', 'reduce','reload', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted','staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'unichr', 'unicode','vars', 'xrange', 'zip']
这个列表中的变量名组成了python中的内置作用域. 概括地讲,前一半是内置 的异常, 而后一半是内置函数.由于LEGB法则python最后将自动搜索这个模块,将会自动得到这个列表中的所有变量名.
因此,有两种方法引用一个内置函数: 通过LEBG法则自动最后找到, 或者手动导入__builtin__模块
>>>zip
<built-infunction zip>
>>>import __builtin__
>>>__builtin__.zip
<built-infunction zip>
也就是说,在本地作用域的变量名可能会覆盖在全局作用域和内置作用域的有着相同变量名的变量,而全局变量名有可能覆盖内置的变量名.
函数也能够简单地使用本地变量名隐藏同名全局变量.
Global语句
>>> x = 88 >>> def func(): global x #它是一个命名空间的声明,它告诉python函数打算生成一个或多个全局变量名. x = 99
>>> func() >>> print x 99 >>> b, c = 1, 2 #b和c是全局变量,因为不在函数内部赋值. >>> def all_global(): global a #a是全局变量,因为它通过 global语句使用自己明确地映射到了模块的作用域. a = b + c
>>> print a #但是当我们执行打印a时,却报错了,这 因为函数并没有运行前,全局变量a并不存在. Traceback (most recent call last): File "<pyshell#36>", line 1, in <module> print a NameError: name 'a' is not defined >>> all_global() >>> print a #当运行函数后就可以正常使用了. 3 |
本地变量在函数返回时将会消失, 而全局变量不是这样.
最小化文件间的修改
x = 99 ============================================== import first first.x = 88 print(first.x) C:\my_python>python second.py 88 |
一个模块文件的全局变量一旦被导入就成为了这个模块对象的一个属性:导入者自动得到了这个被导入的模块文件的所有全局变量的访问权. 但是最好别这样做, 在文件间进行通信最好的办法就是通过调用函数,传递参数,然后得到其返回值.
#thismod.py var = 99
def local(): var = 0 #改变局部变量var
def glob1(): global var var += 1 #改变全局变量var
def glob2(): var = 0 #改变局部变量var import thismod #导入自己 thismod.var += 1 #改变全局变量var
def glob3(): var = 0 #改变局部变量var import sys glob = sys.modules['thismod'] #从系统模块数组中引用自己 glob.var += 1 #改变全局变量var
def test(): print(var) local(); glob1(); glob2(); glob3() print(var) |
>>> import thismod >>> thismod.test() 99 #函数未被调用所以为99 102 #被 glob1,2,3加了三次 |
作用域和嵌套函数
>>> def f1(): x = 88 def f2(): #在函数f1()中用def语句嵌套函数f2(),注意f2是f1本地作用域中一个本地变量, print x # f2是一个临时函数,仅在f1内部执行过程中存在。 f2()
>>> f1() 88 |
88 >>> def f1(): x = 88 def f2(): print x return f2
>>> action = f1() #f2的函数调用动作的运行是在f1运行后发生的.f2 记住了在f1中嵌套作用域的x. >>> action() #尽管f1已经不处于激活状态. 88 |
通过LEGB查找法则,f2内的x自动映射到了f1的x.
这个嵌套作用域查找在嵌套的函数已经返回后也是有效的.
工厂函数
一个能够记住嵌套作用域的变量值的函数,尽管那个作用域或许已经不存在了.
>>> def maker(n): #定义了一个外部函数,这个函数生成并返回一个嵌套的函数, def action(x): #用外部函数的参数n来计算某个数x的n次幂. return x ** n return action
>>> f = maker(2) >>> f #程序打印了生成的内嵌函数的一个引用. 这个内嵌函数是通过运行内嵌的def而创建的. <function action at 0x020595F0> >>> f(3) #由于内嵌函数记住了整数2, 所以结果: 3 ** 2 = 9 9 >>> f(4) #4 ** 2 = 16. 实际上,在本地作用域内的n被作为执行的状态保留了下来. 16 >>> g = maker(3) #重新执行外部函数maker并指定因变量n为3. >>> g(3) #执行内嵌函数指定自变量x 为3, 因此3 ** 3 = 27 27 >>> f(3) #但是最初的函数f仍然是像从前一样做平方运算. 9
|
使用默认参数来保留嵌套作用域的状态.
较早的版本的python中上面的代码执行会失败, 为了解决这一问题,一般会将默认参数值传递给(记住)一个内嵌作用域内的对象.
>>>def f1():
x = 88
def f2(x = x): #出现在def头部的arg =val的语句表示参数arg在调用时没有值传入进来的时候,
print x #默认会使用值val.
f2()
>>>f1()
88
嵌套作用域查找法则之所以加入到python中就是为了让默认参数不再扮演这种角色。现在,python自动记住了所需要的上层作用域的任意值,为了能够在内嵌的def中使用。
嵌套作用域和lambad
尽管对于def本身来说,嵌套作用域很少使用,但是当开始编写lambda表达式时,将会生成后面会被调用 的一个新的函数,与def语句很相似。就像def,lambda表达式引入了新的本地作用域,多亏了嵌套 作用域查找层,lambda能够看到所有生存在所编写的函数的变量。
>>> def func(): x = 4 action = (lambda n: x ** n) #lambda表达式产生了一个action的函数。 return action >>> x = func() >>> print x(2) 16 >>> def func(): x = 4 action = (lambda n, x=x: x ** n) #通过给参数x设置默认值的方式实现上面方式,但这是没有必要的。 return action
>>> x = func() >>> print x(2) 16
|
由于lambda是表达式,所以它们自然而然的嵌套在了def中,在大多数情况下,给lambda函数通过默认参数传递值也就没有什么 必要了。
使用域与带有循环变量的默认参数相比较
嵌套在一个循环中,并且嵌套的函数引用了一个上层作用域的变量,该变量被循环所改变,所有在这个循环中产生的函数将会有相同的值—在最后一次循环中完成时被引用变量的值。
>>> def makeActions(): acts = [] for i in range(5): acts.append(lambda x: i ** x) return acts
>>> acts = makeActions() >>> acts[0] <function <lambda> at 0x01E77AF0> >>> acts[0](2) #由于i被作为执行的状态保留了下来, 且为循环最后一次的值4 . 16 #因为嵌套作用域中的变量在嵌套的函数被调用 时才进行查找,(查找i的值) >>> acts[2](2) #所以它们实际上记住的是同样的值, (在最后一次循环迭代中循环变量的值) 16 #所以acts[]数组中的lambda表达式中i的值是4. >>> acts[4](2) 16 |
为了能够使有循环中每次的i 值, 必须使用默认参数把当前的修正传递给作用域的变量.因为默认参数是在嵌套函数创建时评估的(而不是在其稍后调用时), 每一个函数记住了自己的变量i的值. >>> def makeActions(): acts = [] for i in range(5): acts.append(lambda x, i=i: i ** x) #采用默认值来保存其循环变量i的值到lambda创建有函数中. return acts
>>> acts = makeActions() >>> acts[0](2) 0 >>> acts[2](2) 4 >>> acts[4] (2) 16 |
任意作用域的嵌套
作用域可以做任意的嵌套, 但是只有内嵌的函数(而不是类,)会被搜索.
>>> def f1(): x = 99 def f2(): def f3(): print x #python将会在所有的内嵌的def中搜索本地作用域, 从内至外, f3() #在引用过函数的本地作用域之后, 并在搜索模块的全局作用域之前进行这一过程. f2()
>>> f1() 99 |
这种代码不可能会在实际中这样使用,在python中,我们遵循平坦要优于嵌套原则.
传递参数
1. 参数传递是通过自动将对象赋值给本地变量来实现的.
2. 在函数内部的参数名的赋值不会影响调用者. (在函数运行时,在函数头部的参数名是一个新的,本地的变量名)
3. 改变函数的可变对象参数的值也许会对调用都有影响.
l 不可变参数是”通过值”进行传递. (像整数和字符串这样的对象是通过对象引用而不是拷贝进行传递的,但是因为你无论怎样都不可能在原处改变不可变对象,实际上就像是创建了一份拷贝)
l 可变对象是通过”指针”进行传递的.(例如,列表和字典,这和C语言的指针传递数组很相似, 可变对象能够在函数内部进行原处改变.)
总的来说,它仅仅是将对象赋值给变量名,并且无论对于可变对象可不可变对象都是这样的.
参数和共享引用
>>> def changer(a, b): a = 2 #因为a 是在函数作用域内的本地变量名. 它仅仅是修改了本地变量名a,并没有影响到调用者的x b[0] = 'spam'
>>> x = 1 >>> l = [1, 2] >>> changer(x, l) #可变对象l >>> x, l (1, ['spam', 2]) 对于变量x 和函数本地变量a 其实就像下面一样的. >>> x = 1 >>> a = x >>> a = 2 >>> print x 1 >>> a 2 >>> l = [1, 2] >>> b = l >>> b[0] = 'spam' >>> print l ['spam', 2] >>> b ['spam', 2] |
避免可变参数的修改
在python中默认通过引用(也就是指针)进行函数的参数传递.这也通常是我们想要的.
>>> l = [1, 2] >>> changer(x, l[:]) >>> l [1, 2] >>> def changer(a, b): b = b[:] a = 2 b[0] = 'spam'
>>> l = [1, 2] >>> changer(x, tuple(l))
Traceback (most recent call last): File "<pyshell#73>", line 1, in <module> changer(x, tuple(l)) File "<pyshell#71>", line 4, in changer b[0] = 'spam' TypeError: 'tuple' object does not support item assignment |
以上这两种拷贝机制都会阻止函数改变对象, 这样做仅仅是防止了这些改变会影响调用者.
我们问题能够将可变对象转换为不可变对象来杜绝这种问题.但是元组在试图改变时会抛出异常.
对参数的输出进行模拟
Return 能够返回任意种类的对象,所以它也能够返回多个值,如果这些值被封装进一个元组或其他的集合类型. 尽管python不支持其他语言所谓的通过引用进行调用 的参数传递. 我们通常通过返回元组并将结果赋值给最初的调用 者的参数变量名来进行模拟.
>>> def multiple(x, y): x = 2 y = [3, 4] return x, y #返回一个元组. # (这里看起来好像返回了两个值,但是实际上只用一个,一个包含有2个元素的元组,它的圆括号是可选的.)
>>> x = 1 >>> l = [1, 2] >>> x , l = multiple(x, l) #用x, l 来接收返回的元组. >>> x, l (2, [3, 4]) |
特定的参数匹配模型
在默认情况下, 参数是通过其位置进行匹配的, 从左至右, 而且必须精确地传递和函数头部参数名一样多的参数.
l 位置 : 从左至右进行匹配
l 关键字参数: 通过参数名进行匹配
l 默认参数: 为没有传入值的参数定义参数值.
l 可变参数: 收集任意多基于位置或关键字的参数. (它们是以字符*开头,收集任意多的额外参数)
l 可变参数: 传递任意多的基于位置或关键字的参数.
关键字参数和默认参数的实例
>>> def f(a, b, c): print a, b, c
>>> f(1, 2, 3) #基于位置的参数传递. 1 2 3 >>> f(c=3, b=2, a=1) #关键字参数传递, 它允许通过变量名进行匹配, 而不是通过位置. 1 2 3 #python将调用中的变量名c匹配给在函数定义头部的名为c的参数. >>> f(1, c=3, b=2) #混合使用基于位置的参数和基于关键字的参数. 1 2 3 |
>>> def f(a, b=2, c=3): print a, b, c #默认参数实例.
>>> f(1) 1 2 3 >>> f(a=1) 1 2 3 >>> f(1, 4) 1 4 3 >>> f(1, 4, 5) 1 4 5 >>> f(1, c=6) #关键参数从本质上允许我们跳过有默认值的参数. 1 2 6 |
所有基于位置的参数首先依照从左至右的顺序匹配头部的参数.之后再进行基于变量名的进行关键字匹配
任意参数的实例
最后两种匹配扩展, *和**, 是让函数支持接受任意数目的参数的. 它们都可以出现在函数定义或是函数调用中,并且它们在两种场合下有着相关的目的.
收集参数
以下都是在函数定义中使用的.
#python将所有位置相关的参数收集到一个新元组中,并将元组赋值给变量args >>> def f(*args): print args
>>> f() () >>> f(1) (1,) >>> f(1, 2, 3, 4) (1, 2, 3, 4) #**特性与*类似, 它只对关键字参数有效, 将这些关键字参数传递给一个新的字典. >>> def f(**args): print args
>>> f() {} >>> f(a=1, b=2) {'a': 1, 'b': 2} #混合用法 >>> def f(a, *pargs, **kargs): print a, pargs, kargs
>>> f(1, 2, 3, x=1, y=2) 1 (2, 3) {'y': 2, 'x': 1} |
分解参数
以下是发生在调用时的.
>>> def func(a, b, c, d): print a, b, c, d #通过一个元组给函数传递参数. * >>> args = (1, 2) >>> args += (3, 4) >>> func(*args) 1 2 3 4 #通过键/值对的形式分解一个字典, 使其成为独立的关键字参数 ** >>> args = {'a': 1, 'b': 2, 'c': 3} >>> args['d'] = 4 >>> func(**args) 1 2 3 4 #混合以位置,关键字=,分解元组*,分解字典键/值对**的参数传递. >>> func(*(1, 2), **{'d': 4, 'c': 4}) 1 2 4 4 >>> func(1, *(2, 3), **{'d': 4}) 1 2 3 4 >>> func(1, c=3, *(2,), **{'d': 4}) 1 2 3 4 |
注意: 不要混淆函数头部或是函数调用时*/**的语法: 在头部, 它意味着收集任意数目的参数, 而在调用时, 它解包任意数目的参数.
关键字参数和默认参数的混合
>>> def func(spam, eggs, toast=0, ham=0): #位置和默认参数的混合定义. print(spam, eggs, toast, ham)
>>> func(1, 2) #使用位置参数赋值和定义时的默认值调用 (1, 2, 0, 0) >>> func(1, ham=1, eggs=0) #使用位置参数赋值和关键字参数调用 (1, 0, 0, 1) >>> func(spam=1, eggs=0) #关键字参数和使用定义时的默认值调用 (1, 0, 0, 0) >>> func(toast=1, eggs=2, spam=3) #关键字参数和使用定义时的默认值调用 (3, 2, 1, 0) >>> func(1, 2, 3, 4) #以位置参数赋值形式来完成调用. (1, 2, 3, 4) |
Min调用
设计一个函数能够计算任意参数集合和任意对象数据类型集合中的最小值. 也就是说, 这个函数应该接受零个或多个参数: 希望传递多小就可以传递多少.
>>> def min1(*args): res = args[0] #使用分片去掉第一个得到了剩余的参数. for arg in args[1:]: if arg < res: res = arg return res
>>> def min2(first, *rest): for arg in rest: if arg < first: first = arg return first
>>> def min3(*args): tmp = list(args) tmp.sort() #sort方法是用c语言进行编写的. 有时它要比其他的程序运行快. return tmp[0] #在python中 sort例程是以C写成的, 使用高度优化的算法,试着利用被排序元素间的部分次序. #这种排序称为”timsort”, 以其创造者Tim peter命名. >>> print min1(3, 4, 1, 2) 1 >>> print min2('bb', 'aa') aa >>> print min3([2, 3], [1, 1], [3, 3]) [1, 1] |
#计算最大值和最小值. >>> def minmax(test, *args): res = args[0] for arg in args[1:]: if test(arg, res): res = arg return res
>>> def lessthan(x, y): return x < y
>>> def grtrthan(x, y): return x > y
>>> print minmax(lessthan, 4, 2, 1, 5, 6, 3) 1 >>> print minmax(grtrthan, 4, 2, 1, 6, 5, 3) 6 |
一个更有用的例子:通用set函数
编写一个intersect函数, 来从任意的参数中收集所有曾经中任意操作对象中共有的元素.
编写一个union函数,来从任意多的参数中收集所有曾经在任意操作对象中出现过的元素.
>>> def intersect(*args): #计算元组中args所有元素的交集. res = [] for x in args[0]: for other in args[1:]: if x not in other: break else: res.append(x) return res
>>> def union(*args): #计算元组中所有元素的合集. res = [] for seq in args: for x in seq: if not x in res: res.append(x) return res
# from inter2 import intersect, union//当函数命名在inter2.py文件中时, 在交互模式下可以用from导入函数. >>> s1, s2, s3 = 'SPAM', 'SCAM', 'SLAM' >>> intersect(s1, s2), union(s1, s2) (['S', 'A', 'M'], ['S', 'P', 'A', 'M', 'C']) >>> intersect([1, 2, 3], (1, 4)) [1] >>> intersect(s1, s2, s3) ['S', 'A', 'M'] >>> union(s1, s2, s3) ['S', 'P', 'A', 'M', 'C', 'L'] |
参数匹配: 细节
如果决定使用并混合特定的参数匹配模型, python将会遵循下面有关顺序的法则:
1. 在函数调用中, 所有的非关键字参数(name)必须首先出现, 其后跟随所有的关键字参数(name=vale), 后边跟着*name的形式, 并且如果需要的话,最后是**name的形式.
2. 函数头部, 参数必须以相同的顺序出现: 一般参数(name), 紧跟着默认参数(name=value), 后面如果出现了的话是*name的形式, 如果使用了的话最后是**name
如果 你使用了任何其他的顺序混合了参数, 你将会得到一个语法错误, 因为其他顺序的混合会产生歧义. Python内部是使用以下的步骤来在赋值前进行参数匹配的.
1. 通过位置分配非关键字参数.
2. 通过匹配变量名分配关键字参数.
3. 其他额外的非关键字参数分配到*name元组中.
4. 其他 额外ide关键字参数分配到**name字典中.
5. 用默认值分配给在头部未得到分配的参数.
本章小结
变量作为它所赋值的函数定义的本地变量, 除非他们特定地声明为全局变量. 和我们所看到的一样, 参数是通过 赋值传入函数的, 这也就意味着是通过对象引用, 其真实含义就是通过指针.
可变类型的参数能够像其他共享引用一样, 展示出共同的特性, 除非在传入时, 对像是一份明确的拷贝, 在函数内对传入的可变的改变会影响调用者.
举出三种或四种Python函数中保存状态信息的方法?
虽然函数返回后本地变量的值就会消失, python函数可以利用全局变量, 嵌套函数中引用所在函数作用域的变量或默认值 , 通过它们来保存状态信息. 另一种方式是使用OOP和类, 它所支持的状态保存比前三种技术都要好, 因为它是利用属性赋值运算明确进行状态保存的.
第17章 函数的高级话题
匿名函数: lambda
除了def语句之外, python还提供了一种生成函数对象的表达式形式. Lambda它返回了一个函数而不是将这个函数赋值给一个变量名.
Lambda表达式
Lambda argument1, argument2, … argumentN : expression using arguments
l Lambda是一个表达式, 而不是一个语句. 因为这一点, lambda能够出现在python语法不允许def出现的地方.
l Lambda的主体是一个单个的表达式, 而不是一个代码块. Lambda通常要比def功能要小: 你仅能够在lambda主体中封装有限的逻辑进去, 连if这样的语句都不能够使用. 这是有意设计的—它限制了程序的嵌套:lambda是一个为编写简单函数而设计的.而def用来处理更大的任务.
默认参数也能够在lambda参数中使用, 就像在def中使用一样.
>>> x = (lambda a='fee', b='file', c='foe': a + b + c) >>> x('wee') #参数b和c采用默认值. 'weefilefoe' |
为什么使用lambda
Lambda起到了一种函数速写的作用.
Lambda通常用来编写跳转表(jump table), 也就是行为的列表或字典, 能够按照需要执行相应的动作.
>>> l = [(lambda x: x**2), (lambda x: x**3), (lambda x: x**4)] >>> for f in l: print f(2)
#当需要把小段可执行代码编写进def语句从语法上不能编写进的地方时, #Lambda表达式作为def的一种速写来说是最为有用的. 4 8 16 >>> print l[0](3) 9 |
>>> key = 'got' >>> {'already': (lambda: 2+2), 'got': (lambda: 2*4), 'one': (lambda: 2**6) }[key]() #使用相同的办法用python语言,在字典或其他的数据结构中创建一个行为表. 8 |
Lambda在函数参数里作为行内临时函数的定义, 并且该函数在程序中不再其他地方 使用时也是很方便的.
如何(不要)让python代码变得晦涩难懂
>>> lower = (lambda x, y: x if x < y else y) #在lambda中使用if else这样的三元表达式. >>> lower('bb', 'aa') 'aa' >>> lower('aa', 'bb') 'aa' >>> import sys >>> showall = (lambda x: map(sys.stdout.write, x)) #使用map函数遍历列表.打印每一项. >>> t = showall(['spam\n', 'toas\n', 'egg\n']) spam toas egg >>> showall = lambda x: [sys.stdout.write(line) for line in x] #遍历列表, 并打印列表中每一项. >>> t = showall(('bright\n', 'side\n', 'of\n', 'life\n')) bright side of life |
上面这些技巧必须在万不得已的情况下才使用. 一不小心,它们就会导致代码不可读(晦涩难懂)的python代码
一般来说: 简洁优于复杂, 明确优于晦涩, 而且一个完整的语句要比神秘的表达式要好.
嵌套lambda和作用域
Lambda是嵌套函数作用域查找(在第16章见到的LEGB原则中的E)的最大受益者.
>>> def action(x): return (lambda y: x + y) #在action函数调用的时候,嵌套的lambda能够获取到在上层函数作用域中的变量x的值 >>> act = action(99) >>> act <function <lambda> at 0x01EF2230> >>> act(2) 101 |
>>> action = (lambda x: (lambda y: x + y)) >>> act = action(99) #在嵌套结构让函数调用时创建了一个函数. >>> act(3) 102 >>> ((lambda x: (lambda y: x + y))(99))(4) 103 |
无论上面那种情况, 嵌套的lambda代码都能够获取在上层lambda函数中的变量x. 但是这相当费解.通常来说, 应该避免使用嵌套的lambda.
作为参数来应用函数.
内置函数apply (将在python3.0中消失)
>>> def func(x, y, z): return x + y + z
>>> apply(func, (2, 3, 4)) #将一个函数作为参数传递给apply来调用一个生成函数,并将其参数作为元组传入 9 >>> f = lambda x, y, z: x + y + z >>> apply(f, (2, 3, 4)) #apply也可用于lambda生成的函数. 9 |
为什么要在意: 回调
Lambda的另一个常见的应用就是为python的Tkinter GUI API定义行内的回调函数.
>>> import sys >>> x = Button( #在这里回调处理是通过传递一个用lambda所生产的函数作为command的关键字参数. text = 'Press me', command = (lambda: sys.stdout.write('Spam\n'))) |
注意:不要把apply和map搞混了, apply是执行单个函数的调用, 把参数传入该函数, 这样只进行一次. Map会替序列中每个元素都调用函数, 这样进行多次.
传入关键字参数
>>> def echo(*args, **kwargs): print args, kwargs
>>> echo(1, 2, a=3, b=4) (1, 2) {'a': 3, 'b': 4} >>> pargs = (1, 2) >>> kargs = {'a': 3, 'b':4} >>> apply(echo, pargs, kargs) (1, 2) {'a': 3, 'b': 4} |
在序列中映射函数map
Map函数会对一个序列对象中的每一个元素应用被传入的函数, 并且返回一个包含了所有函数调用结果的一个列表。
>>> counters = [1, 2, 3, 4] >>> def inc(x): return x + 10
>>> map(inc, counters) #用map函数实现对列表counters中每一个元素调用函数inc,并返回一个列表。 [11, 12, 13, 14] >>> map((lambda x: x+3), counters) #在map中还可以使用lambda组成的函数. [4, 5, 6, 7] >>> def mymap(func, seq): #写一个自定义的map函数. res = [] for x in seq: res.append(func(x)) return res
>>> map(inc, [1, 2, 3]) [11, 12, 13] >>> mymap(inc, [1, 2, 3]) [11, 12, 13] #pow函数返回第一个参数的第二参数的指数幂. >>> pow(3, 4) #它是 3*3*3*3 = 81 81 >>> map(pow, [1, 2, 3], [2, 3, 4]) # 1*1=2, 2*2*2=8, 3*3*3*3=81. [1, 8, 81] |
Map调用与在第13章中学到过的列表解析很相似, 但是map对每一个元素都应用了函数调用 而不是任意的表达式. 因为这点限制, 使它不太通用. 而map在某些情况下比列表解析运行起来更快.
函数式编程工具: filter和reduce
在python内置函数中, map函数无疑是函数式编程工具中最简单的函数代表, 函数式编程的意思就是对序列应用一些函数的工具.
>>> range(-5, 5) [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4] >>> filter((lambda x: x>0), range(-5, 5)) #从range(-5, 5)列表中找出大于0的数. [1, 2, 3, 4] >>> reduce((lambda x, y: x +y), [1, 2, 3, 4]) #以第一个元素为初始值, 计算以后的每一个元素的和集. 10 >>> reduce((lambda x, y: x * y), [1, 2, 3, 4]) #以第一个元素为初值, 计算剩余的元素的乘集. 24 >>> l = [1, 2, 3, 4] #模拟第一个reduce程序, 计算和集. >>> res = l[0] >>> for x in l[1:]: res = res + x
>>> res 10
>>> import operator #引入operator模块. >>> reduce(operator.add, [2, 4, 6]) #利用operator模块的add方法来完成对列表的和 集的计算. 12 >>> reduce((lambda x, y: x + y), [2, 4, 6]) #利用lambda匿名函数来完成对列表的各集的计算. 12 |
重访列表解析:映射
Python2.0引入了 新特性(列表解析表达式), 列表解析可以成为一个比map和filter更通用的工具, 有时候通过基于函数的另类视角进行分析, 有肋于深入理解它.
列表解析基础
>>> ord('s') #ord转换一个字符为ascii 115 >>> res = [] >>> for x in 'spam': #通过循环来完成转换’spam’序列中的每一个字符为ascii码, 并加入到列表res中. res.append(ord(x))
>>> res [115, 112, 97, 109] >>> res = map(ord, 'spam') #通过map函数调用ord来对’spam’中每一个字符进行转换到列表res中. >>> res [115, 112, 97, 109] >>> res = [ord(x) for x in 'spam'] #通过列表表达式来完成对序列’spam’的每一个字符进行转换. >>> res [115, 112, 97, 109] >>> |
列表解析在一个序列的值上应用一个任意表达式, 将其结果收集到一个新的列表中并返回, 从语法上来说, 列表解析 是由方括号封装起来的(为了提醒它们构造了一个列表).它们的简单形式是在方括号中编写一个表达式, 其中的变量在后边跟随着的看起来就像一个for循环的头部一样的语句.有着相同的变量名的变量. Pyhton之后将这个表达式的应用循环中每次迭代的结果收集起来.
>>> [x ** 2 for x in range(10)] #通过列表表达式计算0-10的平方. [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> map((lambda x: x**2), range(10)) #通过map函数调用计算0-10的平方. [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] |
增加测试和嵌套循环
>>> [x for x in range(5) ifx % 2 == 0] #使用if分支的列表表达式. If分支路过条件不为真的情况下的元素 [0, 2, 4] >>> filter((lambda x: x%2 == 0), range(5)) #使用filter过滤函数来完成对偶数的筛选. [0, 2, 4] >>> res = [] #上面列表表达式的代码模拟. >>> for x in range(5): if x % 2 == 0: res.append(x)
>>> res [0, 2, 4]
>>> [x **2 for x in range(10) if x % 2 == 0] #用列表表达式计算10以内的偶数的平方. [0, 4, 16, 36, 64] #用map函数来实现. 可以看到比列表表达式要复杂得多了. >>> map((lambda x: x**2), filter((lambda x: x%2==0), range(10))) [0, 4, 16, 36, 64] |
而实际上, 列表解析还能够更加通用. 你可以在一个列表解析中编写任意数量的嵌套的for循环, 并且每一个都有可选的关联的if测试. 它有如下形式:
[ expression for target1 in sequence1[if condition]
For target2in sequence2 [if condition]…
For target2in sequenceN [if condition] ]
当for分名嵌套在列表解析中时, 它们工作起来就像等效的嵌套 的for循环语句.
>>> res = [x + y for x in [0, 1, 2] for y in [100, 200, 300]] #存在两for语句的双重循环的列表表达式. >>> res [100, 200, 300, 101, 201, 301, 102, 202, 302] >>> res = [] >>> for x in [0, 1, 2]: #上面的列表表达式代码模拟实现. for y in [100, 200, 300]: res.append(x + y)
>>> res [100, 200, 300, 101, 201, 301, 102, 202, 302] #排列5以内的一个偶数和一个奇数的所有可能组合. >>> [(x, y) for x in range(5) if x % 2 == 0 for y in range(5) if y % 2 == 1] [(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)] #而如果上面使用map和filter的等效形式往往将会更复杂也会有深层的嵌套问题. |
列表解析和矩阵
矩阵也被称为多维数组,用python编写矩阵的一个基本方法就是使用嵌套的列表结构。
>>> M = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> N = [[2, 2, 2], [3, 3, 3], [4, 4, 4]] >>> M[1] #打印第一行的所有元素. [4, 5, 6] >>> M[1][2] #打印第一行第二列元素. 6 >>> [row[1] for row in M] #取出矩阵M中每一行的第一个元素. [2, 5, 8] >>> [M[row][1] for row in (0, 1, 2)] #找出矩阵M中0, 1, 2行中的第一个元素. [2, 5, 8] >>> [M[i][i] for i in range(len(M))] #取出行和列相同的元素. [1, 5, 9] >>> [M[row][col] * N[row][col] for row in range(3) for col in range(3)] #将矩阵M和N对应位元素相乘. [2, 4, 6, 12, 15, 18, 28, 32, 36] >>> [[M[row][col] * N[row][col] for col in range(3)] for row in range(3)] [[2, 4, 6], [12, 15, 18], [28, 32, 36]] #最后一个表达式是有效的, 因为row迭代是外层的循环. #对于每个row, 它运行嵌套的列的迭代创建矩阵每一行的结果. >>> res = [] >>> for row in range(3): tmp = [] for col in range(3): tmp.append(M[row][col] * N[row][col]) res.append(tmp)
>>> res [[2, 4, 6], [12, 15, 18], [28, 32, 36]] |
理解列表解析
通用性使得列表解析变得难以理解, 特别是在嵌套的时候. 尽管如此, 对于当前额外的复杂度来说有可观的性能优势: 基于对运行当前python的测试, map调用比等效的for循环要快两倍,而列表解析往往比map调用要稍快一些. 速度上的差距是来自于底层的实现上, map和列表解析是在解释器中以C语言的速度来运行的, 比python的for循环代码在PVM中步进运行要快得多.
重访迭代器: 生成器
编写函数能够返回一个值, 并且稍后还可以从它刚才离开的地方 仍然返回值, 这样的函数被认作是生成器, 因为它们随时间生成一个序列的值. 不像一般的函数会生成值后退出, 生成器函数在生成值后自动挂起并暂停它们的执行和状态.
生成器和一般的函数之间代码上最大的再不同就是一个生成器yield一个值, 而不是return 一个值. Yield语句将会将函数关起, 并向它的调用者返回一个值, 但是保存足够的状态信息为了让其能够 在函数从它挂起的地方恢复.这能够允许这些函数不断的产生一系列的值, 而不是一次计算所有的值. 之后将值以类似列表之类的形式来返回.
>>> def gensquares(N): #这个函数将会不断的生成一系列的数字的平方. for i in range(N): yield i**2 #这个函数在每次循环时都会产生一个值, 之后将其返还给它的调用者. #当它被暂停后它的上一个状态被保存了下来, 并且在yield语句之后控制器马上被回收, >>> for i in gensquares(5): print i, ":",
0 : 1 : 4 : 9 : 16 : >>> x = gensquares(4) >>> x <generator object gensquares at 0x01D15BC0> #当打印它的返回值时, 得到的是一个生成器对象, 它支持迭代器协议(也就是说, next方法可以开始这个函数, #或者说从它上次yield值后的地方恢复, 以及在得到一系列的值的最后一个时, 产生StopIteration异常). >>> x.next() 0 >>> x.next() 1 >>> x.next() 4 >>> x.next() 9 >>> x.next()
Traceback (most recent call last): File "<pyshell#14>", line 1, in <module> x.next() StopIteration |
For循环与生成器工作起来是一样的: 通过重复调用 next方法, 直到捕获一个异常.
如果一个不支持这种协议的对象进行这种迭代, for循环会使用索引协议进行迭代.
同时上面的代码还可以通过下面的方法来实现. >>> for x in [n**2 for n in range(5)]: print x, ':',
0 : 1 : 4 : 9 : 16 : >>> for x in map((lambda x: x**2), range(5)): print x, ':',
0 : 1 : 4 : 9 : 16 : |
尽管如此, 生成器能够运行函数在第一线做了所有的工作, 当结果的列表很大或者在处理每一个结果都需要很多时间的时候, 这一点尤其有用. 生成器将在loop迭代中处理一系列的时间分布开来. (还有一个简单的替代方案将对象保存到迭代中的状态在第6部分介绍)
扩展生成器函数协议: send和next
在python2.5中, 生成器函数协议中增加了一个send方法, send方法像next方法一样, 但是它提供了一种调用者与生成器之间进行通信的方法, 从而能够影响它的操作.
Yield现在是一个表达式的形式, 可以返回传入的元素来发送, 而不是一个语句, 值是通过调用本身的send(value)方法传给生成器的. 之后恢复生成器的代码, 并且yield表达式返回了为了发送而传入的值. 如果调用了正常的next()方法, yield返回None.
例如, 用send方法, 编写一个能够被它的调用者终止的生成器,
这部分是python的一些高级特性, 查看 python的标准库获得更多细节.
迭代器和内置类型.
生成器表达式:迭代器遇到列表解析
在最近版本的python中, 迭代器和列表解析的概念形成了这个语言的一个新的特性, 生成器表达式. 从语法上来讲, 生成器表达式就像一般的列表解析一样, 但是它们是括在圆括号中而不是方括号中的.
>>> [x **2 for x in range(4)] #List comprehension: build a list [0, 1, 4, 9] >>> (x**2 for x in range(4)) #Generator expression: make an iterable <generator object <genexpr> at 0x01D15AD0> #从执行过程上来讲, 生成器表达式不是在内存中构建结果, 而是返回一个生成器对象, #这个对象将会支持迭代协议并在任意的迭代语境的操作中, 获得最终结果列表中的一部分. >>> G = (x ** 2 for x in range(4)) >>> G.next() 0 >>> G.next() 1 >>> G.next() 4 >>> G.next() 9 >>> G.next()
Traceback (most recent call last): File "<pyshell#28>", line 1, in <module> G.next() StopIteration 我们一般不会机械的使用next迭代器来操作生成器表达式, 因为for循环会自动触发.. >>> for num in (x**2 for x in range(4)): print '%s, %s' % (num, num/2.0) #实际上, 每一个迭代的语境都会这样, 包括sum, map和sorted等内置函数 , 以及any, all 和list内置函数等.
0, 0.0 1, 0.5 4, 2.0 9, 4.5 #注意, 如果生成器表达式是在其他的括号之内的话,就像函数调用那样, 生成器自身的括号就不是必须的了, #尽管这样, 在下面第二个sorted调用 中, 还是需要额外的括号. >>> sum(x **2 for x in range(4)) #省略生成器表达式括号 14 >>> sorted(x**2 for x in range(4)) [0, 1, 4, 9] >>> sorted((x**2 for x in range(4)), reverse=True) #由于sorted这时有两个参数,所以生成器表达式加了括号. [9, 4, 1, 0] >>> import math >>> map(math.sqrt, (x**2 for x in range(4))) [0.0, 1.0, 2.0, 3.0] |
生成器表达式大体上可以认为是对内存空间的优化: 它们不需要像方括号的列表解析一样, 一次构造出整个结果列表. 它们在实际中运行起来可能稍慢一些, 所以它们可能只对于非常大的结果集合的运算来说是最优的选择.
对迭代的各种方法进行计时.
总结: 列表解析要比for循环语句有速度方面的性能优势, 而且map会依据调用方法的不同表现出更好或更差的性能. 上一节介绍的生成器表达式看起来比列表解析速度更慢一些, 但是它们把内存需求降到了最小.
让我们用一个小程序来测试一下:
import time, sys reps = 1000 size = 10000
def tester(func, *args): startTime = time.time() for i in range(reps): func(*args) elapsed = time.time() – startTime #计算时间 return elapsed
def forStatement(): res = [] for x in range(size): res.append(abs(x)) #用for循环
def listComprehension(): #用列表解析 res = [abs(x) for x in range(size)]
def mapFunction(): #用map函数 res = map(abs, range(size))
def generatorExpression(): #用生成器表达式. res = list(abs(x) for x in range(size))
print sys.version tests = (forStatement, listComprehension, mapFunction, generatorExpression) for testfunc in tests: print testfunc.__name__.ljust(20), '=>', tester(testfunc) >>> ================================ RESTART ================================ >>> #这是size在10000时的测试结果. 2.7.3 (default, Apr 10 2012, 23:31:26) [MSC v.1500 32 bit (Intel)] forStatement => 1.38800001144 listComprehension => 0.796000003815 mapFunction => 0.609000205994 generatorExpression => 0.952000141144 #这四个测试分别创建了一千次结果列表.每一个都依次执行了一千万步. #可以看出map操作要快一些,列表解析其次. #当改变abs操作这加法操作时时间又发生了变化. import time, sys reps = 1000 size = 10000
def tester(func, *args): startTime = time.time() for i in range(reps): func(*args) elapsed = time.time() - startTime return elapsed
def forStatement(): res = [] for x in range(size): res.append(x+10)
def listComprehension(): res = [x+10 for x in range(size)]
def mapFunction(): res = map((lambda x: x+10), range(size))
def generatorExpression(): res = list(x+10 for x in range(size))
print sys.version tests = (forStatement, listComprehension, mapFunction, generatorExpression) for testfunc in tests: print testfunc.__name__.ljust(20), '=>', tester(testfunc) >>> ================================ RESTART ================================ >>> 2.7.3 (default, Apr 10 2012, 23:31:26) [MSC v.1500 32 bit (Intel)] forStatement => 1.3109998703 listComprehension => 0.546000003815 mapFunction => 1.30999994278 generatorExpression => 0.732999801636 #可以看出列表解析快一些,生成器表达式其次. #map调用需要进行函数调用使得它变得和for循环语句一样慢, 尽管for循环语句的版本使用了更多的代码. |
因为解释器的优化是相当内在化的, 像这里的python代码的性能分析是很难处理的事情. 猜出哪一种方法的性能是最佳的几乎是不可能的. 当然在目前的测试情况下, 我们所能确定的就是对于目前的python, 在map调用中使用用户定义的函数至少会导致map慢2倍以上, 而列表解析对这样的测试运行最快.
性能不应该是最优先考虑的. 首先要为可读性和简洁性的标准编写.如果有必要的话, 之后再进行优化.
函数设计概念
当你开始使用函数时, 就开始面对如何将组件聚合在一起的选择了. 例如, 如何将任务分解成为更有针对性的函数(导致了聚合性), 函数将如何通信(耦合性)等. 你需要深入考虑诸如聚合性, 耦合性以及函数的大小等性质其中一些可以归类于结构分析和设计的范畴.
l 耦合性: 对于输入使用参数并且对于输出使用return语句.
n 让函数独立于它外部的东西. 参数和return 语句通常就是隔离对外部依赖关系的最好的办法, 从而让代码中只剩少量醒目位置.
l 耦合性: 只有在真正必要的情况下使用全局变量.
n 它们引发了依赖关系和计时的问题. 会导致程序调试和修改的
l 耦合性: 不要改变可变类型的参数, 除非调用者希望这样做.
n 这会导致调用者和被调用者之间的耦合性.
l 聚合性: 每个函数都应该有一个单一的, 统一的目标.
n 一个函数中都应该做一件事: 这件事可以用一个简单说明句来总结.
l 大小: 每一个函数应该相对较小. 保持简单, 保持简短.
l 耦合: 避免直接改变在另一个模块文件中的变量.
n 在可能的时候使用读取函数, 而不是直接进行赋值语句.
如果没有使用类, 全局变量通常是模块中函数保留调用中状态的最佳方式, 如果都在预料中, 副作用就没什么危险.
函数是对象: 简洁调用
由于函数在运行时是对象, 你可以编写通用化程序来处理它们. 函数对象能够进行赋值, 传递给其他的函数以及数据结构中排序, 这和简单的数字和字符串一样.
1. 函数名就是一个简单的对象的引用, 能够将这个对象重新分配给其他的变量名.
2. 因为参数是通过赋值传递的, 所以给其他函数以参数的形式传入函数也很简单.
3. 甚至可以将函数对象封装在数据结构中, 就像它是整数或字符串一样. 因为python混合类型能够包含任意类型的对象.
函数陷阱
本地变量是静态检测的.
在python定义的一个函数中进行分配的变量名默认为本地变量的, 它们存在于函数的作用域并只在函数运行时存在. Python是静态检测 python的本地变量的, 当编译def代码时, 不是通过发现赋值语句在运行时进行检测的.
>>> x = 99 >>> def selector(): print x
>>> selector() 99 >>> def selector(): print x x = 88
>>> selector()
Traceback (most recent call last): File "<pyshell#9>", line 1, in <module> selector() File "<pyshell#8>", line 2, in selector print x UnboundLocalError: local variable 'x' referenced before assignment #得到了一个未定义的变量名错误, 原因是: python在交互模式或一个模块文件导入时, python读入并编译这级代码. 在编译时, python看到了对x的赋值语句, 并且决定了x将会在函数中的任一地方都将是本地变量名. 但是, 当函数实际运行时, 因为在print执行时赋值语句并没有发生, python告诉正在使用一个未定义的变量名, 根据其变量名规则, 本地变量x是在其被赋值前就使用了. 实际上, 任何在函数内 的赋值将会使其成为一个本地变量名. Import, =, 嵌套def, 嵌套类等, 都会受这种行为影响. 产生这种问题的原因在于被赋值的变量名在函数内部是当作本地变量来对待的, 而不是仅仅在赋值以后的语句中才被当作是本地变量. 实际上, python会在函数中将x作为本地变量, 它就是一个错误. #事实上上面定义的selecor()函数本身就有问题.当使用全局变量x时应该加上global先声明使用全局变量x. >>> def selector(): global x print x x = 88 #这其实是改变了全局变量x为88, 因为已经global x.
>>> selector() 99 #如果真的想打印全局变量, 并在之后设置一个有着相同变量名的本地变量, #导入上层的模块,并使用模块的属性标记来获得其全局变量. >>> x = 99 >>> def selector(): import __main__ #导入上层的模块. print __main__.x #打印上层模块的全局变量x (99). x = 88 #创建本地变量x 为88 print x #打印本地变量x .
>>> selector() 99 88 |
交互模式下的命名空间是一个名为__main__的命名空间. 所以__main__.x得到了全局变量版本的x.
默认和可变对象
默认参数是在def语句运行时被评估并保存的, 而不是在这个函数调用时. 从内部来讲, python会将每一个默认参数保存成一个对象, 附加在这个函数本身.
因为默认参数是在def时被评估的, 如果必要的话, 它能够从整个作用域内保存值.
下面的函数使用了一个空列表作为默认参数, 并在函数每次调用时都对它进行了改变.
>>> def saver(x=[]): #这个默认的列表对象每次调用时,它都没有重置为空列表.当新元素加入后, 列表会变大. x.append(1) print x
>>> saver([2]) [2, 1] >>> saver() [1] >>> saver() [1, 1] >>> saver() [1, 1, 1] |
有些人把这种行为当作一种特性. 因为可变类型的默认参数在函数调用之间保存了它们的状态, 从某种意义上讲它们能够充当C语言中的静态本地变量的角色. 在一定程度上, 它们工作起来就像全局变更, 但是它们的变量名对于函数来说是本地变更, 而且不会与程序中的其他变量名发生冲突.
值得说明的是python中有更好的办法在调用之间保存状态.(例如使用类).
>>> def saver(x = None): if x is None: #如果上面的特性不是你想要的行为的话, x = [] #在函数主体中先判断有没有赋值, 然后再使用默认值, x.append(1) #或者在主体函数开始时对默认值进行简单的拷贝. print x
>>> saver([2]) [2, 1] >>> saver() [1] >>> saver() [1]
>>> def saver(x = None): x = x or [] #如果淌有参数传入的话, x将会默认为None, 然后or语句返回右操作符对象. x.append(1) #但是如果传入一个[]空列表, x将返回的不是传入的空列表而是右边的空列表的值. print x
>>> saver([2]) [2, 1] >>> saver() [1] >>> saver() [1] |
没有return 语句的函数
在python函数中, return 以及yield语句是可选的. 当一个没有精确的返回值的时候, 函数在控制权从函数主体脱离时, 函数将会推迟. 从技术上来讲, 所有的函数都返回了一个值, 如果没有提供return 语句, 函数将自动返回None对象.
没有return 语句的函数与python对应于一些其他语言中所谓的”过程”是等效的. 它们常被当作语句.
本章小节