Python3 - 生成器、迭代器与函数式编程

1 生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。它保存的是算法,所表示的序列是一种惰性序列,因为是在要用到这个generator的时候,才会生成下一个元素。

generator对象都可以作为内建函数next() 函数的参数,但是基本上不会调用它,一方面是因为一般都是通过for循环来使用generator,其次是在最近的Python版本中,加入了generator实例的send()方法,它完全可以替Python内件函数next()。如果用 next() 函数调用,当到迭代完最后一个元素后,就会产生StopIteration错误。若用for循环来使用generator,则不需要关心StopIteration的错误。

1.1 创建generator的方法1(通过range)

1
2
3
4
5
6
7
# 外面用小括号,而非像list那样用中括号。
g = (x * x for x in range(10))
# <generator object <genexpr> at 0x1022ef630>
print(g)

for n in g:
print(n)

1.2 创建generator的方法2(函数中加入yield关键字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def fib(max):
n, a, b = 0, 0, 1
print("start")

while n < max:
res = (yield b)
print("n: %s res: %s b: %s" % (n, res, b))

a, b = b, a + b
n = n + 1




f = fib(3)
print(f)

print(f.send(None))
print(f.send("ok1"))
print(f.send("ok2"))

# 在执行本句后时,会抛出StopIteration错误,因为后面找不到yield语句了。
print(f.send("ok3"))
  • generator.send(value)。它表示,首先将value赋值给当前语句停留处的yield表达式(如果当前语句停留处不是yield表达式,比如generator刚开始执行时并没有yield表达式,则将value设为None)。然后从该停留处继续执行后面的语句,直至遇见下一个yield语句。然后将下一个yield语句生成的值(即下一个yield语句右边的值)作为generator.send(value)的返回值。然后暂停执行。

    注意:如果某一次执行send的过程中,当赋值给当前停留处的yield表达式后,在执行后面的语句中,如果始终没遇见下一个yield表达式,则会抛出错误。

  • generator.close()。关闭当前操作的generator。

1.3 yield from表达式

就是在一个generator里面,不再是yield后面跟一个值,而是yield from后面跟一个子生成器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def accumulate():
tally = 0

while 1:
next = yield
print(next)

if next == 1:
return tally

tally += next

def gather_tallies(tallies):
while 1:
tally = yield from accumulate()
print("ok", tally)
tallies.append(tally)

tallies = []
acc = gather_tallies(tallies)
acc.send(None)

acc.send(0)
acc.send(1)
acc.send(2)
acc.send(1)

acc.close()

print(tallies)
  • 通过send传给外层的生成器的value,会直接传给yield from右边的内层子生成器。

  • 如果内层生成器从当前停留处的yield语句开始执行后,后面还能遇见yield语句,则下次通过send传给外层生成器的value,仍然会直接传到yield from右边的内层子生成器中。否则,内层生成器抛出StopIteration错误,此时,外层生成器的当前停留处的yield from表达式的值为None,然后从停留处继续执行,直至遇见下一个yield from语句时暂停。

  • 如果说内层的子生成器后面还会遇见yield语句,且通过return返回一个值,那么这个值将作为外层生成器中yield from语句的值。然后,外层生成器从当前停留处继续执行,直至遇见下一个yield from语句。

  • 内层生成器生成的值对于外层生成器没多大作用(因为外层生成器yield from右边跟的是一个生成器,内层生成器的值无法传递到外层生成器中)。

2 迭代器

  • 所有的生成器都是迭代器。

  • 所有的基本数据类型都是可迭代类型,但是它们并不是迭代器。可迭代类型是指能够用 for 循环迭代的类型。可以被 next() 函数调用并不断返回下一个值的对象称为迭代器,例如 zip() 函数返回的 zip对象 就是一个迭代器。因此,可迭代类型和迭代器不是一回事。

  • 可通过 isinstance() 函数判断一个对象是否是一个可迭代类型,以及是否是一个迭代器。

  • 为什么基本数据类型不是迭代器? 这是因为 Python 的 Iterator 对象表示的是一个数据流,Iterator 对象可以被 next() 函数调用并不断返回下一个数据,直到没有数据时抛出 StopIteration 错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过 next() 函数实现按需计算下一个数据,所以 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算。 Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

  • 可以通过 iter() 函数,把list、dict、str等可迭代类型变成 Iterator。

3 函数式编程

函数名也是一个变量,它指向一个函数对象。一个变量也可以指向一个函数对象(也即指向这个函数的函数名所指向的对象);函数名可以作为一个参数传入给另一个函数。

3.1 map和reduce

  • map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

    1
    2
    3
    4
    5
    6
    def f(x):
    return x * x

    r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
    # [1, 4, 9, 16, 25, 36, 49, 64, 81]
    print(list(r))

  • reduce()把一个函数作用在一个Iterable上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算。 比如: reduce(f, [x1, x2, x3, x4]) 就相当于是: f(f(f(x1, x2), x3), x4)

  • reduce的应用举例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from functools import reduce

    def str2int(s):
    def fn(x, y):
    return x * 10 + y
    def char2num(s):
    return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]
    # map(char2num, s)返回的是一个迭代器
    return reduce(fn, map(char2num, s))

    注意:reduce()函数是 functools这个模块中的一个函数。

