小言_互联网的博客

周末加班想摸鱼?不如来点Python进阶干货呀!【超详细迭代器、生成器、装饰器使用教程】

524人阅读  评论(0)

Python基础及进阶内容持续更新中!欢迎小伙伴们一起关注学习

目录

写在前面

一、深入理解迭代器和生成器

1、什么是迭代?(Iteration)

2、迭代器(Iterator)

(1)for 循环的迭代过程

(2)可迭代(Iterable)对象

(3)自定义迭代器

(4)迭代器的好处

3、生成器(Generator)

(1)生成器表达式(Generator Expression)

二、生成器表达式和列表生成式

1、列表生成式

(1)列表生成式的写法

2、字典生成式

3、集合生成式

4、生成器表达式

三、给凡人添加超能力:入手装饰器

1、函数中定义函数

2、函数返回函数

3、什么是装饰器?

4、自定义装饰器

(1)装饰器原理

5、functools.wraps 装饰器

6、带参数的装饰器

(1)带参数的装饰器原理


写在前面

Hello,你好呀!我是灰小猿!一个超会写bug的程序猿!

有多少人和我一样仍然在周末痛苦的加班工作中?哈哈哈,快来慢慢的读一下这篇文章,一起慢慢的摸鱼吧!

最近和大家分享了好几篇有关Python从基础到进阶的文章,帮助了很多不知道如何如学习Python的小伙伴,同时也为大家总结了有关Python中常见报错及其解决:感兴趣想学习的小伙伴可以去看一下,同时也欢迎小伙伴们关注我一起学习,之后为大家更新更多干货资源!传送门:

Python入门及进阶:

【全网力荐】堪称最易学的Python基础入门教程

万字长文爆肝Python基础入门【第二弹、超详细数据类型总结】

诺,你们要的Python进阶来咯!【函数、类进阶必备】

常见报错及解决:

全网最值得收藏的Python常见报错及其解决方案,再也不用担心遇到BUG了!

今天就继续来和大家分享一下在Python基础进阶中有关迭代器、生成器、装饰器的详细使用教程,【备好收藏,长文预警!】

一、深入理解迭代器和生成器

1、什么是迭代?(Iteration)

刚听到这个名词可能很多小伙伴会问,什么是迭代呢?在编程中,迭代指的是通过重复执行某个操作,不断获取被迭代对象中的数据。这样的每一次操作就是就是一次迭代。

简而言之,迭代是遍历的一种形式。例如我们之前所学习的 for 循环,它能不断从地从列表、元组、字符串、集合、字典等容器中取出新元素,每次一个元素直至所有元素被取完。这种 for 循环操作就是迭代。

>>> for item in [1, 2, 3, 4, 5]:
… print(item)

1
2
3
4
5

2、迭代器(Iterator)

迭代器是具有迭代功能的对象。我们使用迭代器来进行迭代操作。

列表、元组、字符串、集合、字典这些容器之所以能被迭代,是因为对它们调用内置函数 iter() 将返回一个迭代器,这个迭代器可被用于迭代操作。

iter() 的使用方法:

迭代器 = iter(容器)

>>> numbers = [1, 2, 3, 4, 5]
>>> iterator = iter(numbers)
>>> iterator
<list_iterator object at 0x1074f34a8>

上面的「list_iterator」便是列表的迭代器。这个迭代器可用于迭代列表中的所有元素。

要使用迭代器,只需对迭代器调用内置函数 next(),便可逐一获取其中所有的值。

next() 的使用方法:

值 = next(迭代器)

对于上面的列表迭代器,可以像这样使用它:

>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
3
>>> next(iterator)
4
>>> next(iterator)
5
>>> next(iterator)
Traceback (most recent call last):
     File “”, line 1, in
StopIteration

可以看到,每次调用 next() 将依次返回列表中的一个值。直至所有的值被遍历一遍,此时将抛出 StopIteration 异常以表示迭代终止。

(1)for 循环的迭代过程

for 循环的迭代就是通过使用迭代器来完成的。它在背后所做的事情是:

  1. 对一个容器调用 iter() 函数,获取到该容器的迭代器
  2. 每次循环时对迭代器调用 next() 函数,以获取一个值
  3. 若捕获到 StopIteration 异常则结束循环

(2)可迭代(Iterable)对象

并不是所有的对象都可以被 iter() 函数使用。比如整数:

>>> iter(123)
Traceback (most recent call last):
     File “”, line 1, in
