生成器、迭代器、装饰器

2022-08-15T14:11:51+08:00

因为这3个条目是我觉得是python中用起来很方便的“语法糖”,因此想写个文章总结一下,顺便归拢一下自己的思路。

生成器

我们会经常使用类似这种方便的写法生成一个list

l = [2 * x for x in range(10)]
print(l)
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

但是把[]换成()

l = (2 * x for x in range(10))
print(l)
<generator object <genexpr> at 0x10e5ce0c0>

就变成了生成器,我们可以使用next函数一步一步的获取这个生成器的值,下面调用这个函数11次

print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
0
2
4
6
8
10
12
14
16
18
Traceback (most recent call last):
  File "/Users/skyler.s/py/practise/yield.py", line 17, in <module>
    print(next(g))
          ^^^^^^^
StopIteration

Process finished with exit code 1

因为只有10个元素,因此第11次调用的时候报错了。因此保险的办法还是使用for循环

for v in g:
    print(v)

除了上面的定义方式外,还有一种使用yield定义,就拿最常见的斐波那契数列举例子

def fib():
    n, a, b = 0, 0, 1
    while n < 1000:
        print(b)
        a, b = b, a + b
        n = n + 1
f = fib
print(f)
<function fib at 0x10c2d23e0>

如果稍微修改一下代码

def fib():
    n, a, b = 0, 0, 1
    while n < 1000:
        yield b
        a, b = b, a + b
        n = n + 1
f = fib()
print(f)

修改的地方就2点

  • print b改成了yield b
  • f = fib改成了f = fib()

此时f就不再是个函数了而是一个生成器。获取生成器值的方式就是使用next或者for循环获取。每次执行的时候遇到yield关键字就返回,下次从yield的下一行开始执行。

生成器 vs list

通过上面的例子,感觉生成器与列表很相似,那么它存在的意义是什么呢?还是拿斐波那契数列为例子,如果我们想获取前10000项,那么就得开辟一段内存保存这10000数字,但是使用生成器的话就只需要保存函数的代码,然后依次生成就可以了,无论是100个、100000个还是10000000个成本都是固定的。

迭代器

python中自带的可以使用for循环的数据类型有list、tuple、dict、set、str。除此以外,我们上面讲了生成器也是可以使用for循环迭代数据的。这些能够使用for循环迭代数据的类型或者生成器叫做可迭代对象。

python中专门有个对象类型叫Iterable,因此我们可以使用isinstance()判断一个变量是不是Iterable

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

迭代器与可迭代对象

迭代器一定是可迭代对象,但是可迭代对象不一定是迭代器。只有可以使用next()函数获取下一个元素的类型才是迭代器。因此生成器肯定是迭代器,list、tuple、dict、set、str这些类型是可迭代对象并不是迭代器。

from collections.abc import Iterator
l = [1,2,3]

def func():
    for i in l:
        yield i
f = func()

print(isinstance(l, Iterator))
print(isinstance(f, Iterator))
False
True

但是python内置的iter函数可以直接把可迭代对象转化成迭代器

from collections.abc import Iterator
l = [1,2,3]
print(isinstance(iter(l), Iterator))
True

装饰器

python中一切皆对象,因此一个函数也可以像一个int值一样:

  1. 作为另外一个函数的参数被传递
  2. 作为另外一个函数的返回值
  3. 赋值给一个变量

不带参数的装饰器

这些就是装饰器的基础。一个简单的不带参数的装饰器示例

def my_decorator(func):
    print("in decorator")
    func()
    
@my_decorator
def main():
    print("in main")

执行脚本发现虽然没有调用main函数,但是输出了

in decorator
in main

也就是说main函数及其装饰器函数都执行了。在python中,装饰器函数是会被自动执行的,不需要显示的调用。但是我们的诉求一般是调用某个函数的时候,才会触发其装饰器函数的调用,因此需要对上面的代码稍作调整,需要把逻辑放到一个固定的函数wrapper里面

def my_decorator(func):
    def wrapper():
        print("in decorator")
        func()
    return wrapper

@my_decorator
def main():
    print("in main")

再次执行脚本,是没有任何输出的。如果我们调用main函数就会输出

in decorator
in main

很符合预期。但是如果我们执行

print(main.__name__)

会输出

wrapper

