当前位置 博文首页 > 灰小猿的博客:周末加班想摸鱼?不如来点Python进阶干货呀!【超

    灰小猿的博客:周末加班想摸鱼?不如来点Python进阶干货呀!【超

    作者:[db:作者] 时间:2021-07-17 13:10

    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 的对象

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

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

    (3)自定义迭代器

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

    class MyIterator:
        def __next__(self):
            代码块
    
        def __iter__(self):
            return self
    

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

    class PowerOfTwo:
        def __init__(self):
            self.exponent = 0  					# 将每次的指数记录下来
    
        def __next__(self):
            if self.exponent > 10:
                raise StopIteration
            else:
                result = 2 ** self.exponent		# 以 2 为底数求指数幂
                self.exponent += 1
                return result
    
        def __iter__(self):
            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 的指数幂的迭代器,可以通过生成器来实现:

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

    生成器使用方法:

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

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

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

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

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

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

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

    如:

    >>> 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]
      

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

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

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

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

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

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

      nums = []
      for i in range(1, 11):		# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
          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)列表生成式的写法

    列表生成式的语法如下:

    [对项的操作 for 项 in 可迭代对象]
    

    这个写法怎么理解呢?

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

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

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

    列表 = []
    
    for 项 in 可迭代对象:
    	新项 = 对项的操作(项)
        列表.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 循环来实现的版本。它们之间是等价的:

    chars = []
    for char in ['a', 'b', 'c', 'd', 'e']:
        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 被多次用到。

    上个例子等价于:

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

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

    列表生成式中使用 if

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

    [对项的操作 for 项 in 可迭代对象 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

    上述代码等价于:

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

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

    列表生成式中嵌套 for

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

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

    它等价于:

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

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

    nums = [1, 2, 3]
    chars = ['a', 'b', 'c']
    [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’]

    它等价于:

    nums = [1, 2, 3]
    chars = ['a', 'b', 'c']
    result = []
    for n in nums:
        for c in chars:
            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 本身。也就是可以写成:

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

    例如:

    strings = ['aa', 'bb', 'cc']
    [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’]

    它等价于:

    strings = ['aa', 'bb', 'cc']
    result = []
    for string in strings:
        for char in string:
            result.append(char)
    

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

    ?

    2、字典生成式

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

    字典生成式的写法是:

    {键: 值 for 项 in 可迭代对象}
    

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

    举个例子,有字符串 '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、集合生成式

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

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

    {对项的操作 for 项 in 可迭代对象}
    

    例如:

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

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

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

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

    ?

    4、生成器表达式

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

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

    (对项的操作 for 项 in 可迭代对象)
    

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

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

    (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 中,函数内部是可以嵌套地定义函数的。如:

    def print_twice(word):
        def repeat(times):
            return word * times
        
        print(repeat(2))
    

    >>> print_twice('go ')
    gogo

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

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

    ?

    2、函数返回函数

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

    如:

    def print_words(word):
        def repeat(times):
            return word * times
        
        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,它们就是装饰器。写法为 @装饰器名称

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

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

    def say_hello():
        print('Hello!')
    

    >>> say_hello()
    Hello!