TypeError: ‘int’ object is not iterable

这里抛出 TypeError 异常,提示 int 对象不是可迭代的。

什么是可迭代(的)?

  • 从表面来看,所有可用于 for 循环的对象是可迭代的,如列表、元组、字符串、集合、字典等容器
  • 从深层来看,定义了 __iter__() 方法的类对象就是可迭代的。当这个类对象被 iter() 函数使用时,将返回一个迭代器对象。如果对象具有__iter__() 方法,则可以说它支持迭代协议。

判断一个已有的对象是否是可迭代的,有两个方法:

  1. 通过内置函数 dir() 获取这个对象所有方法,检查是否有 '__iter__'

    >>> ‘__iter__’ in dir(list)
    True
    >>> ‘__iter__’ in dir(int)
    False

  2. 使用内置函数 isinstance() 判断其是否为 Iterable 的对象

    
        
    1. from collections.abc import Iterable
    2. isinstance(对象, Iterable)

    >>> from collections.abc import Iterable
    >>> isinstance([1, 2, 3], Iterable)
    True

(3)自定义迭代器

我们可以自己来定义迭代器类,只要在类中定义 __next__()__iter__() 方法即可。如:


  
  1. class MyIterator:
  2. def __next__(self):
  3. 代码块
  4. def __iter__(self):
  5. return self

我们来写一个迭代器,这个迭代器从 2^0 开始返回 2 的指数幂,至 2^10 终止。


  
  1. class PowerOfTwo:
  2. def __init__(self):
  3. self.exponent = 0 # 将每次的指数记录下来
  4. def __next__(self):
  5. if self.exponent > 10:
  6. raise StopIteration
  7. else:
  8. result = 2 ** self.exponent # 以 2 为底数求指数幂
  9. self.exponent += 1
  10. return result
  11. def __iter__(self):
  12. return self

每次对迭代器使用内置函数 next() 时, next() 将在背后调用迭代器的 __next__() 方法。所以迭代器的重点便是 __next__() 方法的实现。在这个 __next__() 方法中,我们将求值时的指数记录在对象属性 self.exponent 中,求值结束时指数加 1,为下次求值做准备。

对于方法 __iter__() 的实现,我们直接返回迭代器对象自身即可。有了这个方法,迭代器对象便是可迭代的,可直接用于 for 循环。

扩展:如果对象具有 __iter__()__next__() 方法,则可以说它支持迭代器协议

迭代器 PowerOfTwo 使用示例:

>>> p = PowerOfTwo()
>>> next(p)
1
>>> next(p)
2
>>> next(p)
4
>>> next(p)
8
>>> next(p)
16
>>> next(p)
32
>>> next(p)
64
>>> next(p)
128
>>> next(p)
256
>>> next(p)
512
>>> next(p)
1024
>>> next(p)
Traceback (most recent call last):
     File “”, line 1, in
     File “”, line 6, in next
StopIteration

这个迭代器当然也可用于 for 循环:

>>> p = PowerOfTwo()
>>> for item in p:
… print(item)

1
2
4
8
16
32
64
128
256
512
1024

(4)迭代器的好处

  • 一方面,迭代器可以提供迭代功能,当我们需要逐一获取数据集合中的数据时,使用迭代器可以达成这个目的
  • 另一方面,数据的存储是需要占用内存的,数据量越大所占用的内存就越多。如果我们使用列表这样的结构来保存大批量的数据,并且数据使用频率不高的话,就十分浪费资源了。而迭代器可以不保存数据,它的数据可以在需要时被计算出来(这一特性也叫做惰性计算)。在合适的些场景下使用迭代器可以节省内存资源。

3、生成器(Generator)

刚才我们自定义了迭代器,其实创建迭代器还有另一种方式,就是使用生成器

生成器是一个函数,这个函数的特殊之处在于它的 return 语句被 yield 语句替代。

如刚才用于生成 2 的指数幂的迭代器,可以通过生成器来实现:


  
  1. def power_of_two():
  2. for exponent in range( 11): # range(11) 表示左闭右开区间 [0, 11),不包含 11
  3. yield 2 ** exponent # 以 2 为底数求指数幂

生成器使用方法:


  
  1. p = power_of_two() # 以函数调用的方式创建生成器对象
  2. next(p) # 同样使用 next() 来取值

