PEP 424 __length_hint__() - 是否有办法为生成器或 zip 函数做相同的事情?

10
我刚刚发现了 PEP 424(https://www.python.org/dev/peps/pep-0424/)中的迭代器的神奇方法__length_hint__()。哇!这是一种在不耗尽迭代器的情况下获取迭代器长度的方法。
我的问题如下:
  1. 有没有简单的解释说明这个神奇的方法是如何工作的?我只是好奇。
  2. 它是否有限制或者某些情况下无法使用?("hint"听起来有点可疑)
  3. 是否有办法对zip和生成器也获得提示?还是只有迭代器才有这个基本功能?
编辑:顺便说一句,我看到__length_hint__()从当前位置计数到末尾。即部分消耗的迭代器将报告剩余长度。很有趣。
5个回答

7

哇!一种可以获取迭代器长度而不用耗尽迭代器的方法。

不是这样的。 这只是一种获取关于长度可能性的模糊提示,并没有要求它必须准确无误。

这个神奇的机制如何工作呢?有简单的解释吗?

迭代器实现了一个__length_hint__方法,使用特定于迭代器的信息猜测输出元素的数量。这个猜测可能相当不错,也可能非常糟糕。例如,列表迭代器知道自己在列表中的位置和列表的长度,因此可以报告剩余元素的数量。

有限制或无法工作的情况吗?

如果迭代器没有足够的信息来猜测何时会停止,则无法实现有用的__length_hint__。例如,生成器就没有这个方法。无限迭代器也无法实现有用的__length_hint__,因为无法信号无限长度。

是否有办法对zip和生成器获得提示?还是这只适用于迭代器?

zip实例和生成器都是迭代器的一种。不过,zip和生成器类型都没有提供__length_hint__方法。


3
这个功能的目的主要是为了在Cython/C代码中更有效地分配内存。例如,假设一个Cython模块公开了一个函数,该函数接受自定义MyNetworkConnection()对象的可迭代对象,并在内部需要创建和分配内存以在Cython/C代码中表示它们的数据结构。如果我们可以大致估计迭代器中的项目数量,我们可以一次性分配足够大的内存块,以容纳所有项目并最小化重新调整大小。
如果实现__len__(),我们就知道确切的长度,并可用于内存分配。但通常我们实际上并不知道确切的长度,因此估计值通过为我们提供“大概数字”来帮助我们提高性能。
这对于纯Python代码也肯定很有用,例如,可能会有一个用户面向的操作完成时间估计?
对于问题2,由于它只是一个提示,所以您不能指望它是精确的。如果提示过低,您仍然必须考虑分配新的内存,如果提示过高,则必须清理内存。我个人不知道其他限制或潜在问题。
对于问题3,我认为它同样适用于生成器,因为生成器是一个迭代器。
>>> import collections
>>> def my_generator(): yield
>>> gen = my_generator()
>>> isinstance(gen, collections.Iterator)
True

1

对于这个问题有几个答案,但它们都稍微偏离了重点: __length_hint__ 不是魔法。它是一种协议。如果一个对象没有实现该协议,那就是这样。


让我们绕路看一个简单的例子a + b+运算符依赖于a.__add__b.__radd__来实际执行操作。 int实现__add__表示算术加法(1 + 2 == 3),而list实现__add__表示内容连接([1] + [2] == [1, 2])。 这是因为__add__只是一个协议,如果对象提供了它,那么它们必须遵守该协议。 __add__的定义基本上只是“取另一个操作数并返回一个对象”。
+没有单独的、普遍的意义。 如果操作数没有提供__add___radd__,则Python将无能为力。

回到实际问题,这意味着什么?

有没有一个简单的解释来解释这个魔法是如何工作的?我只是好奇。

所有的魔法都列在PEP 424中,但基本上是这样的:尝试 len(obj),回退到 obj.__length_hint__,使用默认值。这就是所有的魔法。

在实践中,一个对象必须根据自身的了解实现__length_hint__。例如,以range backportPy3.6 C Coderange_iterator为例:

return self._stop - self._current

在这里,迭代器知道它的最大长度和提供的长度。如果它不跟踪后者,它仍然可能返回最大长度。无论哪种方式,它都必须使用自身的内部知识。

有限制条件和不能工作的情况吗?(“提示”听起来有点可疑)。

显然,没有实现__length_hint____len__的对象是无法工作的。基本上,任何没有足够了解其状态的对象无法实现它。
链接生成器通常不实现它。例如,(a ** 2 for a in range(5))将不会从range中转发长度提示。如果您考虑到可能存在任意数量的迭代器链,则这是明智的:length_hint只是为了预先分配空间而进行的优化,直接获取要放入该空间的内容可能更快。
在其他情况下,这可能根本是不可能的。无限和随机迭代器属于此类别,但是针对外部资源的迭代器也是如此。
如果一个对象没有实现__length_hint__,那么是不可能的。Zip和生成器可能出于上述效率原因而没有实现__length_hint__。
此外,请注意,zip和生成器对象是它们自己的迭代器。
foo = zip([1,2,3], [1,2,3])
id(foo) == id(iter(foo))  # returns True in py3.5

0
有没有一个简单的解释来说明这个魔法是如何工作的?我只是好奇。
我将展示一些代码供未来读者实验:
- MyClass1 没有实现任何东西,这意味着会发生正常的过度分配。 - MyClass2 只实现了 __len__,并且猜测正确。 - MyClass3 只实现了 __length_hint__,并且猜测略高(n + 2)。 - MyClass4 实现了两者。有用的是看到调用的优先顺序。
from sys import getsizeof
n = 25

class MyClass1:
    def __iter__(self):
        for i in range(n):
            yield i

class MyClass2:
    def __iter__(self):
        for i in range(n):
            yield i

    def __len__(self):
        print("__len__ in MyClass2")
        return n

class MyClass3:
    def __iter__(self):
        for i in range(n):
            yield i

    def __length_hint__(self):
        print("__length_hint__ in MyClass3")
        return n + 2

class MyClass4:
    def __iter__(self):
        for i in range(n):
            yield i

    def __len__(self) -> int:
        print("__len__ in MyClass4")
        return n

    def __length_hint__(self):
        print("__length_hint__ in MyClass4")
        return n + 2

obj1 = MyClass1()
obj2 = MyClass2()
obj3 = MyClass3()
obj4 = MyClass4()

lst1 = list(obj1)
lst2 = list(obj2)
lst3 = list(obj3)
lst4 = list(obj4)

print("obj1:", getsizeof(lst1))
print("obj2:", getsizeof(lst2))
print("obj3:", getsizeof(lst3))
print("obj4:", getsizeof(lst4))

输出:

__len__ in MyClass2
__length_hint__ in MyClass3
__len__ in MyClass4
obj1: 312
obj2: 264
obj3: 280
obj4: 264

n = 25 是有意使用的,因为在这个解释器(3.10.6)中,list 在添加第24个项目后会进行过度分配,因此我们可以清楚地看到差异。

这种提示方式在PEP 424规范部分中提到。

obj1 应该是三个对象中最大的,因为它进行了过度分配。 obj3obj2 大16字节,这是64位机器上两个指针的大小。在 obj4 中,由于 __len__ 返回一个非负的正确整数,Python甚至没有调用 __length_hint__ 方法。


有没有限制或情况无法工作?("hint"听起来有点可疑)
是的,如果返回的猜测远离实际情况,那么会有限制。如果它太低并且迭代仍未完成,则最终会使用正常过度分配行为。如果它太高,则基于如果没有__length_hint__将是什么大小来计算大小。
有没有办法同时获取zip和生成器的提示?还是这只是迭代器的基本特性?
对于zip情况,你可以子类化它,并通过委托给各个可迭代对象的__len__或__length_hint__方法来提供自己的__length_hint__方法(如果可能的话)。是的,它确实减少了内存占用。
下面是一个简单的示例:
from sys import getsizeof

class Zip(zip):
    def __init__(self, *iterables, strict=False) -> None:
        self.ITs = iterables

    def __length_hint__(self):
        try:
            return min(len(it) for it in self.ITs)
        except TypeError:
            return NotImplemented

z1 = list(Zip(range(1_000_000), range(1_000_000)))
z2 = list(zip(range(1_000_000), range(1_000_000)))
print(z1 == z2)
print(getsizeof(z1)) # 8000056
print(getsizeof(z2)) # 8448728

这会减慢速度吗?实际上并不会:

from timeit import timeit

def test_zip():
    z = zip
    return list(z(range(10_000_000), range(10_000_000)))

def test_Zip():
    z = Zip
    return list(z(range(10_000_000), range(10_000_000)))

print(timeit(test_zip, globals=globals(), number=2)) # 1.622484583999721
print(timeit(test_Zip, globals=globals(), number=2)) # 1.578525709999667

0
有没有办法同时获取zip和generator的提示?还是这只是迭代器的基本功能?
对于生成器,我认为没有简单或自动的方法来做到这一点,因为如果你给我一个任意的生成器,我不知道它是如何生成的,我怎么能确定它是否是有限的呢?我需要查看代码,如果使用其他函数,我需要查看这些函数以及它们如何被调用等等...这很快就会变得混乱,所以对于自动方式,所需的工作量比回报要大得多。
至于zip,我不知道为什么它没有这个功能,检查每个元素的每个提示并返回其中最小的似乎很容易,也许他们没有添加它是因为你可以将生成器传递给它,而无法从它们那里获取提示?
所以这可能更适合迭代器,因为它们是使用迭代器协议创建的。
class MyIterator:
    def __iter__(self):
        return self
    def __next__(self):
        ...
        if condition_for_more_values:
            ...
            return next_value
        else:
            raise StopIteration

因此,在这里添加__length_hint__函数的逻辑更容易,而且内置容器(列表、元组、字符串、集合等)具有这样的功能,因为它们是按照这种方式制作的。

class Container:
    ...

    def __len__(self):
        ...

    def iter(self):
        return Container_Iterator(self)

class Container_Iterator:

    def __init__(self,con):
        self.i=0
        self.data=con

    def __iter__(self):
        return self

    def __next__(self):
        if self.i<len(self.data):
            self.i+=1
            return self.data[self.i-1]
        else:
            raise StopIteration

由于Conatiner_Iterator可以访问容器的所有相关信息,因此它知道每个时刻所在的位置,因此它可以给出有意义的提示,而且可以非常简单。

    def __length_hint__(self):
        return len(self.data) - self.i

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接