骑麦兜看落日

[Code]Python学习手册_第四部分_函数

字数统计: 32.4k阅读时长: 143 min
2018/08/05 Share

第十六章_函数基础

  • 本章介绍了函数定义的语法以及defreturn语句的操作,函数调用表达式的行为,以及Python函数中多态的概念和优点
  • 探索本地变量和作用域的概念
  • 介绍生成器和函数式工具
  • 回顾多态
语句 例子
Calls myfunc(“spam”, “eggs”, meat = ham)
def def adder(a, b,*c):
return return a+b+c[0]
global def changer(): global x; x = ‘new’
nonlocal def changer(): nonlocal x; x = ‘new’
yield def squares(x): for i in range(x): yield i ** 2
lambda Funcs = [lambda x: x*2, lambda x: x3]

为何使用函数

函数是一个通用的程序结构部件

  • 最大化的代码重用和最小化代码冗余

    Python的函数通过一种简单的办法去打包逻辑算法,使其能够在之后不止在一处、不止一次地使用

    函数允许整合以及通用化代码,以便这些代码能够在之后多次使用

    因为允许一处编写多处运行,使程序中减少代码的冗余成为现实,并为代码的维护节省了不少的力气

  • 流程的分解

    函数提供了一种将一个系统分割为定义完好的不同部分的工具

    独立的实现较小的任务要比一次完成整个流程要容易得多

编写函数

  • def是可执行的代码

    Python的函数由def语句编写

    def是一个可执行的语句,直到Python运行了def后函数才存在

    if语句、while循环甚至是其他的def中嵌套是合法的

  • def创建了一个对象并将其赋值给某一变量名

    当Python运行到def语句时,它将会生成一个新的函数对象并将其赋值给这个函数名,函数名变成了某一个函数的引用,函数对象还可以赋值给其他的变量名,保存在列表之中

    函数也可以通过lambda表达式来创建

  • lambda创建一个对象但将其作为结果返回

    可以用lambda表达式创建函数,把函数定义内联到语法上一条def语句不能工作的地方

  • return将一个结果对象发送给调用者

    当函数被调用时,其调用者停止运行直到这个函数完成了它的工作,之后函数才将控制权返回调用者

    函数是通过return语句将计算得到的值传递给调用者的,返回值成为函数调用的结果

  • yield向调用者发回一个结果对象,但是记住它离开的地方

    像生成器这样的函数也可以通过yield语句来返回值,并挂起它们的状态以便稍后能够恢复状态

  • global声明了一个模块级的变量并被赋值

    在默认情况下,所有在一个函数中被赋值的对象,是这个函数的本地变量,并且仅在这个函数运行的过程中在存在

    为了分配一个可以在整个模块中都可以使用的变量名,函数需要在global语句中声明

    通常情况下,变量名需要关注它的作用域(也就是说变量存储的地方),并且是通过实赋值语句将变量名绑定至作用域的

  • nonlocal声明了将要赋值的一个封闭的函数变量

    nonlocal语句允许一个函数来赋值一条语法封闭的def语句的作用域中已有的名称

    这就允许封闭的函数作为保留状态的一个地方一当一个函数调用的时候,信息被记住了一而不必使用共享的全局名称

  • 函数是通过赋值(对象引用)传递的

    参数通过赋值传递给了函数,调用者以及函数通过引用共享对象,但是不需要别名

    改变函数中的参数名并不会改变调用者中的变量名,但是改变传递的可变对象可以改变调用者共享的那个对象

  • 参数、返回值以及变量并不是声明

    在函数中没有类型约束,可以传递任意类型的参数给函数,函数也可以返回任意类型的对象

    所以一个函数可以用在很多类型的对象上,任意支持兼容接口(方法和表达式)的对象都能使用,无论它们是什么类型

def语句

def语句格式为

1
2
def <name>(arg1,arg2,...,argN):
<statements>

def语句包含了首行并有一个缩进的代码块跟随在后边,这个代码块就是函数的的主体