3.2 filter

和map()类似,filter()也接收一个函数和一个序列。和map()不同的时,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。例如:

1
2
3
4
5
6
def is_odd(n):
return n % 2 == 1

list1 = list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))
# [1, 5, 9, 15]
print(list1)

3.3 sorted

sorted(iterable[, key][, reverse]) 其中,key 是一个可调用对象,用以指定 iterable 中的具体元素。sorted 将一个iterable类型的对象内的元素进行排序,并返回一个list。如:

1
2
3
list1 = sorted((36, 5, -12, 9, -21))
# [-21, -12, 5, 9, 36]
print(list1)

除此之外,它还可以接收一个key函数来实现自定义的排序。例如:

1
2
3
4
# abs()是求绝对值的函数
list1 = sorted([36, 5, -12, 9, -21], key=abs)
# [5, 9, -12, -21, 36]
print(list1)

第三个参数reverse,默认情况下为False,表示是由小到大进行排序的。要进行反向排序,可以将其置为True。例如:

1
2
3
4
# str.lower()表示把字符串里的每个字符全变成小写的
list1 = sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)
# ['Zoo', 'Credit', 'bob', 'about']
print(list1)

也可以用来对一个 dict 按 key 或者 value 排序:

1
2
3
4
5
6
d = {"a": 3, "c": 2, "b": 1}

# 按 value 降序排列。
ret = sorted(d.items(), key = lambda x:x[1], reverse = True)
# [('a', 3), ('c', 2), ('b', 1)]。
print(ret)

3.4 函数返回的是一个函数名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum

# 这里是事实上是将得到的结果,即sum这个函数名所指向的函数对象赋值给了f
f = lazy_sum(1, 3, 5, 7, 9)
# <function lazy_sum.<locals>.sum at 0x101c6ed90>
print(f)
# 25
print(f())

lazy_sum函数返回的是一个内层定义的函数名。

需要注意的是,调用lazy_sum函数时,返回的是一个内层函数的函数名,而非这个内层函数的调用后返回的结果。这里事实上是将得到的结果,即sum这个函数名所指向的函数对象赋值给了f。然后想要返回f所指向函数对象的调用产生的结果,则需要调用f()函数。 函数名和函数调用是不一样的。函数名是指向的一个函数对象,可以看作一个变量,并没有涉及到这个函数的调用执行。而函数调用则是调用执行,会返回一个None或其他结果。

3.5 闭包

函数和对象的根本目的是以某种逻辑方式组织代码,并提高代码的可重复使用性(reusability)。闭包也是一种组织代码的结构,它同样提高了代码的可重复使用性。

内层函数引用了外层函数的变量(对于内层函数来说,这些变量就是环境变量),这个外层函数返回的引用了环境变量的内层函数的名字,就是一个闭包。也就是说,外层函数返回的这个闭包,相关参数和变量都保存在其中。当调用外层函数时,返回的闭包里的内层函数并没有立刻执行,直到之后再调用这个内层函数才开始执行。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def count():
fs = []
for i in range(1, 4):
# 内层函数,其定义种引用了外层函数里的变量i。i就叫做环境变量
def f():
return i*i
fs.append(f)
# 返回的是一个包含3个闭包的list
return fs

f1, f2, f3 = count()
# 9
print(f1())
# 9
print(f2())
# 9
print(f3())

这里返回的是一个内层函数list,实质上是一次返回多个内层函数,每个内层函数都是一个闭包。 > 注意:返回的函数并没有立刻执行,而是直到调用了f()才执行。而f()函数中所使用的 i ,根据LEGB规则是外层函数的局部变量。因此,当返回完3个函数时,i 已经变为3,因此分别调用这个三个函数时,得到的值全部是9。

因此,返回闭包时牢记的一点就是:返回函数尽量不要引用任何循环变量,或者后续会发生变化的变量。

如果非得要引用循化变量或者后续会发生变化的变量,可采用带参数的函数调用的方式得到解决。可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def count():
def f(j):
def g():
return j*j
return g

fs = []
for i in range(1, 4):
# 这里的f(i)是一个函数调用语句,会立刻被执行,因此i的当前值被传入到f()中
fs.append(f(i))
return fs

f1, f2, f3 = count()
# 1
print(f1())
# 4
print(f2())
# 9
print(f3())

3.6 匿名函数

匿名函数顾名思义,就是没有名字的函数。格式为: lambda 参数1, 参数2, ..., 参数n: 表达式 匿名函数返回的就是表达式的值。 它有个限制,就是只能有一个表达式。用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。 此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数。同样,也可以把匿名函数作为返回值返回。 例如:

1
lambda x: x * x

相当于是:

1
2
def f(x):
return x * x

3.7 装饰器

装饰器是一个函数,它可以接收已经定义好,但想增加额外功能(比如在调用这个函数时,想打印日志等),且又不想改动的函数,作为参数。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def log(func):
def wrapper(*args, **kw):
print("call %s(): " % func.__name__)
func(*args, **kw)
return wrapper

