2024 Python3.10 系统入门+进阶(十四):Python三大神器迭代器、生成器、装饰器详解

一、迭代器

1.1 可迭代对象(Iterable)

在 Python 中,迭代是指通过逐步访问一个容器(如列表、元组、字典等)或可迭代对象(如生成器、文件对象等)中的每个元素的过程。迭代让你能够依次处理容器中的每一个元素,而不需要显式地知道它们的索引或数量。迭代是 Python 处理集合、序列和其他可遍历数据结构的核心机制。我们可以对 list、tuple、str 等类型的数据使用 for...in... 的循环语法从其中依次拿到数据进行使用,其实这样的过程就是迭代。但是,是否所有的数据类型都可以放到 for...in... 的语句中,然后让 for...in... 每次从中取出一条数据供我们使用,即供我们迭代吗?

In [8]: for i in 100:
   ...:     print(i)
   ...:
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[8], line 1
----> 1 for i in 100:
      2     print(i)

TypeError: 'int' object is not iterable

In [9]: class IterableTest(object):
   ...:     def __init__(self):
   ...:         self.items = []
   ...:
   ...:     def add(self, value):
   ...:         self.items.append(value)
   ...:

In [10]: it1 = IterableTest()
    ...: it1.add('amo')
    ...: it1.add('jerry')
    ...: it1.add('paul')
    ...: it1.add('crystal')
    ...: for i in it1:
    ...:     print(i)
    ...:
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 6
      4 it1.add('paul')
      5 it1.add('crystal')
----> 6 for i in it1:
      7     print(i)

TypeError: 'IterableTest' object is not iterable

我们自定义了一个容器类型 IterableTest,在将一个存放了多个数据的 IterableTest 对象放到 for...in... 的语句中,发现 for...in... 并不能从中依次取出一条数据返回给我们,也就说我们随便封装了一个可以存放多条数据的类型却并不能被迭代使用。用不太准确地话讲,我们把可以通过 for...in... 这类语句迭代读取一条数据供我们使用的对象称之为 可迭代对象(Iterable)。 Python 提供了 collections.abc.Iterable 来检测一个对象是否是可迭代对象:

In [3]: from collections.abc import Iterable

In [11]: isinstance(it1, Iterable)
Out[11]: False

In [12]: isinstance([10, 20, 30], Iterable)
Out[12]: True

In [13]: isinstance(1, Iterable)
Out[13]: False

In [14]: isinstance('abcdefg', Iterable)
Out[14]: True

我们分析对可迭代对象进行迭代使用的过程,发现每迭代一次(即在 for...in... 中每循环一次)都会返回对象中的下一条数据,一直向后读取数据,直到迭代了所有数据后结束。那么,在这个过程中就应该有一个 "人" 去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。我们把这个能帮助我们进行数据迭代的 "人" 称为 迭代器(Iterator)。 可迭代对象的本质就是可以向我们提供一个这样的中间 "人" 即迭代器帮助我们对其进行迭代遍历使用。可迭代对象通过 __iter__ 方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据。那么也就是说,一个具备了 __iter__ 方法的对象,就是一个可迭代对象。示例:

In [15]: class IterableTest(object):
    ...:     def __init__(self):
    ...:         self.items = []
    ...:
    ...:     def add(self, value):
    ...:         self.items.append(value)
    ...:
    ...:     def __iter__(self):
    ...:         """返回一个迭代器"""
    ...:         # 我们暂时忽略如何构造一个迭代器对象
    ...:         pass
    ...:

In [16]: it1 = IterableTest()

In [17]: isinstance(it1, Iterable)
Out[17]: True  # # 这回测试发现添加了__iter__方法的it1对象已经是一个可迭代对象了

In [18]: it1.add('amo')

In [19]: it1.add('jerry')

In [20]: it1.add('paul')

In [21]: for i in it1:  # 迭代还是报错,是因为我们还未定义迭代器,更加说明迭代器"人"的身份
    ...:     print(i)
    ...:
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[21], line 1
----> 1 for i in it1:
      2     print(i)

TypeError: iter() returned non-iterator of type 'NoneType'

list、tuple 等都是可迭代对象,我们可以通过 iter() 函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用 next() 函数来获取下一条数据,iter() 函数实际上就是调用了可迭代对象的 __iter__ 方法。iter() 函数的使用可以参考文章 Python 常用内置函数详解(五):all()、any()、filter()、iter()、map()、next()、range()、reversed()、sorted、zip()函数详解 中的 十、iter()函数——用于生成迭代器 小节。

In [22]: name_list = ['Amo', 'Jerry', 'Paul', 'Ben', 'Crystal']

In [23]: iter1 = iter(name_list)  # 注意要用一个变量接收,返回一个迭代器

In [24]: next(iter1)
Out[24]: 'Amo'

In [26]: next(iter1)
Out[26]: 'Jerry'

In [27]: next(iter1)
Out[27]: 'Paul'

In [28]: next(iter1)
Out[28]: 'Ben'

In [29]: next(iter1)
Out[29]: 'Crystal'

In [30]: next(iter1)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Cell In[30], line 1
----> 1 next(iter1)

StopIteration:

注意,当我们已经迭代完最后一个数据之后,再次调用 next() 函数会抛出 StopIteration 的异常,来告诉我们所有数据都已迭代完成,不用再执行 next() 函数了。

1.2 迭代器

