【问题标题】:How does parent class "enumerate" access an argument not set as an instance variable?父类如何“枚举”访问未设置为实例变量的参数?
【发布时间】:2018-05-17 22:39:15
【问题描述】:

我昨天偶然发现了一个question,它涉及枚举一个可迭代类型并产生递减索引以及可迭代项中的升序项。

在:

letters = ['a', 'b', 'c']
for i, letter in revenumerate(letters):
    print('{}, {}'.format(i, letter))

输出:

2, a
1, b
0, c 

我决定尝试创建一个enumerate 的子类来重新定义__iter____next__ 方法,而不是应用内置reverse 两次或简单地编写i = len(letters) - i - 1 来编写快速可靠的答案。

我原来的工作解决方案的代码如下:

class revenumerate(enumerate):
    def __init__(self, iterable, start=0):
        self._len = len(iterable)
        self._start = start
        if isinstance(iterable, dict):
            self._data = iterable.keys()
        else:
            self._data = iterable

    def __iter__(self):
        _i = self._len
        for _item in self._data:
            _i -= 1
            _ind = _i + self._start
            yield _ind, _item

    def __next__(self):
        _i, _item = super().__next__()
        _ind = self._len +  2 * self._start - _i - 1
        return _ind, _item

然而,我现在意识到这段代码有冗余,因为enumerate.__iter__ 似乎产生了__next__ 的结果,这是有道理的。删除重新定义的__iter__ 后,我意识到self._data 没有在任何地方使用,所以我从__init__ 中删除了最后四行,留下以下代码,它仍然提供所需的行为。

class revenumerate(enumerate):
    def __init__(self, iterable, start=0):
        self._len = len(iterable)
        self._start = start

    def __next__(self):
        _i, _item = super().__next__()
        _ind = self._len +  2 * self._start - _i - 1
        return _ind, _item

现在看来,传递给revenumerate 的可迭代参数除了确定整数self._len 之外没有其他用途。

我的问题是 - iterable 存储在哪里,super().__next__ 如何访问它?

使用 PyCharm 调试器快速查看 builtins.py 并没有提供很多帮助来解决这个问题(或者在我看来,在这个阶段),而且我没有很好地遍历 Python 源代码存储库。我的猜测与父类enumerate__new____init__ 方法有关,或者它的父类object

