python——列表生成式/生成器/迭代器/闭包/装饰器

目录

一、语法糖的概念

二、列表生成式

1、基础语法格式

2、带过滤功能语法格式

3、循环嵌套语法格式

4、if ... else

三、生成器

方法1.把一个列表生成式的[]改成()

方法2.函数定义中包含yield关键字

四、迭代器

总结

五、闭包

六、装饰器


一、语法糖的概念


“语法糖”,从字面上看应该是一种语法。“糖”,可以理解为简单、简洁。其实我们也已经意识到,没有这些被称为“语法糖”的语法,我们也能实现相应的功能,而 “语法糖”使我们可以更加简洁、快速的实现这些功能。 只是Python解释器会把这些特定格式的语法翻译成原本那样复杂的代码逻辑而已,没有什么太高深的东西。

我们使用和介绍过的语法糖有:

  • if...else 三元表达式: 可以简化分支判断语句,如 x = y.lower() if isinstance(y, str) else y
  • with语句: 用于文件操作时,可以帮我们自动关闭文件对象,使代码变得简洁;
  • 装饰器: 可以在不改变函数代码及函数调用方式的前提下,为函数增加增强性功能;
  • 列表生成式: 用于生成一个新的列表
  • 生成器: 用于“惰性”地生成一个无限序列

二、列表生成式


1、基础语法格式

[exp for iter_var in iterable]

工作过程:

  • 迭代iterable中的每个元素;
  • 每次迭代都先把结果赋值给iter_var,然后通过exp得到一个新的计算值;
  • 最后把所有通过exp得到的计算值以一个新列表的形式返回。

2、带过滤功能语法格式

[exp for iter_var in iterable if_exp]

工作过程:

  • 迭代iterable中的每个元素,每次迭代都先判断if_exp表达式结果为真,如果为真则进行下一步,如果为假则进行下一次迭代;
  • 把迭代结果赋值给iter_var,然后通过exp得到一个新的计算值;
  • 最后把所有通过exp得到的计算值以一个新列表的形式返回。
>>> [x * x for x in range(1, 11) if x % 2 == 0]
[4, 16, 36, 64, 100]

3、循环嵌套语法格式

[exp for iter_var_A in iterable_A for iter_var_B in iterable_B]

工作过程:
每迭代iterable_A中的一个元素,就把ierable_B中的所有元素都迭代一遍。

例子:生成全排列:

>>> [m + n for m in 'ABC' for n in 'XYZ']
['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

4、if ... else

使用列表生成式的时候,有些童鞋经常搞不清楚if...else的用法。

>>> [x for x in range(1, 11) if x % 2 == 0]
[2, 4, 6, 8, 10]

但是,在for后的判断不能在最后的if加上else

>>> [x for x in range(1, 11) if x % 2 == 0 else 0]
  File "<stdin>", line 1
    [x for x in range(1, 11) if x % 2 == 0 else 0]
                                              ^
SyntaxError: invalid syntax

这是因为跟在for后面的if是一个筛选条件,不能带else,否则如何筛选?

if写在for前面必须加else,否则报错: 

>>> [x if x % 2 == 0 for x in range(1, 11)]
  File "<stdin>", line 1
    [x if x % 2 == 0 for x in range(1, 11)]
                       ^
SyntaxError: invalid syntax

 这是因为for前面的部分是一个表达式,它必须根据x计算出一个结果。因此,考察表达式:x if x % 2 == 0,它无法根据x计算出结果,因为缺少else,必须加上else

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

三、生成器


受到内存限制,列表容量肯定是有限的。创建一个较大的列表,占用很大的存储空间,而且如果我们只访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。在Python中,这种一边循环一边计算的机制,称为生成器:generator     即:按照某种算法不断生成新的数据,直到满足某一个指定的条件结束。

生成器是一个特殊的程序,可以被用作控制循环的迭代行为,python中生成器是迭代器的一种,使用yield返回值函数,每次调用yield会暂停,而可以使用next()函数和send()函数恢复生成器。

生成器类似于返回值为数组的一个函数,这个函数可以接受参数,可以被调用,但是,不同于一般的函数会一次性返回包括了所有数值的数组,生成器一次只能产生一个值,这样消耗的内存数量将大大减小,而且允许调用函数可以很快的处理前几个返回值,因此生成器看起来像是一个函数,但是表现得却像是迭代器

方法1.把一个列表生成式的[]改成()

>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>

通过next()函数获得generator的下一个返回值,没有更多的元素时,抛出StopIteration的错误。

方法2.函数定义中包含yield关键字

那么这个函数就不再是一个普通函数,而是一个generator。generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

def my_range(start, end):
    for n in range(start, end):
        ret = yield 2*n + 1
        print(ret)
g3 = my_range(3, 6)

生成器的特性:

  • 只有在调用时才会生成相应的数据
  • 只记录当前的位置
  • 只能next,不能prev

调用生成器产生新的元素,有两种方式:

  • 调用内置的next()方法
  • 使用循环对生成器对象进行遍历(推荐)
  • 调用生成器对象的send()方法

著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:1, 1, 2, 3, 5, 8, 13, 21, 34, 

斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'

注意,赋值语句:

a , b = b ,a + b :计算过程:首先计算b = b ,a + b,即 b = b = 1, a +b = 0 + 1 = 1 ;接下来赋值: a = b = 1;  b=a+b

相当于:tuple赋值,左边a,b是变量,右边b,a+b是对象

t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]