通过上面的分析,我们已经知道,迭代器是用来帮助我们记录每次迭代访问到的位置,当我们对迭代器使用 next() 函数的时候,迭代器会向我们返回它所记录位置的下一个位置的数据。实际上,在使用 next() 函数的时候,调用的就是迭代器对象的 __next__ 方法。所以,我们要想构造一个迭代器,就要实现它的 __next__ 方法。但这还不够,python 要求迭代器本身也是可迭代的,所以我们还要为迭代器实现 __iter__ 方法,而 __iter__ 方法要返回一个迭代器,迭代器自身正是一个迭代器,所以迭代器的 __iter__ 方法返回自身即可。一个实现了 __iter__ 方法和 __next__ 方法的对象,就是迭代器。注意: 迭代器的特点是惰性求值,即当你需要元素时才会生成,而不会一次性将所有元素生成完毕。

# -*- coding: utf-8 -*-
# @Time    : 2024-09-08 14:57
# @Author  : AmoXiang
# @File: 迭代器.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

from collections.abc import Iterable
from collections.abc import Iterator


class IterableTest(object):
    def __init__(self):
        self.items = []

    def add(self, value):
        self.items.append(value)

    def __iter__(self):
        return IteratorTest(self.items)


class IteratorTest(object):
    def __init__(self, items):
    	# idx用来记录当前访问到的位置
        self.idx = 0
        self.items = items
	
	# 这里我做了测试,没有__iter__方法也是可以正常迭代的,但是此时isinstance(IteratorTest([]), Iterator)返回False
	# 意思是要迭代对象的__iter__方法返回的对象中包含__next__方法即可迭代,不一定要实现__iter__方法
    def __iter__(self):
        return self

    def __next__(self):
        if len(self.items) > self.idx:
            value = self.items[self.idx]
            self.idx += 1
            return value
        else:
            raise StopIteration()


it1 = IterableTest()
it1.add(1)
it1.add(2)
it1.add(3)
it1.add(4)
it1.add(5)
print(isinstance(IteratorTest([]), Iterator))
for i in it1:
    print(i)

1.3 可迭代对象与迭代器的区别

  1. 是否实现 __next__() 方法:
    • 可迭代对象没有 __next__() 方法,不能直接获取下一个元素。
    • 迭代器实现了 __next__() 方法,可以用 next() 来获取下一个元素。
  2. 遍历方式:
    • 可迭代对象可以被 for 循环遍历,但 for 循环实际上是在内部将可迭代对象转换为迭代器。
    • 迭代器只能通过 next() 逐步遍历,或者通过 for 循环来遍历。
  3. 一次性遍历:
    • 可迭代对象可以多次被遍历,例如对一个列表可以多次使用 for 循环。
    • 迭代器只能遍历一次,遍历结束后无法重置。
  4. 内存消耗:
    • 可迭代对象通常会将所有元素保存在内存中(如列表、元组等)。
    • 惰性求值,迭代器按需生成元素,节省内存,适合处理大数据或无限序列。

1.4 for…in…循环的本质

在 Python 中,for 循环用于遍历可迭代对象或者迭代器。它的工作原理实际上是基于迭代器协议。理解 for 循环的原理包括以下几个核心步骤:将可迭代对象转换为迭代器,并通过迭代器来获取元素,直到遇到 StopIteration 异常。以下是详细解释:

  1. 判断是否是可迭代对象:当 for 循环接收到一个对象时,它首先会检查该对象是否是一个可迭代对象(即是否实现了 __iter__() 方法)。如果是,for 循环会调用这个方法获取一个迭代器。
  2. 获取迭代器:通过 iter() 函数,for 循环将可迭代对象转换为一个迭代器。如果对象本身就是迭代器,它会直接使用它,而不会创建新的迭代器。
  3. 调用 next() 获取元素:for 循环会不断调用迭代器的 __next__() 方法,逐一获取下一个元素。
  4. 捕获 StopIteration:当迭代器耗尽所有元素时,__next__() 方法会抛出 StopIteration 异常。for 循环内部捕获该异常,并在后台自动终止循环,而不会显示异常信息。

使用 while 和迭代器,我们可以手动实现 for 循环的逻辑:

from collections.abc import Iterable

lis1 = [1, 2, 3, 4, 5, 6]
if isinstance(lis1, Iterable):
    # 1. 获取可迭代对象的迭代器
    lis1_iter = iter(lis1)
    while True:
        try:
            # 2. 通过 next() 获取下一个元素
            value = next(lis1_iter)
            # 3. 处理元素
            print(value)
        except StopIteration:
            # 4. 捕获 StopIteration 异常,终止循环
            break
else:
    raise TypeError

1.5 迭代器的应用场景

我们发现迭代器最核心的功能就是可以通过 next() 函数的调用来返回下一个数据值。如果每次返回的数据值不是在一个已有的数据集合中读取的,而是通过程序按照一定的规律计算生成的,那么也就意味着可以不用再依赖一个已有的数据集合,也就是说不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,这样可以节省大量的存储(内存)空间。举个例子,比如,数学中有个著名的斐波拉契数列(Fibonacci),数列中第一个数为1,第二个数为1,其后的每一个数都可由前两个数相加得到:1, 1, 2, 3, 5, 8, 13, 21, 34, …,现在我们想要通过 for...in... 循环来遍历迭代斐波那契数列中的前 n 个数。那么这个斐波那契数列我们就可以用迭代器来实现,每次迭代都通过数学计算来生成下一个数。

class Fibonacci(object):
    def __init__(self, num):
        self.num = num
        self.a = 1
        self.b = 1
        self.idx = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.idx <= self.num:
            value = self.a
            self.a, self.b = self.b, self.a + self.b
            self.idx += 1
            return value
        else:
            raise StopIteration


fi1 = Fibonacci(3)
for i in fi1:
    print(i)

二、生成器