首行

  • def`的首行定义了函数名,将函数名赋值为函数对象
  • 在括号中包含了0个以上的参数(形参).函数调用时,参数名赋值为括号中传递来的对象

函数主体

  • 函数主体包含一条可选return语句,表示函数调用的结束,并将结果返回至函数调用出,它可以在函数主体的任何地方,没有return时函数在执行完函数主体时结束并返回None对象
  • 函数可以有yield语句,产生一系列值在每次调用时返回

def语句是实时执行的

def语句在运行时创建一个新的函数对象,并将其赋值给一个变量名,def之中的代码在函数调用后解释

1
2
3
4
5
6
7
8
9
10
if test:
def func(): # Define func this way
...
else:
def func(): # Or else this way
...
...
func()
othername = func # Assign function object
othername() # Cakk func again

def语句可以出现在语句可以出现的地方,包括嵌套在其他语句中

函数名只是引用了函数对象的变量,可以通过新的变量名进行调用

1
2
3
def func(): ...		# Create function object
func() # Call object
func.attr = value # Attach attributes

函数允许任意的属性附加到记录信息以供随后使用


第一个例子:定义和调用

函数描绘了两个方面

  • 定义:def创建一个函数
  • 调用:表达式告诉Python去运行函数主体

定义

1
2
3
>>> def times(x, y):	# Create and assign function
... return x * y # Body executed when called
...

当Python运行到def语句时会创建一个新的函数对象,封装这个函数的代码并将这个对象赋值给变量名

调用

1
2
3
4
5
6
7
8
9
10
>>> def times(x, y):	# Create and assign function
... return x * y # Body executed when called
...
>>> times(2, 4) # Arguments in parentheses
8
>>> x = times(3.14, 4) # Sace the result object
>>> x
12.56
>>> times('Ni', 4) # Functions are "typeless"
'NiNiNiNi'

def语句运行后,可以在程序中通过在函数名后增加括号调用函数

括号中可以包含一个或多个对象参数,这些参数通过赋值传递给函数头部的参数名

return语句返回的对象可以赋值给另一个变量

由于在Python中未对变量、参数或者返回值有变量的声明,所以可以把*用作数字的乘法或是序列的重复

Python中的多态

表达式的意义取决于表达式中对象类型的行为称为多态

函数的参数支持所有的对象类型,只要对象支持所预期的接口,函数就能处理,否则会检测到错误并自动抛出一个异常

Python为对象编写接口,而不是数据类型编写接口


第二个例子:寻找序列的交集

1
2
3
4
5
6
7
8
9
>>> seq1 = "spam"
>>> seq2 = "scam"
>>> res = []
>>> for x in seq1:
... if x in seq2:
... res.append(x)
...
>>> res
['s', 'a', 'm']

这个程序被设置为只能列出定义好的变量并且不能继续使用

定义

编写寻找序列的交集可以将for循环封装在一个函数之中

  • 把代码放在函数中能够成为一个想运行多少次就运行多少次的工具
  • 调用者可以传递任意类型的参数,函数对于任意两个希望寻找其交集的序列(或者其他可迭代的类型)都是通用的
  • 当逻辑由一个函数进行封装的时候,需要修改重复性的任务时,只需要在函数里进行修改搜索交集的方式就可以了
  • 在模块文件中编写函数意味着它可以被计算机中的任意程序来导入和重用
1
2
3
4
5
6
def intersect(seq1, seq2):
res = [] # Start empty
for x in seq1: # Scan seq1
if x in seq2: # Common item?
res.append(x) # Add to end
return res

将代码封装在函数中,使它成为一个通用搜索交集的工具

调用

1
2
3
4
5
6
7
8
9
10
11
>>> def intersect(seq1, seq2):
... res = []
... for x in seq1:
... if x in seq2:
... res.append(x)
... return res
...
>>> s1 = "SPAM"
>>> s2 = "SCAM"
>>> intersect(s1, s2) # Strings
['S', 'A', 'M']

def语句运行后,可以通过在括号中传递对象来调用这个函数

重访多态

1
2
3
4
5
6
7
8
9
10
>>> def intersect(seq1, seq2):
... res = []
... for x in seq1:
... if x in seq2:
... res.append(x)
... return res
...
>>> x = interset([1, 2, 3], (1, 4)) # Mixed types
>>> x # Saved result object
[1]

Python中的函数是多态的,只要函数参数类型支持拓展对象接口就可以处理

当传入了不支持这些接口的对象,Python将呼自动检测出不匹配,并抛出一个异常

本地变量

本地变量只在def内的函数中可见,并且仅在函数运行时存在

  • 在函数内被赋值的变量是本地变量
  • 通过赋值传入的参数是本地变量

所有的本地变量都会在函数调用时出现,在函数退出时消失

return返回结果对象,引用结果对象的本地变量消失


本章习题

  1. 编写函数有什么意义

    函数是Python避免程序代码冗余的最基本方式:把代码分解成函数,意味着未来只有一个运算的代码的拷贝需要更新

    函数是Python中代码重用的基本单位:在函数中包装代码,就使其成为可再利用的工具,可在许多程序中调用它

    函数可让我们把复杂系统分割为可管理的部分,而每一部分都可独立进行开发

  2. 什么时候Python将会创建函数

    当Python运行到并执行def语句时,函数就会被创建,这个语句会创建函数对象,并将其赋值给函数名

  3. 当一个函数没有return语句时,它将返回什么

    如果控制流程来到函数主体末尾并没有运行return语句,函数就会传回None对象

    这类函数通常是通过表达式语句调用,并将其None结果赋值给变量通常是没有意义的

  4. 在函数定义内部的语句什么时候运行

    函数主体(嵌套在函数定义语句中的代码)在函数稍后通过一个调用表达式调用时执行

    函数每次被调用,主体都会全新运行一次

  5. 检查传入函数的对象类型有什么错误

    检查传入函数的对象类型,实质上就是破坏函数的灵活性,把函数限制在特定的类型上

    没有这类检查时,函数可能处理所有的对象类型:任何支持函数所预期的接口的对象都能用(接口一词是指函数所执行的一组方法和表达式运算符)


第十七章 作用域

  • 本章深入介绍Python作用域(变量定义以及查找的地方)以及参数传递(传递给函数作为其输入对象的方式)
  • 学习嵌套函数作用域和函数属性
  • 学习通用的设计观点

Python作用域基础

Python中变量名在赋值时绑定到特定的命名空间,被赋值的位置决定了这个变量名能被访问到的作用域

创建、改变或查找变量名都是在命名空间中进行的

变量的作用域有变量在程序文件中源代码的位置决定,而不是由函数调用决定,称为语义作用域

函数中的所有变量名与这个函数的命名空间相关联

  • 一个在def内定义的变量名能够被def内的代码使用,不能在函数的外部引用这样的变量名
  • def之中的变量名与def之外的变量名并不冲突,即使别处使用相同的变量名

不同地方赋值变量所对应的作用域

  • 如果一个变量在def内赋值,它被定位在这个函数之内
  • 如果一个变量在一个嵌套的def中赋值,对于嵌套的函数来说,它是非本地的
  • 如果在def之外赋值,它就是整个文件全局的

作用域法则

函数提供了嵌套的命名空间(作用域),使其内部使用的变量名本地化,以便函数内部使用的变量名不会与此函数外的变量名产生冲突

  • 函数定义的本地作用域与模块定义的全局作用域关系
  • 内嵌模块是全局作用域

    每个模块都是一个全局作用域,即一个模块文件顶层的变量的命名空间

    其他模块文件的全局变量称为成为一个模块对象的属性,能够像简单的变量一样使用

  • 全局作用域的作用范围仅限于单个文件

    全局指在一个文件的顶层的变量名仅对这个文件内部的代码而言是全局的

    变量名由模块文件分开,只有精确地导入一个模块文件才能够使用这个文件中定义的变量名

  • 每次对函数的调用都创建了一个新的本地作用域

  • 赋值的变量名除非声明为全局变量或非本地变量,否则均为本地变量

    所有函数定义内部的变量名是位于本地作用域内的,需要通过global语句给一个在函数内部却需要位于模块文件顶层命名空间的变量名赋值

    通过nonlocal语句给位于一个嵌套的def中的名称赋值

  • 所有其他的变量名都可以归纳为本地、全局或者内置的

    内置的变量名由Python的预定义__builtin__模块提供

  • 交互模式运行的代码输入到__main__模块的内置模块中,所以交互模式也在模块中创建名称,遵守常规的作用域规则
  • 一个函数内部的任何类型的赋值都会把变量名划定为本地的,包括=语句、import中的模块名称、def中的函数名称、函数参数名称等
  • 具有可变性的对象只有在对变量名赋值时才可以划分为本地对象,对一个名称赋值并不是修改一个对象

变量名解析:LEGB原则

所有在函数def语句或lambda语句内赋值的变量名默认均为本地变量

函数能够在函数内部以及全局作用域直接使用变量名,但是必须声明为非本地变量和全局变量去改变其属性

对于def语句

  • 变量名引用分为三个作用域进行查找:首先是本地,之后是函数内(如果有的话),之后全局,最后是内置
  • 在默认情况下,变量名赋值会创建或者改变本地变量
  • 全局声明和非本地声明将赋值的变量名映射到模块文件内部的作用域

Python变量名解析机制有时称为LEGB法则

  • 当在函数中使用未认证的变量名时,Python搜索4个作用域[本地作用域(L)、上一层结构中deflambda本地作用域(E)全局作用域(G)内置作用域(B)]并且在第一处能够找到这个变量名的地方停下来,如果在搜索中没有找到变量名会报错
  • 当在函数中给一个变量名赋值时,Python总是创建或改变为本地作用域的变量名或者显式的声明为全局变量
  • 当在所有函数之外给一个变量名赋值时,本地作用域与全局作用域是相同的

作用域实例

1
2
3
4
5
6
7
8
9
# Global scope
X = 99 # X and func assigned in module:global

def func(Y): # Y and Z assigned in function:locals
# Local scope
Z = X + Y # X is a global
return Z

func(1) # func in module:result = 100

变量名隔离机制的意义在于本地变量是作为临时的变量名,只有在函数运行时才需要它们,不会与模块命名空间内的变量或其他函数内的变量产生冲突

全局变量名:X, func

X与func是在模块文件顶层注册的所以是全局变量,它能够在函数内部进行引用而不需要特意声明为全局变量

def语句在这个模块文件顶层将一个函数对象赋值给了变量名func

本地变量名:Y, Z

Y和Z是本地变量(并且只在函数运行时存在),因为他们都是在函数定义内部进行赋值的:Z是通过=语句赋值的,而Y是由于参数总是通过赋值来进行传递的

内置作用域

内置作用域是__builtin__的内置模块,必须先使用import语句导入才能使用内置作用域

1
2
3
>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

dir调用得到的变量名组成了Python中的内置作用域,其中前一半是内置的异常,后一半是内置函数

根据LEGB法则,Python最后自动搜索这个模块,不需要导入即可使用这些变量名

1
2
3
4
>>> zip				# The normal way
<class 'zip'>
>>> builtins.zip # The hard way
<class 'zip'>

通过LEGB法则或者手动导入__builtin__模块可以引用内置函数

1
2
3
4
5
6
7
8
9
10
11
12
def hider():
open = 'spam' # Local variable,hides built-in
...
open('data.txt') # This won't open a file now in this scope!

X = 88 # Global X

def func():
X = 99 # Local X:hides global

func()
print(X) # Prints 88:unchanged

根据LEGB查找流程,在本地作用域的变量名可能会覆盖在全局作用域和内置作用域的有着相同变量名的变量

在Python 2.6中违反通用性

由于在Python 2.6中名称TrueFalse是内置作用域中的变量而不是保留字,通过True = False语句为它们重新赋值是可能的,这样只会在它出现的作用域中重新定义True,其他作用域仍然在内置作用域中查找

在Python 2.6中可以使用builtin.True = False在整个Python过程中把True重置为False

Python 3.0中取消了这种类型的赋值,TrueFalseNone一样作为保留字


global语句

global语句是一个命名空间的声明,在函数中创建修改一个或多个全局变量名

  • 全局变量是位于模块文件内部的顶层的变量名
  • 全局变量如果是在函数内被赋值的话,必须经过声明
  • 全局变量名在函数的内部不经过声明也可以被引用
1
2
3
4
5
6
7
X = 88			# Global X
def func():
global X
X = 99 # Global X:outside def

func()
print(X) # Prints 99

global语句包含关键字global,其后跟着一个或多个由逗号分开的变量名,当在函数主体被赋值或引用时,所有列出来的变量名将被映射到整个模块的作用域

最小化全局变量

若将本地变量改为全局变量,由于变量的值取决于函数调用的顺序,而函数自身是任意顺序进行排列的,导致调试起来变得困难,所以尽可能少的使用全局变量

1
2
3
4
5
6
7
8
X = 99
def func1():
global X
X = 88

def func2():
global X
X = 77

global声明使相同变量名的值取决于最后调用的函数

全局变量不像那些依赖于包含本地变量的函数构成的代码,全局变量使得程序更难理解和使用

当不使用面向对象的编程方法以及类的话,全局变量是Python中最直接保存状态信息的方法,但本地变量在函数返回时消失

全局变量在并行线程中在不同的函数之间称为了共享内存,扮演了通讯工具的角色

最小化文件间的修改

编写程序往往不会直接修改另一个文件的变量

1
2
3
4
5
6
7
# first.py
X = 99 # This code doesn't know about second.py

# second.py
import first
print(first.X)
first.X = 88

每个模块都是自包含的命名空间,必须导入一个模块才能在另一个模块中看到它内部的变量

一个模块文件的全局变量被导入后就成为了这个模块对象的一个属性

一个模块对导入模块的变量重新赋值可能很难发现,并且会让两个文件有过于强的相关性,没有其中一个文件很难理解或重用另一个文件,导致代码不灵活或引发bug

1
2
3
4
5
6
7
8
9
10
# first.py
X = 99

def setX(new):
global X
X = new

# second.py
import first
first.setX(88)

在文件间进行通信最好的办法是通过调用函数,传递参数,得到其返回值,使用accessor函数管理这种变化

这种办法可以提高可读性和可维护性

其他访问全局变量的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# thismod.py

var = 99 # Global variable == module attribute

def loval():
var = 0 # Change local var

def glob1():
global var # Declare global(normal)
var += 1 # Change global var

def glob2():
var = 0 # Change loval var
import thismod # Import myself
thismod.var += 1 # Change global var

def glob3():
var = 0 # Change local var
import sys # Import system table
glob = sys.modules['thismod'] # Get module object(or use __name__)
glob.var += 1 # Change global var

def test():
print(var)
local(); glob1(); glob2(); glob3()
print(var)

全局变量构成了被导入对象的属性,能够通过使用导入嵌入的模块并对其属性进行赋值来伪造global语句

全局变量与模块的属性是等效的


作用域和嵌套函数

LEGB查找法则中E包括了任意嵌套函数内部的本地作用域

嵌套作用域也叫静态嵌套作用域

嵌套作用域的细节

  • 一个引用首先在本地(函数内)作用域查找变量名;之后会在代码的语法上嵌套了的函数中的本地作用域,从内到外查找,之后查找当前的全局作用域(模块文件) ,最后再内置作用域内(模块_ builtin_ )
  • 全局声明将会直接从全局(模块文件)作用域进行搜索。
  • 在默认情况下,赋值会创建或改变了变量名的当前作用域
    • 如果在函数内部声明为全局变量,将会创建或改变变量名为整个模块的作用域
    • 如果在函数内声明为nonlocal,赋值会修改最近的嵌套函数的本地作用域中的名称

嵌套作用域举例

1
2
3
4
5
6
7
8
9
X = 99				# Global scope name:not used

def f1():
X = 88 # Enclosing def local
def f2():
print(X) # Reference made in nested def
f2()

f1() # Prints 88:enclosing def local

f1()内的def语句生成一个函数,并将其赋值给变量名f2,f2f1的本地作用域内的一个本地变量

通过LEGB法则,f2内的变量x自动映射了f1的本地作用域内的变量x

1
2
3
4
5
6
7
8
def f1():
X = 88
def f2():
print(X) # Remembers X in enclosing def scope
return f2() # Return f2 but don't call it

action = f1() # Make,return function
action() # Call it now:prints 88

嵌套作用域的查找法则在函数被返回后仍然有效

工厂函数

作用域已不存在也能够记住嵌套作用域的变量值的函数叫做闭合或者工厂函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> def maker(N):
... def action(X): # Make and return action
... return X ** N # action retains N fron enclosing scope
... return action
...
>>> f = maker(2) # Pass 2 to N
>>> f
<function maker.<locals>.action at 0x104205a60>
>>> f(3) # Pass 3 to X,N remembers 2:3 ** 2
9
>>> f(4) # 4 ** 2
16
>>> g = maker(3) # g remembers 3, f remembers 2
>>> g(3) # 3 ** 3
27
>>> f(3) # 3 ** 2
9

通过外层函数返回一个内嵌函数的引用

外层函数的本地作用域的变量作为执行的状态信息被内层函数保留了下来

当只调用内嵌函数时,尽管外部函数已经返回了值并退出,但仍会记住外层函数的本地作用域的变量名

如果再次调用外层函数,将得到一个新的有不同状态信息的嵌套函数,但最初的嵌套函数仍是原始的状态信息

对一个工厂函数的每次调用,都会得到自己的状态信息的集合

嵌套的作用域常常被lambda函数创建表达式使用,函数嵌套通常用作装饰器

类是一个更好的像这样进行”记忆”的选择

使用默认参数来保留嵌套作用域的状态

1
2
3
4
5
6
7
def f1():
x = 88
def f2(x=x): # Remember enclosing scope X with defaults
print(x)
f2()

f1() # Prints(88)

def头部的arg = val语句表示参数arg在调用时没有值传入进来时默认使用值val

1
2
3
4
5
6
7
8
9
>>> def f1():
... x = 88 # Pass x along instead of nesting
... f2(2) # Forward reference okay
...
>>> def f2(x):
... print(x)
...
>>> f1()
2

避免在def中嵌套def会降低复杂度

嵌套作用域和lambda

lambdadef语句相似,将会生成后面调用的一个新的函数

lambda是一个表达式,能够使用在def中不能使用的地方

1
2
3
4
5
6
7
8
9
10
11
def func():
x = 4
action = (lambda n: x ** n) # x remembered from enclosing def
return action

x = func()
print(x(2)) # Prints 16, 4 ** 2
def func():
x = 4
action (lambda n, x=x:x ** n) # Pass x in manually
return action

由于嵌套作用域查找层,lambda能够看到所有在所编写的函数中可用的变量

可以使用默认参数从上层作用域传递值给lambda

作用域与带有循环变量的默认参数相比较

如果在一个函数的循环中嵌套lambda或者def语句,并且嵌套的函数引用了外层作用域的循环变量,所有在这个循环中产生的函数都将是最后一次循环完成时被引用变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def makeActions():
... acts = []
... for i in range(5): # Tries to remember each i
... acts.append(lambda x: i ** x) # All remember same last i
... return acts
...
>>> acts = makeActions()
>>> acts[0]
<function makeActions.<locals>.<lambda> at 0x104205950>
>>> acts[0](2) # All are 4 ** 2,value of last i
16
>>> acts[2](2) # This should be 2 ** 2
16
>>> acts[4](2) # This should be 4 ** 2
16

嵌套作用域中的变量在嵌套的函数被调用时才进行查找,所以它们实际上是最后一次循环迭代中循环变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def makeActions():
... acts = []
... for i in range(5): # Use defaults instead
... acts.append(lambda x, i=i: i ** x) # Remember current i
... return acts
...
>>> acts = makeActions()
>>> acts[0](2) # 0 ** 2
0
>>> acts[2](2) # 2 ** 2
4
>>> acts[4](2) # 4 ** 2
16

因为默认参数是在嵌套函数创建时评估的,必须使用默认参数把当前的值传递给嵌套作用域的变量,每一个函数记住自己的变量的值

任意作用域的嵌套

1
2
3
4
5
6
7
8
9
10
>>> def f1():
... x = 99
... def f2():
... def f3():
... print(x) # Found in f1's local scope!
... f3()
... f2()
...
>>> f1()
99

Python会在搜索模块的全局作用域之前从内至外在所有内嵌的def中搜索本地作用域


nonlocal语句

nonlocal语句使内层的def函数可以对任意层外层的嵌套函数的作用域的变量进行读取和写入访问

在声明nonlocal变量时,变量名必须已经存在于该外层的嵌套函数的作用域中

nonlocal基础

1
2
def func():
nonlocal name1, name2, ...
  • nonlocal只在函数内有意义,允许一个内层函数来修改一个任意层外层函数的作用域中定义的一个或多个变量名

  • nonlocal使得变量名的查找直接从外层的def的作用域开始,而不是从声明函数的本地作用域开始

  • nonlocal中列出的名称必须在外层的def中提前定义过,否则产生一个错误

  • nonlocal变量名只能出现在外层的def中,而不能在模块的全局作用域中或def之外的内置作用域中

  • nonlocal声明不会改变通用的名称引用LEGB规则

    查找规则的限制

    • global使得作用域查找从嵌套的模块的作用域开始,并且允许对那里的名称赋值,如果名称不存在于该模块中,作用域查找继续到内置作用域,但是,对全局名称的赋值总是在模块的作用域中创建或修改它们
    • nonlocal限制作用域查找只是嵌套的def,不会继续到全局或内置作用域,要求名称已经提前创建,并且允许对它们赋值

nonlocal应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> def tester(start):
... state = start # Referencing nonlocals works normally
... def nested(label):
... print(label, state) # Remembers state in enclosing scope
... return nested
...
>>> F = tester(0)
>>> F('spam')
spam 0
>>> F('ham')
ham 0
>>> def tester(start):
... state = start
... def nested(label):
... print(label, state)
... state += 1 # Cannot change by default
... return nested
...
>>> F = tester(0)
>>> F('spam')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in nested
UnboundLocalError: local variable 'state' referenced before assignment

默认情况下,不允许修改嵌套的def作用域中的名称

使用nonlocal进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> def tester(start):
... state = start # Each call gets its own state
... def nested(label):
... nonlocal state # Remembers state in enclosing scope
... print(label, state)
... state += 1 # Allowed to change it if nonlocal
... return nested
...
>>> F = tester(0)
>>> F('spam') # Increments state on each call
spam 0
>>> F('ham')
ham 1
>>> F('eggs')
eggs 2
>>> G = tester(42) # Make a new tester that starts at 42
>>> G('spam')
spam 42
>>> G('eggs') # My state information updated to 43
eggs 43
>>> F('bacon') # But F's is where it left off:at 3
bacon 3 # Each call has different state information

使用nonlocal声明后即可修改变量,即使内层函数被返回后外层函数退出也是有效的

多次调用外层函数可以在内存中获得其状态信息的多个副本,互不影响

边界情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> def tester(start):
... def nested(label):
... nonlocal state # Nonlocals must already exist in enclosing def!
... state = 0
... print(label, state)
... return nested
...
File "<stdin>", line 3
SyntaxError: no binding for nonlocal 'state' found
>>> def tester(start): # Globals don't have to exist yet when declared
... def nested(label): # This creates the name in the module now
... global state
... state = 0
... print(label, state)
... return nested
...
>>> F = tester(0)
>>> F('abc')
abc 0
>>> state
0

nonlocal语句声明的变量名必须在一个嵌套的def作用域中赋值过,否则会得到一个错误

1
2
3
4
5
6
7
8
9
10
>>> spam = 99
>>> def tester():
... def nested():
... nonlocal spam # Must be in a def, not the module!
... print('Current=', spam)
... spam += 1
... return nested
...
File "<stdin>", line 3
SyntaxError: no binding for nonlocal 'spam' found

nonlocal限制作用域查找仅为嵌套的def,不会在嵌套的模块的全局作用域或所有def之外的内置作用域中查找

为什么使用nonlocal

在Python中,有各种不同的办法来记住跨函数和方法的状态信息

1
2
3
4
5
6
7
8
9
10
11
>>> def tester(start):
... state = start # Each call gets its own state
... def nested(label):
... nonlocal state # Remembers state in enclosing scope
... print(label, state)
... state += 1 # Allowed to change it if nonlocal
... return nested
...
>>> F = tester(0)
>>> F('spam')
spam 0

nonlocal语句允许在内存中保持可变状态的多个副本,并且解决了在类无法保证的情况下的简单的状态的保持

对外层函数的每次调用都创建了可变信息的一个小小的自包含宝,可变信息的名称不会与程序的其他部分产生任何冲突

与全局共享状态

直接把状态信息移到全局作用域实现nonlocal效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> def tester(start):
... global state # Move it out to the module to change it
... state = start # global allows changese in module scope
... def nested(label):
... global state
... print(label, state)
... state += 1
... return nested
...
>>> F = tester(0)
>>> F('spam') # Each call increments shared global state
spam 0
>>> F('eggs')
eggs 1
>>> G = tester(42) # Resets state's single copy in global scope
>>> G('toast')
toast 42
>>> G('bacon')
bacon 43
>>> G('ham') # Oops -- my counter has been overwritten!
ham 44

内层函数和外层函数都需要使用global声明

这样做可能会引发全局作用域中的名称冲突,且只考虑到状态信息的单个共享副本,再次调用外层函数将会重新设置状态信息

使用类的状态(预览)

使用带有属性的类可以让状态信息的访问比隐式的范围查找规则更明确,通过利用显式属性赋值而不是作用域查找

类的每个实例都得到状态信息的一个新副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> class tester:							# Class-based alternative
... def __init__(self, start): # On object construction
... self.state = start # save state explicitly in new object
... def nested(self, label):
... print(label, self.state) # Reference state explicitly
... self.state += 1 # Changes are always allowed
...
>>> F = tester(0) # Create instance, invoke __init__
>>> F.nested('spam') # F is passed to self
spam 0
>>> F.nested('ham')
ham 1
>>> G = tester(42) # Each instance gets new copy of state
>>> G.nested('toast') # Changing one does not impact others
toast 42
>>> G.nested('bacon')
bacon 43
>>> F.nested('eggs') # F's state is where it left off
eggs 2
>>> F.state # State may be assessed outside class
3

状态信息在对象创建的时候显式的保存在对象中

1
2
3
4
5
6
7
8
9
10
11
12
>>> class tester:
... def __init__(self, start):
... self.state = start
... def __call__(self, label): # Intercept direct instance calls
... print(label, self.state) # So .nested() not required
... self.state += 1
...
>>> H = tester(99)
>>> H('juice') # Invokes __call__
juice 99
>>> H('pancakes')
pancakes 100

__call__获取一个实例上的直接调用

使用函数属性的状态

附加给内层函数一个函数属性可以实现与nonlocal相同的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> def tester(start):
... def nested(label):
... print(label, nested.state) # nested is in enclosing scope
... nested.state += 1 # Change attr,not nested itself
... nested.state = start # Initial state after func defined
... return nested
...
>>> F = tester(0)
>>> F('spam') # F is a 'nested' with state attached
spam 0
>>> F('ham')
ham 1
>>> F.state # Can access state outside functions too
2
>>> G = tester(42) # G has own state, doesn't overwrite F's
>>> G('eggs')
eggs 42
>>> F('ham')
ham 2

内层函数的函数名是外层函数的一个本地对象,可以被外层函数自由地索引

由于内层函数只是对本地变量修改引用而不是赋值,所以不需要nonlocal


本章习题

  1. 下面的代码会输出什么,为什么

    1
    2
    3
    4
    5
    >>> X = 'Spam'
    >>> def func():
    ... print(X)
    ...
    >>> func()

    这里的输出是'Spam',因为函数引用的是所在模块中的全局变量(因为不是在函数中赋值的,所以被当作是全局变量)

  2. 下面的代码会输出什么,为什么

    1
    2
    3
    4
    5
    6
    7
    >>> X = 'Spam'
    >>> def func():
    ... X = 'NI!'
    ...
    >>> func()

    >>> print(X)

    这里的输出也是'Spam',因为在函数中赋值变量会将其变成本地变量,从而隐藏了同名的全局变量

    print语句会找到没有发生改变的全局(模块)作用域中的变量

  3. 下面的代码会打印什么内容,为什么

    1
    2
    3
    4
    5
    6
    7
    8
    >>> X = 'Spam'
    >>> def func():
    ... X = 'NI!'
    ... print(X)
    ...
    >>> func()

    >>> print(X)

    这会在一行上打印'NI',在另一行打印'Spam',因为函数中引用的变量会找到其本地变量,而print中引用的变量会找到其全局变量

  4. 下面的代码会输出什么,为什么

    1
    2
    3
    4
    5
    6
    7
    8
    >>> X = 'Spam'
    >>> def func():
    ... global X
    ... X = 'NI'
    ...
    >>> func()

    >>> print(X)

    这次只打印了'NI',因为全局声明会强制函数中赋值的变量引用其所在的全局作用域中的变量

  5. 下面的代码会输出什么,为什么

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    >>> X = 'Spam'
    >>> def func():
    ... X = 'NI'
    ... def nested():
    ... print(X)
    ... nested()
    ...
    >>> func()

    >>> X

    这个例子的输出还是'NI'一行,而'Spam'在另一行,因为嵌套函数中的print语句会在所在的函数本地作用域中发现变量名,而末尾的print会在全局作用城中发现这个变量

  6. 这段代码在Python 3.0下会输出什么,为什么

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> def func():
    ... X = 'NI'
    ... def nested():
    ... nonlocal X
    ... X = 'Spam'
    ... nested()
    ... print(X)
    ...
    >>> func()

    这个示例打印出'Spam',因为nonlocal语句意味着在嵌套函数中对X赋值,以修改嵌套函数的本地作用域中的X

    没有这条语句,这个赋值将会把X当作是嵌套函数的本地变量,使它成为一个不同的变量,那么这段代码将会打印出'NI'

  7. 举出三种或四种Python函数中保存状态信息的方法

    尽管函数返回的时候本地变量的值已经不在了,我们可以使用共享的全局变量、嵌套函数内的嵌套函数作用域引用,或者使用默认参数值来让一个Python函数保持状态信息,函数属性有时候允许把状态附加到函数自身,而不是在作用域中查找,另一种替代方法是使用类来OOP,有时候比其他任何基于作用域的技术更好地支持状态保持,因为它使得属性赋值很明确


第十八章 参数

  • 学习Python中的参数传递的概念,即对象作为输入发送给函数的方式
  • 介绍关键字参数、默认参数和任意参数收集器等工具
  • 学习了可变参数如何表现出与其他的对象共享引用一样的行为

传递参数

函数传递参数时的关键点

  • 参数的传递是通过自动将对象赋值给本地变量名来实现的

    函数参数是调用者发送的的共享对象引用值

    引用是以指针的形式实现的,所有的参数都是通过指针进行传递的,作为参数被传递的对象从来不自动拷贝

  • 在函数内部的参数名的赋值不会影响调用者

    在函数运行时,在函数头部的参数名是一个新的、本地的变量名,这个变量名是函数的本地作用域内的

    函数参数名和调用者作用域中的变量名没有别名

  • 改变函数的可变对象参数的值也许会对调用者有影响

    因为参数是简单地赋值给传入的对象,函数能够就地改变传入的可变对象,其结果会影响调用者

    可变参数对于函数来说可以做输入和输出

Python通过赋值进行传递的机制

  • 不可变参数”通过值”进行传递

    像整数和字符串这样的对象是通过对象引用而不是拷贝进行传递的,但是因为不可能在原处改变不可变对象,实际的效果像创建了一份拷贝

  • 可变对象通过”指针”进行传递

    列表和字典这样的对象也通过对象引用进行传递,可变对象能够在函数内部进行原处的改变

参数和共享引用

对可变对象在原处的修改会影响其他引用了该对象的变量

1
2
3
4
5
6
7
>>> def f(a):       # a is assigned to (references) passed pbject
... a = 99 # Changes local variable a only
...
>>> b = 99
>>> f(b) # a and b both reference same initially
>>> print(b) # b is not changed
99

函数内部的参数只存在于调用的函数之中,对其赋值不会影响到函数调用作用域中的变量

函数内部的参数与函数调用中的参数共享传递的对象,对参数名进行重新赋值后不再为共享引用

1
2
3
4
5
6
7
8
9
>>> def changer(a, b):      # Arguments assigned references to objects
... a = 2 # Changes local name's value only
... b[0] = 'spam' # Changes shared object in-place
...
>>> X = 1
>>> L = [1, 2] # Caller
>>> changer(X, L) # Pass immutable and mutable object
>>> X, L # X is unchanged,L is different!
(1, ['spam', 2])

当参数传递可变对象时,对对象的原处修改可能在函数退出后依然有效,并由此影响到调用者

  • 函数作用域内的本地变量名赋值对函数调用者没有影响,它仅仅把本地变量修改为引用另一个对象,并没有改变调用者作用域中的变量名的引用
  • 若函数作用域内的本地变量名引用了一个可变对象,对对象原处修改的结果会在函数返回后影响调用者作用域中的变量名的引用
1
2
3
4
5
6
7
8
9
10
>>> X = 1
>>> a = X # They share the same object
>>> a = 2 # Resets 'a' only,'X' is still 1
>>> print(X)
1
>>> L = [1, 2]
>>> b = L # They share the same object
>>> b[0] = 'spam' # In-place change:'L' sees the change too
>>> print(L)
['spam', 2]

自动对传入的参数进行赋值的效果与运行一系列简单的赋值语句是相同的

避免可变参数的修改

对可变参数的原处修改的行为是参数传递在Python中工作的方式

在Python中,默认通过引用(即指针)进行函数的参数传递实现了不需要创建多个拷贝就可以在程序中传递很大的对象,并且能够按照需要方便地更新这些对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> def changer(a, b):
... a = 2
... b [0] = 'spam'
...
>>> X = 1
>>> L = [1, 2]
>>> changer(X, L[:]) # Pass a copy,so our 'L' does not change
>>> def changer(a, b):
... b = b[:] # Copy input list so we don't impact caller
... a = 2
... b[0] = 'spam' # Changes our list copy only
...
>>> L = [1, 2]
>>> changer(X, tuple(L)) # Pass a tuple,so changes are errors
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in changer
TypeError: 'tuple' object does not support item assignment

如果不信函数内部在原处的修改影响传递给它的对象,可以简单地创建一个明确的可变对象的拷贝或者可以在函数内部进行拷贝

可以将可变对象转换为不可变对象来杜绝参数在原处修改,但是这种办法会让函数失去了调用原来对象的特定方法的能力

函数能够升级为传入可变对象的形式

在原处修改可以只修改定义良好的API的一部分而不是产生副本来修改

对参数输出进行模拟

return语句能够返回任意种类的值,所以能够将其封装进一个元组或其他的集合类型返回多个值

1
2
3
4
5
6
7
8
9
10
>>> def multiple(x, t):
... x = 2 # Changes local names only
... y = [3, 4]
... return x, y # Return new values in a tuple
...
>>> X = 1
>>> L = [1, 2]
>>> X, L = multiple(X, L) # Assign results to caller's names
>>> X, L
(2, [3, 4])

可以返回元组并将结果解包,赋值给调用者的参数变量名来改变调用者的参数的值

在Python 3.0中,def语句头部不允许元组解包参数,而循环语句则支持


特定的参数匹配模型

Python提供了改变调用过程中传入的变量引用与函数头部参数名匹配优先级的工具来编写支持更复杂的调用模式的函数

默认情况下,参数通过其位置从左至右进行匹配,参数和函数头部参数名必须一样多

参数还能通过定义变量名进行匹配,默认参数值以及对于额外参数的容器匹配

基础知识

匹配模型是可选的,必须根据变量名匹配对象,匹配完成后在传递机制的底层依然是赋值

匹配的模型

  • 位置:从左至右进行匹配 一-般情况下是通过位置按照从左到右的顺序把参数值传递给函数头部的参数名进行匹配

  • 关键字参数:通过参数名进行匹配 通过在调用时使用参数的变量名name=value语法定义哪个函数接受值

  • 默认参数:为没有传入值的参数定义参数值 如果调用时传入的值过少的话,函数使用语法name=value能够为参数定义接受的默认值

  • 可变参数:收集任意多基于位置或关键字的参数 函数能够使用特定的参数,它们是以字符*开头,收集任意多的额外参数,这个特性叫做可变参数

  • 可变参数解包:传递任意多的基于位置或关键字的参数 调用者能够再使用*语法将参数集合打散,分成参数

  • Keyword-only参数:参数必须按照名称传递 在Python 3.0中,函数也可以指定参数,参数必须用带有关键参数的名字(而不是位置)来传递

    这样的参数通常用来定义实际参数以外的配置选项

匹配语法

特定的参数匹配模式可以自由地确认有多少参数是必须传递给函数的

语法 位置 解释
func(value) 调用者 常规参数:通过位置进行匹配
func(name=value) 调用者 关键字参数:通过变量名匹配
func(*sequence) 调用者 以name传递所有的对象,并作为独立的基于位置的参数
func(**dict) 调用者 以name成对的传递所有的关键字/值,并作为独立的关键字参数
def func(name) 函数 常规参数:通过位置或变量名进行匹配
def func(name=value) 函数 没有在调用中传递时的默认参数值
def func(*name) 函数 匹配并收集(在元组中)所有包含位置的参数
def func(**name) 函数 匹配并收集(在字典中)所有包含位置的参数
def func(*args,name) 函数 参数必须在调用中按照关键字传递
def func(*, name=value)

函数调用和定义的匹配模式

  • 在函数的调用中
    • 简单的通过变量名位置进行匹配
    • 关键字参数使用name=value的形式告诉Python依照变量名进行匹配
    • 在调用中使用*sequence或者**dict允许我们在一个序列或字典中相应地封装任意多的位置相关或者关键字的对象,并且在将它们传递给函数的时候,将它们解包为分开的、单个的参数。
  • 在函数的头部
    • 一个简单的变量名是通过位置或变量名进行匹配的
    • name=value的形式定义了默认的参数值
    • *name的形式收集了任意的额外不匹配的参数到元组中
    • **name的形式将会收集额外的关键字参数到字典之中
    • 跟在*name或一个单独的*之后的、任何正式的或默认的参数名称,都是keyword-only参数,并且必须在调用中按照关键字传递

关键字参数和默认参数

  • 关键字参数允许使用其变量名去标记参数,让调用变得更有意义
  • 默认参数允许创建任意可选的参数,并在函数定义中提供了默认值
  • 函数头部的默认参数和调用中的关键字组合允许挑选要覆盖哪些默认参数

细节

Python使用并混合特定的参数匹配模式遵循的顺序

  • 在函数调用中,参数的顺序为:任何位置参数(value)、关键字参数(name=value)、*sequence形式的组合、**dict形式
  • 在函数头部,参数的顺序为:任何一般参数(name)、任何默认参数(name=value)、*name形式、namename=value的keyword-only参数、**name形式

Python内部参数匹配的步骤

  1. 通过位置分配非关键字参数
  2. 通过匹配变量名分配关键字参数
  3. 其他额外的非关键字参数分配到*name元组中
  4. 其他额外的关键字参数分配到**name字典中
  5. 用默认值分配给在头部未得到分配的参数

匹配模式后检测每个参数是否只传入了一个值,之后传递给参数名的对象赋值

关键字参数和默认参数的实例

1
2
3
4
>>> def f(a, b, c): print(a, b, c)
...
>>> f(1, 2, 3)
1 2 3

没有使用特殊的匹配语法时Python默认会通过为止从左至右匹配变量名

关键字参数

关键字参数在调用中起到了数据标签的作用并且可以与默认值参数配合使用

1
2
3
4
5
6
>>> def f(a, b, c): print(a, b, c)
...
>>> f(c=3, b=2, a=1)
1 2 3
>>> f(1, c=3, b=2)
1 2 3

关键字参数允许通过变量名进行匹配,Python将函数调用的变量名匹配并将值传递给函数定义头部的变量名

使用关键字参数时蚕食是通过变量名而不是根据位置进行传递进行传递的

混合使用基于位置的参数和基于关键字的参数时先将基于位置的参数按照从左至右顺序匹配头部的参数,之后再进行基于变量名进行关键字的匹配

默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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

默认参数允许创建函数可选的参数,如果没有传入值的话,参数在函数运行前被赋予默认值

传入参数时默认按照从左至右的位置顺序进行匹配,使用关键字参数可以跳过默认值参数

关键字参数和默认参数的混合

name=value的形式在调用时代表关键字参数,def头部中代表默认值参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def func(spam, eggs, toast=0, ham=0):   # First 2 required
print((spam, eggs, toast, ham))

func(1, 2)
func(1, ham=1, eggs=0)
func(spam=1, eggs=0)
func(toast=1, eggs=2, spam=3)
func(1, 2, 3, 4)

(1, 2, 0, 0)
(1, 0, 0, 1)
(1, 0, 0, 0)
(3, 2, 1, 0)
(1, 2, 3, 4)
[Finished in 0.6s]

当关键字参数在调用过程中使用时,参数排列的位置并没有关系,Python通过变量名而不是位置进行匹配

任意参数的实例

***让函数支持接受任意数目的参数,可以出现在函数定义或是函数调用中

收集参数

1
2
3
4
5
6
7
8
>>> def f(*args): print(args)
...
>>> f()
()
>>> f(1)
(1,)
>>> f(1, 2, 3, 4)
(1, 2, 3, 4)

在函数定义中,*将所有位置相关的参数传递给一个新的元组

参数可以索引或在一个for循环中步进

1
2
3
4
5
6
>>> def f(**args): print(args)
...
>>> f()
{}
>>> f(a=1, b=2)
{'a': 1, 'b': 2}

在函数定义中,**将所有关键字参数传递给一个新的字典

参数能够通过一般的字典工具进行处理

1
2
3
4
>>> def f(a, *pargs, **kargs): print(a, pargs,kargs)
...
>>> f(1, 2, 3, x=1, y=2)
1 (2, 3) {'x': 1, 'y': 2}

函数头部能够混合一般参数、*参数以及**去实现更灵活的调用方式

解包参数

解包参数在不能预测将要传入的函数的参数的数量时候很方便

*接受任何可迭代对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> 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
>>> func(1, *(2, 3), d=4)
1 2 3 4
>>> f(1, *(2,), c=3, **{'d':4})
1 (2,) {'c': 3, 'd': 4}
>>> func(1, *(2,), c=3, **{'d':4})
1 2 3 4

在调用函数时使用*语法会解包参数的集合而不是创建参数

在 调用函数时使用**会以键/值对的形式解包一个字典使其成为独立的参数

在调用中可以混合普通的参数、基于位置的参数以及关键字参数

应用函数通用性

1
2
3
4
5
6
if <test>:
action,args = func1, (1,) # Call func1 with 1 arg in this case
else:
action, args = func2, (1, 2, 3) # Call func2 with 3 args here
...
action(*args) # Dispatch generically

解包参数可以在编写一段脚本之前不知道一个函数调用需要多少参数

1
2
3
4
5
6
7
8
9
10
11
12
def tracer(func, *pargs, **kargs):      # Accept arbitrary arguments
print('calling:', func.__name__)
return func(*pargs, **kargs) # Pass along arbitrary arguments

def func(a, b, c, d):
return a + b + c + d

print(tracer(func, 1, 2, c=3, d=4))

calling: func
10
[Finished in 0.1s]

当编写脚本时无法直接编写一个函数调用,可以用序列操作构建一个参数列表,并解包参数以调用它

废弃的apply内置函数(Python 2.6)

Python 2.6内置函数apply可以实现*args**args调用语法的效果

1
2
3
> func(*pargs, **kargs)
> apply(func, pargs, kargs)
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Python 3.0

>>> def echo(*args, **kwargs): print(args, kwargs)
...
>>> echo(1, 2, a=3, b=4)
(1, 2) {'a': 3, 'b': 4}

# Python 2.6

>>> def echo(*args, **kwargs): print(args, kwargs)
...
>>> pargs = (1, 2)
>>> kargs = {'a':3, 'b':4}
>>> apply(echo, pargs, kargs)
((1, 2), {'a': 3, 'b': 4})

解包调用语法形式比apply函数新,与def头部的*pargs**kargs收集器对称且可以传递额外的参数

Python 3.0 keyword-Only参数

keyword-only参数必须按照关键字而不是位置传递参数

1
2
3
4
5
6
7
8
9
10
11
>>> def kwonly(a, *b, c):
... print(a, b, c)
...
>>> kwonly(1, 2, c=3)
1 (2,) 3
>>> kwonly(1, c=3)
1 () 3
>>> kwonly(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: kwonly() missing 1 required keyword-only argument: 'c'

keyword-only参数为关键字参数,必须使用关键字语法传递,出现在参数列表中*args之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def kwonly(a, *, b, c,):
... print(a, b, c)
...
>>> kwonly(1, c=3, b=2)
1 2 3
>>> kwonly(c=3, b=2, a=1)
1 2 3
>>> kwonly(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: kwonly() takes 1 positional argument but 3 were given
>>> kwonly(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: kwonly() missing 2 required keyword-only arguments: 'b' and 'c'

在参数列表中使用*字符表示跟在*后面的所有参数都作为关键字传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def kwonly(a, *, b=1, c, d=2):
... print(a, b, c, d)
...
>>> kwonly(3, c=4)
3 1 4 2
>>> kwonly(3, c=4, b=5)
3 5 4 2
>>> kwonly(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: kwonly() missing 1 required keyword-only argument: 'c'
>>> kwonly(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: kwonly() takes 1 positional argument but 3 were given

keyword-only参数可以使用默认值,没有默认值的keyword-only参数必须使用关键字传递

排序规则

1
2
3
4
5
6
7
8
9
10
>>> def kwonly(a, **pargs, b, c):
File "<stdin>", line 1
def kwonly(a, **pargs, b, c):
^
SyntaxError: invalid syntax
>>> def kwonly(a, **, b, c):
File "<stdin>", line 1
def kwonly(a, **, b, c):
^
SyntaxError: invalid syntax

函数定义keyword-only参数不能出现在**args后面,一个**不能独自出现在参数列表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> def f(a, *b, **d, c=6): print(a, b, c, d)   # Keywrod-only before **!
File "<stdin>", line 1
def f(a, *b, **d, c=6): print(a, b, c, d)
^
SyntaxError: invalid syntax
>>> def f(a, *b, c=6, **d): print(a, b, c, d) # Collect args in header
...
>>> f(1, 2, 3, x=4, y=5) # Default used
1 (2, 3) 6 {'x': 4, 'y': 5}
>>> f(1, 2, 3, x=4, y=5, c=7) # Override default
1 (2, 3) 7 {'x': 4, 'y': 5}
>>> f(1, 2, 3, c=7, x=4, y=5) # Anywhere in keywords
1 (2, 3) 7 {'x': 4, 'y': 5}
>>> def f(a, c=6, *b, **d): print(a, b, c, d) # c is not keyword-only
...
>>> f(1, 2, 3, x=4)
1 (3,) 2 {'x': 4}

函数定义keyword-only参数必须编写在**args任意关键字形式之前,*args任意位置形式之后

1
2
3
4
5
6
7
8
9
10
11
12
>>> def f(a, *b, c=6, **d): print(a, b, c, d)   # KW-only between * and **
...
>>> f(1, *(2, 3), **dict(x=4, y=5)) # Unpack args at call
1 (2, 3) 6 {'x': 4, 'y': 5}
>>> f(1, *(2, 3), **dict(x=4, y=5), c=7) # Keywords before **args
1 (2, 3) 7 {'x': 4, 'y': 5}
>>> f(1, *(2, 3), c=7, **dict(x=4, y=5)) # Override default
1 (2, 3) 7 {'x': 4, 'y': 5}
>>> f(1, c=7, *(2, 3), **dict(x=4, y=5)) # After or before *
1 (2, 3) 7 {'x': 4, 'y': 5}
>>> f(1, *(2, 3), **dict(x=4, y=5, c=7)) # Keyword-only in **
1 (2, 3) 7 {'x': 4, 'y': 5}

函数调用keyword-only可以出现在**args之前之后或包含在**args之中,可以出现在*args之前或之后

为何使用keyword-only参数

keyword-only使得很容易允许一个函数既接受任意多个要处理的位置参数,也接受作为关键字传递的配置选项

1
2
process(X, Y, Z)            # use flag's default
process(X, Y, notify=True) # override flag default

没有keyword-only参数则只能提供默认值并验证没有传递多余的关键字

min调用

编写一个函数计算任意参数集合和任意对象数据类型集合中的最小值,要求函数接受零个或多个参数,能够使用所有的Python对象类型

  • 第一个要求可以使用*将参数收集到一个元组中,通过简单的loop依次步进处理每一个参数
  • 第二个要求可以不管类型进行简单地比较,让Python执行正确的比较

满分

操作方法

  • 第一个函数获取了第一个参数(args是一个元组),并且使用分片去掉第一个得到了剩余的参数(一个对象同自己比较是没有意义的,特别是这个对象是一个较大的结构时)
  • 第二个版本让Python自动获取第一个参数以及其余的参数,因此避免了进行一次索引和分片
  • 第三个版本通过对内置函数list的调用让一个元组转换为一个列表,之后调用list内置的sort方法来实现比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def min1(*args):
res = args[0]
for arg in args[1:]
if arg < res:
res = arg
return res

def min2(first, *rsst):
for arg in rest:
if arg < first:
first = arg
return first

def min3(*args):
tmp = list(args)
tmp.sort()
return tmp[0]

print(min1(3, 4, 1, 2))
print(min2("bb", "aa"))
print(min3([2, 2], [1, 1], [3, 3]))

1
aa
[1, 1]
[Finished in 0.2s]

三种解决办法在运行时产生了相同效果

在没有参数传入时三种方法都会抛出异常

加分点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 # See also:lambda
def grtrthan(x, y): return x > y

print(minmax(lessthan, 4, 2, 1, 5, 6, 3)) # Self-test code
print(minmax(grtrthan, 4, 2, 1, 5, 6, 3))

1
6
[Finished in 0.1s]

使用当的函数计算最大值或最小值需要评估对比表达式,如内置函数eval,或者传入一个任意的比较函数

结论

min函数与max函数都是Python内置函数


一个更有用的例子:通用set函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def intersect(*args):
res = []
for x in args[0]: # Scan first sequence
for other in args[1:]: # For all other args
if x not in other: # Item in each one
break # No:break out of loop
else: # Yes:add items to end
res.append(x)
return res

def union(*args):
res = []
for seq in args: # For all args
for x in seq: # For all nodes
if not x in res:
res.append(x) # Add new items to result
return res

>>> from inter2 import intersect, union
>>> s1, s2, s3 = "SPAM", "SCAM", "SLSM"
>>> intersect(s1, s3), union(s1, s2)
(['S', 'A', 'M'], ['S', 'P', 'A', 'M', 'C'])
>>> intersect([1, 2, 3], (1, 4)) # Two operands
[1]
>>> intersect(s1, s2, s3) # Mixed types
['S', 'A', 'M']
>>> union(s1, s2, s3) # Three operands
['S', 'P', 'A', 'M', 'C', 'L']

这个函数能对任意数目的序列进行公共部分挑选的函数,通过使用可变参数的匹配形式*args去收集传入的参数

参数在调用时作为元组args传入


模拟Python 3.0 print函数

通过from __future__ import print_function导入Python 3.0的print函数

使用*args任意位置元组以及**args任意关键字参数字典可以模拟Python 3.0 print函数所做的大多数工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
"""
Emlate most of the 3.0 print function for use in 2.X
call signature: print30(*args, sep=' ', end='\n', file=None)
"""
import sys

def print30(*args, **kargs):
sep = kargs.pop('sep', ' ') # Keyword are default
end = kargs.pop('end', '\n')
file = kargs.pop('file', sys.stdout)
if kargs: raise TypeError('extra keywords: %s' % kargs)
output = ''
first = True
for arg in args:
output += ('' if first else sep) + str(arg)
first = False
file.write(output + end)

print30(1, 2, 3)
print30(1, 2, 3, sep='') # Suppress separator
print30(1, 2, 3, sep='...')
print30(1, [2], (3,), sep='...') # Various object types

print30(4, 5, 6, sep='', end='') # Suppress newline
print30(7, 8, 9)
print30() # Add newline (or blank line)

import sys
print30(1, 2, 3, sep='??', end='.\n', file=sys.stderr) # Redirect to file

'''
1 2 3
123
1??2??3.
1...2...3
1...[2]...(3,)
4567 8 9

1??2??3.
[Finished in 0.2s]
'''

使用Keyword-Only参数

使用keyword-only参数编写可以自动验证配置文件

1
2
3
4
5
6
7
8
9
# Use keyword-only args

def print30(*args, sep=' ', end='\n', file=sys.stdout):
output = ''
first = True
for arg in args:
output += ('' if first else sep) + str(arg)
first = False
file.write(output + end)

keyword-only参数可以简化一类既接受参数又接受选项的函数

keyword-only是可选参数,但是会忽略掉额外的参数

为什么要在意:关键字参数

高级参数匹配模式更复杂,且完全是可选的

由于一些Python工具使用高级参数匹配模式,了解这些模式的常识是很重要的


本章习题

  1. 如下代码的输出是什么,为什么

    1
    2
    3
    4
    >>> def func(a, b=4, c=5):
    ... print(a, b, c)
    ...
    >>> func(1, 2)

    这里的输出是125,因为1和2按照位置传递给了a和b,并且c在调用中被忽略了,默认为5

  2. 如下代码的输出是什么,为什么

    1
    2
    3
    4
    >>> def func(a, b, c=5):
    ... print(a, b, c)
    ...
    >>> func(1, c=3, b=2)

    这次的输出是123:1按照位置传递给a,2和3按照名称传递给b和c(当像这样使用关键字参数的时候,从左到右的顺序无关紧要)

  3. 如下代码的输出是什么,为什么

    1
    2
    3
    4
    >>> def func(a, *pargs):
    ... print(a, pargs)
    ...
    >>> func(1, 2, 3)

    这段代码打印出1 (2,3),因为1传递给a,*pargs把其他的位置参数收集到一个新的元组对象中

    可以用任何迭代工具来步进任何的额外的位置参数元组(例如,for arg in pargs:)

  4. 如下代码打印出什么,为什么

    1
    2
    3
    4
    >>> def func(a, **kargs):
    ... print(a, kargs)
    ...
    >>> func(a=1, c=3, b=2)

    代码打印出1, {'c': 3,'b': 2},因为1按照名称传递给a,**kargs把其他关键字参数收集到一个字典中

    可以用任何迭代工具来步进任何额外的关键字参数字典(例如,for key in kargs: )

  5. 最后一次运行时,如下代码的输出是什么,为什么

    1
    2
    3
    >>> def func(a, b, c=3, d=4): print(a, b, c, d)
    ...
    >>> func(1, *(5, 6))

    这里的输出是1564:1按照位置匹配a,5和6按照*name位置匹配b和c(6覆盖了c的默认值),并且d默认为4,因为它没有传递一个值

  6. 举出三种以上函数和调用者能够交流结果的方法

    函数可以用return语句、修改传入的可变参数以及通过设置全局变量来返回其结果

    全局变量一般都很少应用,因为这会让代码难以理解和使用

    return语句通常是最好的选择,但是在有准备的情况下,修改可变对象也是可以的

    函数也可以和系统组件进行通信,例如文件和套接字


第十九章 函数的高级话题

  • 介绍递归函数、函数属性和注解、lambda表达式、mapfilterreduce等函数式编程工具以及通用的函数设计思想

函数设计概念

应该竭力使函数和其他编程组件中的外部依赖性最小化,函数的自包含性越好,它越容易被理解、复用和修改

  • 耦合性:对于输入使用参数并且对于输出使用return语句

    一般来讲,需要力求让函数独立于它外部的东西,参数return语句通常就是隔离对代码中少数醒目位置的外部的依赖关系的最好办法

  • 耦合性:只有在真正必要的情况下使用全局变量

    全局变量引发了依赖关系和计时的问题,会导致程序调试和修改的困难

  • 耦合性:不要改变可变类型的参数,除非调用者希望这样做

    函数会改变传入的可变类型对象,会导致很多调用者和被调用者之间的耦合性,这种耦合性会导致一个函数过于特殊和不友好

    Python的类依赖于修改传入的可变对象:类的函数会自动设置传入参数self的属性从而修改每个对象的状态信息

  • 聚合性:每一个函数都应该有一个单一的、统一的目标

    在设计完美的情况下,每个函数中都应该做一件事

    一个函数中把所有步骤都混合在一起的代码很难重用

  • 大小:每一个函数应该相对较小

    Python代码是以简单明了而著称,一个过长或者有着深层嵌套的函数往往就成为设计缺陷的征兆

  • 耦合:避免直接改变在另一个模块文件中的变量

    在文件间改变变量会导致模块文件间的耦合性,就像全局变量产生了函数间的耦合一样:模块难于理解和重用,在可能的时候使用读取函数,而不是直接进行赋值语句


递归函数

递归函数是直接或间接地调用自身以进行循环的函数

递归允许程序遍历拥有任意的、不可预知的形状的结构,可以替换简单循环和迭代

用递归求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def mysum(L):
... print(L) # Trace recursive levels
... if not L: # L shorter at each level
... return 0
... else:
... return L[0] + mysum(L[1:])
...
>>> mysum([1, 2, 3, 4, 5])
[1, 2, 3, 4, 5]
[2, 3, 4, 5]
[3, 4, 5]
[4, 5]
[5]
[]
15

在每一层,这个函数都递归地调用自己来计算列表剩余的值的和,这个和随后加到前面的一项中,当列表变为空的时候,递归循环结束并返回0

使用递归的时候,对函数调用的每一个打开的层级在运行时调用堆栈上都有自己的一个函数本地作用域的副本

1
2
3
4
5
6
7
8
9
>>> def mysum(L):
... if not L: return 0
... return nonempty(L) # Call a function that calls me
...
>>> def nonempty(L):
... return L[0] + mysum(L[1:]) # Indirectly recursive
...
>>> mysum([1.1, 2.2, 3.3, 4.4])
11.0

递归可以是直接的也可以是间接的

编码替代方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def mysum(L):
return 0 if not L else L[0] + mysum(L[1:]) # Use ternary expression

def mysum(L):
return L[0] if len(L) == 1 else L[0] + mysum(L[1:]) # Any type,assume one

def mysum(L):
first, *rest = L
return first if not rest else first + mysum(rest) # Use 3.0 ext seq assign

>>> mysum([1]) # mysum([]) fails in last 2
1
>>> mysum([1, 2, 3, 4, 5])
15
>>> mysum(('s', 'p', 'a', 'm')) # But various types now work
'spam'
>>> mysum(['spam', 'ham', 'eggs'])
'spamhameggs'

可以使用Python的三元if/else表达式替代某些代码

可以针对任何可加和的类型一般化

可以使用拓展序列赋值解包

循环语句VS递归

循环语句不需要在调用堆栈上针对每次迭代都有一个本地作用域的副本,避免一般会与函数调用相关的速度成本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> L = [1, 2, 3, 4, 5]
>>> sum = 0
>>> while L:
... sum += L[0]
... L = L[1:]
...
>>> sum
15

>>> L = [1, 2, 3, 4, 5]
>>> sum = 0
>>> for x in L: sum += x
...
>>> sum
15

while使得事情更为具体,并且不需要定义一个支持递归调用的函数

for循环自动迭代,且比递归在内存空间和执行时间方面效率更高

处理任意结构

递归(或者对等的显式的基于堆栈的算法)可以要求遍历任意形状的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def sumtree(L):
tot = 0
for x in L: # For each item at this level
if not isinstance(x, list):
tot += x # Add numbers directly
else:
tot += sumtree(x) # Recur for sublists
return tot

L = [1, [2, [3, 4], 5], 6, [7, 8]] # Arbitrary nesting
print(sumtree(L)) # Prints 36

# Pathological cases

print(sumtree([1, [2, [3, [4, [5]]]]])) # Prints 15(right-heavy)
print(sumtree([[[[[1], 2], 3], 4], 5])) # Prints 15(left-heavy)

简单的循环语句没有作用,因为这不是线性迭代

嵌套的循环语句也没有作用,因为子列表可能嵌套到任意的深度并且以任意的形式嵌套


函数对象:属性和注解

Python函数是对象,自身全部存储在内存块中

函数可以跨程序自由地传递和间接调用

函数支持与调用无关的属性存储和注解操作

间接函数调用

函数对象可以赋值给函数名、传递给其他函数、嵌入到数据结构、从一个函数返回给另一个函数等等

函数可以由一个函数表达式后面的括号中的列表参数调用

1
2
3
4
5
6
7
8
>>> def echo(message):      # Name echo assigned to function object
... print(message)
...
>>> echo('Direct call') # Call object through original name
Direct call
>>> x = echo # Now x references the function too
>>> x('Indirect call!') # Call object through name by adding()
Indirect call!

def语句将函数名赋值为一个函数对象的引用,可以把这个函数对象赋值给其他的变量名并通过变量名调用

1
2
3
4
5
>>> def indirect(func, arg):            # Call the passed-in object by adding ()
... func(arg)
...
>>> indirect(echo, 'Argument call!') # Pass the function to another function
Argument call!

被调用者可以把函数作为参数添加到括号中并调用传入的函数

1
2
3
4
5
6
>>> schedule = [(echo, 'Spam!'), (echo, 'Ham!')]
>>> for (func, arg) in schedule:
... func(arg) # Call functions embedded in containers
...
Spam!
Ham!

可以把函数对象的内容填入到数据结构中并使用

1
2
3
4
5
6
7
8
9
10
>>> def make(label):            # Make a function but don't call it
... def echo(message):
... print(label + ':' + message)
... return echo
...
>>> F = make('Spam') # Label in enclosing scope is retained
>>> F('Ham!') # Call the function that make retkr
Spam:Ham!
>>> F('Eggs!')
Spam:Eggs!

函数可以创建并返回以便之后使用

函数内省

函数是对象,可以用常规的读写工具来处理函数

1
2
3
4
5
6
7
8
9
10
>>> def func(a):
... b = 'spam'
... return b * a
...
>>> func(8)
'spamspamspamspamspamspamspamspam'
>>> func.__name__
'func'
>>> dir(func)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

调用表达式是定义在函数对象上的一个操作

可以通用地检查函数的属性

1
2
3
4
5
6
7
8
>>> func.__code__
<code object func at 0x1041f4930, file "<stdin>", line 1>
>>> dir(func.__code__)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> func.__code__.co_varnames
('a', 'b')
>>> func.__code__.co_argcount
1

内省工具可以探索函数实现细节

函数附加的代码对象提供了函数的本地变量和参数等方面细节

函数属性

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def func(a):
... b = 'spam'
... return b * a
...
>>> func.count = 0
>>> func.count += 1
>>> func.count
1
>>> func.handles = 'Button-Press'
>>> func.handles
'Button-Press'
>>> dir(func)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'handles']

可以向函数附加任意的用户定义的属性,直接把状态信息附加到函数对象,而不必使用全局、非本地和类等其他技术

属性与对象相关而不是与作用域相关,函数退出后仍然保留

Python 3.0中的函数注解

Python 3.0中可以给函数对象附加注解信息,即与函数和结果相关的任意的用户定义的数据

声明注解信息是可选的,并且附加到函数对象的__annotations__属性

注解只在def语句中有效,在lambda表达式无效

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def func(a, b, c):
... return a + b + c
...
>>> func(1, 2, 3)
6

>>> def func(a: 'spam', b: (1, 10), c: float) -> int:
... return a + b + c
...
>>> func(1, 2, 3)
6
>>> func.__annotations__
{'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}

函数注解编写在def头部,使函数头部语法通用化

对于参数,函数注解出现在紧随参数名之后的:之后

对于返回值,函数注解出现在参数列表之后的一个->之后

当注解出现的时候,Python将他们收集到注解键为参数名或return的字典中并且将它们附加给函数对象自身

1
2
3
4
5
6
7
8
9
10
11
12
>>> def func(a: 'spam', b, c: 99):
... return a + b + c
...
>>> func(1, 2, 3)
6
>>> func.__annotations__
{'a': 'spam', 'c': 99}
>>> for arg in func.__annotations__:
... print(arg, '=>', func.__annotations__[arg])
...
a => spam
c => 99

注解是附加到一个Python对象的Python对象,可以直接处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> def func(a: 'spam' = 4, b: (1, 10) = 5, c: float = 6) -> int:
... return a + b + c
...
>>> func(1, 2, 3)
6
>>> func() # 4 + 5 + 6(all default)
15
>>> func(1, c=10) # 1 + 5 + 10(keywords work normally)
16
>>> func.__annotations__
{'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}

>>> def func(a:'spam'=4, b:(1, 10)=5, c:float=6)->int:
... return a + b + c
...
>>> func(1, 2)
9
>>> func.__annotations__
{'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}

编写注解仍然可以使用默认值,注解出现在默认值之前

参数、默认值及注解之间的空格是可选的,省略可能会提高代码的可读性


匿名函数:lambda

Python提供了生成函数对象表达式形式lambda,这个表达式返回一个函数而不是赋值给一个变量名

lambda表达式

1
lambda`表达式的形式为`lambda argument1, argument2, ..., argumentN: expression using arguments
  • lambda是一个表达式, 而不是一个语句

    lambda能够出现在Python语法不允许def出现的地方,如在一个列表常量中或者函数调用的参数中

    作为一个表达式,lambda返回一个函数,可以选择性地赋值给一个变量名,def语句总是得在头部将一个函数赋值给一个变量名,而不是将这个函数作为结果返回

  • lambda的主体是一个单个的表达式,而不是一个代码块

    这个lambda的主体就像放在def主体的return语句中的代码一样,简单地将结果写成一个顺畅的表达式,而不是明确的返回

    因为仅限于表达式,lambda通常要比def功能要小,仅能够在lambda主体中封装有限的逻辑进去,限制了程序的嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> def func(x, y, z): return x + y + z
...
>>> func(2, 3, 4)
9

>>> f = lambda x, y, z: x + y + z
>>> f(2, 3, 4)
9

>>> x = (lambda a="fee", b="fie", c="foe": a + b + c)
>>> x("wee")
'weefiefoe'

>>> def knights():
... title = 'Sir'
... action = (lambda x: title + ' ' + x) # Title in enclosing def
... return action # Return a function
...
>>> act = knights()
>>> act('robin')
'Sir robin'

deflambda能够做同样种类的工作

lambda表达式创建函数可以通过明确地将结果赋值给一个变量名后通过这个变量名调用函数

lambda表达式参数中可以使用默认参数

lambda主体中的代码遵循作用域查找法则,自动从上层函数中、模块中以及内置作用域中查找变量

为什么使用lambda

lambda表达式允许在使用的代码内嵌入一个函数的定义,在仅需要嵌入小段可执行代码的情况下会带来一个更简洁的代码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
L = [lambda x: x ** 2,      # Inline function definition
lambda x: x ** 3,
lambda x: x ** 4] # A list of 3 callable functions

for f in L:
print(f(2)) # Prints 4, 8, 16

print(L[0](3)) # Prints 9

'''
4
8
16
9
[Finished in 0.2s]
'''

def f1(x): return x ** 2
def f2(x): return x ** 3 # Define named functions
def f3(x): return x ** 4

L = [f1, f2, f3] # Reference by name

for f in L:
print(f(2)) # Prints 4,8,16

print(L[0](3)) # Prints 9

'''
4
8
16
9
[Finished in 0.2s]
'''

lambda用来编写跳转表,能够按照需要执行相应的动作

lambda表达式可以把小段的可执行代码编写进def语句从语法上不能编写进的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> key = 'got'
>>> {'already': (lambda: 2 + 2),
... 'got': (lambda: 2 * 4),
... 'one': (lambda: 2 ** 6)}[key]()
8

>>> def f1(): return 2 + 2
...
>>> def f2(): return 2 * 4
...
>>> def f3(): return 2 ** 6
...
>>> key = 'one'
>>> {'already': f1, 'got': f2, 'one': f3}[key]()
64

可以用Python中的字典或者其他的数据结构来构建更多种类的行为表,提供了在单个情况出现的函数

Python创建字典时,每个嵌套的lambda都生成并留下了一个在之后能够调用的函数,通过键索引来取回其中一个函数,而()使被取出的函数被调用

如何(不要)让Python代码变得晦涩难懂

在Python中可以基于表达式编写足够多的语句

1
2
3
4
5
>>> lower = (lambda x, y: x if x < y else y)
>>> lower('bb', 'aa')
'aa'
>>> lower('aa', 'bb')
'aa'

lambda中嵌套逻辑结构可以使用if/else三元表达式

1
2
3
4
5
6
7
8
9
10
11
12
>>> import sys
>>> showall = lambda x: list(map(sys.stdout.write, x))
>>> t = showall(['spam\n', 'toast\n', 'eggs\n'])
spam
toast
eggs
>>> 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

lambda中执行循环可以嵌入map调用或列表解析表达式

嵌套lambda和作用域

lambda可以在嵌套函数作用域中查找变量

1
2
3
4
5
6
7
8
>>> def action(x):
... return (lambda y: x + y) # Make and return function.remember x
...
>>> act = action(99)
>>> act
<function action.<locals>.<lambda> at 0x104708378>
>>> act(2) # Call what action returned
101

lambda出现在def中,并且在上层函数调用的时候,嵌套的lambda能够获取到在上层函数作用域中变量名的值

1
2
3
4
5
6
>>> action = (lambda x: (lambda y: x + y))
>>> act = action(99)
>>> act(3)
102
>>> ((lambda x: (lambda y: x + y))(99))(4)
103

lambda能够获取任意上层lambda中的变量名

为什么要在意:回调

lambda为Python的tkinter GUI API定义行内的回调函数

1
2
3
4
5
6
7
import sys
from tkinter import Button, mainloop # Tkinter in 2.6
x = Button(
text='Press me',
command=(lambda:sys.stdout.write('Spam\n')))
x.pack()
mainloop()

会跳出力气通过传递一个用lambda所产生的函数作为command的关键字参数

1
2
3
4
5
class MyGui:
def makewidgets(self):
Button(command=(lambda: self.onPress("spam")))
def onPress(self, message):
...use message...

嵌套的函数作用域法则使回调处理器可以自动查找编写时所在的函数中的变量名


在序列中映射函数:map

map函数会对一个序列对象中的每一个元素应用被传入的函数,并且返回一个包含了所有函数调用结果的一个可迭代对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> counters = [1, 2, 3, 4]
>>> updated = []
>>> for x in counters:
... updated.append(x + 10) # Add 10 to each item
...
>>> updated
[11, 12, 13, 14]

>>> def inc(x): return x + 10 # Function to be run
...
>>> list(map(inc, counters)) # Collect results
[11, 12, 13, 14]
>>> list(map((lambda x: x + 3), counters)) # Function expression
[4, 5, 6, 7]

map函数期待一个函数,可以传入lambda表达式

1
2
3
4
5
6
7
8
9
10
11
>>> def mymap(func, seq):
... res = []
... for x in seq: res.append(func(x))
... return res
...
>>> def inc(x): return x + 10
...
>>> list(map(inc, [1, 2, 3])) # Built-in is an iterator
[11, 12, 13]
>>> mymap(inc, [1, 2, 3]) # Ours builds a list
[11, 12, 13]

自己编写的一般映射工具

map是内置函数,总是可用并总是以同样的方式工作,还有性能方面的优势

1
2
3
4
>>> pow(3, 4)                               # 3**4
81
>>> list(map(pow, [1, 2, 3], [2, 3, 4])) # 1**2,2**3,3**4
[1, 8, 81]

提供多个序列作为参数时能够并行返回分别以每个序列中的元素作为函数对应参数得到的结果的列表

对于多个序列,map期待一个N参数的函数用于N序列


函数式编程工具:filter和reduce

函数式编程就是对序列应用一些函数的工具

filter基于某一测试函数过滤元素

reduce对每对元素都应用函数并运行到最后结果

1
2
3
4
5
6
7
8
9
10
11
12
>>> list(range(-5, 5))                              # An iterator in 3.0
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
>>> list(filter((lambda x: x > 0), range(-5, 5))) # An iterator in 3.0
[1, 2, 3, 4]

>>> res = []
>>> for x in range(-5, 5):
... if x > 0:
... res.append(x)
...
>>> res
[1, 2, 3, 4]

rangefilter返回可迭代对象,需要list调用来显示其所有结果

filter函数会将返回值为真的序列元素键入到结果的列表中

可以用for循环等效替代,但是运行速度慢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> res
[1, 2, 3, 4]
>>> from functools import reduce
>>> 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]
>>> res = L[0]
>>> for x in L[1:]:
... res = res + x
...
>>> res
10

>>> def myreduce(function, sequence):
... tally = sequence[0]
... for next in sequence[1:]:
... tally = function(tally, next)
... return tally
...
>>> myreduce((lambda x, y: x + y),[1, 2, 3, 4, 5])
15
>>> myreduce((lambda x, y: x * y), [1, 2, 3, 4, 5])
120

reduce位于functools模块,接受一个迭代器来处理,但它自身不是一个迭代器,返回一个单个的结果

reduce传递一个累加器以及列表的下一个元素给当前函数

1
2
3
4
5
>>> import operator, functools
>>> functools.reduce(operator.add, [2, 4, 6]) # Function-based +
12
>>> functools.reduce((lambda x, y: x + y), [2, 4, 6])
12

内置的operator模块提供了内置表达式对应的函数


本章习题

  1. lambda表达式和def语句有什么关系

    lambdadef都会创建函数对象,以便稍后调用

    lambda是表达式,可以嵌入函数定义中def语法上无法出现的地方

    lambda的使用总是可以用def来替代,并且通过变量名来引用函数

    从语法上来讲,lambda只允许单个的返回值表达式,因为它不支持语句代码块,不适用于较大的函数

  2. 使用lambda的要点是什么

    lambda允许“内联”小单元延迟的可执行代码,并且以默认参数和封闭作用域变量的形式为其提供状态信息

    lambda可以通过编写一条def来替代它,并且用变量名来引用该函数

    lambda可以嵌套小段只是用一次的推迟的代码

    通常出现在GUI这样的基于回调的程序中,并且与mapfilter这些期待一个处理函数的函数工具密切相关

  3. 比较和对比mapflterreduce

    这3个内置函数都对一个序列(可迭代)对象以及集合结果中的各项应用另一个函数

    map把每一项传递给函数并收集结果

    filter收集那些函数返回一个True值的项

    reduce通过对一个累加器和后续项应用函数来计算一个单个的值,在functools模块中可用

  4. 什么是函数注解,如何使用它们

    函数注解是函数的参数及其结果的语法上的修饰,它会收集到分配给函数的__annotations__属性的一个字典中

    Python在这些注解上没有放置语义含义,而是直接将其包装,以供其他工具潜在地使用

  5. 什么是递归函数,如何使用它们

    递归函数调用本身可以直接地或间接地进行,从而实现循环

    它们可以用来遍历任意形状的结构,也可以用来进行一般性迭代

  6. 编写函数的通用设计规则是什么

    函数通常应该较小,尽可能自包含,拥有单一的、统一的用途,并且与输入参数和返回值等其他部分通信

    如果期待修改的话,它们可以使用可变的参数来与结果通信,并且一些类型的程序暗含其他的通信机制


第二十章 迭代和解析,第二部分

  • 回顾解析和迭代概念
  • 介绍内置解析和迭代工具
  • 学习生成器函数以及相关的生成器表达式
  • 度量了迭代替代方案的性能
  • 概况目前为止所遇到的各种工具
  • 回顾与函数相关的常见错误

回顾列表解析:函数式编程工具

列表解析把任意一个表达式而不是一个函数应用于一个迭代对象中的元素

列表解析可以成为一个比mapfilter更通用的工具

列表解析与map

序列解析由[]封装起来,其中有一个表达式,其后跟着类似for循环头部的语句

列表解析在一个序列的值上应用一个任意表达式,将其结果收集到一个新的列表中并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> res = []
>>> for x in 'spam':
... res.append(ord(x))
...
>>> res
[115, 112, 97, 109]

>>> res = list(map(ord, 'spam')) # Apply function to sequence
>>> res
[115, 112, 97, 109]

>>> res = [ord(x) for x in 'spam'] # Apply expression to sequence
>>> res
[115, 112, 97, 109]

map函数把一个函数映射遍一个序列

序列解析把一个表达式映射遍一个序列

1
2
3
4
>>> [x ** 2 for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> list(map((lambda x: x ** 2), range(10)))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

列表解析在应用一个任意表达式的时候更方便

map使用lambda在行内编写

增加测试和嵌套循环

for之后可以编写一个if语句来增加选择逻辑,可以当成与内置的filter类似的工具

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> [x for x in range(5) if x % 2 == 0]
[0, 2, 4]

>>> list(filter((lambda x: x % 2 == 0), range(5)))
[0, 2, 4]

>>> res = []
>>> for x in range(5):
... if x % 2 == 0:
... res.append(x)
...
>>> res
[0, 2, 4]

filter需要创建一个lambda函数

1
2
3
4
>>> [x ** 2 for x in range(10) if x % 2 == 0]
[0, 4, 16, 36, 64]
>>> list(map((lambda x: x**2), filter((lambda x: x % 2 ==0), range(10))))
[0, 4, 16, 36, 64]

列表解析中混合一个if分支以及任意表达式完成与filtermap相同的功效

1
2
3
[ expression for target1 in iterable1 [if condition1]
for target2 in iterable2 [if condition2] ...
for targetN in iterableN [if conditionN] ]

列表解析中可以编写任意数量的嵌套的for循环,并且每一个都有可选的关联的if测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> [x + y for x in 'spam' for y in 'SPAM']
['sS', 'sP', 'sA', 'sM', 'pS', 'pP', 'pA', 'pM', 'aS', 'aP', 'aA', 'aM', 'mS', 'mP', 'mA', 'mM']

>>> res = [x + y for x in [0, 1, 2] for y in [100, 200, 300]]
>>> 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]

列表解析能够不使用列表的数字索引遍历字符串,并收集合并后的结果

列表解析中嵌套for分句时等效于嵌套的for循环

1
2
3
4
5
6
7
8
9
10
11
12
>>> [(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)]

>>> res = []
>>> for x in range(5):
... if x % 2 == 0:
... for y in range(5):
... if y % 2 == 1:
... res.append((x, y))
...
>>> res
[(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)]

嵌套的for从句中附加if分句过滤出每个序列中需要进行迭代的元素

将列表解析的forif分句在其中进行嵌套可以得到等效的for循环语句

列表解析和矩阵

使用Python编写矩阵的基本方法是使用嵌套的列表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> 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]
[2, 5, 8]
>>> [M[row][1] for row in (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 col in range(3)] for row in range(3)]
[[2, 4, 6], [12, 15, 18], [28, 32, 36]]
>>> 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]]

使用通常的索引操作能够索引行,以及索引行中的列

列表解析自动扫描行和列,提取对角线元素

理解列表解析

map调用比等效的for循环快两倍,列表解析比map调用稍快一些

for循环让逻辑变得更清晰

map和列表解析都是表达式,能够出现在for循环语句不能出现的地方使用

为什么要在意:列表解析和map

列表解析运行表达式,显示出所有结果

map运行函数,是一个迭代器,根据需求产生结果

1
2
3
4
5
6
7
8
>>> open('myfile').readlines()
['aaa\n', 'bbb\n', 'ccc\n']
>>> [line.rstrip() for line in open('myfile').readlines()]
['aaa', 'bbb', 'ccc']
>>> [line.rstrip() for line in open('myfile')]
['aaa', 'bbb', 'ccc']
>>> list(map((lambda line: line.rstrip()), open('myfile')))
['aaa', 'bbb', 'ccc']

使用列表解析或map调用从所有行中去掉换行符

1
2
3
4
5
>>> listoftuple = [('bob', 35, 'mgr'), ('mel', 40, 'dev')]
>>> [age for (name, age, job) in listoftuple]
[35, 40]
>>> list(map((lambda row: row[1]), listoftuple))
[35, 40]

列表解析作为一种列选择操作使用


重访迭代器:生成器

Python提供了在需要时产生结果的工具

  • 生成器函数:编写为常规的def语句,但是使用yield语句一次返回一个结果,在每个结果之间挂起和继续它们的状态
  • 生成器表达式类似于的列表解析,但是返回按需产生结果的一个对象,而不是构建一个结果列表

生成器函数:yield VS return

生成器函数是可以返回一个值并随后从退出的地方继续的函数

状态挂起

生成器函数自动在生成值的时刻挂起并继续函数的执行,对于提前计算一系列值以及在类中手动保存和恢复状态很有用

生成器函数在挂起时保存的信息状态包含整个本地作用域,当函数恢复时使本地变量信息的可用

yield语句挂起函数并向调用者发送回一个值,并保留足够的状态信息使得函数能够从它离开的地方继续,当继续时函数在上一个yield返回后立即继续执行

迭代协议整合

可迭代对象定义了__next__方法,返回迭代中的下一项或者引发StopIteration异常

生成器函数包含yield语句编译为生成器,返回一个可迭代对象,该对象支持__next__方法,由for循环或其他迭代背景使用迭代协议遍历一个序列或值的生成器

生成器函数可能有return语句在def语句块的末尾终止值的生成,此时生成器的__next__方法引发一个StopIteration异常

生成器函数编写包含yield语句的def语句,自动支持迭代协议,并且由此可能用着任何迭代环境中以随时间并根据需要产生结果

生成器函数应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> def gensquares(N):
... for i in range(N):
... yield i ** 2 # Resume here later
...
>>> for i in gensquares(5): # Resume the function
... print(i, end=' : ') # Print last yielded value
...
0 : 1 : 4 : 9 : 16 :
>>> x = gensquares(4)
>>> x
<generator object gensquares at 0x103a30048>
>>> next(x)
0
>>> next(x)
1
>>> next(x)
4
>>> next(x)
9
>>> next(x)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

生成器函数每次都会产生一个值返还给调用者,当暂停时它的状态被保存下来并在yield语句之后被控制器回收,为了终止生成器,函数可以使用一个无值的返回语句

直接调用生成器函数得到一个生成器对象,支持迭代协议,调用__next__方法可以开始这个函数或从上次yield值后的地方恢复,并在得到最后一个值时产生StopIteration异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> def buildsquares(n):
... res = []
... for i in range(n): res.append(i ** 2)
... return res
...
>>> for x in buildsquares(5): print(x, end=' : ')
...
0 : 1 : 4 : 9 : 16 :
>>> for x in [n ** 2 for n in range(5)]:
... print(x, end=' : ')
...
0 : 1 : 4 : 9 : 16 :
>>> for x in map((lambda n: n ** 2), range(5)):
... print(x, end=' : ')
...
0 : 1 : 4 : 9 : 16 :

for循环、map或者列表解析可是实现相同的功能,但是生成器在内存使用和性能方面更好

生成器允许函数避免临时再做所有的工作,在loop迭代中将处理一系列值的时间分布开来,提供将类的对象保存到迭代中的状态的替代方案,能够进行自动的保存和恢复函数变量

拓展生成器函数协议:send和next

send方法生成一系列结果的下一个元素,并提供了一种调用者与生成器之间进行通信的方法从而影响它的操作

yield是一个表达式的形式,需要包含在括号中,可以返回send传入的值

throw(type)方法将在生成器内部最后一个yield时产生一个终止迭代的GeneratorExit异常以及一个close方法

next(X)函数调用对象的X.__next__方法,可以应用于所有的可迭代对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> def gen():
... for i in range(10):
... X = (yield i + 3) * 2
... print(X)
...
>>> G = gen()
>>> next(G) # Must call next() first, to start generator
0
>>> G.send(77) # Advance, and send value to yield expression
154
4
>>> G.send(88)
176
5
>>> next(G) # next() and X.__next__() send None
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in gen
TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'

当使用拓展生成器函数协议时,值可以通过调用G.send(value)发送给生成器,生成器中的yield表达式返回传入的值,之后恢复生成器的代码,调用正常的next方法时yield返回None

生成器表达式:迭代器遇到列表解析

生成器表达式像列表解析一样,但是是在()中而不是[]

生成器表达式是对内存空间的优化,不需要一次构造出整个结果列表

1
2
3
4
5
6
>>> [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 0x103a320a0>
>>> list(x ** 2 for x in range(4)) # List comprehension equivalence
[0, 1, 4, 9]

编写一个列表解析等同于在一个list内置调用中包含一个生成器表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> G = (x ** 2 for x in range(4))
>>> next(G)
0
>>> next(G)
1
>>> next(G)
4
>>> next(G)
9
>>> next(G)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

>>> for num in (x ** 2 for x in range(4)):
... print('%s, %s' % (num, num / 2.0))
...
0, 0.0
1, 0.5
4, 2.0
9, 4.5

>>> 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)
[9, 4, 1, 0]
>>> import math
>>> list(map(math.sqrt, (x ** 2 for x in range(4))))
[0.0, 1.0, 2.0, 3.0]

生成器表达式返回一个生成器对象,这个对象支持迭代协议并可以在任意的迭代语境中操作

生成器表达式在()之内,但是在函数调用中则不需要自身的()

生成器函数 VS 生成器表达式

同样的迭代可以用一个生成器函数或一个生成器表达式编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> G = (c * 4 for c in 'SPAM')			# Generator expression
>>> list(G) # Force generator to produce all results
['SSSS', 'PPPP', 'AAAA', 'MMMM']

>>> def timesfour(S): # Generator function
... for c in S:
... yield c * 4
...
>>> G = timesfour('spam')
>>> list(G) # Iterate automatically
['ssss', 'pppp', 'aaaa', 'mmmm']

>>> G = (c * 4 for c in 'SPAM')
>>> I = iter(G) # Iterate manually
>>> next(I)
'SSSS'
>>> next(I)
'PPPP'
>>> G = timesfour('spam')
>>> I = iter(G) # Iterate automatically
>>> next(I)
'ssss'
>>> next(I)
'pppp'

生成器函数能够编写更多的逻辑并使用更多的状态信息

表达式和函数支持自动和手动迭代,list函数自动调用迭代,next手动迭代

生成器是单迭代器对象

生成器函数和生成器表达式自身都是迭代器,只支持一次活跃迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
>>> def timesfour(S):
... for c in S:
... yield c * 4
...
>>> G = timesfour('spam')
>>> iter(G) is G
True
>>> G = (c * 4 for c in 'SPAM')
>>> iter(G) is G # My iterator is myself:G has __next__
True

>>> G = (c * 4 for c in 'SPAM') # Make a new generator
>>> I1 = iter(G) # Iterate manually
>>> next(I1)
'SSSS'
>>> next(I1)
'PPPP'
>>> I2 = iter(G) # Second iterator at same position!
>>> next(I2)
'AAAA'
>>> list(I1) # Collect the rest of I1's items
['MMMM']
>>> next(I2) # Other iterators exhausted too
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> I3 = iter(G) # Ditto for new iterators
>>> next(I3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> I3 = iter(c * 4 for c in 'SPAM') # New generator to start over
>>> next(I3)
'SSSS'

>>> G = timesfour('spam') # Generator functions work the same way
>>> I1, I2 = iter(G), iter(G)
>>> next(I1)
'ssss'
>>> next(I1)
'pppp'
>>> next(I2) # I2 at same position as I1
'aaaa'

一个生成器的迭代器是生成器自身

手动地使用多个迭代器来迭代结果流,会指向相同的位置

一旦任何迭代器运行到终止,所有的迭代器都将用尽,必须产生一个新的生成器重新开始

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> L = [1, 2, 3, 4]
>>> I1, I2 = iter(L), iter(L)
>>> next(I1)
1
>>> next(I1)
2
>>> next(I2) # Lists support multiple iterators
1
>>> del L[2:] # Changes reflected in iterators
>>> next(I1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

某些内置类型支持多个迭代器并且在一个活动迭代器中传递并反映他们的原处修改

用迭代工具模拟zip和map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> S1 = 'abc'
>>> S2 = 'xyz123'
>>> list(zip(S1, S2)) # zip pairs items from iterables
[('a', 'x'), ('b', 'y'), ('c', 'z')]

# zip pairs items, truncates at shortest

>>> list(zip([-2, -1, 0, 1, 2])) # Single sequence: 1-ary tuples
[(-2,), (-1,), (0,), (1,), (2,)]
>>> list(zip([1, 2, 3], [2, 3, 4, 5])) # N sequences: N-ary tuples
[(1, 2), (2, 3), (3, 4)]

# map passes paired itenms to a function, truncates

>>> list(map(abs, [-2, 1, 0, 1, 2])) # Single sequence: 1-ary function
[2, 1, 0, 1, 2]
>>> list(map(pow, [1, 2, 3], [2, 3, 4, 5])) # N sequences: N-ary function
[1, 8, 81]

zipmap配对元素方式相同

编写自己的map(func,…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# map(func, seqs...) workalike with zip

def mymap(func, *seqs):
res = []
for args in zip(*seqs):
res.append(func(*args))
return res

print(mymap(abs, [-2, -1, 0, 1, 2]))
print(mymap(pow, [1, 2, 3], [2, 3, 4, 5]))

'''
[2, 1, 0, 1, 2]
[1, 8, 81]
[Finished in 0.3s]
'''

map收集多个可迭代参数,将其作为zip参数迭代,使用*args参数传递语法解包传入函数

1
2
3
4
5
6
7
8
9
10
11
12
13
# Using a list comprehension

def mymap(func, *seqs):
return [func(*args) for args in zip(*seqs)]

print(mymap(abs, [-2, -1, 0, 1, 2]))
print(mymap(pow, [1, 2, 3], [2, 3, 4, 5]))

'''
[2, 1, 0, 1, 2]
[1, 8, 81]
[Finished in 0.2s]
'''

使用列表解析可以更精简并且可能运行的更快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Using generators: yield and (...)

def mymap(func, *seqs):
res = []
for args in zip(*seqs):
yield func(*args)

def mymap(func, *seqs):
return (func(*args) for args in zip(*seqs))

print(list(mymap(abs, [-2, -1, 0, 1, 2])))
print(list(mymap(pow, [1, 2, 3], [2, 3, 4, 5])))

'''
[2, 1, 0, 1, 2]
[1, 8, 81]
[Finished in 0.2s]
'''

使用生成器函数和表达式产生同样的结果,但是返回支持迭代协议的生成器

使用list强迫生成器运行

编写自己的zip(…)和map(None,…)

使用迭代工具编写zip和Python 2.6的可以填充的map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# zip(seqs...) and 2.6 map(None, seqs...) workalikes

def myzip(*seqs):
seqs = [list(S) for S in seqs]
res = []
while all(seqs):
res.append(tuple(S.pop(0) for S in seqs))
return res

def mymapPad(*seqs, pad=None):
seqs = [list(S) for S in seqs]
res = []
while any(seqs):
res.append(tuple((S.pop(0)) if S else pad) for S in seqs)
return res

S1, S2 = 'abc', 'xyz123'
print(myzip(S1, S2))
print(mymapPad(S1, S2))
print(mymapPad(S1, S2, pad=99))

'''
[('a', 'x'), ('b', 'y'), ('c', 'z')]
[('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]
[('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]
[Finished in 0.3s]
'''

这里编写的函数可以在任何类型的可迭代对象上运行,因为通过list内置函数传入参数

一个可迭代对象中所有元素为Trueall返回True,一个可迭代对象中任何元素为TrueanyTrue

keyword-only参数pad指定填充对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Using generators: yield

def myzip(*seqs):
seqs = [list(S) for S in seqs]
while all(seqs):
yield tuple(S.pop(0) for S in seqs)

def mymapPad(*seqs, pad=None):
seqs = [list(S) for S in seqs]
while any(seqs):
yield tuple((S.pop(0) if S else pad) for S in seqs)

S1, S2 = 'abc', 'xyz123'
print(list(myzip(S1, S2)))
print(list(mymapPad(S1, S2)))
print(list(mymapPad(S1, S2, pad=99)))

'''
[('a', 'x'), ('b', 'y'), ('c', 'z')]
[('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]
[('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]
[Finished in 0.5s]
'''

使用yield转换为生成器

使用list调用激活生成器和迭代器产生结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Alternate implementation with lengths

def myzip(*seqs):
minlen = min(len(S) for S in seqs)
return [tuple(S[i] for S in seqs) for i in range(minlen)]

def mymapPad(*seqs, pad=None):
maxlen = max(len(S) for S in seqs)
index = range(maxlen)
return [tuple((S[i] if len(S) > i else pad) for S in seqs) for i in index]

S1, S2 = 'abc', 'xyz123'
print(myzip(S1, S2))
print(mymapPad(S1, S2))
print(mymapPad(S1, S2, pad=99))
'''
[('a', 'x'), ('b', 'y'), ('c', 'z')]
[('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]
[('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]
[Finished in 0.2s]
'''

通过计算最小和最大参数长度编写嵌套的列表解析来遍历参数所以范围

参数只能是序列或类似的而不是任意的可迭代对象

minmax的参数是生成器表达式,range是一个可迭代对象

1
2
3
4
5
6
7
8
9
10
11
12
13
# Using generators:(...)

def myzip(*seqs):
minlen = min(len(S) for S in seqs)
return (tuple(S[i] for S in seqs) for i in range(minlen))

S1, S2 = 'abc', 'xyz123'
print(list(myzip(S1, S2)))

'''
[('a', 'x'), ('b', 'y'), ('c', 'z')]
[Finished in 0.1s]
'''

使用圆括号将列表构建器转换为生成器

使用list调用激活生成器和迭代器产生结果

为什么你会留意:单次迭代

1
2
3
4
5
def myzip(*args):
iters = list(map(iter, args))
while iters:
res = [next(i) for i in iters]
yield tuple(res)

使用iternext对任何类型的可迭代对象都有效

一个迭代器用尽时由next引发StopIteration异常终止生成器函数,与 return语句有相同效果

在Python 3.0中,map返回一个单次可迭代对象,如果没有使用list内置函数创建一个支持多次迭代的对象,在循环中运行一次列表解析后,iters将会永远为空进入无限循环

内置类型和类中的值生成

可以用遵守迭代协议的类实现任意的用户定义的的生成器对象,这样的类有一个__iter__方法,由内置的iter函数调用,返回一个对象,该对象有__next__方法,由next函数调用

1
2
3
4
5
6
7
8
9
10
11
12
>>> D = {'a':1, 'b':2, 'c':3}
>>> x = iter(D)
>>> next(x)
'a'
>>> next(x)
'b'
>>> for key in D:
... print(key, D[key])
...
a 1
b 2
c 3

字典拥有在每次迭代中产生键的迭代器,可以手动迭代或使用for循环、map调用、列表解析以及其他环境等自动迭代工具

1
2
3
4
5
>>> for line in open('temp.txt'):
... print(line, end='')
...
Tis but
a flesh wound.

在文件迭代器中,载入文件的行


Python 3.0解析语法概况

  • 对于集合

    新的常量形式{1, 3, 2}等同于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)是一个任意的表达式

  • 对于字典

    新的字典解析语法{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)一样工作

1
2
3
4
5
6
7
8
9
10
11
>>> [x * x for x in range(10)]					# List comprehension:builds list
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] # like list(generator expr)

>>> (x * x for x in range(10)) # Generator expression:produces items
<generator object <genexpr> at 0x1039cdfc0> # Parens are often optional

>>> {x * x for x in range(10)} # Set comprehension, new in 3.0
{0, 1, 64, 4, 36, 9, 16, 49, 81, 25} # {x, y} is a set in 3.0 too

>>> {x:x * x for x in range(10)} # Dictionary comprehension, new in 3.0
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

所有解析语法

解析集合和字典解析

1
2
3
4
5
6
7
8
>>> {x * x for x in range(10)}				# Comprehension
{0, 1, 64, 4, 36, 9, 16, 49, 81, 25}
>>> set(x * x for x in range(10)) # Genereator and type name
{0, 1, 64, 4, 36, 9, 16, 49, 81, 25}
>>> {x: x * x for x in range(10)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
>>> dict((x, x * x) for x in range(10))
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

集合解析和字典解析是把生成器表达式传递给类型名的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> res = set()
>>> for x in range(10): # Set comprehension equivalent
... res.add(x * x)
...
>>> res
{0, 1, 64, 4, 36, 9, 16, 49, 81, 25}

>>> res = {}
>>> for x in range(10): # Dict comprehension equivalent
... res[x] = x * x
...
>>> res
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

解析语法总是可以用手动代码来构建结果对象

1
2
3
4
5
>>> G = ((x, x * x) for x in range(10))
>>> next(G)
(0, 0)
>>> next(G)
(1, 1)

集合解析和列表解析接受迭代器,但是根据需要产生结果的概念

根据需要产生键和值可以用生成器表达式

针对集合和字典的拓展的解析语法

1
2
3
4
5
6
>>> [x * x for x in range(10) if x % 2 == 0]		# Lists are ordered
[0, 4, 16, 36, 64]
>>> {x * x for x in range(10) if x % 2 == 0} # But sets are not
{0, 64, 4, 36, 16}
>>> {x: x * x for x in range(10) if x % 2 == 0} # Neither are dict keys
{0: 0, 2: 4, 4: 16, 6: 36, 8: 64}

集合和字典解析支持嵌套相关的if子句从结果中过滤掉元素

1
2
3
4
5
6
>>> [x + y for x in [1, 2, 3] for y in [4, 5, 6]]	# Lists keep duplicates
[5, 6, 7, 6, 7, 8, 7, 8, 9]
>>> {x + y for x in [1, 2, 3] for y in [4, 5, 6]} # Buts sets do not
{5, 6, 7, 8, 9}
>>> {x : y for x in [1, 2, 3] for y in [4, 5, 6]} # Neither do dict keys
{1: 6, 2: 6, 3: 6}

集合和字典解析支持嵌套的for循环,但是这两种类型具有无序性互异性,导致结果看上去缺乏直接性

1
2
3
4
5
6
7
8
>>> {x + y for x in 'ab' for y in 'cd'}
{'bc', 'ad', 'bd', 'ac'}
>>> {x + y: (ord(x), ord(y)) for x in 'ab' for y in 'cd'}
{'ac': (97, 99), 'ad': (97, 100), 'bc': (98, 99), 'bd': (98, 100)}
>>> {k * 2 for k in ['spam', 'ham', 'sausage'] if k[0] == 's'}
{'sausagesausage', 'spamspam'}
>>> {k.upper(): k * 2 for k in ['spam', 'ham', 'sausage'] if k[0] == 's'}
{'SPAM': 'spamspam', 'SAUSAGE': 'sausagesausage'}

集合和字典解析可以在任何类型的可迭代对象上迭代


对迭代的各种方法进行计时

列表解析要比for循环语句有速度方面的性能优势

map会依据调用方法的不同表现出更好或更差的性能

对模块计时

解释器优化是内部化的问题,无法猜测哪种方案会执行的最好,最好在自己的计算机上用自己的Python版本对自己的代码计时

性能不应是编写Python代码首要关心问题,应该在可读性和简单性的基础上编写代码,在需要的时候性能优化

1
2
3
4
5
6
7
8
9
10
11
12
# File mytimer.py

import time
reps = 1000
repslist = range(reps)

def timer(func, *pargs, **kargs):
start = time.clock()
for i in repslist:
ret = func(*pargs, **kargs)
elapsed = time.clock() - start
return (elapsed, ret)

这个模块通过获取开始时间、调用函数固定的次数并且用开始时间减去停止时间,从而对使用任何位置和关键字参数调用任意函数进行计时,完成后调用的总时间与被计时的函数的最终返回值作为一个元组返回

  • Python的time模块允许访问当前时间,精度随着每个平台而有所不同
  • range调用放到了计时循环之外,它的构建成本不会计算到计时函数中
  • reps计数是一个全局变量,可以修改:mytimer.reps = N

计时脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sys, mytimer
reps = 10000
repslist = range(reps)

def forLoop():
res = []
for x in repslist:
res.append(abs(x))
return res

def listComp():
return [abs(x) for x in repslist]

def mapCall():
return list(map(abs, repslist))

def genExpr():
return list(abs(x) for x in repslist)

def genFunc():
def gen():
for x in repslist:
yield abs(x)
return list(gen())

print(sys.version)
for test in (forLoop, listComp, mapCall, genExpr, genFunc):
elapsed , result = mytimer.timer(test)
print('-' * 33)
print('%-9s: %.5f => [%s...%s]' % (test.__name__, elapsed, result[0], result[-1]))

'''
3.6.4 (v3.6.4:d48ecebad5, Dec 18 2017, 21:07:28)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]
---------------------------------
forLoop : 2.11010 => [0...9999]
---------------------------------
listComp : 1.23212 => [0...9999]
---------------------------------
mapCall : 0.65460 => [0...9999]
---------------------------------
genExpr : 1.67717 => [0...9999]
---------------------------------
genFunc : 1.66981 => [0...9999]
[Finished in 7.6s]
'''

这段脚本测试了五种构建结果列表的替代方法

必须通过内置list调用来运行生成器表达式、生成器函数与map,迫使他们产生所有值,否则可能得到没有真正工作的生成器

内置属性__name__给出函数的名称

计时结果

1
2
3
4
5
6
7
8
9
10
11
12
13
3.6.4 (v3.6.4:d48ecebad5, Dec 18 2017, 21:07:28) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]
---------------------------------
forLoop : 2.11010 => [0...9999]
---------------------------------
listComp : 1.23212 => [0...9999]
---------------------------------
mapCall : 0.65460 => [0...9999]
---------------------------------
genExpr : 1.67717 => [0...9999]
---------------------------------
genFunc : 1.66981 => [0...9999]
[Finished in 7.6s]

map比列表解析略快一点,且都比for循环快很多

生成器表达式比列表解析运行的慢,尽管把一个生成器表达式包装到一个list调用中,使得其功能等同于一个带有方括号的列表解析,两种表达式的内部实现看上去有所不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sys, mytimer
reps = 10000
repslist = range(reps)

def forLoop():
res = []
for x in repslist:
res.append(x + 10)
return res

def listComp():
return [x + 10 for x in repslist]

def mapCall():
return list(map(lambda x: x + 10, repslist))

def genExpr():
return list(x + 10 for x in repslist)

def genFunc():
def gen():
for x in repslist:
yield x + 10
return list(gen())

print(sys.version)
for test in (forLoop, listComp, mapCall, genExpr, genFunc):
elapsed , result = mytimer.timer(test)
print('-' * 33)
print('%-9s: %.5f => [%s...%s]' % (test.__name__, elapsed, result[0], result[-1]))

'''
3.6.4 (v3.6.4:d48ecebad5, Dec 18 2017, 21:07:28)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]
---------------------------------
forLoop : 1.93515 => [10...10009]
---------------------------------
listComp : 1.05818 => [10...10009]
---------------------------------
mapCall : 1.97749 => [10...10009]
---------------------------------
genExpr : 1.41341 => [10...10009]
---------------------------------
genFunc : 1.44341 => [10...10009]
[Finished in 8.3s]
'''

abs换成真正的加法操作

1
2
3
4
5
6
7
8
9
10
11
12
13
3.6.4 (v3.6.4:d48ecebad5, Dec 18 2017, 21:07:28) 
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]
---------------------------------
forLoop : 1.93515 => [10...10009]
---------------------------------
listComp : 1.05818 => [10...10009]
---------------------------------
mapCall : 1.97749 => [10...10009]
---------------------------------
genExpr : 1.41341 => [10...10009]
---------------------------------
genFunc : 1.44341 => [10...10009]
[Finished in 8.3s]

map调用一个用户定义的函数比for循环语句慢

列表解析运行最快

计时模块替代方案

计时模块的不足

  • 使用time.clock调用计时代码在Windows上是最好的,但是time.time在某些UNIX平台上可能提供更好的解析
  • 调整重复的次数需要修改模块级别的全局变量,如果要使用timer函数并且有多个导入者共享的话,这是不太理想的安排
  • 计时器必须通过运行测试函数很多次才能工作,要考虑随机的系统载入的波动,在所有的测试中选择最好的时间,而不是总的时间,可能会更好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File mytimer.py

"""
timer(spam, 1, 2, a=3, b=4, _reps=1000) calls and times spam(1, 2, a=3) _reps times, and returns total time for all runs, with final result;
best(spam, 1, 2, a=3, b=4, _reps=50) runs best-of-N timer to filter oyt any system load variation, and returns best time among _reps tests;
"""

import time, sys
if sys.platform[:3] == 'win':
timefunc = time.clock # Use time.clock on Windows
else:
timefunc = time.time # Better resolution on some Unix platforms

def trace(*args): pass # Or:print args

def timer(func, *pargs, **kargs):
_reps = kargs.pop('_reps', 1000) # Passed-in or default reps
trace(func, pargs, kargs, _reps)
repslist = range(_reps) # Hoist range out for 2.6 lists
start = timefunc()
for i in repslist:
ret = func(*pargs, **kargs)
elapsed = timefunc() - start
return (elapsed, ret)

def best(func, *pargs, **kargs):
_reps = kargs.pop('_reps', 50)
best = 2 ** 32
for i in range(_reps):
(time, ret) = timer(func, *pargs, _reps=1, **kargs)
if time < best: best = time
return (best, ret)

文件顶部的文档字符串描述了模块的目标和用途

使用字典的pop操作从用于测试的参数中删除_reps参数并为其提供一个默认值

trace函数修改为print可以在开发过程中分总参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import sys, mytimer
reps = 10000
repslist = range(reps)

def forLoop():
res = []
for x in repslist:
res.append(x + 1)
return res

def listComp():
return [x + 1 for x in repslist]

def mapCall():
return list(map(lambda x: x + 1, repslist))

def genExpr():
return list(x + 1 for x in repslist)

def genFunc():
def gen():
for x in repslist:
yield x + 1
return list(gen())

print(sys.version)
for tester in (mytimer.timer, mytimer.best):
print('<%s>' % tester.__name__)
for test in (forLoop, listComp, mapCall, genExpr, genFunc):
elapsed , result = tester(test)
print('-' * 35)
print('%-9s: %.5f => [%s...%s]' % (test.__name__, elapsed, result[0], result[-1]))

'''
3.6.4 (v3.6.4:d48ecebad5, Dec 18 2017, 21:07:28)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]
<timer>
-----------------------------------
forLoop : 2.07234 => [1...10000]
-----------------------------------
listComp : 1.11021 => [1...10000]
-----------------------------------
mapCall : 1.95846 => [1...10000]
-----------------------------------
genExpr : 1.40802 => [1...10000]
-----------------------------------
genFunc : 1.41008 => [1...10000]
<best>
-----------------------------------
forLoop : 0.00157 => [1...10000]
-----------------------------------
listComp : 0.00081 => [1...10000]
-----------------------------------
mapCall : 0.00167 => [1...10000]
-----------------------------------
genExpr : 0.00120 => [1...10000]
-----------------------------------
genFunc : 0.00120 => [1...10000]
[Finished in 8.6s]
'''

多次运行测试可以过滤掉系统载入波动

列表解析在大多数情况下表现很好,使用内置函数时map表现更好

在Python 3.0中使用keyword-only参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File mytimer.py(3.X only)

"""
Use 3.0 keyword-only default arguments, instead of ** and dict pops.
No need to hoist range() out of test in 3.0:a generator, not a list
"""

import time, sys
trace = lambda *rags: None # or print
timefunc = time.clock if sys.platform == 'win32' else time.time

def timer(func, *pargs, _reps=1000, **kargs):
trace(func, pargs, kargs, _reps)
start = timefunc()
for i in range(_reps):
ret = func(*pargs, **kargs)
elapsed = timefunc() - start
return (elapsed, ret)

def best(func, *pargs, _reps=50, **kargs):
best = 2 ** 32
for i in range(_reps):
(time, ret) = timer(func, *pargs, _reps=1, **kargs)
if time < best: best = time
return (best, ret)

使用keyword-only参数可以简化计时器模块代码

这个版本与前面的版本以同样的方式使用并产生相同的结果

交互提示模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> from mytimer import timer, best
>>> def power(X, Y): return X ** Y # Test function
...
>>> timer(power, 2, 32) # Total time,last result
(0.0013279914855957031, 4294967296)
>>> timer(power, 2, 32, _reps=1000000) # Override default reps
(0.6220171451568604, 4294967296)
>>> timer(power, 2, 100000)[0] # 2 ** 100,000 tot time @1,000 reps
0.48587584495544434
>>> best(power, 2, 32) # Best time, last result
(1.6689300537109375e-06, 4294967296)
>>> best(power, 2, 100000)[0] # 2 ** 100,000 best time
0.0004100799560546875
>>> best(power, 2, 100000, _reps=500)[0] # Override default reps
0.00040984153747558594

>>> timer(power, 2, 1000000, _reps=1)[0] # 2 ** 1,000,000:total time
0.01019287109375
>>> timer(power, 2, 1000000, _reps=10)[0]
0.06375718116760254
>>> best(power, 2, 1000000, _reps=1)[0] # 2 ** 1,000,000:best time
0.009803056716918945
>>> best(power, 2, 1000000, _reps=10)[0] # 10 is sometimes as good as 50
0.0054590702056884766
>>> best(power, 2, 1000000, _reps=50)[0] # Best resolution
0.005347728729248047

可以从交互提示模式测试计时脚本

对于小函数,计时器的代码成本与被计时的函数一样显著,计时器可以帮助判断代码替代方案的相对速度而不应该绝对地取计时器结果

其他建议

Python标准库中timeit模块自动对代码计时,支持命令行使用模式,并且解决了一些特定于平台的问题

可以使用str.format方法代替%格式化表达式


函数陷阱

本地变量是静态检测的

Python定义的在一个函数中进行分配的变量名默认为本地变量,存在于函数的作用域并值在函数运行时存在

Python是静态检测Python的本地变量的,通过在def语句运行时检测赋值语句判断是否为本地变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> X = 99
>>> def selector(): # X used but not assigned
... print(X) # X found in global scope
...
>>> selector()
99

>>> def selector():
... print(X) # Does not yet exist!
... X = 88 # X classified as a local name(everywhere)
... # Can also happen for "import X","def X"...
>>> selector()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in selector
UnboundLocalError: local variable 'X' referenced before assignment

没有在函数中赋值的变量名会在整个模块文件中查找

在函数编译时,Python检测到X的赋值语句,将X作为本地变量名,但是函数实际运行时还没被赋值就使用了,从而得到一个未定义变量名的错误

任何在函数体内的赋值都将会使其成为一个本地变量名,import=、嵌套def、嵌套类等都会受到这种行为的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
X = 99
>>> def selector():
... global X # Force X to be global(everywhere)
... print(X)
... X = 88
...
>>> selector()
99

X = 99
>>> def selector():
... import __main__
... print(__main__.X)
... X = 88
... print(X)
...
>>> selector()
88
88

使用global语句强制作为全局变量,在函数内的赋值同时会改变全局变量

为了使用相同变量名的本地变量,可以导入上层模块并使用模块的属性标记获得全局变量,.运算从命名空间对象中获取变量的值,交互模式下的命名空间为__main__

默认和可变对象

默认参数在def语句运行时而不是函数调用时评估并保存,Python将默认参数保存成一个对象,附加在函数本身

1
2
3
4
5
6
7
8
9
10
11
12
>>> def saver(x=[]):	# Saces away a list object
... x.append(1) # Changes same object each time!
... print(x)
...
>>> saver([2]) # Default not used
[2, 1]
>>> saver() # Default used
[1]
>>> saver() # Grows on each call!
[1, 1]
>>> saver()
[1, 1, 1]

函数在调用之间保存了同一个默认参数的对象,这个对象是def语句执行时被创建的,修改可变的默认参数要十分小心

可变的默认参数工作起来像全局变量,但是变量名对于函数来说是本地变量而不会与程序中的其他变量名发生冲突

1
2
3
4
5
6
7
8
9
10
11
12
>>> def saver(x=None):
... if x is None: # None argument passed?
... x = [] # Run code to make a new list
... x.append(1) # Changes new list object
... print(x)
...
>>> saver([2])
[2, 1]
>>> saver() # Doesn't grow here
[1]
>>> saver()
[1]

可以在函数主体的开始对默认参数进行简单的拷贝或者将默认参数值的表达式移至函数体内部

if语句可以用x = x or []代替,如果没有参数传入x将会默认为None,or返回右边的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> def saver():
... saver.x.appen(1)
... print(saver.x)
...
>>> def saver():
... saver.x.append(1)
... print(saver.x)
...
>>> saver.x = []
>>> saver()
[1]
>>> saver()
[1, 1]
>>> saver()
[1, 1, 1]

通过函数属性可以实现可变默认值

没有return语句的函数

在Python函数中,return语句是可选的,当没有返回值时函数在控制权从函数主体脱离时推出,函数返回None对象

1
2
3
4
5
6
7
8
9
10
11
12
>>> def proc(x):
... print(x) # No return is a None return
...
>>> x = proc('testing 123...')
testing 123...
>>> print(x)
None

>>> list = [1, 2, 3]
>>> list = list.append(4) # append is a "procedure"
>>> print(list) # append changes list in-place
None

赋值为一个没有返回值的函数时不会导致错误而是得到None

嵌套作用域的循环变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
>>> def makeActions():
... acts = []
... for i in range(5): # Tries to remember each i
... acts.append(lambda x: i ** x) # All remember same last i
... return acts
...
>>> acts = makeActions()
>>> acts[0]
<function makeActions.<locals>.<lambda> at 0x104205950>
>>> acts[0](2) # All are 4 ** 2,value of last i
16
>>> acts[2](2) # This should be 2 ** 2
16
>>> acts[4](2) # This should be 4 ** 2
16

>>> def makeActions():
... acts = []
... for i in range(5): # Use defaults instead
... acts.append(lambda x, i=i: i ** x) # Remember current i
... return acts
...
>>> acts = makeActions()
>>> acts[0](2) # 0 ** 2
0
>>> acts[2](2) # 2 ** 2
4
>>> acts[4](2) # 4 ** 2
16

嵌套作用域中的变量在嵌套的函数被调用时才进行查找,所以它们实际上是最后一次循环迭代中循环变量的值

因为默认参数是在嵌套函数创建时评估的,必须使用默认参数把当前的值传递给嵌套作用域的变量,每一个函数记住自己的变量的值


本章习题

  1. 列表解析放在方括号和圆括号中有什么区别

    []的列表解析会一次在内存中产生结果列表
    ()实际上是生成器表达式:不会一次产生结果列表,而是会返回一个生成器对象,用在迭代环境中时,一次产生结果中的一个元素

  2. 生成器和迭代器有什么关系

    生成器是支持迭代协议的对象:它们有__ next__ 方法,重复前进到系列结果中的下个元素,以及到系列尾端时引发异常

    在Python中,我们可以用def、加()的列表解析的生成器表达式以及以类定义特殊方法__ iter__来创建生成器对象,通过它们来编写生成器函数

  3. 如何分辨函数是否为生成器函数

    生成器函数在其代码中的某处会有一个yield语句
    除此之外,生成器函数和普通函数语法上相同,但是,它们由Python特别编译,以便在调用的时候返回一个可迭代的对象

  4. yield语句是做什么的

    当有了yield语句时,这个语句会让Python把函数特定的编译成生成器;当调用时,会返回生成器对象,支持迭代协议

    yield语句运行时,会把结果返回给调用者,让函数的状态挂起,当调用者再调用__next__方法时,这个函数就可以重新在上次yield语句后继续运行

    生成器也可以有return语句,用来终止生成器

  5. map调用和列表解析有什么关系?比较并对比两者

    map调用类似于列表解析,两者都会收集对序列或其他可迭代对象中每个元素应用运算后的结果(一次一个项目),从而创建新列表

    其主要差异在于,map会对每个元素应用函数,而列表解析则是应用任意的表达式,map需要一个lambda函数才能应用其他种类的表达式,因此列表解析更通用一些

    列表解析也支持扩展语法,例如,嵌套for循环和if分句从而可以包含内置函数filter的功能

CATALOG
  1. 1. 第十六章_函数基础
    1. 1.1. 为何使用函数
    2. 1.2. 编写函数
      1. 1.2.1. def语句
      2. 1.2.2. def语句是实时执行的
    3. 1.3. 第一个例子:定义和调用
      1. 1.3.1. 定义
      2. 1.3.2. 调用
      3. 1.3.3. Python中的多态
    4. 1.4. 第二个例子:寻找序列的交集
      1. 1.4.1. 定义
      2. 1.4.2. 调用
      3. 1.4.3. 重访多态
      4. 1.4.4. 本地变量
    5. 1.5. 本章习题
  2. 2. 第十七章 作用域
    1. 2.1. Python作用域基础
      1. 2.1.1. 作用域法则
      2. 2.1.2. 变量名解析:LEGB原则
      3. 2.1.3. 作用域实例
      4. 2.1.4. 内置作用域
      5. 2.1.5. 在Python 2.6中违反通用性
    2. 2.2. global语句
      1. 2.2.1. 最小化全局变量
      2. 2.2.2. 最小化文件间的修改
      3. 2.2.3. 其他访问全局变量的方法
    3. 2.3. 作用域和嵌套函数
      1. 2.3.1. 嵌套作用域的细节
      2. 2.3.2. 嵌套作用域举例
        1. 2.3.2.1. 工厂函数
        2. 2.3.2.2. 使用默认参数来保留嵌套作用域的状态
        3. 2.3.2.3. 嵌套作用域和lambda
        4. 2.3.2.4. 作用域与带有循环变量的默认参数相比较
        5. 2.3.2.5. 任意作用域的嵌套
    4. 2.4. nonlocal语句
      1. 2.4.1. nonlocal基础
      2. 2.4.2. nonlocal应用
        1. 2.4.2.1. 使用nonlocal进行修改
        2. 2.4.2.2. 边界情况
      3. 2.4.3. 为什么使用nonlocal
        1. 2.4.3.1. 与全局共享状态
        2. 2.4.3.2. 使用类的状态(预览)
        3. 2.4.3.3. 使用函数属性的状态
    5. 2.5. 本章习题
  3. 3. 第十八章 参数
    1. 3.1. 传递参数
      1. 3.1.1. 参数和共享引用
      2. 3.1.2. 避免可变参数的修改
      3. 3.1.3. 对参数输出进行模拟
    2. 3.2. 特定的参数匹配模型
      1. 3.2.1. 基础知识
      2. 3.2.2. 匹配语法
      3. 3.2.3. 细节
      4. 3.2.4. 关键字参数和默认参数的实例
        1. 3.2.4.1. 关键字参数
        2. 3.2.4.2. 默认参数
        3. 3.2.4.3. 关键字参数和默认参数的混合
      5. 3.2.5. 任意参数的实例
        1. 3.2.5.1. 收集参数
        2. 3.2.5.2. 解包参数
        3. 3.2.5.3. 应用函数通用性
        4. 3.2.5.4. 废弃的apply内置函数(Python 2.6)
      6. 3.2.6. Python 3.0 keyword-Only参数
        1. 3.2.6.1. 排序规则
        2. 3.2.6.2. 为何使用keyword-only参数
      7. 3.2.7. min调用
        1. 3.2.7.1. 满分
        2. 3.2.7.2. 加分点
        3. 3.2.7.3. 结论
    3. 3.3. 一个更有用的例子:通用set函数
    4. 3.4. 模拟Python 3.0 print函数
      1. 3.4.1. 使用Keyword-Only参数
      2. 3.4.2. 为什么要在意:关键字参数
    5. 3.5. 本章习题
  4. 4. 第十九章 函数的高级话题
    1. 4.1. 函数设计概念
    2. 4.2. 递归函数
      1. 4.2.1. 用递归求和
      2. 4.2.2. 编码替代方案
      3. 4.2.3. 循环语句VS递归
      4. 4.2.4. 处理任意结构
    3. 4.3. 函数对象:属性和注解
      1. 4.3.1. 间接函数调用
      2. 4.3.2. 函数内省
      3. 4.3.3. 函数属性
      4. 4.3.4. Python 3.0中的函数注解
    4. 4.4. 匿名函数:lambda
      1. 4.4.1. lambda表达式
      2. 4.4.2. 为什么使用lambda
      3. 4.4.3. 如何(不要)让Python代码变得晦涩难懂
      4. 4.4.4. 嵌套lambda和作用域
      5. 4.4.5. 为什么要在意:回调
    5. 4.5. 在序列中映射函数:map
    6. 4.6. 函数式编程工具:filter和reduce
    7. 4.7. 本章习题
  5. 5. 第二十章 迭代和解析,第二部分
    1. 5.1. 回顾列表解析:函数式编程工具
      1. 5.1.1. 列表解析与map
      2. 5.1.2. 增加测试和嵌套循环
      3. 5.1.3. 列表解析和矩阵
      4. 5.1.4. 理解列表解析
      5. 5.1.5. 为什么要在意:列表解析和map
    2. 5.2. 重访迭代器:生成器
      1. 5.2.1. 生成器函数:yield VS return
        1. 5.2.1.1. 状态挂起
        2. 5.2.1.2. 迭代协议整合
        3. 5.2.1.3. 生成器函数应用
        4. 5.2.1.4. 拓展生成器函数协议:send和next
      2. 5.2.2. 生成器表达式:迭代器遇到列表解析
      3. 5.2.3. 生成器函数 VS 生成器表达式
      4. 5.2.4. 生成器是单迭代器对象
      5. 5.2.5. 用迭代工具模拟zip和map
        1. 5.2.5.1. 编写自己的map(func,…)
        2. 5.2.5.2. 编写自己的zip(…)和map(None,…)
      6. 5.2.6. 为什么你会留意:单次迭代
      7. 5.2.7. 内置类型和类中的值生成
    3. 5.3. Python 3.0解析语法概况
      1. 5.3.1. 解析集合和字典解析
      2. 5.3.2. 针对集合和字典的拓展的解析语法
    4. 5.4. 对迭代的各种方法进行计时
      1. 5.4.1. 对模块计时
      2. 5.4.2. 计时脚本
      3. 5.4.3. 计时结果
      4. 5.4.4. 计时模块替代方案
        1. 5.4.4.1. 在Python 3.0中使用keyword-only参数
        2. 5.4.4.2. 交互提示模式
      5. 5.4.5. 其他建议
    5. 5.5. 函数陷阱
      1. 5.5.1. 本地变量是静态检测的
      2. 5.5.2. 默认和可变对象
      3. 5.5.3. 没有return语句的函数
      4. 5.5.4. 嵌套作用域的循环变量
    6. 5.6. 本章习题