【问题讨论】:

    标签: python python-3.x inheritance iterator


    【解决方案1】:

    builtins.py 是个谎言。 PyCharm 做到了。如果您想查看builtins 模块的真实源代码,那就是 Python Git 存储库中的Python/bltinmodule.cenumerate本身在Objects/enumobject.c中实现。

    enumerate 迭代器在 C 级 en_sit 结构槽中存储一个迭代器覆盖其底层对象:

    typedef struct {
        PyObject_HEAD
        Py_ssize_t en_index;           /* current index of enumeration */
        PyObject* en_sit;          /* secondary iterator of enumeration */
        PyObject* en_result;           /* result tuple  */
        PyObject* en_longindex;        /* index for sequences >= PY_SSIZE_T_MAX */
    } enumobject;
    

    设置在enumerate.__new__:

    static PyObject *
    enum_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
    {
        ...
        en->en_sit = PyObject_GetIter(seq);
    

    它设置在__new__ 中的事实是即使您忘记调用super().__init__ 仍然有效的原因。


    为此子类化enumerate 没有多大意义。 enumerate 仅记录为可调用;它是一个类并支持子类化这一事实是一个实现细节。此外,enumerate 并没有得到很多使用,而且您的迭代器和 enumerate 迭代器之间的关系听起来并不像“is-a”。像 zvone 那样将您的功能实现为生成器,更加简洁明了。

    【讨论】:

    • 感谢您的简洁解释和指导。我对 Github 上的 Python 源代码库不熟悉,所以这正是我所追求的。
    【解决方案2】:

    enumerate 所做的或多或少*是这样的:

    def enumerate(iterable):
        counter = 0
        for item in iterable:
            counter += 1
            yield counter, item
    

    您可以注意到的一件事是它不知道可迭代的长度。它甚至可以无限长,但 enumerate 仍然有效。

    revenumerate 的问题在于,您首先必须计算有多少项目才能生成第一个项目,因此您实际上必须创建所有枚举项目的列表,然后将它们向后生成(至少如果您希望您的 revenumerate 与任何可迭代对象一起工作,例如 enumerate)。

    一旦您接受了不可避免的限制,剩下的就很简单了:

    def revenumerate(iterable):
        all_items = list(iterable)
        counter = len(all_items)
        for item in reversed(all_items):
            counter -= 1
            yield counter, item
    

    (*) enumerate 实际上是一个类,但这是它的行为。请参阅 my other answer 了解其工作原理以及 __next__ 的作用。

    【讨论】:

      【解决方案3】:

      其他人已经回答了您关于代码如何工作的具体问题,因此这是使用 zip() 实现反向枚举器的另一种方法:

      def revenumerate(iterable, start=None):
          if start is None:
              start = len(iterable) - 1
          return zip(range(start, -1, -1), iterable)
      
      >>> revenumerate('abcdefg')
      <zip object at 0x7f9a5746ec48>
      >>> list(revenumerate('abcdefg'))
      [(6, 'a'), (5, 'b'), (4, 'c'), (3, 'd'), (2, 'e'), (1, 'f'), (0, 'g')]
      >>> list(revenumerate('abcdefg', 100))
      [(100, 'a'), (99, 'b'), (98, 'c'), (97, 'd'), (96, 'e'), (95, 'f'), (94, 'g')]
      

      revenumerate() 返回的zip 对象与enumerate() 返回的enumerate 对象非常相似。

      默认情况下,项目将从可迭代的小于一的长度开始枚举,这要求长度是有限的。如果您只想从任意值开始计数或处理无限迭代,您可以提供一个开始值以进行倒计时。

      >>> from itertools import count
      >>> g = revenumerate(count(), 1000)
      >>> next(g)
      (1000, 0)
      >>> next(g)
      (999, 1)
      >>> next(g)
      (998, 2)
      >>> next(g)
      (997, 3)
      >>> next(g)
      (996, 4)
      

      如果您尝试在未指定起始值的情况下处理无限迭代:

      >>>> revenumerate(count())
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 3, in revenumerate
      TypeError: object of type 'itertools.count' has no len()
      

      这可以防止解释器进入无限循环。如果适合您的应用程序,您可以处理异常并引发自己的异常。

      【讨论】:

      • 感谢您的意见!我问这个问题更多是因为我不明白为什么我的代码有效,也不知道如何解决 Github 上的 Python 源代码,但我真的很高兴看到解决这个问题的新的和独特的方法!
      【解决方案4】:

      在我的previous answer 中,我写了我会怎么做,但这里有一些关于__iter____next__ 的实际问题的答案...

      可迭代

      为了使对象可迭代,它必须实现方法__iter__,该方法必须返回一个迭代器。

      这里有一些简单的例子:

      class A:
          def __iter__(self):
              return iter([1, 2, 3])
      
      class B:
          def __iter__(self):
              yield 'a'
              yield 'b'
      

      这些可以迭代:

      >>> A().__iter__()
      <list_iterator object at 0x00000000029EFD30>
      
      >>> iter(A())  # calls A().__iter__()
      <list_iterator object at 0x00000000029EFF28>
      
      >>> list(A())  # calls iter(A) and iterates over it
      [1, 2, 3]
      
      >>> list(B())  # calls iter(B) and iterates over it
      ['a', 'b']
      

      迭代器

      __iter__ 返回的对象是一个迭代器。迭代器必须实现__next__ 方法。

      例如:

      >>> it = iter(B())  # iterator
      
      >>> it.__next__()
      'a'
      
      >>> next(it)  # calls it.__next__()
      'b'
      
      >>> next(it)  # raises StopIteration because there is nothing more
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      StopIteration
      

      自定义迭代器

      class MyIterator:
          def __init__(self):
              self.value = 5
          def __next__(self):
              if self.value > 0:
                  self.value -= 1
                  return self.value
              else:
                  raise StopIteration()
      
      class MyIterable:
          def __iter__(self):
              return MyIterator()
      
      >>> list(MyIterable())
      [4, 3, 2, 1, 0]
      

      编辑:正如 cmets 中的其他人所提到的,迭代器应始终实现返回 self__iter__(正如我在下面的示例中所做的那样)。这个要求可以在PEP-0234Python docs读到:

      想要成为迭代器的类应该实现两个方法: next() 的行为如上所述的方法,以及 __iter__() 返回self的方法。

      可迭代迭代器

      一个可迭代的迭代器?好吧,如果一个类同时实现了__iter____next__,那么它就是两者:

      class IterableIterator:
          def __init__(self):
              self.value = 11
      
          def __next__(self):
              if self.value < 17:
                  self.value += 1
                  return self.value
              else:
                  raise StopIteration()
      
          def __iter__(self):
              return self
      
      >>> list(IterableIterator())
      [12, 13, 14, 15, 16, 17]
      

      enumerate

      enumerate 实际上做了这样的事情:

      class enumerate:
          def __init__(self, iterable, start=0):
              self.iterator = iter(iterable)
              self.n = start - 1
      
          def __iter__(self):
              return self
      
          def __next__(self):
              self.n += 1
              next_item = next(self.iterator)
              return self.n, next_item
      

      所以,为了回答你的问题,在你的super().__next__() 中,你在这里调用了这个__next__,它使用了它存储在构造函数中的迭代器。

      【讨论】:

      • 值得注意的是,所有迭代器也应该是可迭代的,它们应该总是有一个返回self__iter__方法。如果没有这种行为,您将无法使用您已经在大多数地方创建的迭代器(不能在循环中,也不能通过将其传递给期望可迭代的函数或类构造函数,如 list)。这些地方都在他们正在迭代的对象上调用iter,所以你需要一个__iter__ 方法,即使它是一个微不足道的方法。
      • 迭代器是supposed to 有一个__iter__ 返回self,任何不这样做的迭代器都违反了迭代器协议。这通常不会被检查,这会导致主要工作的迭代器损坏,直到它们最终生成一些实际上依赖于迭代器协议的这一部分的代码。
      • @user2357112 谢谢,我已将其添加到答案中。
      猜你喜欢
      • 2011-02-21
      • 1970-01-01
      • 1970-01-01
      • 2016-03-06
      • 2019-02-01
      • 1970-01-01
      • 2022-01-12
      • 2016-06-27
      • 1970-01-01
      相关资源
      最近更新 更多