关于生成器表达式与生成器函数分别参考:

  1. https://blog.csdn.net/xw1680/article/details/141675619 文章中的 四、生成器表达式 小节
  2. https://blog.csdn.net/xw1680/article/details/141959853 文章中的 二、生成器函数 小节

2.1 生成器与迭代器的关系

生成器本质上是迭代器:所有的生成器都是迭代器,但并不是所有的迭代器都是生成器。生成器遵循迭代器协议,它们实现了 __iter__()__next__() 方法,因此可以用于 for 循环或通过 next() 函数手动获取下一个值。生成器是迭代器的一种特殊实现:生成器提供了一种简便的方式来创建迭代器。通过生成器函数(使用 yield 关键字),可以轻松实现迭代器的功能,而不需要显式地实现 __iter__()__next__() 方法。

2.2 生成器与迭代器的区别

特性生成器迭代器
创建方式使用 yield 关键字的函数或生成器表达式创建通过实现 __iter__()__next__() 方法的类创建
写法简洁,使用 yield 返回值需要显式编写 __iter__()__next__() 方法
状态管理自动保存执行状态,每次调用 next() 从上次暂停处继续需要手动管理内部状态
一次性使用一次生成后不能重置,只能遍历一次可以通过重新创建迭代器对象来重置迭代
内存效率惰性求值,按需生成数据,节省内存迭代器不一定具有惰性求值,但一般来说内存效率也较高
返回值yield 生成值并暂停执行,直到下次调用 next()next() 返回元素,并手动管理结束条件

2.3 使用场景对比

生成器适用场景:

  1. 当你想快速生成一个迭代器并简化代码时。
  2. 处理数据流、大文件等按需生成数据的场景。
  3. 需要保存局部状态或通过暂停继续执行的场景。

迭代器适用场景:

  1. 当你需要更灵活地控制迭代过程,或者需要实现复杂的迭代逻辑时。
  2. 定制类的迭代行为,或者需要同时迭代多个属性时。

小结: 生成器是迭代器的一种简化实现方式,它通过 yield 关键字让你可以轻松创建惰性求值的迭代器。迭代器是一个更底层的概念,它要求实现 __iter__()__next__() 方法,用来逐一返回数据。生成器更简洁,适用于大部分惰性生成数据的场景;而迭代器则提供了更多的控制,适用于复杂的迭代逻辑。通过生成器,开发者可以更快、更直观地创建迭代器,而当迭代逻辑非常复杂时,手动实现迭代器则可以提供更多灵活性。

三、装饰器

装饰器是程序开发中经常会用到的一个功能,用好了装饰器,开发效率如虎添翼,所以这也是 Python 面试中必问的问题,但对于好多初次接触这个知识的人来讲,这个功能有点绕,自学时直接绕过去了,然后面试问到了就挂了,因为装饰器是程序开发的基础知识,这个都不会,别跟人家说你会 Python,看了下面的文章,保证你学会装饰器。

3.1 装饰器的由来

需求:为一个加法函数增加记录实参的功能

# -*- coding: utf-8 -*-
# @Time    : 2024-09-10 14:19
# @Author  : AmoXiang
# @File: 装饰器的由来.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

def add(x, y):
    print('add called. x={}, y={}'.format(x, y))  # 增加的记录功能
    return x + y


add(4, 5)

上面的代码满足了需求,但有缺点,记录信息的功能,是一个单独的功能。显然和 add 函数耦合太紧密。加法函数属于业务功能,输出信息属于非功能代码,不该放在 add 函数中。提供一个函数 logger 完成记录功能:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-10 14:19
# @Author  : AmoXiang
# @File: 装饰器的由来.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

def add(x, y):
    # print('add called. x={}, y={}'.format(x, y))  # 增加的记录功能
    return x + y


def logger(fn):
    print('调用前增强')
    ret = fn(4, 5)
    print('调用后增强')
    return ret


print(logger(add))

改进传参以及柯里化:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-10 14:19
# @Author  : AmoXiang
# @File: 装饰器的由来.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

def add(x, y):
    # print('add called. x={}, y={}'.format(x, y))  # 增加的记录功能
    return x + y


def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs)  # 参数解构
        print('调用后增强')
        return ret

    return wrapper


# log1 = logger(add)  # 用log1变量接收 这样操作真的ok嘛?
# value = log1(4, 5)

# print(value)
# print(logger(add)(1, 2))

# 为什么要add变量覆盖,你想象一下,你是在添加功能,add()这个业务函数肯定在之前有很多地方已经
# 调用过了,如果改为其他的变量接收,那么之前调用add()函数的地方都要用新的变量名重新进行调用
add = logger(add)  # 直接将之前的logger变量替换
print(add(100, 200))

有没有更简单的方式呢,答案肯定是有的,使用 @ 语法改进上述代码:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-10 14:19
# @Author  : AmoXiang
# @File: 装饰器的由来.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680


def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        ret = fn(*args, **kwargs)  # 参数解构
        print('调用后增强')
        return ret

    return wrapper


@logger  # 在函数上方使用@符号+函数名的方式 等价于==> add = logger(add)
def add(x, y):
    # def add(x, y):
    # print('add called. x={}, y={}'.format(x, y))  # 增加的记录功能
    return x + y


# log1 = logger(add)  # 用log1变量接收
# value = log1(4, 5)

# print(value)
# print(logger(add)(1, 2))

# add = logger(add)  # 直接将之前的logger变量替换
# print(add(100, 200))

print(add(1, 3))
print(add(100, 300))