生成器的关键在于 yield 语句yield 语句的作用和 return 语句有几分相似,都可以将结果返回。不同在于,生成器函数执行至 yield 语句,返回结果的同时记录下函数内的状态,下次执行这个生成器函数,将从上次退出的位置(yield 的下一句代码)继续执行。当生成器函数中的所有代码被执行完毕时,自动抛出 StopIteration 异常。

我们可以看到,生成器的用法和迭代器相似,都使用 next() 来进行迭代。这是因为生成器其实就是创建迭代器的便捷方法,生产器会在背后自动定义 __iter__()__next__() 方法。

(1)生成器表达式(Generator Expression)

可以用一种非常简便的方式来创建生成器,就是通过生成器表达式。生成器的写法非常简单,但是灵活性也有限,所能表达的内容相对简单。

生成器表达式的写法如下:

生成器 = (针对项的操作 forin 可迭代对象)

如:

>>> letters = (item for item in ‘abc’)
>>> letters
<generator object at 0x1074a8228>
>>> next(letters)
‘a’
>>> next(letters)
‘b’
>>> next(letters)
‘c’

>>> letters = (i.upper() * 2 for i in ‘abc’)
>>> next(letters)
‘AA’
>>> next(letters)
‘BB’
>>> next(letters)
‘CC’

 

二、生成器表达式和列表生成式

1、列表生成式

如果我们想要构造一个包含指定元素或者具有某种规则的列表,比如 2 的指数幂序列 [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024], 该怎么做?

  • 最简单的办法,直接将这些数原样写入代码来创建列表:

    nums = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
    

    这种办法当然是可行的,不过也有很大的局限。像这样直接将数据写入代码的做法,叫做硬编码。很多情况下我们不会使用硬编码的方式来创建列表(或者其它容器),因为列表中有什么数据往往在写代码时是不能确定的,通常在程序运行过程中通过计算得到,或从程序外部读入(比如从数据库 / 文件 / 网络中读入)。另外当数据量很大时,使用硬编码也是一件繁琐低效的事。

  • 还有一种办法,创建一个空的列表,之后通过计算(或其它操作)获得各个元素,并添加到列表中:

    
        
    1. nums = []
    2. exponent = 1
    3. while exponent <= 10:
    4. nums.append( 2 ** exponent)
    5. exponent += 1

    >>> nums
    [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]

    这样方式完全可行,也没有什么缺陷。

  • 或者,对一个现有的可迭代对象中的各个元素做处理,构造出一个新的列表:

    
        
    1. nums = []
    2. for i in range( 1, 11): # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    3. nums.append( 2 ** i)

    >>> nums
    [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]

    这段代码可以用一种简洁的方式写出,只需要一行代码:

    nums = [2 ** i for i in range(1, 11)]
    

    >>> nums
    [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]

    这行代码就是我们这个章节要所讲的列表生成式。顾名思义,列表生成式最终生成的是一个列表,它是用已有的可迭代对象来构造新列表的便捷方法。

提示:如果不清楚什么是可迭代对象,可以看一下上一篇文章《深入理解迭代器和生成器》。

 

(1)列表生成式的写法

列表生成式的语法如下:

[对项的操作 forin 可迭代对象]

这个写法怎么理解呢?

首先,这句代码的阅读顺序是:for 项 in 可迭代对象 -> 对项的操作。其次,外围的方括号([])表明这是列表生成式,最终的结果是一个列表。

for 项 in 可迭代对象 这部分和 for 循环很相似,通过迭代可迭代对象,每次取出一个项。对于取出的项,我们可以对它做一些处理,也就是表达式中的 对项的操作 部分。最终,可迭代对象中的所有项都会被迭代和处理,并被收集起来形成一个新的列表。

这个过程用伪代码来描述的话是这样的:


  
  1. 列表 = []
  2. forin 可迭代对象:
  3. 新项 = 对项的操作(项)
  4. 列表.appent(新项)

来看一个例子:

这里有个列表:['a', 'b', 'c', 'd', 'e'],怎样把其中的每个小写字母转换为大写?可以这样:

[char.upper() for char in ['a', 'b', 'c', 'd', 'e']]

>>> [char.upper() for char in [‘a’, ‘b’, ‘c’, ‘d’, ‘e’]]
[‘A’, ‘B’, ‘C’, ‘D’, ‘E’]

如果你不能一下子理解,不妨比较一下用 for 循环来实现的版本。它们之间是等价的:


  
  1. chars = []
  2. for char in [ 'a', 'b', 'c', 'd', 'e']:
  3. chars.append(char.upper())