a = b , b = a + b :计算过程:首先赋值 a = b = 1;接下来计算: b = a + b = 1 + 1 = 2;

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

 把函数改成generator后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代

>>> for n in fib(6):
...     print(n)

但是for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIterationvalue中:

>>> g = fib(6)
>>> while True:
...     try:
...         x = next(g)
...         print('g:', x)
...     except StopIteration as e:
...         print('Generator return value:', e.value)
...         break
Generator return value: done

 通过yield实现在单线程的情况下实现并发运算的效果

import time
def consumer(name):
    print("%s 准备学习啦!" %name)
    while True:
       lesson = yield
 
       print("开始[%s]了,[%s]老师来讲课了!" %(lesson,name))
 
 
def producer(name):
    c = consumer('A')
    c2 = consumer('B')
    c.__next__()
    c2.__next__()
    print("同学们开始上课 了!")
    for i in range(10):
        time.sleep(1)
        print("到了两个同学!")
        c.send(i)
        c2.send(i)
 
结果:
A 准备学习啦!
B 准备学习啦!
同学们开始上课 了!
到了两个同学!
开始[0]了,[A]老师来讲课了!
开始[0]了,[B]老师来讲课了!
到了两个同学!
开始[1]了,[A]老师来讲课了!
开始[1]了,[B]老师来讲课了!
到了两个同学!
开始[2]了,[A]老师来讲课了!
开始[2]了,[B]老师来讲课了!
到了两个同学!
开始[3]了,[A]老师来讲课了!
开始[3]了,[B]老师来讲课了!
到了两个同学!
开始[4]了,[A]老师来讲课了!
开始[4]了,[B]老师来讲课了!
到了两个同学!
开始[5]了,[A]老师来讲课了!
开始[5]了,[B]老师来讲课了!
到了两个同学!
开始[6]了,[A]老师来讲课了!
开始[6]了,[B]老师来讲课了!
到了两个同学!

由上面的例子我么可以发现,python提供了两种基本的方式

   生成器函数:也是用def定义的,利用关键字yield一次性返回一个结果,阻塞,重新开始

   生成器表达式:返回一个对象,这个对象只有在需要的时候才产生结果

对yield的总结

  (1)通常的for..in...循环中,in后面是一个数组,这个数组就是一个可迭代对象,类似的还有链表,字符串,文件。他可以是a = [1,2,3],也可以是a = [x*x for x in range(3)]。