引出装饰器的概念:Python 装饰器(decorators) 是函数式编程中的一种强大功能,用来在不改变原函数代码的情况下,增强或修改函数的行为。装饰器通常是一个函数,它接受另一个函数作为参数,并返回一个新的函数,添加一些额外功能后返回,上述例子中的 logger() 函数就是一个装饰器。装饰器 通过使用 @ 符号来应用于函数。装饰器本质上是一个函数,接受另一个函数作为参数,并返回一个新的函数。装饰器广泛用于日志记录、权限验证、性能监控、缓存等场景。理解装饰器的核心就是理解它们是高阶函数,即可以接受函数作为参数,或返回新的函数。上面示例代码运行图示:
请添加图片描述

3.2 无参装饰器

3.1 装饰器的由来 小节中所举的例子称为无参装饰器,@符号后是一个函数,虽然是无参装饰器,但是@后的函数本质上是单参函数,上例的 logger 函数是一个高阶函数。

@logger  # ==> add=logger(add) logger本质上单参函数接收add函数 
def add(x, y):
    # def add(x, y):
    # print('add called. x={}, y={}'.format(x, y))  # 增加的记录功能
    return x + y

日志记录装饰器简单实现:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-10 15:20
# @Author  : AmoXiang
# @File: 日志记录装饰器.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

import time
import datetime


def logger(fn):
    def wrapper(*args, **kwargs):
        print('调用前增强')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)  # 参数解构
        print('调用后增强')
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret

    return wrapper


@logger  # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    time.sleep(2)
    return x + y


print(add(100, 200))

装饰器本质:可以总结为高阶函数与闭包的结合。
在这里插入图片描述

3.3 带参装饰器

文档字符串 (注释),Python 文档字符串 Documentation Strings,在函数(类、模块)语句块的第一行,且习惯是多行的文本,所以多使用三引号,文档字符串也算是合法的一条语句,惯例是首字母大写,第一行写概述,空一行,第三行写详细描述,可以使用特殊属性 __doc__ 访问这个文档。

def acosh(*args, **kwargs): # real signature unknown
    """ Return the inverse hyperbolic cosine of x. """
    pass

def asin(*args, **kwargs): # real signature unknown
    """
    Return the arc sine (measured in radians) of x.
    
    The result is between -pi/2 and pi/2.
    """
    pass

给之前的示例 add() 函数加上文档字符串如下:

def add(x, y):
    """这是加法函数的文档"""
    return x + y


# add's doc = 这是加法函数的文档 能正常显示属于add函数的名称与文档字符串
print("{}'s doc = {}".format(add.__name__, add.__doc__))

将日志记录装饰器稍微改动:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-10 15:28
# @Author  : AmoXiang
# @File: 带参装饰器.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

import time
import datetime


def logger(fn):
    def wrapper(*args, **kwargs):
        """wrapper's doc"""
        print('调用前增强')
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)  # 参数解构
        print('调用后增强')
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret

    return wrapper


@logger  # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add's doc"""
    time.sleep(0.1)
    return x + y

# name=wrapper, doc=wrapper's doc
print("name={}, doc={}".format(add.__name__, add.__doc__))

被装饰后,函数名和文档都不对了。如何解决?functools 模块提供了一个 wraps 装饰器函数,本质上调用的是 update_wrapper,这两个函数用于更新或保持函数装饰器的元数据,语法格式如下:

In [32]: from functools import wraps

In [33]: wraps?
Signature:
wraps(
    wrapped,
    assigned=('__module__', '__name__', '__qualname__', '__doc__', '__annotations__'),
    updated=('__dict__',),
)
Docstring:
Decorator factory to apply update_wrapper() to a wrapper function

Returns a decorator that invokes update_wrapper() with the decorated
function as the wrapper argument and the arguments to wraps() as the
remaining arguments. Default arguments are as for update_wrapper().
This is a convenience function to simplify applying partial() to
update_wrapper().
File:      f:\dev_tools\python\python310\lib\functools.py
Type:      function


In [34]: from functools import update_wrapper

In [35]: update_wrapper?
Signature:
update_wrapper(
    wrapper,
    wrapped,
    assigned=('__module__', '__name__', '__qualname__', '__doc__', '__annotations__'),
    updated=('__dict__',),
)
Docstring:
Update a wrapper function to look like the wrapped function

wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
File:      f:\dev_tools\python\python310\lib\functools.py
Type:      function

将之前日志记录装饰器略微改动,如下图所示:
在这里插入图片描述
函数 wraps:

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)  # 偏函数实现,后面会介绍

装饰器本身可以带参数。带参数的装饰器是一个返回装饰器的函数,它接受参数,并返回一个真正的装饰器函数。再举一个简单的例子:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-10 15:55
# @Author  : AmoXiang
# @File: 带参装饰器2.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

def repeat(num):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(num):
                func(*args, **kwargs)

        return wrapper

    return decorator


@repeat(3)  # say_hello = repeat(3)(say_hello)
def say_hello():
    print("Hello!")


say_hello()

上面示例代码运行图示:
请添加图片描述

3.4 多个装饰器

示例:同一个装饰器装饰多个函数。

# -*- coding: utf-8 -*-
# @Time    : 2024-09-17 16:14
# @Author  : AmoXiang
# @File: 多个装饰器.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680


import datetime
from functools import wraps


def logger(fn):
    @wraps(fn)  # 用被包装函数fn的属性覆盖包装函数wrapper的同名属性
    def wrapper(*args, **kwargs):  # wrapper = wraps(fn)(wrapper)
        """wrapper's doc"""
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)  # 参数解构
        delta = (datetime.datetime.now() - start).total_seconds()
        print('Function {} took {}s.'.format(fn.__name__, delta))
        return ret

    return wrapper


@logger  # 等价于 add = wrapper <=> add = logger(add)
def add(x, y):
    """add function"""


@logger
def sub(x, y):
    """sub function"""


