最近在和导师code review的时候我们遇到代码中的iterator,因此想把迭代器生成器这部分内容捋清楚一点。同时我联想到我十月份的一次面试经历,当时挂在了yield和函数闭包这两个点上,所以也一起梳理一下知识点吧。
迭代器Iterators
我们在在代码中经常会出现用for遍历数据,如下代码:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
能被这样遍历数据的对象可以称为可迭代对象。可迭代对象包括容器、处于打开状态的fies等,更全面的解释就是凡是可以返回一个迭代器的对象都可以称为可迭代对象。这里涉及到几个概念也一并解释。
参考连接
容器container
容器是包含其它数据类型的一种数据结构或数据类型,可以认为容器是一种特殊的自定义的数据结构。容器是把多个元素组织在一起的数据结构,容器中的元素可以逐个迭代获取,可以用in, not in关键字判断元素是否包含在容器中。通常这类数据结构把元素存储在内存中。常见的容器如下:
list,
set,
dictionary,
OrderedDictionary
bytearray
array
string,
frozenset,
tuple,
bytes
关于容器还有可修改与不可修改之分。我们常见的容器,如列表list、字典dict是可以直接修改的。不可修改的有元组tuple,不可修改的数据类型包括整形int,浮点型float,字符串型string。区分可修改与不可修改的关键是,数据更改前后,内存的id是否发生改变,并不是指值不可改变。可变数据类型数据更改前后内存id没有改变,不可变数据类型,数据更改前后内存id发生改变,如下:
#数据类型是不允许改变的,这就意味着如果改变数字数据类型的值,将重新分配内存空间。
#int:不可变数据类型
a = 500
id(a) # 67005664
a = 600
id(a) # 67006064
#str:不可变数据类型
a = 'sss'
id(a) # 68010688
a = 'sds'
id(a) # 68010848
#如果按索引修改字符串,会报错
a[-1] = 'n'
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: 'str' object does not support item assignment
#tuple:不可变数据类型
#元组也是不可修改的,会报错
a = (1,2,3)
a[1] = 3
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
#可以看到列表更改前后,其内存id并没有发生改变,还是同一个对象
#list:可变数据类型
list1 = [1,2,3]
id(list1) # 68019336
list1[1] = 5
id(list1) # 68019336
#dict:可变数据类型
#字典更改前后,id没有发生改变
dict1 = {'name':'wwl','age':24}
id(dict1) # 67936896
dict1['name'] = 'lin'
id(dict1) # 67936896
#set:可变数据类型
#集合更改前后,id没有发生改变
a = {1,2,3}
id(a) # 67789624
a.add(4)
id(a) # 67789624
参考连接
可变与不可变对象又涉及到传值与传址的概念。python中不允许程序员显性的使用传址还是传值操作,它参数传递采用的是“传对象引用”的方式,当函数收到一个可变对象的引用,就能修改对象(如dict,list)的原始值,相当于传址。当函数收到的是一个不可变对象(如数字、字符、元组)的引用,就不能修改原始对象值,相当于传值。所以python的传值和传址是根据传入参数的类型来选择的。
Iterables 可迭代对象和Iterators迭代器
这个概念前面已经提过,凡是可以返回一个迭代器的对象都可以称为可迭代对象。可迭代对象包含容器。文章开头的示例:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
for在访问数据的时候自动调用了容器对象的iter(),此方法返回一个迭代器对象。迭代器定义了__next__()方法,该方法每次只能访问容器中的一个元素,当没有元素时,next()会引发StopIteration异常,告诉for循环终止。例子如下:
s = 'abc'
it = iter(s)
it # <str_iterator object at 0x10c90e650>
next(it) # 'a'
next(it) # 'b'
next(it) # 'c'
next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration
分析了迭代器的原理,我们也可以自己定义一个类来实现迭代器功能。定义一个__iter__()方法,它返回带有__next__()方法的对象。如果类定义了__next__(),那么__iter__()可以只返回self:
class Reverse:
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
rev = Reverse('spam')
iter(rev) # <__main__.Reverse object at 0x00A1DB50>
for char in rev:
print(char)
# m
# a
# p
# s
生成器Generators
生成器时构建迭代器的一个工具,它和普通的函数区别在于返回数据时用yield而不是return。每次调用__next__(),生成器将从停止的地方恢复(它记住所有数据值和最后执行的语句)。
def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
for char in reverse('golf'):
print(char)
# f
# l
# o
# g
可以用生成器完成的任何工作也可以用基于类的迭代器完成。但是生成器代码简洁,因为它的__iter__()和__next__()方法是自动创建的。
另一个关键特性是每次调用自动保存本地变量和执行状态。这使得函数比使用self等实例变量(self.index 和 self.data.)的方法更容易编写,也更清晰。
除了自动创建方法和保存程序状态外,当生成器终止时,它们会自动抛出 StopIteration。所以用生成器构建迭代器比编写一个普通的函数来的简洁方便,可以用更少地中间变量写流式代码。相比其它容器对象它更能节省内存和CPU。
python中生成器有两种类型:生成器函数和生成器表达式。生成器函数就是包含yield参数的函数,生成器表达式与列表解析式类似。
#创建一个列表
numbers = [1, 2, 3, 4, 5, 6]
[x * x for x in numbers] # [1, 4, 9, 16, 25, 36]
#set
{x * x for x in numbers} # {1, 4, 9, 16, 25, 36}
#dict
{x: x * x for x in numbers} # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}
#用生成器表达式
lazy_squares = (x * x for x in numbers)
lazy_squares # <generator object <genexpr> at 0x10d1f5510>
next(lazy_squares) # 1
list(lazy_squares) # [4, 9, 16, 25, 36]
yield
在上一节的生成器中已经使用到yield了,可知一个带有yield的函数就是一个生成器。生成器函数和普通函数不一样,只有调用next()函数的时候才会执行函数语句,在for循环中会自动调用next()方法,函数执行过程中遇到一个yield会中断一次,返回一个迭代值,函数保存自己的变量和状态,下次迭代时(也就是下次next()的时候)从yield下一条语句继续执行,函数恢复之前的状态,直到遇到下一个yield返回迭代值,如此循环。
通常的for后面跟上可迭代对象,进行数据处理,它的缺点是所有数据都在内存里,如果有海量的数据就会非常消耗内存。但生成器是可以迭代的,但是每次只可以读取一次,用的时候才生成,减少了内存开销。
参考链接
闭包函数
首先给出闭包函数的必要条件:
闭包函数必须返回一个函数对象
闭包函数返回的那个函数必须引用外部变量(一般不能是全局变量),而返回的那个函数内部不一定要return
def line_conf(a, b):
def line(x):
return a * x + b
return line
# 定义两条直线
line_A = line_conf(2, 1) # y=2x+b
line_B = line_conf(3, 2) # y=3x+2
# 打印x对应y的值
print(line_A(1)) # 3
print(line_B(1)) # 5
嵌套函数line中的代码访问了a和b变量,line本身函数体内并不存在这两个变量,所以会逐级向外查找,往上走一层就找到了来自主函数line_conf传递的a, b。若往外直至全局作用域都查找不到的话代码会抛异常。当需要定义N条直线时,代码量会大量减少。和缩减嵌套函数的优点类似,函数开头需要做一些额外工作,当需要多次调用该函数时,如果将那些额外工作的代码放在外部函数,就可以减少多次调用导致的不必要开销,提高程序的运行效率。
闭包函数引用的外部变量不一定就是其父函数的参数,也可以是父函数作用域内的任意变量。
def line_conf():
a = 1
b = 2
def line(x):
print(a * x + b)
return line
显式地查看“闭包”
闭包比普通的函数多了一个 closure 属性,该属性记录着自由变量的地址。当闭包被调用时,系统就会根据该地址找到对应的自由变量,完成整体的函数调用。
L = line_conf()
print(line_conf().__closure__) #(<cell at 0x05BE3530: int object at 0x1DA2D1D0>,
# <cell at 0x05C4DDD0: int object at 0x1DA2D1E0>)
for i in line_conf().__closure__: #打印引用的外部变量值
print(i.cell_contents) #1 ; #2
__closure__属性返回的是一个元组对象,包含了闭包引用的外部变量。
若主函数内的闭包不引用外部变量,就不存在闭包,主函数的_closure__属性永远为None。
若主函数没有return子函数,就不存在闭包,主函数不存在_closure__属性
而且函数的__closure__属性是只读的,不能由人为修改
def line_conf(a):
b = 1
def line(x):
return a * x + b
return line
line_A = line_conf(2)
b = 20
print(line_A(1)) # 3
line_A对象作为line_conf返回的闭包对象,它引用了line_conf下的变量b=1,在print时,全局作用域下定义了新的b变量指向20,最终结果仍然引用的line_conf内的b。这是因为,闭包作为对象被返回时,它的引用变量就已经确定(已经保存在它的__closure__属性中),不会再被修改。闭包在被返回时,它的所有变量就已经固定,形成了一个封闭的对象,这个对象包含了其引用的所有外部、内部变量和表达式。当然,闭包的参数例外。
闭包可以保存运行环境
_list = []
for i in range(3):
def func(a):
return i+a
_list.append(func)
for f in _list:
print(f(1))
结果是3, 3, 3。 因为,在Python中,循环体内定义的函数是无法保存循环执行过程中的不停变化的外部变量的,即普通函数无法保存运行环境!想要让上面的代码输出1, 2, 3并不难,可以使用闭包:
_list = []
for i in range(3):
def func(i):
def f_closure(a):
return i + a
return f_closure
_list.append(func(i))
for f in _list:
print(f(1))