它的缺点也很明显,就是所有数据都在内存里面,如果有海量的数据,将会非常耗内存。

  (2)生成器是可以迭代的,但是只可以读取它一次。因为用的时候才生成,比如a = (x*x for x in range(3))。!!!!注意这里是小括号而不是方括号。

  (3)生成器(generator)能够迭代的关键是他有next()方法,工作原理就是通过重复调用next()方法,直到捕获一个异常。

  (4)带有yield的函数不再是一个普通的函数,而是一个生成器generator,可用于迭代

  (5)yield是一个类似return 的关键字,迭代一次遇到yield的时候就返回yield后面或者右面的值。而且下一次迭代的时候,从上一次迭代遇到的yield后面的代码开始执行

  (6)yield就是return返回的一个值,并且记住这个返回的位置。下一次迭代就从这个位置开始。

  (7)带有yield的函数不仅仅是只用于for循环,而且可用于某个函数的参数,只要这个函数的参数也允许迭代参数。

  (8)send()和next()的区别就在于send可传递参数给yield表达式,这时候传递的参数就会作为yield表达式的值,而yield的参数是返回给调用者的值,也就是说send可以强行修改上一个yield表达式值。

  (9)send()和next()都有返回值,他们的返回值是当前迭代遇到的yield的时候,yield后面表达式的值,其实就是当前迭代yield后面的参数。

  (10)第一次调用时候必须先next()或send(),否则会报错,send后之所以为None是因为这时候没有上一个yield,所以也可以认为next()等同于send(None)

四、迭代器


a迭代的含义

  迭代器即迭代的工具,那什么是迭代呢?
  #迭代是一个重复的过程,每次重复即一次迭代,并且每次迭代的结果都是下一次迭代的初始值

b为何要有迭代器?

对于序列类型:字符串、列表、元组,我们可以使用索引的方式迭代取出其包含的元素。但对于字典、集合、文件等类型是没有索引的,若还想取出其内部包含的元素,则必须找出一种不依赖于索引的迭代方式,这就是迭代器

c可迭代对象

可迭代对象指的是内置有iter方法的对象,即字符串、元组、列表、集合、字典、文件,

可以直接作用于for循环的数据类型有以下几种:一类是集合数据类型,如listtupledictsetstr等;一类是generator。

这些可以直接作用于for循环的对象统称为可迭代对象Iterable

可以使用isinstance()判断一个对象是否是Iterable对象:

>>> from collections.abc import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

可以被next()函数调用并不断返回下一个值的对象称为迭代器Iterator

可以使用isinstance()判断一个对象是否是Iterator对象:

>>> from collections.abc import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False

生成器都是Iterator对象,但listdictstr虽然是Iterable,却不是Iterator

这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

listdictstrIterable变成Iterator可以使用iter()函数

>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True

总结

凡是可作用于for循环的对象都是Iterable类型;

凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;

集合数据类型如listdictstr等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。

Python的内置模块 itertools

就是用来操作迭代器的一个模块,包含的函数都是能够创建迭代器来用于 for循环或者 next(), itertools用于高效循环的迭代函数集合,其中很多函数的作用我们平时要写很多代码才能达到,而在运行效率上反而更低,毕竟人家是系统库

其itertools库中的函数主要分为三类,分别为无限迭代器,有限迭代器,组合迭代器。

1,无限迭代器(Infinite Iterators)

  这些函数可以生成无限的迭代器,概述如下:

1.1 count()

  count([start=0, step=1]) 接收两个可选整形参数,第一个指定了迭代开始的值,第二个指定了迭代的步长。此外,start参数默认为0,step参数默认为1,可以根据需要来把这两个指定为其他值,或者使用默认参数。

>>> import itertools
>>> natuals = itertools.count(1)
>>> for n in natuals:
...     print(n)
...     if n > 5:
...         break
 
...
1
2
3
4
5

因为count() 会创建一个无限的迭代器,所以上述代码会打印出自然数序列,根本停不下来,只能按Ctrl+C退出