不符合预期啊。其实装饰器就是把原函数的逻辑装饰器函数的逻辑包裹在了一起,然后以wrapper函数返回,main函数其实就变成了wrapper函数内部的一个对象,它的metadata也被wrapper给吞噬掉了。如果main函数还想保持“独立自主”那么可以变成下面这样

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper():
        print("in decorator")
        func()
    return wrapper

@my_decorator
def main():
    print("in main")

print(main.__name__)
  1. from functools import wraps
  2. 在wrapper函数出使用@wraps(func),代表这个被包裹后的函数使用原函数的metadata

带参数的装饰器

上面的例子中,装饰器函数my_decorator只有一个参数,用于表示原函数。如果我想要增加其他参数怎么办呢?


from functools import wraps

def my_decorator(extra_param):
    def between_decorator_and_wrapper(func):
        @wraps(func)
        def wrapper():
            print(extra_param)
            print("in decorator")
            func()
        return wrapper
    return between_decorator_and_wrapper

@my_decorator("123")
def main():
    print("in main")

main()

其实也很好理解,这次my_decorator返回了一个“入参是函数“的函数,extra_param这个变量与between_decorator_and_wrapper这个函数都是my_decorator内部的一个对象,between_decorator_and_wrapper 就理所当然的作为闭包可以引用my_decorator中的任意数据. 我们再做一次修改,让装饰器可以接收任意的参数


from functools import wraps

def my_decorator(*args, **kwargs):
    def between_decorator_and_wrapper(func):
        @wraps(func)
        def wrapper():
            print(args[0])
            print(kwargs["key"])
            print("in decorator")
            func()
        return wrapper
    return between_decorator_and_wrapper

@my_decorator("123", key="456")
def main():
    print("in main")

main()
123
456
in decorator
in main

这里有个小tips需要注意,不要把逻辑写到wrapper函数之外,因为这部分逻辑会在脚本被引用的时候自动执行。所以带参数的装饰器虽然外面多包裹了一层,但是所有的逻辑还是要写到最里面的wrapper里面。

系统自带的装饰器

@functools.wraps

这个在上面的例子中出现好几次了,应该是最常用的装饰器了,它的作用就是保留原函数的metadata

@staticmethod

用于将类中的一个方法定义为一个静态方法。

在类中定义的函数,如果没有@classmethod@staticmethod装饰,那么这个方法是属于具体的对象的,这个方法会有一个固定的参数self,代表一个具体的对象。

如果加有@classmethod装饰器,那么这个方法会有一个固定的参数cls,代表类。

如果加有@staticmethod装饰器就是静态方法。这个方法只是定义在类空间中,使用上与一般函数无异。

看个具体例子

class MyClass(object):
    title = 'i belong to the class'

    def __init__(self):
        self.instance_title = 'i belong to the instance'

    @staticmethod
    def static_method():
        return "i am static method"

    @classmethod
    def class_method(cls):
        return cls.title

    def instance_method(self):
        return self.instance_title

static_method可以有参数,但是需要外面传。

@classmethod

用于定义类方法。上面例子中class_method就是类方法,通过cls参数访问只属于类的方法和属性

@property

使用property装饰的函数可以使用对象名.方法名的方式调用方法。有一个可选的装饰器方法名.setter用于调用另一个同名函数

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * (self._radius ** 2)

circle = Circle(5)
# 调用@property装饰的radius方法
print(circle.radius)
# 调用@property装饰的area方法
print(circle.area)

# 会调用@radius.setter装饰的radius方法
circle.radius = 10
# 10
print(circle.radius)  
# 10 * 10 * 3.14159
print(circle.area)    

@overload

用于定义一系列名称相同但是签名不同的函数,这些个函数每一个都使用@overload装饰,最终会有一个名称相同但是没有@overload装饰的函数来执行具体的逻辑

class MyClass(object):
    @overload
    def transform(self, data:int) -> str:
        ...

    @overload
    def transform(self, data:str) -> int:
        ...

    def transform(self, data):
        if isinstance(data, int):
            return str(data)
        elif isinstance(data, str):
            return int(data)
关于

我叫Skyler是一个喜欢足球的飞行员!

骗你的啦,⚽️和✈️只是我的理想啦,我目前是个喜欢🍺的程序员!我目前在帝都,但是很喜欢青岛和深圳,很感谢你能看完About Me !