Python学习笔记——8.函数与类进阶
现在关注几个函数和类的进阶概念:迭代器,生成器,闭包,装饰器等。
迭代器
Python 内置的列表等可以支持 for 遍历,本质上是因为列表等提供了迭代所需要的接口,我们可以让自定义类也支持迭代遍历。
首先,for 遍历的实质,以下面的例子说明 1
2
3
4
5
6
7
8
9s = 'abcd'
it = iter(s)
print(next(it)) # a
print(next(it)) # b
print(next(it)) # c
print(next(it)) # d
print(next(it)) # 迭代终止,抛出StopIteration异常
对于一个自定义容器类
Demo
,我们希望它支持迭代器遍历,那么需要实现如下的内容:
- 容器类(或者说可迭代对象)提供
Demo.__iter__
方法,返回一个迭代器类Iter
,迭代器需要记录当前状态 - 迭代器类提供
Iter.__next__
方法,返回容器中的元素,并且在迭代完成后抛出StopIteration
异常,这个异常会被 for 语句自动捕获。
例如一个反向迭代器 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class ReverseIter: # 反向迭代器
def __init__(self,container):
self.data = container.data
self.index = len(self.data)
def __next__(self):
if self.index == 0:
raise StopIteration # 迭代结束,抛出异常
self.index -= 1
return self.data[self.index]
class DemoContainer: # 容器,负责提供反向迭代器
def __init__(self,data):
self.data = data
def __iter__(self):
return ReverseIter(self)
for i in DemoContainer('abcde'):
print(i,end=' ')
'''
e d c b a
'''
生成器
含有 yield
关键字的函数就是生成器,可以视作一个函数化的迭代器,或者说将函数变成了一个有状态的可迭代的对象。
在调用生成器函数时,返回的其实不是函数或者它的结果,而是一个生成器对象,生成器自动支持迭代器协议,这个生成器对象是有状态的,可以不断调用
next
来遍历,最终可能抛异常终止;或者直接使用
for
语句进行遍历。
生成器在执行遇到 yield
关键字时抛出结果,暂停,等到下一次调用生成器时从此位置继续运行,直到遇到
yield
或者 return
。
例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20def fibonacci():
cnt = 0
a, b = 0, 1
while cnt < 10:
cnt += 1
yield b
a, b = b, a + b
for i in fibonacci():
print(i,end=' ')
'''
1 1 2 3 5 8 13 21 34 55
'''
s = fibonacci()
print(next(s)) # 1
print(next(s)) # 1
print(next(s)) # 2
print(next(s)) # 3
这个例子中,我们可以不提供迭代次数上限,此时不仅可以计算任意长的数列项,而且最重要的是:计算中不需要占用大量内存,只需要维持一个有内部状态的函数对象。(其实用类也可以实现,这里 Python 直接提供是因为对于 Python 解释器而言没有困难,函数也是对象,并不存在真实的调用栈)
注意:
- 只要含有关键字
yield
,无论是否会执行到这个语句,整个函数都会变成生成器 - 对于生成器,它的
return
只是一个结束标志,它不会把后面的值返回给调用者,而是会被自动识别为StopIteration,即抛出迭代终止的异常
这两点可以通过下面的例子呈现
1 | def gen_data(num): |
闭包
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
Python 中的闭包概念,以一个例子说明: 1
2
3
4
5
6
7
8
9
10
11def print_msg(): # 外层函数
msg = "hello,world" # 外层函数内的局部变量
def printer(): # 内层函数
print(msg)
return printer
another = print_msg() # 返回内层函数
# 虽然退出了外层函数,仍然可以调用内层函数,并访问外层函数内的局部变量
another()
# hello,world
在 Python 中一切都是对象,包括函数也是对象,因此不像 C/C++ 存在严格的函数调用栈,局部变量的生存周期并不需要那么严格,Python 提供了一个延长局部变量生存周期的方式——闭包。
如上例,外层函数 print_msg
和内层函数
printer
,内层函数可以访问外层函数的局部变量
msg
,我们调用 print_msg
返回内层函数,此时虽然外层函数已经退出,但是内层函数
printer
被获取了,因此内层函数可以访问的 msg
也被保留下来,仍然可以被调用。
Python 在实现上,对每一个函数对象都附带了一个
__closure__
属性,是记录当前的上下文中自由变量的元组,例如前文中的外层函数和内层函数
1
2
3
4print_msg.__closure__
# 空
another.__closure__
# (<cell at 0x0000017F9600C6D0: str object at 0x0000017F95F5B3B0>,)
闭包的应用场景:
避免全局变量的使用,虽然也可以使用类来记录信息;
1
2
3
4
5
6
7
8
9
10
11
12
13def init():
# 这两个变量只能通过这里返回的wrapper访问,避免全局变量的污染
msg = 'hi'
num = 10
def wrapper():
return msg,num
return wrapper
a = init() # 获取内层函数,可以在任何时间调用,解包出具体数据
x1,x2 = a() # 获取具体数据
print(x1,x2)
# hi 10对于简单的只有一个方法的类,可以换成闭包来进行等价实现,见下例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Add: # 通过类实现
def __init__(self,x):
self.x = x
def __call__(self,y):
return self.x + y
a = Add(3); print(a(5)) # 8
def add(x): # 通过闭包实现,x被内层函数也记住了
def wrapper(y):
return x+y
return wrapper
b = add(3); print(b(5)) # 8
简而言之,闭包是包含上下文的函数,或者说函数以及相应的上下文。 在 C++中也支持实现闭包:通过 lambda 表达式,它可以捕获自己所需的局部变量,以值拷贝或者引用的形式;通过仿函数也是一样的道理。 在 Python 中同样的道理:既可以使用支持调用的自定义类来实现具有上下文的函数,或者直接使用函数闭包功能,由于 Python 函数也是对象,因此实际上函数和类没有太多的区别,存在内部状态的函数就是 Python 的闭包。
装饰器
装饰器可以对函数进行自定义的包装:当一个函数被装饰器装饰时,Python 解释器会将原有函数作为参数传递给装饰器函数,然后将装饰器函数返回的新函数绑定到原有函数的名称上,从而实现对原有函数的增强。
装饰器 @
其实就是函数闭包的一个语法糖。
装饰器语法如下,其中装饰器 decorator
或者含参数的装饰器
decorator(args)
1
2
3
4
5
6
7
8
9
10
11
def func(*args,**kwargs):
...
# 大致相当于 func = decorator(func)
def func(*args,**kwargs):
...
# 大致相当于 func = decorator(args)(func)
注意:被装饰的函数名 func
是最后的变量名称,但是实际上并不存在 func
重新赋值的过程,Python 不会临时把 func
绑定到被装饰前的函数上。可以理解为存在一个临时的变量名称
func_temp_xxx
(不会和已有变量重名)指向了这个函数对象,然后调用装饰器函数
1
2
3
4
5
def func_temp_xxx(*args,**kwargs):
...
func = decorator(args)(func_temp_xxx)
两个经典情景为函数调用计时和日志。下文中的 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
37
38import time
# 自定义装饰器
def timer(func):
def wrapper(*args, **kwargs):
t1 = time.time()
func(*args, **kwargs) # 这是函数真正执行的地方,参数原样转发
t2 = time.time()
cost_time = t2-t1 # 计算时长
print("Time cost: {}s".format(cost_time))
return wrapper
# 使用装饰器的函数
def want_sleep(sleep_time):
print("want_sleep")
time.sleep(sleep_time)
# 不使用装饰器的等价函数
def want_sleep2_temp(sleep_time):
print("want_sleep2")
time.sleep(sleep_time)
want_sleep2 = timer(want_sleep2_temp) # 不使用装饰器@语法
want_sleep(3)
'''
want_sleep
Time cost: 3.0067501068115234s
'''
want_sleep2(3)
'''
want_sleep2
Time cost: 3.010910749435425s
'''
例二,自定义日志函数 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20def logger(func):
def wrapper(*args, **kwargs):
print('[begin {}]'.format(func.__name__))
func(*args, **kwargs)
print('[end {}]'.format(func.__name__))
return wrapper
def test(x):
print("hello, ",x)
test("Alex")
'''
[begin test]
hello, Alex
[end test]
'''
需要注意的是,在使用装饰器时,函数名以及其它属性等已经发生了改变。
如果希望保留原有的属性信息,可以使用 functools.wraps
装饰器将被装饰函数的属性以原样传递给
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
26from functools import wraps
def log_before(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}...")
return func(*args, **kwargs)
return wrapper
def log_before_no_wraps(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}...")
return func(*args, **kwargs)
return wrapper
def hello1(name):
print(f"Hello, {name}")
def hello2(name):
print(f"Hello, {name}")
print(hello1.__name__) # 输出:hello1
print(hello2.__name__) # 输出: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
33def logger(level): # 这一层负责处理装饰器参数
def logger_with_level(func): # 这里是无参数的装饰器,由于闭包特性,可以访问到level
def wrapper(*args, **kwargs):
print('[{}][begin {}]'.format(level,func.__name__))
func(*args, **kwargs)
print('[{}][end {}]'.format(level,func.__name__))
return wrapper
return logger_with_level
def test(x):
print("hello, ",x)
def test2(x):
print("hi, ",x)
test("Alex")
'''
[debug][begin test]
hello, Alex
[debug][end test]
'''
test2("Bob")
'''
[info][begin test2]
hi, Bob
[info][end test2]
'''
注意:装饰器的作用效果(尤其是含参数时)是在 @XXX
所属的函数被定义的那一刻决定的,对于模块中的,则是在加载的时刻起作用的。因此,必须在被装饰函数定义之前设置参数,在定义之后修改参数是无效的。
基于类的装饰器
前面介绍的都是基于函数的闭包特性而得到的装饰器,装饰器本身是一个函数,但是实际上定义了
__call__
的自定义类也可以实现同样的效果,并且由于类的接口更多,可以实现更丰富的功能。
- 如果是不带参数的装饰器,可以使用
__init__
接收被装饰函数,使用__call__
接收函数参数并调用被装饰函数,此时我们不再需要定义或返回wrapper
。 - 如果是带参数的装饰器,多了一层逻辑,可以使用
__init__
接收装饰器参数,使用__call__
接收被装饰函数,在__call__
内部定义wrapper
并返回。
不带参数的类装饰器比较简单,下面的例子是带参数的日志装饰器,可以设置日志等级并过滤输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Logger:
log_level = "DEBUG"
level_map = {"DEBUG":0,"INFO":1,"ERROR":2}
def __init__(self,level):
self.level = level
def __call__(self,func):
def wrapper_true(*args,**kwargs):
print('[{}][begin {}]'.format(self.level,func.__name__))
func(*args, **kwargs)
print('[{}][end {}]'.format(self.level,func.__name__))
def wrapper_false(*args,**kwargs):
func(*args, **kwargs)
if (Logger.level_map[self.level] >= Logger.level_map[Logger.log_level]):
return wrapper_true
else:
return wrapper_false
需要注意的是,装饰器在函数定义时起作用,因此全局日志等级的设置必须在函数定义之前。
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
28Logger.log_level = "ERROR" # 必须在所有被装饰函数定义之前设置
def test1(x):
print("hello,",x)
def test2(x):
print("hi,",x)
def test3(x):
print("hey,",x)
test1('Alex')
# hello, Alex
test2('Bob')
# hi, Bob
test3('John')
'''
[ERROR][begin test3]
hey, John
[ERROR][end test3]
'''
如上所示,因为全局日志等级设置为
ERROR
,只有第三个函数调用时触发了日志提示。
多个装饰器
装饰器可以复合使用,复合的效果与前后顺序有关,例如 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
42def say(msg): # 这一层负责处理装饰器参数
def say_msg(func): # 这里是无参数的装饰器,由于闭包特性,可以访问到level
def wrapper(*args, **kwargs):
print("[begin]",msg)
func(*args, **kwargs)
print("[end]",msg)
return wrapper
return say_msg
def test1():
print("test1")
# 大致相当于 test1 = say("hi")(say("hello")(test1))
def test2():
print("test2")
# 大致相当于 test2 = say("hello")(say("hi")(test2))
test1()
'''
[begin] hi
[begin] hello
test1
[end] hello
[end] hi
'''
test2()
'''
[begin] hello
[begin] hi
test2
[end] hi
[end] hello
'''
基于装饰器的单例模式
在 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
27
28
29
30
31
32
33
34
35def Singleton(cls):
_instance = {}
def get_instance():
print("1:")
if cls.__name__ not in _instance:
print("2:")
_instance[cls.__name__] = cls() # 构造新的对象,并存储到字典中
return _instance[cls.__name__]
return get_instance
class Demo:
def __init__(self):
print("3:")
pass
# 大致相当于 Demo = Singleton(Demo)
# 第一次创建对象
a = Demo()
'''
1:
2:
3:
'''
# 第二次并没有创建对象
b = Demo()
'''
1:
'''
# 两个变量指向同一个对象
print(id(a) == id(b)) # True
偏函数
Python 的 patrial
函数可以固定函数的某些参数,形成一个新的函数,对含有多个函数的复杂函数简化调用方式。它的实现原理大致如下
1
2
3
4
5
6
7
8
9
10# 定义一个函数,它接受三个参数
def partial(func, *args, **keywords):
def newfunc(*fargs, **fkeywords):
newkeywords = keywords.copy() # 首先是被固定的关键字参数字典
newkeywords.update(fkeywords) # 添加自由的关键字参数
return func(*args, *fargs, **newkeywords) # 位置参数则是固定的在前,自由的在后
newfunc.func = func
newfunc.args = args
newfunc.keywords = keywords
return newfunc
几个例子如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from functools import partial
def add(a,b):
print(f"a={a},b={b},sum={a+b}")
add(1,2)
# a=1,b=2,sum=3
# 固定位置参数在前,剩下参数b
partial(add,1)(3)
#a=1,b=3,sum=4
# 固定关键字参数,剩下参数a
partial(add,b=1)(3)
# a=3,b=1,sum=4
# 固定关键字参数可以被再次用关键字参数覆盖
partial(add,b=1)(a=2,b=3)
# a=2,b=3,sum=5
@property
property 是一个内置函数,通常以装饰器的形式调用,这个函数解决的问题就是 Python 中实例的属性增删读写太过自由,不能方便地对参数进行限制。
property 函数支持将最多 3
个类方法和一个字符串绑定起来,对外统一提供一个伪装属性,property
的参数分别是读取,修改,删除和说明字符串,当然 4
个接口并不是都需要的,可以缺省,常用的就是前两个 1
2
3
4
5
6
7property(fget=None, fset=None, fdel=None, doc=None)
# 调用时可以缺省后面的内容
age = property(get_age,set_age,del_age,"This is age")
age = property(get_age,set_age,del_age)
age = property(get_age,set_age)
age = property(get_age)
也可以使用如下的写法,每次传递一个参数,装饰器 @property
就是这样调用的。 1
2
3
4
5
6
7
8
9
10# 第一次不传递任何参数
age = property()
age = age.getter(get_age)
age = age.setter(set_age)
age = age.deleter(del_age)
# 第一次直接传递setter
age = property(set_age)
age = age.setter(set_age)
age = age.deleter(del_age)
下面的例子很好地解释了 property,这里我们在内部存储的属性是
_age
,为这个属性的读写提供了接口 get_age
和
set_age
,然后使用 property
函数将它们绑定起来,对外部提供了一个伪装属性 age
。
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
35class Data:
def __init__(self,age):
self.age = age
def get_age(self):
print("call get_age")
return self._age
def set_age(self,age):
print("call set_age")
self._age = age
def del_age(self):
print("call del_age")
del self._age
age = property(get_age,set_age,del_age) # 得到伪装属性age
a = Data(1)
# call set_age
print(a.age)
# call get_age
# 1
a.age = 2
# call set_age
print(a.age)
# call get_age
# 2
del a.age
# call del_age
或者,我们可以使用装饰器实现,这里把所有的相关接口都使用
age
这个名称,由于 Python
不会立即把被装饰函数绑定到函数名,因此并不会导致重名覆盖问题。按照
property 的顺序,要求 @property
对应的是读取接口,并且写在最前面。 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
36class Data:
def __init__(self,age):
self.age = age
def age(self):
print("call get_age")
return self._age
def age(self,age):
print("call set_age")
self._age = age
def age(self):
print("call del_age")
del self._age
a = Data(1)
# call set_age
print(a.age)
# call get_age
# 1
a.age = 2
# call set_age
print(a.age)
# call get_age
# 2
del a.age
# call del_age
注:个人觉得不使用装饰器语法,而是直接调用 property
函数的形式去伪装一个属性,代码的可读性更高。
通过这里 property
提供的伪属性,我们可以让类达成如下几个效果
- 只读不可修改的属性:设置
property
但是不设置setter
- 不可删除的属性:让
deleter
绑定的接口总是触发异常
类方法与静态方法
在这一部分,我们区分类的不同方法:
- 普通方法(实例方法)(默认的方法,不使用任何装饰器)
- 类方法(基于
@classmethod
装饰器) - 静态方法(基于
@staticmethod
装饰器)
它们都可以通过类名或实例名调用,但是对于参数的处理逻辑不同:
- 对于实例方法,第一个参数建议是
self
,代表实例对象,在方法内部使用self.xxx
读写的是实例对象的属性- 以类名调用时,第一个参数
self
需要提供一个实例对象; - 以实例名调用时,会将当前实例对象传入作为第一个参数(通常即
self
)
- 以类名调用时,第一个参数
- 对于类方法,第一个参数建议是
cls
,代表类对象,在方法内部使用cls.xxx
或<className>.xxx
读写的都是类对象的属性,无论以类名还是实例名调用,都要求缺省第一个参数cls
,会将类对象或者实例所属的类对象传入作为第一个参数(通常即cls
), - 对于静态方法,不需要任何特殊参数,也不会进行任何的自动传参,相当于一个普通函数,以类名还是实例调用没有区别
对于类方法和静态方法,虽然使用类名和实例名调用没有区别,但还是建议使用类名调用。
例如 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
46class Demo:
def f1(self,v):
print("call normal method")
print(f"self={self},v={v}")
def f2(cls,v):
print("call class method")
print(f"cls={cls},v={v}")
def f3(v):
print("call static method")
print(f"v={v}")
a = Demo()
a.f1(1)
Demo.f1(a,1) # 或者 Demo.f1(Demo(),1)
'''
call normal method
self=<__main__.Demo object at 0x00000237F340AD90>,v=1
call normal method
self=<__main__.Demo object at 0x00000237F340AD90>,v=1
'''
a.f2(2)
Demo.f2(2)
'''
call class method
cls=<class '__main__.Demo'>,v=2
call class method
cls=<class '__main__.Demo'>,v=2
'''
a.f3(3)
Demo.f3(3)
'''
call static method
v=3
call static method
v=3
'''
推导式
列表推导式
格式为 1
2
3
4
5[out_exp_res for out_exp in input_list]
or
[out_exp_res for out_exp in input_list if condition]
例如 1
2
3
4lis = ['Bob','Tom','alice','Jerry','Wendy','Smith']
lis2 = [name.upper()for name in lis if len(name)>3]
print(lis2)
# ['ALICE', 'JERRY', 'WENDY', 'SMITH']
字典推导式
格式为 1
2
3
4
5{ key_expr: value_expr for value in collection }
or
{ key_expr: value_expr for value in collection if condition }
例如 1
2
3
4lis = ['Google','Runoob', 'Taobao']
dic = {key:len(key) for key in lis}
print(dic)
# {'Google': 6, 'Runoob': 6, 'Taobao': 6}
集合推导式
格式为 1
2
3
4
5{ expression for item in Sequence }
or
{ expression for item in Sequence if conditional }
例如 1
2
3s = {i**2 for i in (1,2,3)}
print(s)
# {1, 4, 9}
元组推导式
或者说是生成器表达式,格式为 1
2
3
4
5(expression for item in Sequence )
or
(expression for item in Sequence if conditional )
注意它返回的不是一个元组,而是一个生成器对象,可以使用
tuple
转化为元组
例如 1
2
3
4
5a = (x for x in range(1,10))
print(a)
# <generator object <genexpr> at 0x000002485A6EF740>
print(tuple(a))
# (1, 2, 3, 4, 5, 6, 7, 8, 9)