视频链接:

https://space.bilibili.com/70431433?spm_id_from=333.1007.0.0

文章代码链接:

GitHub - zyf-ngu/Qmatter: 跟着问题学-AI大模型入门系列笔记和代码 · GitHub

5.可迭代对象Iterable和迭代器iterator

为了节省内存,即把数据一个一个的加载进来,Python提供了可迭代对象iterable迭代器iterator2个概念,

可迭代对象iterable就是内部实现了__iter__方法的,是存储数据的容器,但不直接返回输出数据,而是iter方法返回迭代器输出数据,类似于一个空竹竿存了若干豆子,但不能直接倒出来,只能拿钩子勾出来;

迭代器iterator就是内部实现了__next__方法的,是获取数据的指针,类似于一个钩子可以把一个豆子取出来,并指向下一个豆子。

iterable是个容器,可以提供iterator,可以理解为完整的链表或者链表的表头;iterator是一个可以指向下一个对象的对象,可以理解为链表的一个单元;两者结合弥补了python不能用指针的缺陷,非常适合在链表这种数据结构中使用;

具体到代码实现,每次调用内部方法iterator.__iter__(),会返回一个迭代器iterable,这个迭代器会记录当前容器的数据位置在哪里,然后便可以调用内部方法iterable.__next__()就可以把指向的数据取出来,然后再指向下一个数据,如果容器里没有数据,再获取就要报错返回。

这就是最基本的可迭代对象和迭代器,从这个概念来看,可迭代对象才是真正可迭代的,存储数据的,而迭代器最初只是获取数据的方法而已(后面还会再完善,迭代器本身也是可迭代对象)。代码如下:

#最基础的可迭代对象和迭代器对象
class counter_iteratble():
    def __init__(self,start,end):
        self.start=start
        self.end=end
    def __iter__(self):
        return counter_iterator(self.start,self.end)

class counter_iterator():
    def __init__(self,current,end):
        self.current=current #记录当前位置
        self.end=end
    def __next__(self):
        if self.current<=self.end:
            value=self.current
            self.current+=1
            return value
        else:
            return StopIteration

if __name__=='__main__':
    iterable1=my_iterable(1,3)
    iterator1=iter(iterable1)
    print('第一次实例化迭代器',next(iterator1))
    print(next(iterator1)) # 自动计数累加
    iterator2=iter(iterable1)  # 计数器会初始化
    print('第二次实例化迭代器',next(iterator2))

for iter_num in iterable1:   # 可迭代对象使用for循环遍历
      print('iter_num',iter_num)

(1)可迭代的数据是如何体现的?如上面的数值区间,也就表示可迭代对象区间为[start,end],而迭代器的初始化是包括最末尾end,和当前位置current(注意这只是当前位置索引,用start来初始化,不用0是因为数值区间start可能不从0开始);而可迭代数据类型包括很多种,如字符串,列表,元组等,此时

class ListIterable:

    def __init__(self, items):  # items 可以是任意数据集合

        self.items = items   

    def __iter__(self):

        return ListIterator(self.items)

class ListIterator:

    def __init__(self, items):

        self.items = items

        self.index = 0

    def __next__(self):

        if self.index < len(self.items):

            value = self.items[self.index]  # 返回字符串/任意类型数据

            self.index += 1

            return value

        else:

            raise StopIteration  # 正确做法是抛出异常(非返回ValueError)

# 使用示例

iterable = ListIterable(["A", "B", "C"])

for item in iterable:

    print(item)  # 输出 A → B → C

(2)迭代器只能通过iter()调用实现吗?能否自己实例化?并不是,只是for循环自动调用iter()来获取迭代器,然后不断调用next()直到捕获StopIteration异常,迭代器本身也可实例化。

(3)iter(),next()和类的方法 __iter__ 和 __next__有什么关系?上面的iter()和next()是python的内置函数,与类的特殊方法 __iter__ 和 __next__ 之间是接口与实现的关系,内置函数调用了类的方法,iter(obj) 等价于 obj.__iter__(),是显式地遍历可迭代对象,而for循环提供了隐式的遍历方法

