【问题标题】:Does python iterator cost additional memory?python迭代器是否需要额外的内存?
【发布时间】:2021-12-19 05:37:43
【问题描述】:

我想知道如果 my_iter = iter(iterable_obj) 复制iterable_obj? 换句话说,上面的调用是否需要额外的内存?

【问题讨论】:

  • iter(iterable_object) 的行为由iterable_object.__iter__ 控制,它可以做任何事情。不过,一般来说,迭代器应该是内存高效的(小的常量开销),但这并不是保证。并且没有理由期望iter(iterable_object) 应该复制iterable_object,这会很奇怪(尽管在某些情况下可能是可能的)。因此,对于所有 内置 可迭代对象,it = iter(iterable) 将是一个小的、恒定的内存开销
  • 顺便说一句,这确实与内存泄漏无关。
  • 这能回答你的问题吗? is iterable object a copy of original object?
  • 感谢您的讨论!我认为@JasonS 给出了一个快速的答案。此外,正如 Juanpa 所提到的,虽然它在理论上可以复制,但在实践中和设计上不应该。

标签: python iterator


【解决方案1】:

它会复制吗?可能是。但不应该。

可以复制,但它不应该。它应该只在现有数据结构上提供迭代,并且迭代所需的内存开销最小。例如,list iterator 仅存储对列表的引用以及索引。

做什么它有什么作用?

做什么它有什么作用?那要看。 iter 函数将提供一个迭代器,用于在整个广阔世界中任何可能的可迭代对象,包括一个你明天才写的可迭代类,具有复杂的内部数据结构。 iter 怎么可能做到这一点?人工智能?魔法?不。嗯......实际上是的,魔法。即使用所谓的“魔术方法”(或“dunder 方法”)。在这种情况下,__iter____getitem__。诀窍是,iter 知道如何迭代可迭代对象。 iterable 确实如此。并使用这两种魔术方法之一使迭代可访问。 iter 函数只是调用它的代码(想要迭代)和可迭代对象(提供迭代)之间的一个简单中间人。

__iter__ 方法返回迭代器的示例:

class MyIterable:
    def __iter__(self):
        return iter('abcde')

print(list(MyIterable()))

输出:

['a', 'b', 'c', 'd', 'e']

__getitem__ 方法返回索引 0、1、2 等元素的示例(直到 IndexError):

class MyIterable:
    def __getitem__(self, index):
        return 'abcde'[index]

print(list(MyIterable()))

输出:

['a', 'b', 'c', 'd', 'e']

那么做什么 iter(iterable) 做什么?取决于可迭代的功能。它可能会复制,也可能不会,它可能会试图让你的房子着火。

对于像列表迭代器这样简单的东西,选择是显而易见的:使用对列表的引用和迭代器所在位置的索引既简单又高效。

更有趣的案例:二叉搜索树迭代器

让我们考虑一个不太明显并且您可能很想复制的情况:二叉搜索树迭代器,它提供按排序顺序对树的值进行迭代。让我们考虑三种可能的实现,其中 n 是树中值的数量。树将表示为BST 节点对象的结构:

class BST:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

可能的实现1:递归迭代器

class BST:
    ...
    def __iter__(self):
        if self.left:
            yield from self.left
        yield self.value
        if self.right:
            yield from self.right

优点:

  • 简单的代码。
  • O(1) 时间和空间用于迭代器创建。
  • 懒惰,只按照请求的次数进行迭代。
  • 迭代期间只有 O(h) 内存,其中 h 是树的高度。如果树是平衡的,则可能低至 Θ(log n),如果树非常不平衡,则可能高达 Θ(n)。

缺点:

  • 慢。每个值都通过整个迭代器堆栈传递到根。因此,迭代整个树至少需要 Θ(n log n) 到 Θ(n²) 的时间。

可能的实现 2:将值复制到列表中

由于缓慢的迭代,尤其是二次时间,非常令人失望,我们可以将树中的所有值复制到一个列表中并返回一个迭代器:

class BST:
    ...
    def __iter__(self):
        values = []
        def collect(node):
            if node:
                collect(node.left)
                values.append(node.value)
                collect(node.right)
        collect(self)
        return iter(values)

优点:

  • 简单的代码。
  • 线性时间迭代。

缺点:

  • Θ(n) 记忆。
  • Θ(n) 时间已经创建迭代器,甚至在开始实际迭代之前。

可能的实现 3:迭代

这是一个使用堆栈的迭代。堆栈将保存其值及其子树仍需要迭代的节点:

class BST:
    ...
    def __iter__(self):
        node = self
        stack = []
        while node or stack:
            while node:
                stack.append(node)
                node = node.left
            node = stack.pop()
            yield node.value
            node = node.right

结合了前两种实现的优点(既节省时间又节省内存),但缺点是不容易。与前两个实现不同,我觉得有必要对它的工作原理添加一点解释,如果你以前没有看过它,你可能还需要考虑一下。

结论

如果这对您来说只是一个小练习并且您没有效率问题,那么前两个实现很好并且易于编写。尽管将值复制到列表中并不是真正的普通迭代器,因为复制值基本上不是迭代的意思。不过,这与内存无关。递归生成器和迭代方法也可以从 O(log n) 和 O(n) 内存中取用任何地方,但这是组织数据,对于促进迭代有些必要。他们没有复制 content 数据。

如果它是认真使用的 BST 包,那么我会发现前两个实现的缺点是不可接受的,并且会使用迭代实现。一次编写的工作量更大,但具有优势和适当的迭代器。

顺便说一句,如果节点也有对其 节点的引用,迭代器可以使用它来使用 O(1) 内存进行高效迭代。留给读者练习:-P

代码

可使用的 BST 代码 (Try it online!):

from random import shuffle

class BST:

    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

    def insert(self, value):
        if value < self.value:
            if self.left:
                self.left.insert(value)
            else:
                self.left = BST(value)
        elif value > self.value:
            if self.right:
                self.right.insert(value)
            else:
                self.right = BST(value)

    def __repr__(self):
        return f'BST({self.value}, {self.left}, {self.right})'

    def __iter__(self):
        yield from self.left or ()
        yield self.value
        yield from self.right or ()

    def __iter__(self):
        values = []
        def collect(node):
            if node:
                collect(node.left)
                values.append(node.value)
                collect(node.right)
        collect(self)
        return iter(values)

    def __iter__(self):
        node = self
        stack = []
        while node or stack:
            while node:
                stack.append(node)
                node = node.left
            node = stack.pop()
            yield node.value
            node = node.right

# Build a random tree
values = list(range(20)) * 2
shuffle(values)
tree = BST(values[0])
for value in values[1:]:
   tree.insert(value)

# Show the tree
print(tree)

# Iterate the tree in sorted order
print(list(tree))

样本输出:

BST(1, BST(0, None, None), BST(17, BST(10, BST(6, BST(2, None, BST(4, BST(3, None, None), BST(5, None, None))), BST(9, BST(7, None, BST(8, None, None)), None)), BST(15, BST(11, None, BST(13, BST(12, None, None), BST(14, None, None))), BST(16, None, None))), BST(18, None, BST(19, None, None))))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-08-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多