>>> chars
[‘A’, ‘B’, ‘C’, ‘D’, ‘E’]

再来谈谈 [对项的操作 for 项 in 可迭代对象] 中的 对项的操作,这个操作它可以简单,也可以很复杂。

简单来看,我们可以直接使用 本身而不做任何处理。如:

>>> [char for char in ‘ABCDEF’]
[‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’]

当然如果是要得到这个结果,我们应该直接使用 list('ABCDEF')

>>> list(‘ABCDEF’)
[‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’]

复杂来看,我们可以对 进行一系列的处理。如分别将 'abcde' 中每个字母的大写形式和小写形式放到元组中:

[(char.upper(), char) for char in 'abcde']

>>> [(char.upper(), char) for char in ‘abcde’]
[(‘A’, ‘a’), (‘B’, ‘b’), (‘C’, ‘c’), (‘D’, ‘d’), (‘E’, ‘e’)]

这里我们将每个 char 转换为了 (char.upper(), char),并且其中 char 被多次用到。

上个例子等价于:


  
  1. result = []
  2. for char in 'abcde':
  3. result.append((char.upper(), char))

>>> result
[(‘A’, ‘a’), (‘B’, ‘b’), (‘C’, ‘c’), (‘D’, ‘d’), (‘E’, ‘e’)]

列表生成式中使用 if

在列表生成式的中,每次迭代的 是可以被筛选过滤的,使用 if 关键字。如:

[对项的操作 forin 可迭代对象 if 对项的判断]

它的阅读顺序是:for 项 in 可迭代对象 -> if 对项的判断 -> 对项的操作

每次迭代时所取出的 ,要先经过 对项的判断,如果结果为 True,才会由 对项的操作 处理。如果 对项的判断 的结果为 False,后续 对项的操作 会被跳过,此时最终列表的长度也会减少。

举个例子,[2 ** i for i in range(1, 11)] 可以生成出 20~210 间的整数,如果我们只想要其中的奇数次方的值,该怎么做?

这时就可以在列表中使用 if 关键字:

[2 ** i for i in range(1, 11) if i % 2 == 1 ]

>>> [2 ** i for i in range(1, 11) if i % 2 == 1 ]
[2, 8, 32, 128, 512]

这里的阅读顺序是:

  1. for e in range(1, 11)
  2. if e % 2 == 1
  3. 2 ** e

上述代码等价于:


  
  1. nums = []
  2. for i in range( 1, 11):
  3. if i % 2 == 1:
  4. nums.append( 2 ** i)

>>> nums
[2, 8, 32, 128, 512]

列表生成式中嵌套 for

列表生成式中的 for 中还可以再嵌套 for。如:

[对项1和(或)项2的操作 for1 in 可迭代对象1 for2 in 可迭代对象2]

它等价于:


  
  1. 列表 = []
  2. for1 in 可迭代对象 1:
  3. for2 in 可迭代对象 2:
  4. 新项 = 对项 1和(或)项 2的操作()
  5. 列表.append(新项)

看起来有点复杂,我们看个例子:


  
  1. nums = [ 1, 2, 3]
  2. chars = [ 'a', 'b', 'c']
  3. [c * n for n in nums for c in chars]

>>> nums = [1, 2, 3]
>>> chars = [‘a’, ‘b’, ‘c’]
>>> [c * n for n in nums for c in chars]
[‘a’, ‘b’, ‘c’, ‘aa’, ‘bb’, ‘cc’, ‘aaa’, ‘bbb’, ‘ccc’]

它等价于:


  
  1. nums = [ 1, 2, 3]
  2. chars = [ 'a', 'b', 'c']
  3. result = []
  4. for n in nums:
  5. for c in chars:
  6. result.append(c * n)

>>> result
[‘a’, ‘b’, ‘c’, ‘aa’, ‘bb’, ‘cc’, ‘aaa’, ‘bbb’, ‘ccc’]

[对项1和(或)项2的操作 for 项1 in 可迭代对象1 for 项2 in 可迭代对象2] 中的 可迭代对象2 可以是 项1 本身。也就是可以写成:

[对项和(或)子项的操作 forin 可迭代对象 for 子项 in 项]

例如:


  
  1. strings = [ 'aa', 'bb', 'cc']
  2. [char for string in strings for char in string]