1.2 cycle()

  cycle(iterable) 是用一个可迭代对象中的元素来创建一个迭代器,并且复制自己的值,一直无限的重复下去。

>>> import itertools
>>> cs = itertools.cycle('ABC') # 注意字符串也是序列的一种
>>> for c in cs:
...     print(c)   # 具有无限的输出,可以按ctrl+c来停止
...
'A'
'B'
'C'
'A'
'B'
'C'
...

1.3  repeat()

  repeat(ele, [, n]) 是将一个元素重复 n 遍或者无穷多变,并返回一个迭代器。不过如果提供第二个参数就可以限定重复次数。

>>> ns = itertools.repeat('A', 5)
>>> for n in ns:
...     print n
...
A
A
A
A
A

无限序列只有在 for 迭代时才会无限的迭代下去,如果只是创建了一个迭代对象,它不会实现把无限个元素生成出来,事实上也不可能在内存中创建无限多个元素。无限序列虽然可以无限迭代下去,但是我们通常会通过 takewhile() 等函数根据条件判断来截取一个有限的序列:

>>> natuals = itertools.count(1)
>>> ns = itertools.takewhile(lambda x: x <= 5, natuals)
>>> for n in ns:
...     print n
...
1
2
3
4
5

2,有限迭代器(Iterators Terminating on the Shortest Input Sequence)

  这里的函数有十来个,如下:

2.1 chain()

  chain(*iterables) 可以把多个可迭代对象组合起来,形成一个更大的迭代器。比如:

for iter in itertools.chain('lebron', 'james'):
    print(iter)
  
...  
l
e
b
r
o
n
j
a
m
e
s
...

2.2  groupby()

  groupby(iterable, key=None) 可以把相邻元素按照 key 函数分组,并返回相应的 key 和 groupby,如果key函数为None,则只有相同的元素才能放在一组。

>>> for key, group in itertools.groupby('AAABBBCCAAA'):
...     print(key, list(group))  
...
A ['A', 'A', 'A']
B ['B', 'B', 'B']
C ['C', 'C']
A ['A', 'A', 'A']

实际上挑选规则是通过函数完成的,只要作用于函数的两个元素返回的值相等,这两个元素就被认为是在一组的,而函数返回值作为组的key。如果我们要忽略大小写分组,就可以让元素“A”和‘a’都返回相等的key。

>>> for key, group in itertools.groupby('AaaBBbcCAAa', lambda c: c.upper()):
...     print key, list(group)
...
A ['A', 'a', 'a']
B ['B', 'B', 'b']
C ['c', 'C']
A ['A', 'A', 'a']

2.3  itertools.accumulate()

  accumulate(iterable, [, func]) 可以计算出一个迭代器,这个迭代器是由特定的二元函数的累计结果生成的,如果不指定的话,默认函数为求和函数

