📚 文档目录
🎃 类型和对象 - 🎈 程序结构与函数编程 - 🎏 面向对象编程

程序结构与执行

Python程序由一些语句序列组成,这些语句包括变量赋值、函数定义、类和模块导入等,加载源文件时,解释器始终顺序执行每条语句,直到再无语句可以执行,这种执行模式同样适用于作为主程序运行的文件和通过import加载的库文件

条件语句可由if以及可省的elifelse子句构成

在一些编程语言中存在switch语句,譬如C语言中实现成绩等级判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
int a;
printf("Input your score:");
scanf("%d",&a);
switch(a/10){
case 10:
case 9: printf("\nA"); break;
case 8: printf("\nB"); break;
case 7: printf("\nC"); break;
default:printf("\nD");
}
return 0;
}

Python中没有switch关键字,可以通过以下方式替代实现

  1. 利用字典实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    switch={
    10: 'A', #此处需重复定义内容
    9: 'A',
    8: 'B',
    7: 'C',
    }

    a=input('Input your score:')
    print(switch.get(int(int(a)/10),'D'))
  2. 创建一个switch类来处理程序的流转:
    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
    class switch:
    def __init__(self,value):
    self.value=value
    self.fall=False #如果case匹配,且没有break,则置为True,表示继续向下执行,且不论下面的case是否匹配

    def __iter__(self):
    yield self.match #由于yield关键字,__iter__()方法变成一个生成器方法,当调用该方法时将返回一个生成器对象(而生成器又是迭代器,因此switch类实例是一个可迭代对象),此时__iter__()内部实际还未执行,直到调用生成器对象的__next__()方法,返回match函数对象

    def match(self,*args): #args只允许接收0或1个参数
    if self.fall or not args: #如果fall为True,则返回True表示继续执行当前的case子句,或当前case子句没有匹配参数,则表示为default默认分支(请将default放在最后一行,但并无代码严格检查,此代码仅为demo展示)
    return True
    elif args[0]==self.value: #匹配成功
    self.fall=True
    return True
    else: #匹配失败
    return False

    score=96
    for case in switch(score//10): #这个for只迭代一次,case循环变量指向的就是match()函数对象
    if case(10):
    pass
    if case(9):
    print('A')
    break
    if case(8):
    print('B')
    break
    if case(7):
    print('C')
    break
    if case(): #default分支
    print('D')

不过实在没必要,特别是上述第二种方式,过于词不达意,这种for循环的写法就很迷,但是好像也找不到更好的写法

可以使用while(满足条件时不断循环直至条件不再满足)或for(遍历可迭代对象)实现循环,一般推荐使用for循环,如果想在迭代过程中同时获取下标索引,可以将可迭代对象iterable替换成enumerate(iterable),其等同于zip(range(len(iterable)),iterable)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a=10
while (a>0): #while expression等同于while(expression),即加不加括号都一样,expression表达式所得也是一个python对象嘛,而(obj)就并未变成一个元组对象,它仍是obj对象本身,有obj is (obj)成立,除非写成(obj,),这才变成了一个长度为1的元组对象。括号常用于表达式中,用于界定不同运算的优先级别,最内层的括号中的子表达式最先运算,譬如:((a+b)*c)-2
a-=1
print(a)
print() #输出一个空行

i=888
for i in range(10):
print(i)
print(f'i:{i}') #注意输出的是i:9,因为迭代变量(i)的作用域并非for语句私有,即如果前面已经定义过一个相同名称的变量,它的值将被改写

#迭代过程中同时获取下标索引
a='muggledy'
for index,element in enumerate(a):
print(index,element)

注(for-else语句):else语句会在for循环结束之后执行,除非循环过程中遇到break(或者遇到其他意外如抛出异常,当然也不会执行到else语句),特别地,如果循环根本就没有开始(可迭代序列长度为0时),也会执行else语句,一般来说,else语句可以用于编写循环正常结束时的某些后续操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for i in range(4):
pass
else:
print('demo1: for循环全部遍历结束')

for i in []:
pass
else:
print('demo2: for循环全部遍历结束')

for i in range(4):
if i==2:
break
else:
print('demo3: for循环全部遍历结束')

'''OUTPUT
demo1: for循环全部遍历结束
demo2: for循环全部遍历结束
'''

异常会中断正常的程序执行流,可以使用raise exception([value])来手动抛出异常(exception是异常类型,内置异常类型请查阅文献[1]-p70,value是一个与指定异常相关的可选参数,通常是一条自定义的错误消息,如果raise语句不带任何参数,则将会重复引发最近一次出现的异常,如果最近没有出现异常,则抛出运行时异常RuntimeError: No active exception to reraise),使用try-except(能够嵌套使用)可以捕捉异常,except语句中用于编写异常处理程序,其还有一个可选的修饰符as var,可以将捕捉到的异常实例赋给变量var,另外可以在一个try块后跟随多个except块以捕捉多种不同类型的异常,当然也可以在一个except中同时捕捉,这时你可以在except语句块内使用isinstance进行异常类型判断,示例:

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
#示例1
try:
raise
except: #不带任何异常类型时将捕捉所有异常,也可以捕捉指定类型异常:except RuntimeError:
print('出现异常!')

#示例2
try:
raise RuntimeError('运行错误啦!')
except Exception as e: #将异常实例赋值给变量e(Exception是所有非退出异常类型的基类,这样你就可以捕捉任意派生自Exception的异常类型)
print('异常:',e)

#示例3(示例3和示例4都是不可执行的伪代码)
try:
do something may cause IOError, TypeError or NameError...
except IOError as e:
process IOError...
except TypeError as e:
process TypeError...
except NameError as e:
process NameError...

#示例4(示例3的另一种写法)
try:
do something may cause IOError, TypeError or NameError...
except (IOError,TypeError,NameError) as e: #注意括号不可省略
if isinstance(e,IOError):
process IOError...
elif isinstance(e,TypeError):
process TypeError...
else isinstance(e,NameError):
process NameError...

try-except支持else子句(可省),但必须跟随在最后一个except子句后,如果try块中的内容并未引发异常,就会执行else子句中的代码(除非try块直接return返回了)。try-except还支持finally子句(可省),其必须写在最最后,无论try是否出现异常都会执行finally块中的内容(即使在try块、except块或else块中遇到return,也会执行finally块),且在exceptelse之后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
try:
f=open('foo.txt','r')
except IOError as e:
print(f'Fail to open!({e})')
else:
print('Open successfully!')
finally: #如果文件打开(f存在)则关闭之,否则什么也不做
try:
f.close()
except:
pass

#小结:try-except是基本的异常捕获流程,else和finally则都是可选的,一般的执行顺序是:首先执行try中语句,如果没有异常,则跳转到else块,最后是finally块;如果try块中存在异常,则由except捕获异常,最后进入finally块

关于异常捕捉中return返回值的问题,试想,如果try块中存在return语句,通常我们认为函数遇到return会立即返回,确实如此吗?如果存在finally块,那么其中的代码还会执行吗?具体看下面的例子:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#try-except-finally结构
#示例1
def f(a):
try:
return a+1 #首先计算表达式a+1的值然后保存下来,暂不return,转而执行finally块,(由于finally中没有遇到return,于是)结束后会回到此处,return之前保存的表达式值
except:
return a-1
finally:
a+=4
print(f'a={a}')

print(f(1))

'''OUTPUT
a=5
2
'''

#示例2
def f(a):
try:
return a/0 #出现除0异常,显然此return永远也不会执行,直接进入except块
except:
return a-1 #计算表达式a-1的值,然后先保存下来,继续执行finally中的代码,(由于finally中没有遇到return,于是)结束后回到此处,return之前保存的表达式值
finally:
a+=4
print(f'a={a}')

print(f(1))

'''OUTPUT
a=5
0
'''

#示例3
def f(a):
try:
return a+1 #首先计算表达式a+1的值然后保存下来,暂不return,转而执行finally块
except:
return a-1
finally:
a+=4
print(f'a={a}')
return a #在finally中遇到return,那就不管了,函数直接返回,OVER

print(f(1))

'''OUTPUT
a=5
5
'''

#try-except-else-finally结构
#示例4
def f(a):
try:
return a+1 #首先计算表达式a+1的值然后保存下来,暂不return,但因为return的存在,虽然没有异常也不会执行else块,但是finally必须执行,(由于finally中没有遇到return,于是)结束后仍回到此处,return之前保存下来的a+1的值,OVER
except:
return a-1
else:
return a+2
finally:
a+=4
print(f'a={a}')

print(f(1))

'''OUTPUT
a=5
2
'''

#示例5
def f(a):
try:
a+1
except:
return a-1
else:
return a+2 #try中没有异常和return,于是执行else块,首先保存下表达式a+2的值,然后进入finally,(由于finally中没有遇到return,于是)结束后仍回到此处,return之前保存下来的值,OVER
finally:
a+=4
print(f'a={a}')

print(f(1))

'''OUTPUT
a=5
3
'''

#示例6
def f(a):
try:
a+1
except:
return a-1
else:
return a+2 #try中没有异常和return,于是跳转到else块,(由于finally的存在)首先保存下表达式a+2的值,然后进入finally
finally:
a+=4
print(f'a={a}')
return a #finally中有return,于是不会再回到else块中的return位置处,此时函数直接返回,OVER

print(f(1))

'''OUTPUT
a=5
5
'''

要自定义异常类型,需继承Exception基类,引发异常时传递的参数即是__init__()构造函数的参数,可以重载__str__()方法,以自定义异常输出的错误消息,示例:

1
2
3
4
5
6
7
8
9
10
11
class myException(Exception):
def __init__(self,*args): #可以接收n个(任意个)错误信息
self.args=args

def __str__(self):
return f"myException: {','.join(map(lambda i:str(i),self.args))}"

try:
raise myException('错误信息1','错误信息2',8848)
except myException as e:
print(e)

正确地管理各种资源(如文件句柄的获取与关闭、互斥锁的获取与释放和数据库的连接与断开等),在涉及异常时通常都比较棘手,异常的引发很可能导致控制流跳过负责释放某些关键资源的语句,with语句支持在由上下文管理器对象控制的运行时上下文中执行一系列语句,譬如:

1
2
3
4
5
6
#此种写法可以确保文件资源被安全释放
with open('debuglog.txt','a') as f:
f.write('Debugging\n')
...
f.write('Done\n')
#在控制流离开with语句块时会自动执行文件关闭的操作,不论with语句块中是否引发异常

with obj语句在控制流进入和离开其后的相关代码块时,允许(上下文管理器)对象obj管理所发生的的事情,执行with obj语句时,将执行obj.__enter__()来指示进入一个新的上下文,当控制流离开该上下文时,就会执行obj.__exit__(type,value,traceback),如果没有引发异常,__exit__()方法的三个参数均被设为None,否则,它们将包含导致控制流离开上下文的异常相关的类型、值和跟踪信息,__exit__()方法返回TrueFalse,用于指示被引发的异常是否得到处理,如果返回False,引发的任何异常都将被继续向外抛出。with obj语句接受一个可选的as var修饰符,此时obj.__enter__()方法的返回值将被赋给变量var

自定义一个上下文管理器的伪代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class contextor: #类实现的“上下文管理器”
def __init__(self,*args,**kwargs):
#初始化上下文管理器
...

def __enter__(self):
#可以是获取资源等操作
...
return something #返回值将赋给with的as从句所定义的变量var

def __exit__(self, type, value, traceback):
#可以是释放资源等操作
...
return True or False #是否向with语句块外抛出异常

with contextor(*args,**kwargs) as var:
#with语句块
...

还有一种定义上下文管理器的方式,借助contextlib模块的contextmanager装饰器实现,伪代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from contextlib import contextmanager

@contextmanager
def contextor(*args,**kwargs): #函数版本的“上下文管理器”
#语句块1
...
try:
yield expression
except:
#捕捉with语句块内可能发生的异常,如果不捕捉,将被抛给with的外层,这也意味着语句块2(等同于上下文管理器的__exit__()方法代码)不会执行,这与上下文管理器的精神相违背(即不论with语句块内是否引发异常,__exit__()方法都会执行),因此一般都要在yield位置捕捉可能的异常
#2023.5.2 update: 由于支持try...finally写法,因此可以选择外抛异常同时确保语句块2的执行,即将语句块2写在finally中
...
#语句块2
...

with contextor(*args,**kwargs) as var:
#with语句块
...

contextmanager装饰器的原理很简单,其实现依赖于一个名为_GeneratorContextManager的类,这是一个定义了__enter__()__exit__()方法的上下文管理器类型,而被装饰的函数contextor是一个生成器函数,根据装饰器原理有contextor=contextmanager(contextor),返回的是一个闭包函数对象,其包含有一个指向真实contextor函数地址的变量,姑且记作func,当你调用contextor(*args,**kwargs)时将使用argskwargsfunc来初始化构造一个_GeneratorContextManager类实例,姑且记作c,具体的,在其初始化函数__init__()中会执行func(*args,**keargs)得到一个生成器对象,并保存为实例属性,姑且记作c.g,配合with使用时(with contextor(*args,**kwargs) as var),将自动调用c__enter__()方法,而__enter__()的实现即是执行return c.g.__next__(),换句话说,上述生成器函数中yield之前的语句块1将作为上下文管理器的__enter__()方法定义,且yield表达式值将赋给变量var,当离开with语句块,将自动调用c__exit__()方法,而__exit__()的实现即是再一次调用c.g.__next__(),换句话说,yield之后的语句块2将作为上下文管理器的__exit__()方法定义,另外,由于with语句块内部可能引发异常,异常将被c__exit__()方法接收,默认下c.__exit__()返回False,即异常将被抛给with的外层,如果你不想抛出异常,则需要在上述生成器函数的yield位置处捕捉,因为c.__exit__()中会在存在异常的情况下,调用生成器对象的throw()方法,具体的,执行c.g.throw(type,value,traceback),该方法会在yield位置处抛出参数所指异常,如果你手动捕捉了异常,程序会继续向下执行一次生成器的next()方法,即执行语句块2,否则意味着用户定义的上下文管理器的退出方法不会被调用,这一点千万注意。不管何种情况,我们总是应该将语句块2写在finally中,并通过是否写except来决定是否将异常抛到with语句块外

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#示例1(来自文献[1]-p73)
class ListTransaction:
def __init__(self,thelist):
self.thelist=thelist

def __enter__(self):
self.workingcopy=list(self.thelist)
return self.workingcopy

def __exit__(self,type,value,tb):
if type is None:
self.thelist[:]=self.workingcopy
return True #返回True,避免with语句块内的异常继续向上抛给外层

items=[1,2,3]
with ListTransaction(items) as working:
working.append(6)
working.append(7)
#raise ValueError('xxx')
print(items) #[1, 2, 3, 6, 7],如果上一行注释取消,则输出[1, 2, 3]

#示例2
from contextlib import contextmanager
@contextmanager
def ListTransaction(thelist):
workingcopy=list(thelist)
try:
yield workingcopy
except ValueError as e:
print(e)
thelist[:]=workingcopy

items=[1,2,3]
with ListTransaction(items) as working:
working.append(6)
working.append(7)
raise ValueError('xxx')
print(items)
'''OUTPUT
xxx
[1, 2, 3, 6, 7]
'''

#示例3
from contextlib import contextmanager
@contextmanager
def ListTransaction(thelist):
workingcopy=list(thelist)
yield workingcopy #对比示例2,如果不在yield位置处捕捉异常,当with语句块中引发异常时候,yield后面的代码将不会被执行(该部分代码是作为上下文管理器的__exit__()方法定义)
thelist[:]=workingcopy

items=[1,2,3]
try:
with ListTransaction(items) as working:
working.append(6)
working.append(7)
raise ValueError('xxx')
except ValueError as e:
print(e)
print(items)

'''OUTPUT
xxx
[1, 2, 3]
'''

#示例4(2023.5.2 update)
from contextlib import contextmanager
@contextmanager
def ListTransaction(thelist):
workingcopy=list(thelist)
try:
yield workingcopy #对比示例3,虽然没有except捕捉异常,但是finally确保了thelist[:]=workingcopy的执行
finally:
thelist[:]=workingcopy

items=[1,2,3]
try:
with ListTransaction(items) as working:
working.append(6)
working.append(7)
raise ValueError('xxx')
except ValueError as e:
print(e)
print(items)
'''OUTPUT
xxx
[1, 2, 3, 6, 7]
'''

一般格式为assert expression[, msg],其中expressionFalse时,将触发AssertionError异常,msg是可选的自定义错误消息,注意assert不应用于必须执行以确保程序正确的代码,因为如果python运行在最优模式(通过对解释器使用-O选项进入该模式),断言语句将不会执行,那么何时使用assert呢?assert语句用于检查应该始终为真的内容

函数式编程

函数定义使用def关键字(另外可以使用lambda关键字创建表达式形式的匿名函数,形如lambda *args, **kwargs : expression),通常函数的第一条语句会使用文档字符串来描述函数的用途,可以在查看函数帮助时获取到该文档(help(func)),譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def f():
'''This is a demo.
Author: muggledy
'''
pass

print(help(f))

'''OUTPUT
Help on function f in module __main__:

f()
This is a demo.
Author: muggledy
'''

函数作为对象也具有属性,譬如上述文档字符串就存储于函数对象的__doc__属性中,函数名存储在__name__属性中,事实上,可以给函数对象绑定任意属性,这些新增属性保存在函数对象的__dict__属性中,要查看对象的所有属性,则使用dir(obj)函数,或者通过getattr(obj,attr)获取对象的指定属性(另hasattr(obj,attr)检查对象是否具有名为attr的属性,setattr(obj,attr,value)为对象设置属性),函数都是可调用的(callable(func)返回True

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> class A: pass
...
>>> a=A()
>>> setattr(a,'can\'t be touched','zj') #setattr()相当逆天,你可以通过该方法为对象设置一些不能够通过属性访问符.号访问的属性
>>> a.__dict__
{"can't be touched": 'zj'}
>>> a.can't be touched
File "<stdin>", line 1
a.can't be touched
^
SyntaxError: EOL while scanning string literal
>>> getattr(a,'can\'t be touched')
'zj'
>>> hasattr(a,'can\'t be touched')
True

要调用函数,只要使用函数名加上用圆括号括起来的参数即可(函数名和括号之间允许有空格),python中函数传参的形式主要有五种,分别为位置传递、关键字传递、默认传递、包裹传递和解包裹传递(后两种在前一章节已经说过了,就不重复了),看示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#位置传递
def f1(x,y,z):
print(f'{x}+{y}+{z}={x+y+z}')

f1(1,2,3) #1+2+3=6 #即不具名传递,仅凭位置上的一一对应

#关键字传递
f1(4,z=6,y=5) #给所传递的参数值具名的一种传递方式,明确告诉某一参数值是交由函数的哪一个参数变量接收的,如此我们就不需要按照位置参数的固定位置序号排列,可以在函数定义中定义命名关键字参数或者可变字典参数,这两种参数都必须以键值对形式传递,见后

#默认传递
def f2(x,y,z=6):
print(f'{x}+{y}+{z}={x+y+z}')

f2(2,3) #默认参数传递即隐式传递,具有默认值的参数在函数调用时可以不明确给出

若按照参数类型划分,则可分为位置参数、默认参数、可变参数和命名关键字参数,其中默认参数又分为默认位置参数和默认关键字参数(且只有命名关键字参数可以有默认值,其他关键字参数没有或者说没法设置默认值),可变(长)参数又分为可变元组参数和可变关键字参数(或可变字典参数),这里只单说一下“命名关键字参数”,函数参数定义中位于**args(可变元组参数)之后以及可变关键字参数之前(假设存在的话)的就是命名关键字参数,在调用时其必须具名传递,命名关键字参数可以有默认值,也可以没有,譬如:

1
2
3
4
def f3(x,*,y=1,z): #x为位置参数,y和z是命名关键字参数,前者有默认值
print(x,y,z)

f3(1,z=3) #如果调用f3(1,1,3)将报错TypeError: f3() takes 1 positional argument but 3 were given

不同类型参数的组合使用(定义和调用时注意顺序,基本原则是,位置参数在前,关键字参数在后):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#示例1
def f(a,b,c=3,*args): #位置参数+带默认值的位置参数+可变元组参数
print(a,b,c,*args)

f(1,2,c=3) #1 2 3
f(a=1,b=2,c=3) #1 2 3
#f(a=3,b=2,c=1,0) #SyntaxError: positional argument follows keyword argument
f(1,2,3,4,5,6,7) #1 2 3 4 5 6 7

#示例2
def f(a,b,*,x,y,**kwargs): #位置参数+命名关键字参数+可变字典参数
print(a,b,x,y,kwargs)

f(1,2,x=3,y=4,z=5) #1 2 3 4 {'z': 5}
#f(1,2,x=3,z=4) #TypeError: f() missing 1 required keyword-only argument: 'y'

#示例3
def f(a,b,c=3,*args,x=4,y,**kwargs): #最全组合:位置参数+带默认值的位置参数+可变元组参数+(带默认值的,可带可不带)命名关键字参数+可变关键字参数
print(a,b,c,args,x,y,kwargs)

f(1,2,4,3,x=5,y=6,z=7) #1 2 4 (3,) 5 6 {'z': 7}
f(1,2,4,3,y=6,z=7) #1 2 4 (3,) 4 6 {'z': 7}
#f(b=2,a=1,3,4,y=6,x=5,z=7) #SyntaxError: positional argument follows keyword argument

在编程时,尽量避免传递可变对象(如列表、字典等),因为在函数内部对可变对象的修改将反映在原始对象中,若无法避免,则应在函数中使用可变对象的拷贝,另外函数可以不显式写return语句,在运行结束时候,会默认返回None

关于函数的作用域问题(lambda表达式遵循与函数相同的作用域规则),python中只有函数、类、模块会产生作用域(在作用域内定义的变量仅此作用域可见,但注意不同的作用域可能是包含关系,如全局作用域的范围包含局部作用域,全局作用域定义的变量在局部作用域也可访问),代码块不会产生作用域(如for循环变量会直接覆写外部的同名变量,或者说for循环变量可以在外部使用,而不仅限于for循环内部),每执行一个函数,就会创建新的局部命名空间,包含了函数参数名称和在函数体内定义的变量名称,在函数内访问变量首先会在局部空间中查找,找不到再去全局空间查找,具体的,python中有四种作用域:Local(局部作用域)、Enclosing(嵌套作用域)、Global(全局作用域)、Built-in(内置作用域),且变量的查找遵循“LEGB”规则,以内层嵌套函数为例,其首先搜索自己的局部命名空间,没有找到将由内而外在外层函数的局部命名空间中一层层地寻找(因为可以多层函数嵌套),仍没有找到,才会到全局命名空间和内置命名空间查找,如果还没有,则引发NameError异常

可以在函数内部使用global关键字将(局部)变量动态声明为全局变量,使其位于全局命名空间,影响全局作用域,如:

1
2
3
4
5
6
7
8
def f():
global x #global语句可以放在函数内任意行或重复使用
x=8 #如果外层已经存在此变量,则函数内部的修改将体现在外层空间
print('局部命名空间:',locals()) #空,此函数未创建任何局部变量
print('全局命名空间:',globals()) #上面创建的变量x位于全局命名空间

f()
print(x) #8

类似的,对于嵌套函数,尽管它可以访问到嵌套作用域中的变量,一旦尝试对其执行如赋值等操作(如果是可变对象,则可以修改之,且无需声明nonlocal),实际会在嵌套函数的局部命名空间中新建一个同名变量,python3提供了nonlocal关键字,该声明会将名称动态绑定为外层函数中首次访问到的同名变量(多层嵌套时,可能多个外层函数都定义了同一变量,具体绑定哪个就看谁最靠内最先被访问到),于是可以在嵌套函数中修改并体现在外层,譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def f():
i=4
j=7
k=13
def inner1():
nonlocal i
j=70
def inner2():
nonlocal i,j #nonlocal限定于python3且嵌套函数使用,用于绑定至外层函数的局部变量,当声明为nonlocal变量时,该变量必须能在嵌套作用域中查获(见__closure__属性),否则报错,具体的,此处i绑定为f中的变量i,j绑定在inner1中的变量j
i=6
print(j) #70
j=8
print(k) #13
i=5
print(i) #5
inner2()
print(i) #6
inner1()
print(j) #7

f()

变量必须先赋值或定义才能使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
i=3
def f():
print(i) #i实际是局部变量,打印前需先赋值
i+=3 #尽管函数内部可以访问全局命名空间中的变量,一旦对其进行赋值等操作(可变对象则可以随意修改,而无需声明`global`),将会在函数内部创建一个同名的局部变量
print(i)

f()

'''OUTPUT
Traceback (most recent call last):
File "C:\Users\muggledy\Desktop\ttt2.py", line 7, in <module>
f()
File "C:\Users\muggledy\Desktop\ttt2.py", line 3, in f
print(i)
UnboundLocalError: local variable 'i' referenced before assignment
'''

函数作为第一类对象(所谓第一类对象的特性是,对象可以赋值给变量、对象可以被当做参数传递、对象可以被当做函数的返回值返回、对象可以作为元素被添加到容器类型中),在下面的例子中,函数接受另一个函数作为输入并调用它:

1
2
3
4
#定义于文件foo.py,示例来自文献[1]-p79
x=6
def callf(func):
return func()

下面的例子使用了上述函数:

1
2
3
4
5
6
import foo
x=8 #如果注释掉此句,将报错NameError: name 'x' is not defined
def helloworld():
return f'Hello World. x is {x}'

print(foo.callf(helloworld)) #Hello World. x is 8

注意helloworld()使用的x的值是在与它相同的环境中定义的,事实上,解释器会将函数及其所在执行环境整个打包在一起,得到的函数对象称为“闭包函数”,每个函数都拥有一个指向定义该函数的全局命名空间的__globals__属性,对于嵌套函数,闭包同样将捕捉嵌套函数执行所需的整个环境,因此除了__globals__属性,还有一个指向定义该函数的嵌套作用域空间的__closure__属性,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def greeting_conf(prefix):
def greeting(name):
print (prefix+' '+name) #greeting函数访问了外部定义的局部变量prefix,事实上在greeting_conf返回后,对greeting_conf可见的局部命名空间已经随着栈帧的销毁而销毁了,即locals()不可再获得,但是嵌套函数greeting会保存所需的执行环境,也就是说greeting_conf中定义的部分(为内层函数所使用到的)局部变量会继续存在于greeting的__closure__属性中
return greeting

mGreeting=greeting_conf('Good Morning')
aGreeting=greeting_conf('Good Afternoon') #绑定了不同__closure__属性的闭包函数aGreeting和mGreeting是两个不同的函数对象,两者具有不同的执行环境,体现在两者id的不同
mGreeting('dy')
mGreeting('zj')
aGreeting('dxd')
aGreeting('zj')

print(mGreeting.__closure__,mGreeting.__closure__[0].cell_contents)
print(aGreeting.__closure__,aGreeting.__closure__[0].cell_contents)

'''OUTPUT
Good Morning dy
Good Morning zj
Good Afternoon dxd
Good Afternoon zj
(<cell at 0x000002A9B0E97288: str object at 0x000002A9B10DA170>,) Good Morning
(<cell at 0x000002A9B10687C8: str object at 0x000002A9B10DA1B0>,) Good Afternoon
'''

在开发过程中很可能需要对一些已有的函数进行改进,假设有一个用户注册函数register(),需要提交手机号码、用户名和注册邮箱信息,其中手机号码是数据库主键,具有唯一性,有天上司要求用户名也必须唯一,出于某些原因还不能破坏原有代码,怎么办?使用装饰器可以轻松完成这一工作,只需要编写一个检测是否存在相同用户名的装饰器函数if_unique_username(),然后在原有的register()函数定义前添加一行语句@if_unique_username就ok了,等价于执行register=if_unique_username(register)@就是简化该操作的语法糖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def if_unique_username(func): #装饰器函数
def _(name,phone,email):
'''DOC: 检测用户名唯一性函数'''
print('用户姓名唯一性检测')
return func(name,phone,email)
#_.__doc__=func.__doc__ #如果借助functools模块,可以将此句替换为functools.update_wrapper(_,func),效果等同于使用@functools.wraps(func)修饰_()函数
return _

@if_unique_username #装饰器的实现原理依据闭包函数可以访问其执行环境变量或者说可以访问其嵌套作用域空间变量,这个嵌套作用域空间是绑定在函数对象上的(__closure__)
def register(name,phone,email): #被装饰函数
'''DOC: 用户注册函数'''
print('手机号唯一性检测')
print('...')
print(f'用户({name},{phone},{email})注册成功')

register('dy','152618xxxxx','31012xxxxx@qq.com')

'''OUTPUT
用户姓名唯一性检测
手机号唯一性检测
...
用户(dy,152618xxxxx,31012xxxxx@qq.com)注册成功
'''

有时候被装饰函数带有多种类型参数,譬如上述示例中装饰器if_unique_username()内层函数也需要定义和被装饰函数register一模一样的参数,其实没必要这样麻烦,可以用(*args,**kwargs)参数组合来接收任意数量的位置或关键字参数,因此实现一个通用装饰器如下:

1
2
3
4
5
6
7
8
def decorator(func):
@wraps(func) #更简易使用我的改进版@wraps_fix,后面会详细介绍
def wrapper(*args,**kwds): #即使没有参数也可以这样写
#其他操作...
ret=func(*args,**kwds)
#其他操作...
return ret
return wrapper

由于数据量太大,上司决定将用户数据分地区存储在不同的数据库里,在登陆的时候根据用户的所在地来选择相应的数据库,我们希望能够给装饰器指定不同的(地区)参数,最终效果大概是:

1
2
3
4
@select_database(area='江苏') #等同于执行:register=select_database(area='江苏')(register)
def register():
#用户注册操作
pass

依据装饰器的原理,该功能其实不难实现,只需要定义一个三层嵌套的装饰器函数:

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
def select_database(area):
def _1(func):
def _2(*args,**kwargs):
'''DOC: 选择地区数据库函数'''
print(f'选择{area}地区数据库')
return func(*args,**kwargs)
#_2.__doc__=func.__doc__ #如果借助functools模块,可以将此句替换为functools.update_wrapper(_2,func),效果等同于使用@functools.wraps(func)修饰_2()函数
return _2
return _1

@select_database('江苏')
def register(name,phone,email):
'''DOC: 用户注册函数'''
print('手机号唯一性检测')
print('...')
print(f'用户({name},{phone},{email})注册成功')

register('dy','152618xxxxx','31012xxxxx@qq.com')

'''OUTPUT
选择江苏地区数据库
手机号唯一性检测
...
用户(dy,152618xxxxx,31012xxxxx@qq.com)注册成功
'''

实际上,我们可以堆砌多个装饰器(装饰器链):

1
2
3
4
5
6
7
8
9
@select_database('江苏')
@if_unique_username
def register(name,phone,email): #等同于执行register=select_database('江苏')(if_unique_username(register))
'''DOC: 用户注册函数'''
print('手机号唯一性检测')
print('...')
print(f'用户({name},{phone},{email})注册成功')

register('dy','152618xxxxx','31012xxxxx@qq.com')

现在有一个问题,假设你查看经过修饰后的register()函数文档register.__doc__,会输出DOC: 选择地区数据库函数,这不符合预期,但是原因也很明显,变量名称register最终已实际指向select_database()中的内层嵌套函数_2(),解决这个问题也很容易,只要改一下_2()的函数文档就好了,于是在_2()函数定义后写一句_2.__doc__=func.__doc__、同时在if_unique_username()中的内层嵌套函数_()定义后写一句_.__doc__=func.__doc__即可,当然不只是__doc__属性,__name__等函数属性也发生了变化,另外,上述操作还可以通过装饰器来实现以简化调用(见下我自定义的wraps()函数,然后分别在_2()函数和_()函数定义上方写上一句@wraps(func)即可):

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 wraps(foo):
def _1(func):
def _2(*args,**kwargs):
return func(*args,**kwargs)
_2.__name__=foo.__name__ #此处只修改了__name__和__doc__属性,仅作为demo展示
_2.__doc__=foo.__doc__
return _2
return _1

def decorator(func):
@wraps(func)
def wrapper(): #修饰后的wrapper变量名称实际指向wraps()中的内层嵌套函数_2(),于是原始被装饰函数foo变量名称也是指向_2()函数,因此要将foo()的函数文档赋给_2.__doc__即可
'''wrapper function'''
print('功能扩展')
return func()
return wrapper

@decorator #等同于执行:foo=decorator(foo)=wraps(foo)(wrapper),于是foo变量名实际指向wraps()中的_2()函数
def foo():
'''foo function'''
print('原函数')

print(foo.__name__) #foo
print(foo.__doc__) #foo function

不过标准的解决方案还是调用functools模块的@wraps装饰器,用法同上,官方实现如下(借助偏函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
wrapper.__wrapped__ = wrapped
return wrapper

def wraps(wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
return partial(update_wrapper, wrapped=wrapped,
assigned=assigned, updated=updated)

根据上述源码,除了使用@wraps修饰外,还有一种用法(直接调用update_wrapper()函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from functools import update_wrapper
def decorator(func):
def wrapper():
'''wrapper function'''
print('功能扩展')
return func()
update_wrapper(wrapper,func)
return wrapper

@decorator
def foo():
'''foo function'''
print('原函数')

print(foo.__name__) #foo
print(foo.__doc__) #foo function

另外还可以通过foo=foo.__wrapped__方便地将被装饰函数恢复为原函数(即“解除装饰器”),如果没有使用@functools.wraps,那么__wrapped__属性是不可获得的,需要注意的是,这仅仅适用于单个装饰器的情况,假设你堆砌了多个装饰函数,那么访问__wrapped__属性获取到的就不再是原函数了:

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
from functools import wraps
def decorator1(func):
@wraps(func)
def wrapper1():
'''wrapper1 function'''
print('功能扩展1')
return func()
return wrapper1

def decorator2(func):
@wraps(func)
def wrapper2():
'''wrapper2 function'''
print('功能扩展2')
return func()
return wrapper2

@decorator2
@decorator1
def foo():
'''foo function'''
print('原函数')

foo.__wrapped__() #结果指向decorator1()中的内层函数wrapper1(),而非希望的foo(),原因也很明显,上面对foo()的修饰,等同于执行了foo=decorator2(decorator1(foo)),具体的,第一步执行temp=decorator1(foo)时,根据functools.wraps原理,于是wrapper1.__wrapped__属性被赋值为foo,即指向foo()函数,第二步执行decorator2(temp)时,于是wrapper2.__wrapped__属性被赋值为temp,而temp变量实际指向wrapper1()函数,另修饰后的foo变量实际指向wrapper2()函数,于是foo.__wrapped__返回的是wrapper1()(闭包)函数,不过这时你也发现了,要获取到原函数,只要再迈出一步,即调用foo.__wrapped__.__wrapped__,返回的就是原始的foo()函数了

'''OUTPUT
功能扩展1
原函数
'''

要想实现多层装饰器的解除,可以稍稍修改update_wrapper()函数源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
- wrapper.__wrapped__ = wrapped
+ if hasattr(wrapped, '__wrapped__'):
+ wrapper.__wrapped__ = getattr(wrapped, '__wrapped__')
+ else:
+ wrapper.__wrapped__ = wrapped
return wrapper

如果不想修改源码,可以这样改:

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
from functools import wraps
def decorator1(func):
@wraps(func)
def wrapper1():
'''wrapper1 function'''
print('功能扩展1')
return func()
###额外添加的代码
if hasattr(func,'__wrapped__'):
wrapper1.__wrapped__=getattr(func,'__wrapped__')
else:
wrapper1.__wrapped__=func
###以实现多装饰器的正常解除
return wrapper1

def decorator2(func):
@wraps(func)
def wrapper2():
'''wrapper2 function'''
print('功能扩展2')
return func()
###额外添加的代码
if hasattr(func,'__wrapped__'):
wrapper2.__wrapped__=getattr(func,'__wrapped__')
else:
wrapper2.__wrapped__=func
###以实现多装饰器的正常解除
return wrapper2

@decorator2
@decorator1
def foo():
'''foo function'''
print('原函数')

foo.__wrapped__() #原函数

代码很简单,缺点是代码重复编写,此处我再给出一种解决方案💡:

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
from functools import wraps

def wraps_fix(f1):
def _1(f2):
@wraps(f1)
def _2(*args,**kwds):
return f2(*args,**kwds)
if hasattr(f1,'__wrapped__'):
_2.__wrapped__=getattr(f1,'__wrapped__')
else:
_2.__wrapped__=f1
return _2
return _1

def decorator1(func):
@wraps_fix(func) #完美!
def wrapper1():
'''wrapper1 function'''
print('功能扩展1')
return func()
return wrapper1

def decorator2(func):
@wraps_fix(func)
def wrapper2():
'''wrapper2 function'''
print('功能扩展2')
return func()
return wrapper2

@decorator2
@decorator1
def foo():
'''foo function'''
print('原函数')

foo.__wrapped__() #原函数

除了使用三层嵌套函数实现带参数的装饰器,还可以借助偏函数实现,仍以选择地区数据库注册用户为例:

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
from functools import wraps,partial
def select_database2(area,func=None):
if func is None:
return partial(select_database2,area)
@wraps(func)
def wrapper(*args,**kwargs):
print(f'选择{area}地区数据库')
return func(*args,**kwargs)
return wrapper

@select_database2('江苏')
def register(name,phone,email):
'''DOC: 用户注册函数'''
print('手机号唯一性检测')
print('...')
print(f'用户({name},{phone},{email})注册成功')

register('dy','152618xxxxx','31012xxxxx@qq.com')

'''OUTPUT
选择江苏地区数据库
手机号唯一性检测
...
用户(dy,152618xxxxx,31012xxxxx@qq.com)注册成功
'''

网上看到一种通过类实现带参装饰器的方法,核心是借助于特殊函数__call__()使类实例变得可调用,此时上述案例可以这样写:

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
class select_database3:
def __init__(self,area):
self.area=area

def __call__(self,func):
def wrapper(*args,**kwargs):
print(f'选择{self.area}地区数据库')
return func(*args,**kwargs)
return wrapper

@select_database3('江苏')
def register(name,phone,email):
'''DOC: 用户注册函数'''
print('手机号唯一性检测')
print('...')
print(f'用户({name},{phone},{email})注册成功')

register('dy','152618xxxxx','31012xxxxx@qq.com')

'''OUTPUT
选择江苏地区数据库
手机号唯一性检测
...
用户(dy,152618xxxxx,31012xxxxx@qq.com)注册成功
'''

不带参数的通用的基于类实现的装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
from functools import update_wrapper

class decorator:
def __init__(self,func):
self.func=func
update_wrapper(self,self.func)

def __call__(self,*args,**kwargs):
#其他操作...
ret=self.func(*args,**kwargs)
#其他操作...
return ret

最后再看几个装饰器实现的案例,一是@singledispatch,被@singledispatch装饰的普通函数会变为泛函数,即根据第一个参数的类型以不同方式执行重载函数,称为单分派(而根据多个参数选择指定的函数,称为多分派,顾名思义singledispatch是单分派),如下:

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
from functools import singledispatch

class cattle:
'''牛'''
def __str__(self):
return 'cattle'

class fish:
'''鱼'''
def __str__(self):
return 'fish'

@singledispatch
def eat(obj): #(1) #eat是泛函数,任意类对象只要没有“注册”都将调用该方法,对于注册了的譬如cattle和fish类,则调用对应的重载函数
print(str(obj)+' eats something')

@eat.register(cattle)
def _(obj): #(2) #_是一个无意义名字,当然也可取名为cattle_eat
print(str(obj)+' eats grass')

@eat.register(fish)
def _(obj): #(3)
print(str(obj)+' eats shrimp')

eat(cattle()) #cattle eats grass
eat(fish()) #fish eats shrimp
eat('muggledy') #muggledy eats something

#动物都需要进食,但是不同对象“吃”的行为却各不相同,牛吃草、小鱼吃虾米等等,如果用面向对象继承方式实现,基类“吃”的方法就是上述代码中的(1),牛继承基类并重载“吃”的方法,见(2),同理,小鱼的“吃”见(3)

二是带记忆装置的斐波那契数项计算,上一章我们讨论过了使用递归计算斐波那契数项的问题,代码很容易理解,但是性能却十分差,要改进它,一个合理的想法是为其配备“记忆装置”,对于计算过了的值我们直接从中获取以免重复计算造成资源浪费,利用装饰器我们不需要破坏原有代码,十分优美:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def memory(func):
m={}
def wrapper(n):
if m.get(n) is None:
m[n]=func(n)
return m[n]
return wrapper

@memory
def Fibonacci(n=6):
if n in [1,2]:
return 1
else:
return Fibonacci(n-2)+Fibonacci(n-1)

print(Fibonacci(35))
@lru_cache

由于计算机内存有限,记忆装置的缓存大小也应受限,但我们要做的不仅仅是限制缓存大小,还要基于LRU(Least Recently Used)“最近最少使用”思想来移除缓存中的过时数据,该算法的核心思想是,如果一个数据最近被访问过,则有理由相信它在将来被访问的概率也很高,反之,如果一个数据最近未被访问过,则可以认为它在将来被访问的概率也很低,实现它的最佳方式是使用“基于LRU的有序字典”来实现(按照访问频率高低按序排列,这就需要一个线性表,但是线性表查找时间复杂度为O(n),考虑到字典查找时间复杂度为O(1),因此可以通过next指针将字典中的元素依次串联起来,构成一个有序字典),官方文档collections.OrderedDict中基于OrderedDict实现了这样一个LRU字典,代码简洁明了(最后用下述LRU替换我上面写的memory装饰器函数中的变量m即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class LRU(OrderedDict):
'Limit size, evicting the least recently looked-up key when full'

def __init__(self, maxsize=128, *args, **kwds):
self.maxsize = maxsize
super().__init__(*args, **kwds)

def __getitem__(self, key):
value = super().__getitem__(key)
self.move_to_end(key)
return value

def __setitem__(self, key, value): #这个有序字典从头到尾的使用频率是增高的,新增(__setitem__())的自然在最右边,如果访问(__getitem__())某元素则将其移动到末尾
super().__setitem__(key, value)
if len(self) > self.maxsize:
oldest = next(iter(self))
del self[oldest]

下面进入正题,看看@lru_cache的具体使用,首先导入:from functools import lru_cache,然后用于装饰你希望被装饰的递归函数:@lru_cache(maxsize=128, typed=False),待缓存条目是被装饰函数的输入(作为缓存项的键)和输出(作为缓存项的值),如果maxsize参数设为None,那么缓存可以无限制增长,而当maxsize是2的次幂时,LRU功能表现最佳,如果typed参数设为True,那么同一数值但类型不同的函数参数调用将对应不同的缓存项(须知,对于被装饰函数,相同输入必须具有相同输出,假设输出不具有确定性,譬如受制于随机性函数,则@lru_cache失效,无法达到节省时间的效果)

1
2
3
4
5
6
7
8
9
10
from functools import lru_cache

@lru_cache(maxsize=128, typed=False)
def Fibonacci(n=6):
if n in [1,2]:
return 1
else:
return Fibonacci(n-2)+Fibonacci(n-1)

print(Fibonacci(35)) #9227465

装饰器@lru_cache提供了cache_clear()方法用于清除缓存或使缓存无效。一般说来,当我们使用装饰器之后,将“丢失”被装饰函数,被装饰函数的名字总是指向装饰函数,但@lru_cache类似@wraps也提供了__wrapped__属性,指向原始被装饰函数

三是使用装饰器修饰类对象,变相地执行继承,已知有一个限定了实例属性的类,要扩展一个额外的属性,但是不允许破坏原有代码,显然解决办法是继承这个类,并使新类代替旧类,该过程可以通过装饰器实现,比较新奇:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def addLength(oldcls):
class NewClass(oldcls):
__slots__=['_length']

@property
def length(self):
if hasattr(self, '__len__'):
self._length = len(self)
return self._length

@length.setter
def length(self, value):
self._length = value
return NewClass #返回继承自(oldcls)旧类Tool的新类

@addLength
class Tool: #Tool=addLength(Tool),变量名称Tool指向继承Tool的新类NewClass
__slots__=['width','height']

t = Tool()
t.width=8
t.height=9
t.length = 10 #新增属性

还是以用户注册功能函数register()为例,设其接收两个参数,用户所属国家country和姓名name,对于某一地区,绝大部分人的国家属性相同,那么理应给country参数设置一个默认值,从而简化调用,具体的,定义一个参数配置函数configure(),接收原函数register地址和要设置的参数country值,并返回一个闭包函数(称为原函数的“偏函数”),当调用此闭包函数时,只需要再传入剩下的name参数,闭包函数内部会根据之前保存的country参数和刚传入的name参数去执行原函数register()实现用户注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def register(country,name):
print(f'[{country}]{name}用户注册')
pass

register('中国','muggle')

def configure(func,country): #python内置的实现相同功能的函数名称叫做partial
def _(name):
return func(country,name)
return _

register_china=configure(register,'中国')
register_china('msy')
register_china('xiaodong')

register_japan=configure(register,'日本')
register_japan('艹')
register_japan('SB')

通用的配置函数形式:

1
2
3
4
def configure(func,*args1,**kwargs1):
def _(*args2,**kwargs2):
return func(*args1,*args2,**kwargs1,**kwargs2)
return _

所谓偏函数(或者翻译成局部应用),即是固定住原函数的部分参数(设为k个),以实现简化调用的目的(设原函数共有n个参数,局部应用将一个n元函数转换成一个n-k元函数),另有一个概念叫做“柯里化”,是一种将接受多个参数的函数转变成接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术(柯里化将一个n元函数转换成n个一元函数),可能不太好理解,直接看示例,一个实现三个数相加的加法操作:

1
2
def add(a,b,c):
return a+b+c

其柯里化版本为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def add(a):
def _f1(b):
def _f2(c):
return a+b+c
return _f2
return _f1

add(1)(2)(3) #6

#可化简形式:
def add(a):
return lambda b:lambda c: a+b+c

#或者:
add=lambda a:lambda b:lambda c:a+b+c
add(1)(2)(3) #6

显然,我们不可能每次都花这么大的功夫为一个函数特别定制其柯里化版本,为此,定义了一个转换函数curry(),能够接收一个函数对象作为参数,并将其转换为柯里化版本:

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
#这是一个更具通用性的curry(),随后你除了可以这样调用add(1)(2)(3),还可以add(1)(2,3)或add(1,2)(3)
#改进版本参见:https://github.com/muggledy/myworld/blob/master/src/_base/decorator.py
def curry(func):
argsnum=func.__code__.co_argcount
allargs=[]
def curried(*args):
nonlocal allargs
allargs+=list(args)
if len(allargs)>=argsnum:
ret=func(*allargs[:argsnum])
allargs=[]
return ret
else:
return curried
return curried

'''设计思路
1.curry实现对一个函数的柯里化,因此只接收一个函数参数func,返回一个新的函数curried,其为func的柯里化版本
2.每次curried被传递进参数并调用,curried都会检查是否已接收了足够的参数(使用allargs存储),当其个数大于或等于原函数参数func定义的参数个数(由argsnum记录),此时立即计算最终结果并返回,否则继续返回curried以接收更多参数
'''

curriedAdd=curry(add) #curriedAdd是原函数add的柯里化版本,如果需要覆盖原函数,就直接add=curry(add),而这正好可以使用装饰器的语法糖@来完成,更优美
print(curriedAdd(1)(2)(3)) #6
print(curriedAdd(1)(2,3)) #6
print(curriedAdd(1,2)(3)) #6
print(curriedAdd(1,2)(3,4)) #6
#curriedAdd(1,2,3)(4) #TypeError: 'int' object is not callable,由于curriedAdd(1,2,3)已经返回了正确结果6,整数显然是不可调用的,因此报错,对于curriedAdd(1,2)(3,4),curriedAdd(1,2)返回的是一个函数,还能继续接收参数,但是只需要再接收一个就能正确计算,因此4被忽略(当然你也可以根据参数数量不一致raise报错),最终仍返回6

除了上述通用的配置函数实现,我在PEP文档中找到了一个利用__call__()实现的类版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
class partial(object):

def __init__(*args, **kw):
self = args[0] #__init__()的第一个参数是self对象
self.fn, self.args, self.kw = (args[1], args[2:], kw) #args[1]是原函数,剩下的就是要预先固定的参数

def __call__(self, *args, **kw):
if kw and self.kw:
d = self.kw.copy()
d.update(kw)
else:
d = kw or self.kw
return self.fn(*(self.args + args), **d)

最后要说的是,python中实现偏函数的标准方法是通过from functools import partial,另外,除了为函数预设部分参数值构成偏函数,实际上任何可以通过()调用的都可以进行“偏应用”,譬如类、类实例、类/实例方法都可以通过()调用:

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
from functools import partial

class A:
def __init__(self,m,n):
self.m=m
self.n=n
def __call__(self,k,flag): # k为乘积系数,flag表示是否输出结果
if flag:
print('m*k:',self.m*k,'\nn*k:',self.n*k)
else:
pass
def hello(self,f1,f2): # 如果两个参数都是True,那么输出hello world,否则输出screw it
if f1 and f2:
print('hello,world!')
else:
print('screw it!')

#1.类A的参数m既定为3,那么我们就通过偏函数为类A的构造函数固定住这个参数m,其值为3
A_m=partial(A,3) # 偏应用的类A
obj_1=A_m(4)
print(obj_1.m,obj_1.n) # 3 4
obj_2=A_m(5)
print(obj_2.m,obj_2.n) # 3 5

#2.当类实现了__call__方法后,类实例对象将可以像函数一样被()调用,既然如此,我们为其固定住flag参数为True
obj=A(7,8)
obj_true=partial(obj,flag=True)
obj_true(2) # m*k: 14 \nn*k: 16
obj_true(3) # m*k: 21 \nn*k: 24

#3.实例方法,本质就是函数对象,我们将hello方法的f2参数置为True,是否输出问候语只需要f1参数
obj_hello=partial(obj.hello,f2=True) #事实上,实例属性方法访问返回的也是一个固定了self参数的偏应用
obj_hello(True) # hello,world!
obj_hello(False) # screw it!

在函数内,yield语句还可以用作出现在赋值运算右边的表达式,例如:

1
2
3
4
5
def receiver():
print('Ready to receive')
while True:
n=(yield)
print(f'Got {n}')

以这种方式使用yield语句的函数称为“协程”(协程反映了程序逻辑的一种需求:可重入(子程序/函数的)能力。协程能保留上一次调用时的状态,每次重入时,就相当于进入上一次调用的状态),它的执行是为了响应发送给它的值,其行为也类似于生成器:

1
2
3
4
5
6
7
8
>>> r=receiver() #此时函数尚未执行
>>> r.__next__() #首次调用next()方法以向前执行到第一条yield语句处并暂停
Ready to receive
>>> r.send(1) #发送值到yield处,且发送的值将被直接赋给yield赋值号左边的变量,然后继续向前直至再次遇到yield并暂停
Got 1
>>> print(r.send('Hello'))
Got Hello
None

上述示例中在执行发送前需要先调用一次next()方法,不太方便,可以定义一个装饰器以自动调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def coroutine(func):
def _(*args,**kwargs):
g=func(*args,**kwargs)
next(g) #next()返回第一个遇到的yield语句中的表达式值(yield [expression]),假设expression缺省,则返回None
return g
return _

@coroutine
def receiver():
print('Ready to receive')
while True:
n=(yield)
print(f'Got {n}')

r=receiver()
r.send(1)
r.send('Hello')

协程的运行一般是无限期的,除非它被显式关闭(调用close()方法)或者自己退出,关闭后如果继续给协程发送值,将会引发StopIteration异常,正如之前在关于生成器的内容中所讲,close()操作将会在协程内部yield位置处引发GenaratorExit异常,你可以选择捕捉它。另外调用throw(exctype[,value[,tb]])方法也会在协程内部yield位置处引发异常,异常的类型、值和跟踪对象分别由exctypevaluetb参数指定

如果赋值号右边的yield语句本身含有表达式(不同于上面的var=yield,而是var=yield expression的形式),这个表达式的值将作为send()方法的返回值,此时协程可以使用yield语句同时接收和发出返回值,下面看一个生产者和消费者的示例:

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
import time

@coroutine
def consumer():
r = None
while True:
n = yield r
print('[CONSUMER] Consuming %s...' % n)
time.sleep(1)
r = '200 OK'

def producer(c):
n = 0
while n < 5:
n += 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n) #将生产值发送给消费者,并接收消费者的反馈
print('[PRODUCER] Consumer return: %s' % r)
c.close() #生产结束后关闭消费者,当然也可以在外面关闭

c = consumer()
producer(c)

'''OUTPUT
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
'''

需要注意的是,最开始的next()调用只会执行到赋值号右边的(yield [expression])处(且直到执行send()时才会完成赋值),并返回yield中的表达式expression值,在接下来的send()调用中,协程将从暂停处继起,直到再次遇到(yield [expression])暂停,并将此时计算得到的expression作为send()方法的返回值

  • 列表生成式

    1
    2
    3
    4
    5
    >>> ret=[x * x for x in range(10)]
    >>> ret
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    >>> type(_)
    <class 'list'>
  • 集合生成式

    1
    2
    3
    4
    5
    >>> ret={x * x for x in [-5,-3,-1,1,3,5]}
    >>> ret
    {25, 9, 1}
    >>> type(_)
    <class 'set'>
  • 字典生成式

    1
    2
    3
    4
    5
    >>> ret={x:x for x in range(5)}
    >>> ret
    {0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
    >>> type(_)
    <class 'dict'>
  • 生成器表达式

    和列表生成式几乎一样,只不过将中括号[]替换成()即可,返回的是生成器对象

注1,以上所有的生成式都支持多重for循环,如:[(i,j,k) for i in range(4) for j in range(3) for k in range(2)](注意for循环的顺序是从左至右的,即左边for循环的迭代变量可以出现在右边for循环中但是反之不可以),譬如:

1
2
3
4
5
>>> ret=((x,y) for x in range(5) for y in range(x))
>>> ret
<generator object <genexpr> at 0x0000024BBAABBF10>
>>> list(_)
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2), (4, 0), (4, 1), (4, 2), (4, 3)]

注2,以上所有的生成式都可以搭配if条件判断(没有else),以列表生成式为例:[expression for i in iterable if condition](条件判断的作用同filter()函数,用于过滤序列中的部分元素),由于expression本身也可以是一个条件表达式,因此还可以写成:[expression1 if condition else expression2 for i in iterable],示例:

1
2
3
4
5
6
7
8
>>> [i for i in range(10) if i%3==0] #从0-9的序列中筛选出3的整数倍
[0, 3, 6, 9]
>>> [(1 if i%3==0 else 0) for i in range(10)] #如果是3的整数倍,则置为1否则置为0
[1, 0, 0, 1, 0, 0, 1, 0, 0, 1]
>>> ['3+' if i>3 else str(i) for i in range(10) if i%3==0] #从0-9的序列中筛选出3的整数倍,且是字符串形式,进一步,如果大于6,则置为'3+'
['0', '3', '3+', '3+']
>>> [(i,j) for i in range(5) if i>2 for j in range(3)]
[(3, 0), (3, 1), (3, 2), (4, 0), (4, 1), (4, 2)]