(4)for循环隐式遍历:拿列表,元组,字典,字符串等可迭代对象举例子,遍历取值时常常使用for循环,其内部也是经过了两步(一次获取,循环取值),以列表list为例:

for list in lists:

等价于:

  1. 从可迭代对象获取迭代器iter(lists)
  2. 使用next方法获取数据

while True:

            try:

               item = next(iterator)  # 调用 __next__

            # 执行循环体

            except StopIteration:

               break

值得注意的是,对于自定义的可迭代对象和迭代器对象,两次使用for循环遍历的话,这种正常的可迭代对象每次循环都会重新获取一个可迭代器,因为__iter__方法返回的是可迭代器的实例化对象。

(5)列表等数据结构也是可迭代对象,其内部是如何实现的

# 查看列表的迭代器

numbers = [1, 2, 3, 4, 5]

# 获取列表的迭代器

list_iterator = iter(numbers)

print(type(list_iterator))  # <class 'list_iterator'>

# 手动遍历

print(next(list_iterator))  # 1

print(next(list_iterator))  # 2

print(next(list_iterator))  # 3

# 继续使用 for 循环会从当前位置继续

for num in list_iterator:

    print(num, end=" ")  # 输出: 4 5

迭代器也是可迭代对象

前面说迭代器仅仅是一个工具和获取数据的方法,本身并不能迭代,这有些违反直觉,因为直观上感觉有了一个钩子应该能顺藤摸瓜地把后面的豆子全都取出来,因此,Python做了一个优化,即从任意一个迭代器也能迭代,也是一个可迭代对象,具体到代码就把迭代器也加了iter方法,iter下只需要返回self自身。相当于钩子返回了一个钩子,数据还是借着原来的容器存储的。

但是注意,这样只能遍历一次,因为遍历完后迭代器状态已经结束索引值已经到了最后的数据。而上面的分离方式,每次调用`__iter__`都会返回一个新的迭代器,因此可以多次遍历。

合并的代码(但注意:这样只能遍历一次):`

class MyRange:

def __init__(self, start, end):

self.start = start

self.end = end

self.current = start

def __iter__(self):

return self # 返回自身,因为自身就是迭代器

def __next__(self):

if self.current < self.end:

value = self.current

self.current += 1

return value

else:

raise StopIteration# 使用

my_range = MyRange(1,4)

for num in my_range:

print(num) # 输出1,2,3

# 再次遍历就不会有输出了,因为迭代器已经到头

print("再次遍历:")

for num in my_range: print(num) # 不会输出```

5生成器函数和生成器对象

调用一般的函数或者完成执行某个任务,或者返回某个结果,函数执行完毕就结束。而调用生成器函数不会直接返回值,而是返回一个生成器对象保存到变量里,使用内置函数next时会逐步返回一个值,然后代码继续返回到生成器函数返回生成器对象的位置。

从上面的描述可以看出,生成器对象就是一种特殊的迭代器。生成器对象实现了迭代器协议(即__iter__()和__next__()方法) 因此,生成器对象也是迭代器,同时也是可迭代对象(因为迭代器本身就是可迭代的)。

怎样创建生成器函数呢?

在 Python 里,yield 关键字用于创建生成器函数。生成器函数使用 yield 逐个返回值,每次调用生成器的 __next__() 方法时,函数会从上次 yield 语句的位置继续执行,直到遇到下一个 yield 语句或者函数结束。

下面是一个简单的例子,展示了 yield 的迭代方法:

def simple_generator():

yield 1

yield 2

yield 3

# 创建生成器对象

gen=simple_generator()

#开始迭代

print(next(gen))

print(next(gen))

print(next(gen))

try:

print(next(gen))

except

StopIteration: print("生成器已经耗尽")