from itertools import accumulate<br>
x = accumulate(range(10))
print(list(x))
[0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

如果我们指定这个累计函数,则还能有不同的用法,例如,指定一个最大值函数,或者自己定义的函数

from itertools import accumulate
 
# x1 = accumulate(range(10), max)
# print(list(x1))
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 
x2 = accumulate(range(10), min)
print(list(x2))
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

3,组合迭代器(Combinatoric Iterators)

  组合操作包括排列,笛卡尔积,或者一些离散元素的选择,组合迭代器就是产生这样序列的迭代器,概述如下:

3.1 product()

  product(*iterables, repeat=1) 得到的是可迭代对象的笛卡尔积,*iterables参数表示需要多个可迭代对象。这些可迭代对象之间的笛卡尔积,也可以使用 for 循环来实现例如 product(A, B) 与 ((x, y) for x in A for y in B)就实现一样的功能。

import itertools
 
for i in itertools.product([1,2,3],[4,5,6]):
    print(i)
 
'''
(1, 4)
(1, 5)
(1, 6)
(2, 4)
(2, 5)
(2, 6)
(3, 4)
(3, 5)
(3, 6)
'''

而 repeat() 参数则表示这些可迭代序列重复的次数。例如 product(A, repeat=4)与 product(A, A, A, A)实现的功能一样。

import itertools
for i in itertools.product('ab','cd',repeat = 2):
    print(i)
 
'''
('a', 'c', 'a', 'c')
('a', 'c', 'a', 'd')
('a', 'c', 'b', 'c')
('a', 'c', 'b', 'd')
('a', 'd', 'a', 'c')
('a', 'd', 'a', 'd')
('a', 'd', 'b', 'c')
('a', 'd', 'b', 'd')
('b', 'c', 'a', 'c')
('b', 'c', 'a', 'd')
('b', 'c', 'b', 'c')
('b', 'c', 'b', 'd')
('b', 'd', 'a', 'c')
('b', 'd', 'a', 'd')
('b', 'd', 'b', 'c')
('b', 'd', 'b', 'd')
'''

3.2  permutations()

  permutations(iterable, r=None)返回的是一个可迭代元素的一个排列组合,并且是按照顺序的,且不包含重复的结果。

from itertools import permutations
 
x = permutations((1,2,3))
print(list(x))
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

当然,第二个参数默认为None,它表示的是返回元组(tuple)的长度,我们来尝试一下传入的第二个参数。

from itertools import permutations
 
x = permutations((1, 2, 3), 2)
print(list(x))
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

3.3  combinations()

  combinations(iterable, r) 返回的是可迭代对象所有的长度为 r 的子序列,注意这个与前一个函数 permutations不同,permutations返回的是排列,而 combinations() 返回的是组合。

  下面对比一下combinations() 与 permutations() 函数:

from itertools import permutations, combinations
 
x1 = permutations((1, 2, 3))
x2 = combinations((1, 2, 3), 3)
x11 = permutations((1, 2, 3), 2)
x22 = combinations((1, 2, 3), 2)
print(list(x1))
print(list(x2))
print(list(x11))
print(list(x22))
# [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
# [(1, 2, 3)]
# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
# [(1, 2), (1, 3), (2, 3)]

3.4  combinations_with_replacement()

  combinations_with_replacement(iterable, r) 返回一个可与自身重复的元素组合,用法类似于 combinations。

from itertools import combinations, combinations_with_replacement
 
x1 = combinations((1, 2), 2)
x2 = combinations_with_replacement((1, 2), 2)
print(list(x1))
print(list(x2))
# [(1, 2)]
# [(1, 1), (1, 2), (2, 2)]

五、闭包


什么是闭包:

在一个function内部使用function外部的变量,或者在class的method使用当前method外部的变量,使得当前function和methos将外部变量包裹进来,形成一种封闭区域,这种特性简称闭包。(和scala的柯里化currying操作一致)

闭包的作用:

(1)实现操作的延迟计算。

(2)对相同操作共享部分变量。

(3)实现python的装饰器作用

在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用。这样就构成了一个闭包。

在python语言中形成闭包的三个条件,缺一不可:

1)必须有一个内嵌函数(函数里定义的函数)——这对应函数之间的嵌套
 2)内嵌函数必须引用一个定义在闭合范围内(外部函数里)的变量——内部函数引用外部变量
3)外部函数必须返回内嵌函数——必须返回那个内部函数

按照我们正常的认知,一个函数结束的时候,会把自己的临时变量都释放还给内存,之后变量都不存在了。一般情况下,确实是这样的。但是闭包是一个特别的情况。外部函数发现,自己的临时变量会在将来的内部函数中用到,自己在结束的时候,返回内函数的同时,会把外函数的临时变量送给内函数绑定在一起。所以外函数已经结束了,调用内函数的时候仍然能够使用外函数的临时变量

def outer():
    name = "nicholas"
    def inner(): #  inner()是内函数
        print(name)  #在内函数中 用到了外函数的临时变量
        print("里面的一层", locals())
    print("外面的一层", locals())
    return inner  #外函数的返回值是内函数的引用
a = outer()