"""
logger什么时候执行?
logger执行过几次?
wraps装饰器执行过几次?
wrapper的__name__ 等属性被覆盖过几次?
add.__name__ 打印什么名称?
sub.__name__ 打印什么名称?
"""
print(add.__name__, sub.__name__)

示例2:多个装饰器可以同时作用于一个函数,每个装饰器将依次应用,最靠近函数的装饰器先执行,外层装饰器后执行。

def decorator1(func):
    def wrapper():
        print("Decorator 1")
        func()
    return wrapper

def decorator2(func):
    def wrapper():
        print("Decorator 2")
        func()
    return wrapper

@decorator1
@decorator2
def say_hello():
    print("Hello!")

say_hello()

装饰器的应用顺序从内到外,decorator2 先装饰 say_hello(),然后 decorator1 装饰结果。

四、类型注解和类型检查装饰器

Python 是动态语言,变量可以随时被赋值并改变类型,也就是说 Python 的变量是运行时决定的。

# -*- coding: utf-8 -*-
# @Time    : 2024-09-17 9:55
# @Author  : AmoXiang
# @File: 函数注解.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680


def add(x, y):
    return x + y


print(add(4, 5))
print(add('amo', 'xiang'))
print(add([10], [11]))
# TypeError: unsupported operand type(s) for +: 'int' and 'str'
print(add(4, 'abc'))  # 不到运行时,无法判断类型是否正确

动态语言缺点:

  1. 难发现:由于不能做任何类型检查,往往到了运行时问题才显现出来,或到了线上运行时才暴露出来
  2. 难使用:函数使用者看到函数时,并不知道函数设计者的意图,如果没有详尽的文档,使用者只能猜测数据的类型。对于一个新的 API,使用者往往不知道该怎么使用,对于返回的类型更是不知道该怎么使用

动态类型对类型的约束不强,在小规模开发的危害不大,但是随着 Python 的广泛使用,这种缺点确实对大项目的开发危害非常大。如何解决这种问题呢?

  1. 文档字符串。对函数、类、模块使用详尽的使用描述、举例,让使用者使用帮助就能知道使用方式。但是,大多数项目管理不严格,可能文档不全,或者项目发生变动后,文档没有更新等等。
  2. 类型注解:函数注解、变量注解

4.1 函数注解

在 Python 中,函数类型注解(Function Annotations)用于为函数的参数和返回值提供类型提示。这种注解并不会影响代码的实际执行,只是为开发者提供额外的信息,帮助提高代码的可读性和可维护性。Python 解释器不会强制执行这些类型检查,但开发者或使用类型检查工具(如 mypy)可以利用这些注解来进行静态类型检查。函数类型注解的基本形式是:

def func(arg1: int, arg2: str) -> bool:
    return arg1 > 0 and bool(arg2)

# 1.使用 : 为函数的参数提供类型提示
# 2.使用 -> 为函数的返回值提供类型提示
# arg1 的类型注解为 int(整数)
# arg2 的类型注解为 str(字符串)
# 返回值的类型注解为 bool(布尔值)

虽然这些注解是可选的,它们不会改变函数的行为,但它们可以帮助开发者理解函数的预期输入和输出类型。函数注解存放在函数的属性 __annotations__ 中,字典类型:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-17 9:55
# @Author  : AmoXiang
# @File: 函数注解.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680


def func(arg1: int, arg2: str) -> bool:
    return arg1 > 0 and bool(arg2)


# {'arg1': <class 'int'>, 'arg2': <class 'str'>, 'return': <class 'bool'>}
print(func.__annotations__)

示例1:注解函数参数。

def greet(name: str, age: int) -> str:
    """
    ......
    :param name: name 的类型注解为 str,表示它应接收一个字符串。
    :param age: age 的类型注解为 int,表示它应接收一个整数。
    :return: 返回类型注解为 str,表示返回值应为一个字符串。
    """
    return f"Hello, {name}. You are {age} years old."

示例2:注解没有返回值的函数(None)。如果一个函数不返回任何值,可以使用 None 类型进行注解。

示例3:复杂类型注解。除了基本类型(如 int、str、bool),Python 的 typing 模块提供了一些工具来注解更复杂的数据类型,例如列表、字典、元组等。

from typing import List, Tuple, Dict
from typing import Union, Any, Optional, Callable


# 列表类型注解
def sum_list(numbers: List[int]) -> int:
    """
    :param numbers: List[int] 表示参数 numbers 是一个整数列表
    :return:返回值为 int。
    """
    return sum(numbers)


# 字典类型注解
def count_items(items: Dict[str, int]) -> int:
    """
    :param items:Dict[str, int] 表示一个字典,其键是字符串,值是整数。
    :return:返回值为 int。
    """
    return sum(items.values())


# 元组类型注解
def process_coordinates(coord: Tuple[float, float]) -> float:
    """
    :param coord: Tuple[float, float] 表示元组中有两个 float 类型的元素
    :return: 返回值为 float。
    """
    x, y = coord
    return x + y


# 可选类型注解: 有时函数参数可以为某种类型或者为 None,这种情况下可以使用 Optional。
def greet(name: Optional[str] = None) -> str:
    """
    :param name:Optional[str] 表示参数 name 可以是 str 或 None。
    :return:返回值为 str。
    """
    if name:
        return f"Hello, {name}!"
    return "Hello, stranger!"


#  Callable 类型: 如果一个函数的参数是另一个函数,可以使用 Callable 进行类型注解。
def apply_function(func: Callable[[int, int], int], x: int, y: int) -> int:
    """
    :param func: Callable[[int, int], int] 表示 func 是一个接收两个整数并返回一个整数的函数。
    :param x: x 的类型是 int
    :param y: y 的类型是 int
    :return: 返回值为 int。
    """
    return func(x, y)