在这个例子中,simple_generator 是一个生成器函数,它使用 yield 关键字返回三个值。每次调用 next(gen) 时,函数会从上次 yield 的位置继续执行,直到遇到下一个 yield 语句或者函数结束。当生成器耗尽后,再次调用 next 会引发 StopIteration 异常。

同样地,生成器函数也可以使用for循环遍历:

for x in simple_generator():
       print(x)

每次的循环都会调用一次next内置函数,输出一个结果。

生成器函数遍历树结构对象

下面是一个使用 yield 遍历树结构对象的例子:

#tree yield

class TreeNode:

def __init__(self, value):

self.value = value

self.children = []

def add_child(self, child_node):

self.children.append(child_node)

def traverse(self):

# 先返回当前节点的值

yield self.value

# 递归遍历子节点

for child in self.children:

yield from child.traverse()

# 创建树结构

root = TreeNode(1)

child1 = TreeNode(2)

child2 = TreeNode(3)

root.add_child(child1)

root.add_child(child2)

grandchild1 = TreeNode(4)

grandchild2 = TreeNode(5)

child1.add_child(grandchild1)

child1.add_child(grandchild2)

# 遍历树

for value in root.traverse():

print(value)

代码功能说明

TreeNode 类

__init__ 方法:初始化树节点,每个节点包含一个 value 属性和一个 children 列表,用于存储子节点。

add_child 方法:用于向当前节点添加子节点。

traverse 方法:这是一个生成器函数,用于遍历树结构。首先,使用 yield 返回当前节点的值。然后,使用 yield from 递归调用子节点的 traverse 方法,将子节点及其子树的所有值逐个返回。

树结构的创建:创建了一个简单的树结构,根节点的值为 1,有两个子节点 2 和 3,子节点 2 又有两个子节点 4 和 5。

树的遍历:使用 for 循环遍历根节点的 traverse 生成器,逐个打印树中所有节点的值。

通过这种方式,使用 yield 可以方便地实现树结构的遍历,并且避免了一次性将所有节点的值存储在内存中,提高了内存使用效率。

5.enumerate()函数

enumerate() 函数是 Python 内置函数,用于将一个可迭代对象(如列表、元组、字符串、字典、集合、文件对象等作为参数)组合为一个索引序列(即枚举对象)。它返回的是一个迭代器(枚举对象,每次迭代返回一个元组,包含两个元素:索引(从0开始计数)和原可迭代对象对应索引的元素

for i,j in enumerate()函数将字符串,元组,列表等可迭代对象返回数据下标和对应数据的元组,用for 循环中的i,j来接收。

numbers = [1, 2, 3, 4, 5]
for i ,j in enumerate(numbers):
    print(i,j)

1.惰性求值:与生成器类似,enumerate() 不会立即生成所有元组,而是在迭代过程中逐个产生,节省内存。

2. 可设置起始索引,不再是默认0,注意仍然是遍历全部

print(list(enumerate(numbers,start=1))) #[(2, 1), (3, 2), (4, 3), (5, 4), (6, 5)] 

3. 与可迭代对象解耦:枚举对象独立于原始可迭代对象,对原始可迭代对象的修改不会影响枚举对象(因为枚举对象在创建时已经基于原始可迭代对象创建了自己的迭代器)

为什么 enumerate() 返回的是迭代器?

- 效率:不需要预先计算所有索引-值对,特别适合处理大型数据。

- 通用性:可以与任何可迭代对象配合工作,包括无限序列(如 `itertools.count()`)。

深度学习中经常用的地方是遍历读取数据集,推而广之,dataloader作为一个可迭代对象,其数据下标是batch(在创建dataloader时会把batch size作为参数传入),从0开始,最大数为样本总数除以batch size大小,下标对应的数据是一batch的数据x标签y(这里的x和y是列表)。比如总共有320个样本,batch_size大小为16,则是一个大小为320/16=20的列表,列表中的每个元素则是16个样本数据x[batch_size, ...]和标签y[batch_size, ...]。

for batch,(x,y) in enumerate(dataloader):batch从0到19

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