>>> strings = [‘aa’, ‘bb’, ‘cc’]
>>> [char for string in strings for char in string]
[‘a’, ‘a’, ‘b’, ‘b’, ‘c’, ‘c’]

它等价于:


  
  1. strings = [ 'aa', 'bb', 'cc']
  2. result = []
  3. for string in strings:
  4. for char in string:
  5. result.append(char)

>>> result
[‘a’, ‘a’, ‘b’, ‘b’, ‘c’, ‘c’]

 

2、字典生成式

便捷地构造列表可以使用列表生成式,同样的,想要通过已有的可迭代对象来便捷地构造字典,可以使用字典生成式

字典生成式的写法是:

{键: 值 forin 可迭代对象}

和列表生成式非常相似,不同之处在于它使用的是花括号({}),另外还使用 键: 值 形式。

举个例子,有字符串 'abcde',以每个小字母作为键,对应大写字母作为值的来构造个字典:

{char: char.upper() for char in 'abcde'}

>>> {char: char.upper() for char in ‘abcde’}
{‘a’: ‘A’, ‘b’: ‘B’, ‘c’: ‘C’, ‘d’: ‘D’, ‘e’: ‘E’}

同样的,字典生成式中也可以使用 if 和嵌套 for,使用方法参照列表生成式。

 

3、集合生成式

想要通过已有的可迭代对象来构造集合,可以使用集合生成式

你可能已经猜到了,只需要将列表生成式的方括号([])替换为花括号({})即可:

{对项的操作 forin 可迭代对象}

例如:

{char.lower() for char in 'ABCDABCD'}

>>> {char.lower() for char in ‘ABCDABCD’}
{‘c’, ‘a’, ‘d’, ‘b’}

提示:通过这个例子也能看到集合的重要特性——无序且无重复。

同样的,集合生成式中也可以使用 if 和嵌套 for

 

4、生成器表达式

上面有列表生成式、字典生成式、集合生成式,那么是不是也有「元组生成式」?是不是用圆括号来表示就可以了?

不是的,Python 中并没有「元组生成式」!虽然 Python 中确实有类似的圆括号的写法:

(对项的操作 forin 可迭代对象)

但这可不是什么「元组生成式」,而是我们上一章节学习过的生成器表达式

生成器表达式是一种创建生成器的便捷方法。虽然写法上和列表生成式、字典生成式、集合生成式相似,却有着本质的不同,因为它创建出来的是生成器,而不是列表、字典、集合这类容器。

(char.lower() for char in 'ABCDEF')

>>> g = (char.lower() for char in ‘ABCDEF’)
>>> g
<generator object at 0x103da6c78>
>>> next(g)
‘a’
>>> next(g)
‘b’

提示:如果你对生成器有些遗忘,不妨看下前一篇文章《深入理解迭代器和生成器》。

生成器表达式中同样可以使用 if 和嵌套 for,使用方法和列表生成式相同。

 

三、给凡人添加超能力:入手装饰器

在学习装饰器前,我们先来了解两个函数概念。

1、函数中定义函数

在 Python 中,函数内部是可以嵌套地定义函数的。如:


  
  1. def print_twice(word):
  2. def repeat(times):
  3. return word * times
  4. print(repeat( 2))

>>> print_twice('go ')
gogo

内层函数只能在包裹它的外层函数中使用,而不能在外层函数外使用。比如上面的 repeat() 可以在 print_twice() 中使用,但是不能在 print_twice() 的外部使用。

另外,内层函数中可以使用外层函数的参数或其它变量。如上面的参数 word

 

2、函数返回函数

之前我们学习过,函数可以作为另一个函数的参数。类似的,函数的返回值也可以是一个函数。

如:


  
  1. def print_words(word):
  2. def repeat(times):
  3. return word * times
  4. return repeat

>>> f = print_words(‘go’)
>>> f
<function print_words..repeat at 0x10befe620>

我们调用 print_words() 并用变量 f 接收其返回值,f 是个函数,是 print_words 下的 repeat 函数。

既然 f 是个函数,自然可以被调用,这也就相当于调用 repeat()

>>> f(2)
‘gogo’

扩展:我们直接调用 f(也就是 repeat())时,repeat() 内部会使用变量 word,而这个变量时定义在外层函数 print_words() 中的,却会一直伴随 repeat() 而存在,这在 Python 中叫作闭包。

 

3、什么是装饰器?