# Any 类型: 有时,你可能不知道或者不关心某个参数的具体类型,可以使用 Any 来表示。
def print_value(value: Any) -> None:
    """
    :param value: Any 表示 value 可以是任意类型。
    :return:
    """
    print(value)


# Union 类型: 当一个参数可以是多种类型时,可以使用 Union。
def process_data(data: Union[int, str]) -> None:
    """
    :param data:Union[int, str] 表示 data 可以是 int 或 str 类型。
    :return:
    """
    if isinstance(data, int):
        print(f"Processing an integer: {data}")
    else:
        print(f"Processing a string: {data}")

从 Python 3.9 开始,可以直接使用内置的类型提示而不需要 typing 模块。例如,可以使用 list、dict 而不是 List、Dict。

def sum_list(numbers: list[int]) -> int:
    return sum(numbers)

这种简化的语法从 Python 3.9 开始引入,进一步简化了类型提示的写法。小结:

  1. 函数参数与返回值注解通过 :-> 指定。
  2. 通过 typing 模块,可以为复杂类型如列表、字典、可选类型、函数等进行注解。
  3. Python 不强制类型检查,但类型注解可以帮助编写更清晰、可维护的代码,并与类型检查工具结合使用。

4.2 类型注解

类型注解(Type Hinting)是 Python 3.5 引入的一项功能,旨在提高代码的可读性和可维护性。虽然 Python 是动态类型的语言,变量和函数的类型是运行时才确定的,但通过类型注解,开发者可以提前声明变量和函数的类型,辅助 IDE 提供更好的代码补全和类型检查。类型注解并不会影响代码的执行,主要用于静态分析工具(如 mypy)进行类型检查。Python 类型注解的基本语法是使用 : 类型 进行声明。

# 变量的类型注解
x: int = 5
y: float = 3.14
name: str = "amo"
is_valid: bool = True
# 在上述代码中,x 被标注为整数类型,y 为浮点数,name 为字符串,is_valid 为布尔值。

以下是一些常见的类型注解:

int: 整数类型
float: 浮点数类型
str: 字符串类型
bool: 布尔类型
list: 列表类型
tuple: 元组类型
set: 集合类型
dict: 字典类型
None: 特殊类型,用于表示无返回值的函数

4.3 类型检查

Python 类型检查是一种在运行时或静态分析工具中验证数据类型的方法。Python 是动态类型语言,不要求提前声明变量类型,但通过类型检查,我们可以提高代码的安全性、稳定性和可维护性。类型检查主要通过以下几种方式实现:

  1. 运行时类型检查:通过内置函数如 isinstance() 和 type() 来检查对象的类型。
  2. 静态类型检查:通过类型注解和工具如 mypy 来在代码运行前检查类型。
  3. 自定义类型检查装饰器:通过装饰器进行参数和返回值的类型验证。

4.3.1 运行时类型检查

运行时类型检查是通过内置函数在代码执行期间动态检查变量的类型。

4.3.2 静态类型检查工具 mypy

Python 通过类型注解提供了静态类型检查的支持。静态类型检查不会在运行时进行,而是在开发阶段使用工具来检测代码中的类型问题。mypy 是最常用的 Python 静态类型检查工具,它可以在不执行代码的情况下检查类型错误。

C:\Users\amoxiang>pip install mypy
Collecting mypy
  Downloading mypy-1.11.2-cp310-cp310-win_amd64.whl (9.6 MB)
     ---------------------------------------- 9.6/9.6 MB 6.0 MB/s eta 0:00:00
Collecting tomli>=1.1.0
  Downloading tomli-2.0.1-py3-none-any.whl (12 kB)
Requirement already satisfied: typing-extensions>=4.6.0 in 省略xxx
Collecting mypy-extensions>=1.0.0
  Downloading mypy_extensions-1.0.0-py3-none-any.whl (4.7 kB)
Installing collected packages: tomli, mypy-extensions, mypy
Successfully installed mypy-1.11.2 mypy-extensions-1.0.0 tomli-2.0.1

[notice] A new release of pip is available: 23.0.1 -> 24.2
[notice] To update, run: python.exe -m pip install --upgrade pip

使用 mypy 进行类型检查,编写类型注解的代码后,使用 mypy 来检查代码中的类型问题。

# -*- coding: utf-8 -*-
# @Time    : 2024-09-17 13:23
# @Author  : AmoXiang
# @File: 类型检查.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680


def add(a: int, b: int) -> int:
    return a + b


result = add(10, "20")  # 这里有类型错误

使用 mypy 检查类型:
在这里插入图片描述
mypy 提供了严格的类型检查,能够有效捕捉类型错误,并且可以通过 IDE 插件集成到开发环境中。

4.3.3 自定义类型检查装饰器

通过自定义装饰器,我们可以在运行时检查函数参数和返回值的类型。以下是一个简单的类型检查装饰器的实现。类型检查装饰器示例:

from functools import wraps
from typing import get_type_hints