a() #执行函数内部的inner()函数

如何判断是否是闭包函数

函数名.__closure__ 在函数是闭包函数时,返回一个cell元素;不是闭包时,返回None。
输出cell:

def func():
    name = 'python'
    def inner():
        print(name)
    print(inner.__closure__)  # (<cell at 0x0000027C14EB85E8: str object at 0x0000027C14F54960>,)
    return inner

f = func()
f()

输出None:

name = 'python'
def func():
    def inner():
        print(name)
    print(inner.__closure__)  # None
    return inner

f = func()
f()
#闭包函数的实例
# outer是外部函数 a和b都是外函数的临时变量
def outer( a ):
    b = 10
    # inner是内函数
    def inner():
        #在内函数中 用到了外函数的临时变量
        print(a+b)
    # 外函数的返回值是内函数的引用
    return inner

if __name__ == '__main__':
    # 在这里我们调用外函数传入参数5
    #此时外函数两个临时变量 a是5 b是10 ,并创建了内函数,然后把内函数的引用返回存给了demo
    # 外函数结束的时候发现内部函数将会用到自己的临时变量,这两个临时变量就不会释放,会绑定给这个内部函数
    demo = outer(5)
    # 我们调用内部函数,看一看内部函数是不是能使用外部函数的临时变量
    # demo存了外函数的返回值,也就是inner函数的引用,这里相当于执行inner函数
    demo() # 15

    demo2 = outer(7)
    demo2()#17

在我编写的实例中,我两次调用外部函数outer,分别传入的值是5和7。内部函数只定义了一次,我们发现调用的时候,内部函数是能识别外函数的临时变量是不一样的。python中一切都是对象,虽然函数我们只定义了一次,但是外函数在运行的时候,实际上是按照里面代码执行的,外函数里创建了一个函数,我们每次调用外函数,它都创建一个内函数,虽然代码一样,但是却创建了不同的对象,并且把每次传入的临时变量数值绑定给内函数,再把内函数引用返回。虽然内函数代码是一样的,但其实,我们每次调用外函数,都返回不同的实例对象的引用,他们的功能是一样的,但是它们实际上不是同一个函数对象。

六、装饰器


装饰器,是一种“语法糖”,其本质上就是个函数。

它是一个装饰其他函数的函数,用来为其他函数添加一些额外的功能。

装饰器对被装饰的函数应该是完全透明的,即

  • 不能修改被装饰的函数的源代码
  • 不能修改被装饰的函数的调用方式

高阶函数 + 嵌套函数+闭包 => 装饰器

这里的高阶函数需要同时满足以下两个条件:

  • 接收函数名作为参数 -- 可以实现在不修改被装饰函数源代码的情况下为其添加新的功能
  • 返回内部嵌套函数的函数名 -- 可以实现不用修改函数的调用方式

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。

>>> def now():
...     print('2015-3-25')
>>> f = now
>>> f()
2015-3-25

 比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)

def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

实现该装饰器的使用有两种方法

方法一:
借助Python的@语法,把decorator置于函数的定义处:
@log
def now():
    print('2015-3-25')
调用now()函数,不仅会运行now()函数本身,还会在运行now()函数前打印一行日志:
>>> now()
call now():
2015-3-25

方法二:
now = log(now)
>>> now()
call now():
2015-3-25

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:

def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

这个3层嵌套的decorator用法如下:

@log('execute')
def now():
    print('2015-3-25')

log()已经不再是一个装饰器函数,而是一个返回装饰器函数的函数

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

>>> now = log('execute')(now)

 以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper'

>>> now.__name__
'wrapper'

因为返回的那个wrapper()函数名字就是'wrapper',所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

Python内置的functools.wraps就是增加@functools.wraps(f), 可以保持当前装饰器去装饰的函数的 __name__ 的值不变,所以,一个完整的decorator的写法如下:

import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

或者针对带参数的decorator:

import functools
def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

猜你喜欢

转载自blog.csdn.net/zangba9624/article/details/106207831