好了,回到正题,来看看什么是装饰器。我们在《类进阶》章节中介绍过类方法和静态方法的定义方式,还记得吗,定义它们时需要用到 @classmethod@staticmethod,它们就是装饰器。写法为 @装饰器名称

装饰器用来增强一个现有函数的功能,并且不改变这个函数的调用方式。这种增强是非侵入式的,也就是说无需直接修改函数内部的代码,而是在函数的外部做文章。

举个例子,假设我们有这样一个函数:


  
  1. def say_hello():
  2. print( 'Hello!')

>>> say_hello()
Hello!

这个函数非常简单,每次调用会输出「Hello!」,假如我们想在每次输出「Hello!」的同时附带上当前的时间,像这样:

>>> say_hello()
[ 2019-09-14 16:38:10.942802 ]
Hello!
>>> say_hello()
[ 2019-09-14 16:42:58.409742 ]
Hello!

如果想具备上面的功能,但又不想修改 say_hello() 函数的内部实现,该怎么做?

这就是装饰器的典型使用场景了——非侵入的情况下让函数具备更多的功能。

假设我们已经有了一个能满足该需求的装饰器 @time ,只要像这样来装饰 say_hello() 即可:


  
  1. @time
  2. def say_hello():
  3. print( 'Hello!')

函数的调用方式依然不变:

>>> say_hello()

当然,虽然 Python 中内置有一些装饰器,如 @classmethod@staticmethod,但并没 @time,所以我们需要自己来定义它。

 

4、自定义装饰器

我们来自定义之前所说的装饰器 @time,要求是使用它可以在函数调用时输出调用时间。

这里直接给出 @time 的实现:


  
  1. import datetime # 日期时间相关库,用于后续获取当前时间
  2. def time(func):
  3. def wrapper(*args, **kw):
  4. print( '[', datetime.datetime.now(), ']')
  5. return func(*args, **kw)
  6. return wrapper

我们暂且不关注具体的实现细节,先使用一下看看:


  
  1. @time
  2. def say_hello():
  3. print( 'Hello!')

>>> say_hello()
[ 2019-09-14 16:42:58.409742 ]
Hello!
>>> say_hello()
[ 2019-09-15 09:44:06.155869 ]
Hello!

没有问题,效果和预期相同!那这是什么原理呢?

 

(1)装饰器原理

其实,


  
  1. @time
  2. def say_hello():
  3. print( 'Hello!')

等效于:


  
  1. def say_hello():
  2. print( 'Hello!')
  3. say_hello = time(say_hello)

也就是说,我们用 @time 装饰 say_hello() 时,Python 会在背后做了这样一个操作(重点):

say_hello = time(say_hello)

@time(包括所有装饰器)本质上是个以函数作为参数,并返回函数的函数。不妨回过头来观察下 @time 实现:


   
  1. import datetime # 日期时间相关库,用于后续获取当前时间
  2. def time(func):
  3. def wrapper(*args, **kw):
  4. print( '[', datetime.datetime.now(), ']')
  5. return func(*args, **kw)
  6. return wrapper

say_hello = time(say_hello) 这句代码将函数 say_hello 作为参数来调用 time()time() 将其内部定义的函数返回了出来,并替换了函数 say_hello。结合装饰器实现来看, say_hello() 其实变成了 time() 中的 wrapper()

>>> say_hello
<function time..wrapper at 0x10befea60>

那就来具体看下 wrapper()


  
  1. def wrapper(*args, **kw):
  2. print( '[', datetime.datetime.now(), ']')
  3. return func(*args, **kw)

wrapper() 其实也非常简单,其内部的 print('[', datetime.datetime.now(), ']')[ 时间 ] 的格式将当前时间输出出来,达成了「输出函数调用时间」的目的。其中 datetime.datetime.now() 用于获取当前的时间。

最后一句 return func(*args, **kw) 比较关键,这里调用函数 func() 并将其结果返回出去。func() 是什么?它就是 say_hello()。最初 say_hello 作为参数被传入 time() 中,其参数名便是 func

参数 *args**kw 是什么?还记得我们在《函数进阶》中的内容吗,*args 可以接收一切非关键字参数,而 **kw 可以接收一切关键字参数,两个结合起来一起使用就可以接收一切参数了。用在这里的作用是,接收调用 say_hello() 时的所有参数,并悉数传给 func()