# 定义的装饰器log写在需要装饰的函数的定义前面。
@log
def now():
print("hello world")

# 对于这个例子,事实上在调用now()的时候,解释器会首先执行这个语句:now = log(now),然后再执行now(),此时now指向的函数对象事实上就是wrapper指向的函数对象。
# call now():
# hello world
now()
# wrapper。为now这个变量指向的函数对象的__name__属性值。
print(now.__name__)

这里,这个now函数是之前已经定义好了的函数,现在想在这个now函数调用的时候,在其前面打印日志。对于这个例子,就是一个2层嵌套的装饰器。

这里有两点需要注意:

  • 在定义一个简单的装饰器时,一般需要在被装饰的函数外面套两层,每层定义一个函数。最里面的一层定义的函数的参数,为需要被装饰的函数定义的参数。然后其上面一层的定义的函数的参数为需要被装饰的函数的名字。如果想从外面传一些对象到这个装饰器里面,还可以在最外面套一层,所定义的函数的参数为接收这个对象的参数。对于复杂点的装饰器,可能需要套很多层。

  • 每个函数对象都有自己的一个__name__属性,它的值为这个函数对象的名字。

  • 装饰器一定要置于需要装饰的函数的定义的前面,否则会发生错误。

  • 定义好装饰器后,还需要在需要装饰的函数的定义前面添加一个语句。若定义的装饰器的参数为需要装饰的函数名(2层嵌套的装饰器),则为: @装饰器名 若定义的装饰器的参数为另外的参数(如需要从外部传入一些需要用到的对象的情况),则为: @装饰器名(参数1, 参数2, ..., 参数n)

  • 在调用这个被装饰的函数时,解释器会自动执行调用这个装饰器的语句,并将结果赋值给调用指向这个需要被装饰的函数对象的变量。 对于不需要传入对象到里面的装饰器的情况,则如上面例子所示。对于需要传入对象到里面的装饰器的情况,如下(这是一个三层嵌套的装饰器):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    def log(text):
    def decorator(func):
    def wrapper(*args, **kw):
    print('%s %s():' % (text, func.__name__))
    return func(*args, **kw)
    return wrapper
    return decorator

    # 定义的装饰器log写在需要装饰的函数的定义前面。这里还需要在后面传入某个对象。
    @log("execute")
    def now():
    print("hello world")
    return "OK"

    # 对于这个例子,事实上在调用now()的时候,解释器会首先执行这个语句:now = log("execute")(now),然后再执行now(),此时now指向的函数对象事实上就是wrapper指向的函数对象。
    # execute now()
    # hello world
    # OK
    print(now())
    # wrapper。为now这个变量指向的函数对象的__name__属性值。
    print(now.__name__)

  • 还有一个问题。上边两个例子中,最后打印now.__name__时,发现为wrapper。这是因为,当执行完 now = log("execute")(now) now指向的是wrapper所指向的函数对象,它的名字就是wrapper。这就丢失了被装饰函数的元信息,一些依赖函数签名的代码在执行时就会出错。因而需要将其替换成原来定义的函数的名字,这里就是now。 functools模块里,提供了一个拥有这样功能的函数wraps。其运用如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import functools

    def log(text):
    def decorator(func):
    # 在wrapper()函数的定义前加上这个语句,意思就是,当要调用wrapper()这个函数时,首先执行wrapper = functools.wraps(func)这个语句,然后再执行wrapper()这个函数。
    @functools.wraps(func)
    def wrapper(*args, **kw):
    print('%s %s():' % (text, func.__name__))
    print(wrapper.__name__)
    return func(*args, **kw)
    return wrapper
    return decorator

    @log("execute")
    def now():
    print("hello world")
    return "OK"

    # execute now()
    # now。说明当执行完wrapper = functools.wraps(func)后,wrapper就指向了func这个变量所指向的函数对象,也即now这个变量所指向的函数对象,就是now。
    # hello world
    # OK
    print(now())
    # now。为需要装饰的函数的名字。
    print(now.__name__)

3.8 偏函数

偏函数的作用类似于在函数定义时,设置默认参数值。但是,默认参数值,只能是在函数定义时设置,而且之后再改成其他的值,就会影响之前在一些地方对这个函数的调用。偏函数的作用比设置默认参数更为强大,它是把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。当想从定义上改变这个默认参数值时,在定义一个新的偏函数即可。偏函数 partialfunctools 模块中的一个函数。其定义为:

1
functools.partial(func, *args, **kw)

其返回结果为一个新的函数对象。 其运用方法见下面例子:

1
2
3
4
5
6
7
8
import functools

# 标准库里有多个int的同名函数,其中有一个的定义有两个参数(x, base),x为0-9组成的str类型的对象或对应的bytes类型的对象,base为基底,表示将x看作一串数字,把它转换为10进制数时用到的基底。默认为10。
# 这里事实上就是,将base=2这个关键字参数传递到partial函数的定义中,得到kw = {"base": 2}
int2 = functools.partial(int, base=2)
# 调用int2('1000000')时,事实上是调用int('1000000', **kw)
# 64
print(int2('1000000'))

Reference