def type_checker(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 获取函数的类型注解
        hints = get_type_hints(func)
        # 检查参数类型
        for arg_name, arg_type in hints.items():
            if arg_name != 'return':
                if arg_name in kwargs:
                    value = kwargs[arg_name]
                else:
                    value = args[list(hints.keys()).index(arg_name)]
                if not isinstance(value, arg_type):
                    raise TypeError(f"Argument '{arg_name}' must be of type {arg_type}")
        # 调用原始函数
        result = func(*args, **kwargs)
        # 检查返回值类型
        if 'return' in hints and not isinstance(result, hints['return']):
            raise TypeError(f"Return value must be of type {hints['return']}")
        return result

    return wrapper


# 使用类型检查装饰器
@type_checker
def add(a: int, b: int) -> int:
    return a + b


# 正确调用
print(add(2, 3))  # 输出: 5
# 错误调用
try:
    add(2, "3")  # 会抛出 TypeError
except TypeError as e:
    print(e)  # 输出: Argument 'b' must be of type <class 'int'>

4.4 inspect模块

在 Python 中,inspect 模块提供了对活跃对象的运行时检查工具。它可以检查类、函数、模块等对象的属性、方法,以及获取函数的签名和注解。虽然 inspect 模块没有专门用于类型检查的功能,但它可以帮助我们分析函数的参数类型、返回类型、函数签名等,从而间接实现类型检查的功能。下面将展示如何使用 inspect 模块来获取函数的签名、注解和执行类型检查。

4.4.1 常用方法

inspect.signature() 可以获取函数的签名,其中包含参数名、默认值和注解。语法格式:

In [1]: import inspect

In [2]: inspect.signature?
Signature:
inspect.signature(
    obj,
    *,
    follow_wrapped=True,
    globals=None,
    locals=None,
    eval_str=False,
)
Docstring: Get a signature object for the passed callable.
File:      f:\dev_tools\python\python310\lib\inspect.py
Type:      function

示例:

import inspect


def greet(name: str, age: int = 25) -> str:
    return f"Hello, {name}. You are {age} years old."


# 获取函数签名: 签名对象中包含参数和返回值的详细信息。
sig = inspect.signature(greet)
print(sig)  # (name: str, age: int = 25) -> str

通过 inspect.signature(func).parameters,可以获取函数的参数类型和默认值。示例代码:

import inspect


def greet(name: str, age: int = 25) -> str:
    return f"Hello, {name}. You are {age} years old."


# 获取函数签名的参数
sig = inspect.signature(greet)
for name, param in sig.parameters.items():
    '''
    Parameter: name, Type: <class 'str'>, Default: <class 'inspect._empty'>
    Parameter: age, Type: <class 'int'>, Default: 25
    '''
    print(f"Parameter: {name}, Type: {param.annotation}, Default: {param.default}")

# param.annotation 是参数的类型注解,param.default 是参数的默认值。

使用 inspect.signature(func).return_annotation 可以获取函数的返回类型。

import inspect


def greet(name: str, age: int = 25) -> str:
    return f"Hello, {name}. You are {age} years old."


# 获取函数返回值类型
sig = inspect.signature(greet)
print(f"Return type: {sig.return_annotation}")  # Return type: <class 'str'>

inspect.Parameter,语法格式如下:

In [3]: inspect.Parameter?
Init signature: inspect.Parameter(name, kind, *, default, annotation)
Docstring:
Represents a parameter in a function signature.

Has the following public attributes:

* name : str  # 参数名 字符串类型
    The name of the parameter as a string.
* default : object  # 缺省值
    The default value for the parameter if specified.  If the
    parameter has no default value, this attribute is set to
    `Parameter.empty`.
* annotation  # 类型注解
    The annotation for the parameter if specified.  If the
    parameter has no annotation, this attribute is set to
    `Parameter.empty`.
* kind : str  # 类型
    Describes how argument values are bound to the parameter.
    Possible values: `Parameter.POSITIONAL_ONLY`,
    `Parameter.POSITIONAL_OR_KEYWORD`, `Parameter.VAR_POSITIONAL`,
    `Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`.
File:           f:\dev_tools\python\python310\lib\inspect.py
Type:           type
Subclasses:

示例:

import inspect


def add(x: int, /, y: int = 5, *args, m=6, n, **kwargs) -> int:
    return x + y + m + n


sig = inspect.signature(add)  # 获取签名
print(sig)
print(sig.return_annotation)  # 返回值注解
params = sig.parameters  # 所有参数
print(type(params))
print(params)  # 有序字典OrderedDict
for k, v in params.items():
    print(type(k), k)
    t: inspect.Parameter = v  # 这一步是多余的,但是t使用了变量注解
    print(t.name, t.default, t.kind, t.annotation, sep='\t')
    print('-' * 30)

4.4.2 结合inspect进行类型检查

可以结合 inspect 模块的功能来实现简单的类型检查装饰器。下面是一个示例,展示如何使用 inspect.signature() 来进行参数的类型检查。

import inspect
from functools import wraps


def check(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        sig = inspect.signature(fn)
        params = sig.parameters
        print(args, kwargs)
        print(params)
        values = tuple(params.values())
        for i, v in enumerate(args):
            if values[i].annotation is not values[i].empty and isinstance(v, values[i].annotation):
                print('{}={} is ok'.format(values[i].name, v))  # 有类型注解的检查
        for k, v in kwargs.items():
            if params[k].annotation is not inspect._empty and isinstance(v, params[k].annotation):
                print('{}={} is ok'.format(k, v))  # 有类型注解的检查
        ret = fn(*args, **kwargs)
        return ret

    return wrapper


@check
def add(x, y: int = 7) -> int:
    return x + y


add(x=4, y='amo')
add(x=4, y=5)

4.4.3 其他判断方法

inspect 模块可以获取 Python 中各种对象信息。

inspect.isfunction(add),是否是函数
inspect.ismethod(pathlib.Path().absolute),是否是类的方法,要绑定
inspect.isgenerator(add),是否是生成器对象
inspect.isgeneratorfunction(add),是否是生成器函数
inspect.isclass(add),是否是类
inspect.ismodule(inspect),是否是模块
inspect.isbuiltin(print),是否是内建对象

还有很多 is 函数,需要的时候查阅 inspect 模块帮助:https://docs.python.org/3.10/library/inspect.html#

五、functools之reduce和partial

functools 是 Python 标准库中的一个模块,提供了一些用于处理或扩展函数功能的工具,尤其适合函数式编程风格。该模块中的一些方法在编写高效、简洁的代码时非常有用。以下是 functools 模块中主要方法的详解和示例。

5.1 functools之reduce

参考文章 Python 常用内置函数详解(五):all()、any()、filter()、iter()、map()、next()、range()、reversed()、sorted、zip()函数详解 中的 九、reduce()函数 小节

5.2 functools之partial()偏函数

partial() 函数可以用来固定函数的部分参数,从而生成一个新的函数,简化调用。当函数有很多参数时,部分应用参数可以使得后续调用更加简洁------这个新函数是对原函数的封装。 语法格式如下:

In [7]: import functools

In [8]: functools.partial?
Init signature: functools.partial(self, /, *args, **kwargs)
Docstring:
partial(func, *args, **keywords) - new function with partial application
of the given arguments and keywords.
File:           f:\dev_tools\python\python310\lib\functools.py
Type:           type
Subclasses:

参数说明: 
1.func: 需要固定参数的函数。
2.args: 需要固定的参数。
3.keywords: 需要固定的关键字参数。

示例:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-17 16:28
# @Author  : AmoXiang
# @File: partial_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

from functools import partial
import inspect


def add1(x, y):
    return x + y


new_add1 = partial(add1, y=5)
print(new_add1(4))
print(new_add1(4, y=15))
print(new_add1(x=4))
# TypeError: add1() got multiple values for argument 'y'
# print(new_add1(4, 6))  # 可以吗
print(new_add1(y=6, x=4))
print(inspect.signature(new_add1))


def add2(x, y, *args):
    return x + y + sum(args)


new_add2 = partial(add2, 1, 2, 3, 4, 5)
print(new_add2())
print(new_add2(1))
print(new_add2(1, 2))
# print(new_add2(x=1))  #  TypeError: add2() got multiple values for argument 'x'
# print(new_add2(x=1, y=2))  #
print(inspect.signature(new_add2))

partial 的本质:

def partial(func, *args, **keywords):
    def new_func(*fargs, **fkeywords):  # 包装函数
        new_key_words = keywords.copy()
        new_key_words.update(fkeywords)
        return func(*(args + fargs), **new_key_words)

    new_func.func = func  # 保留原函数
    new_func.args = args  # 保留原函数的位置参数
    new_func.keywords = keywords  # 保留原函数的关键字参数参数
    return new_func


def add(x, y):
    return x + y


foo = partial(add, 4)

六、functools之lru_cache和cache原理

lru_cache() 是一种缓存装饰器,用于将函数的结果缓存起来,以提高多次调用的性能。它基于 最近最少使用(Least Recently Used, LRU) 算法。语法:

In [9]: functools.lru_cache?
Signature: functools.lru_cache(maxsize=128, typed=False)
Docstring:
Least-recently-used cache decorator.

If *maxsize* is set to None, the LRU features are disabled and the cache
can grow without bound.

If *typed* is True, arguments of different types will be cached separately.
For example, f(3.0) and f(3) will be treated as distinct calls with
distinct results.

Arguments to the cached function must be hashable.

View the cache statistics named tuple (hits, misses, maxsize, currsize)
with f.cache_info().  Clear the cache and statistics with f.cache_clear().
Access the underlying function with f.__wrapped__.

See:  https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)
File:      f:\dev_tools\python\python310\lib\functools.py
Type:      function

示例:

# -*- coding: utf-8 -*-
# @Time    : 2024-09-17 16:40
# @Author  : AmoXiang
# @File: lru_cache_demo.py
# @Software: PyCharm
# @Blog: https://blog.csdn.net/xw1680

import functools
import time


# @functools.lru_cache
# def add():
#     pass

# 等价于
# @functools.lru_cache(128)
# def add():
#     pass


@functools.lru_cache()
def add(x, y=5):
    print('-' * 30)
    time.sleep(3)
    return x + y


print(1, add(4, 5))
print(2, add(4, 5))
print(3, add(x=4, y=5))
print(4, add(y=5, x=4))
print(5, add(4.0, 5))
print(6, add(4))  # 到底什么调用才能用缓存呢?

lru_cache 本质: 内部使用了一个字典。 key 是由 _make_key 函数构造出来。

from functools import _make_key

print(_make_key((4, 5), {}, False))
print(_make_key((4, 5), {}, True))
print(_make_key((4,), {'y': 5}, False))
print(_make_key((), {'x': 4, 'y': 5}, False))
print(_make_key((), {'y': 5, 'x': 4}, False))

简单应用:

import functools


@functools.lru_cache(maxsize=None)  # 缓存所有结果
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


print(fibonacci(10))  # 输出: 55
print(fibonacci.cache_info())  # 打印缓存信息
# fibonacci.cache_clear()  # 清空缓存

lru_cache 装饰器应用,使用前提:

  1. 同样的函数参数一定得到同样的结果,至少是一段时间内,同样输入得到同样结果
  2. 计算代价高,函数执行时间很长
  3. 需要多次执行,每一次计算代价高

本质是建立函数调用的参数到返回值的映射,缺点:

  1. 不支持缓存过期,key 无法过期、失效
  2. 不支持清除操作
  3. 不支持分布式,是一个单机的缓存

lru_cache 适用场景,单机上需要空间换时间的地方,可以用缓存来将计算变成快速的查询。学习 lru_cache 可以让我们了解缓存背后的原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Amo Xiang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值