稍作梳理我们就能明白,装饰器之所以能够增强一个函数的功能,其实就是将被装饰函数用新函数替换,虽然还是同一个函数名,但函数内部实现已经变了。而这个新函数的内部在添加了一些功能的后,还会调用之前被装饰的函数。这样就相当于对被装饰的函数做了非侵入的扩展。

 

5、functools.wraps 装饰器

当一个函数不被装饰器装饰时,其函数名称就是自己。如:

>>> def say_hello():
…     print(‘Hello!’)

>>> say_hello
<function say_hello at 0x10efbb1e0>

>>> say_hello.__name__
‘say_hello’

在解释器中直接输入 say_hello,显示其为 function say_hello。使用 say_hello.__name__,可以直接获取到其函数名称,此处显示为 say_hello

如果我们用装饰器 @time 来修饰这个函数,那结果就不同了:

>>> @time
… def say_hello():
…     print(‘Hello!’)

>>> say_hello
<function time..wrapper at 0x10efbb048>

>>> say_hello.__name__
‘wrapper’

可以看到其名字信息被装饰器中的函数 wrapper 覆盖了。

是的,由于装饰器本质上是用一个新的函数来替换被装饰的函数,所以函数的元信息会被覆盖。

那有没有什么方式保留被装饰函数的元信息呢?有的,可以在定义装饰器时使用 @functools.wraps 装饰器。使用如下:


  
  1. import datetime
  2. import functools
  3. def time(func):
  4. @functools.wraps(func)
  5. def wrapper(*args, **kw):
  6. print( '[', datetime.datetime.now(), ']')
  7. return func(*args, **kw)
  8. return wrapper

>>> say_hello
<function say_hello at 0x10ef5c378>

>>> say_hello.__name__
‘say_hello’

可以看到使用 @functools.wraps 后,元信息恢复如初,不留痕迹。

 

6、带参数的装饰器

既然装饰器本质上是个函数,那这个函数能不能有参数呢?答案是可以有。

举个例子,刚才我们输出的时间格式是 [ 2019-09-14 16:42:58.409742 ],如果我们想要自行指定这个格式,可以考虑用装饰器参数的形式来设置。

带时间格式的装饰器如下:


  
  1. import datetime
  2. import functools
  3. def time(format):
  4. def decorator(func):
  5. @functools.wraps(func)
  6. def wrapper(*args, **kw):
  7. print(datetime.datetime.now().strftime(format))
  8. return func(*args, **kw)
  9. return wrapper
  10. return decorator

可以看到,这回装饰器变成了三层函数嵌套的形式。是的,如果需要指定装饰器的参数,那么就需要在原来装饰器的基础上在再加一层函数。

wrapper() 中原本的 print('[', datetime.datetime.now(), ']') 被修改为 print(datetime.datetime.now().strftime(format)),其中的 format 便是装饰器的参数,也就是时间格式。

使用时,在装饰器 @time 后添加括号并写上参数:


  
  1. @time('%Y/%m/%d %H:%M:%S')
  2. def say_hello():
  3. print( 'Hello!')

>>> say_hello()
2019/09/15 10:00:24
Hello!

可以看到时间格式已经根据我们的设置而生效。

扩展:

'%Y/%m/%d %H:%M'datetime 包中用于指定时间格式的字符串,其中:

  • %Y 表示年
  • %m 表示月
  • %d 表示天
  • %H 表示小时
  • %M 表示分钟
  • %S 表示秒。

 

(1)带参数的装饰器原理

带参数的装饰器的实现为什么要三层函数嵌套?看了下面的等效代码你就明白了!


  
  1. @time('%Y/%m/%d %H:%M:%S')
  2. def say_hello():
  3. print( 'Hello!')

等效于:


  
  1. def say_hello():
  2. print( 'Hello!')
  3. say_hello = time( '%Y/%m/%d %H:%M:%S')(say_hello)

而不带参数的装饰器的等效代码是 say_hello = time(say_hello)。对比可以看出,带参数的装饰器的等效代码多了一次函数调用,通过这种方式将装饰器参数传递到内部的两层函数中,这之后便回到了不带参数的装饰器的情形。

关于Python进阶中迭代器、生成器、装饰器的讲解就和大家分享到这里,其中有不懂的地方欢迎小伙伴评论提出!

之后持续为大家分享更多进阶内容,欢迎小伙伴们点赞关注呀!

灰小猿陪你一起进步!


转载:https://blog.csdn.net/weixin_44985880/article/details/116100061
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场