从 itertools.cycle 中提取一个列表

Posted

技术标签:

【中文标题】从 itertools.cycle 中提取一个列表【英文标题】:Extract a list from itertools.cycle 【发布时间】:2012-08-20 12:45:33 【问题描述】:

我有一个包含itertools.cycle 实例的类,我希望能够复制它。一种方法(我能想到的唯一一种方法)是提取初始迭代(它是一个列表),并存储循环所在的位置。

不幸的是,我无法获取用于创建循环实例的列表,似乎也没有明显的方法:

import itertools
c = itertools.cycle([1, 2, 3])
print dir(c)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', 
 '__hash__', '__init__', '__iter__', '__new__', '__reduce__', 
 '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 
 '__subclasshook__', 'next']

我可以想出一些合理的理由来解释为什么对于某些类型的输入迭代不允许这样做,但是对于元组甚至可能是列表(可变性可能是一个问题),我不明白为什么它会不可能。

任何人都知道如果可以从itertools.cycle 实例中提取非无限迭代。如果没有,有谁知道为什么这个想法不好?

【问题讨论】:

为什么需要这个?要复制实例,只需使用相同的源列表重新创建它(您可以将源列表保存在其他位置)。但是,这将创建一个新的迭代器。请记住,迭代器包含比源可迭代更多的状态,例如序列中的当前位置。 您是否考虑过调查itertools.tee?根据你想要这个,我认为你可以做类似myiter,copy = itertools.tee(myiter) 说真的,为什么需要这样做?在这一点上,我倾向于认为您的设计过程存在缺陷 不是disallowed,只是cycle()没有的功能。此外,cycle 是用 C 语言实现的,因此“破解”内部并不那么容易。 【参考方案1】:

这是不可能的。如果您查看itertools.cycle 代码,您会发现它没有存储序列的副本。它只创建一个可迭代对象并将可迭代对象中包含的值存储在一个新创建的列表中:

static PyObject *
cycle_new(PyTypeObject *type, PyObject *args, PyObject *kwds)

    PyObject *it;
    PyObject *iterable;
    PyObject *saved;
    cycleobject *lz;

    if (type == &cycle_type && !_PyArg_NoKeywords("cycle()", kwds))
        return NULL;

    if (!PyArg_UnpackTuple(args, "cycle", 1, 1, &iterable))
        return NULL;
    /* NOTE: they do not store the *sequence*, only the iterator */
    /* Get iterator. */
    it = PyObject_GetIter(iterable);
    if (it == NULL)
        return NULL;

    saved = PyList_New(0);
    if (saved == NULL) 
        Py_DECREF(it);
        return NULL;
    

    /* create cycleobject structure */
    lz = (cycleobject *)type->tp_alloc(type, 0);
    if (lz == NULL) 
        Py_DECREF(it);
        Py_DECREF(saved);
        return NULL;
    
    lz->it = it;
    lz->saved = saved;
    lz->firstpass = 0;

    return (PyObject *)lz;

这意味着当做:

itertools.cycle([1,2,3])

您创建的列表只有 1 个引用,它保存在循环使用的迭代器中。 当迭代器耗尽时,迭代器被删除并创建一个新的迭代器:

    /* taken from the "cycle.next" implementation */
    it = PyObject_GetIter(lz->saved);
    if (it == NULL)
        return NULL;
    tmp = lz->it;
    lz->it = it;
    lz->firstpass = 1;
    Py_DECREF(tmp);   /* destroys the old iterator */

这意味着在执行一个循环后列表被销毁。

无论如何,如果您需要访问此列表,只需在调用 itertools.cycle 之前在某处引用它即可。

【讨论】:

【参考方案2】:

如果您有办法知道cycle 产生的对象的某些属性,那么您可以推断出内部列表。例如,如果您知道循环中的所有对象都是不同的,并且除了您之外没有其他对象从 cycle 迭代器中读取,那么您可以简单地等待您看到的第一个对象再次出现(使用 is 进行测试不是==) 来终止内部列表。

但是如果没有这些知识,就无法保证,并且您选择的任何猜测循环的方法在某些情况下都会失败。

【讨论】:

【参考方案3】:

好的,所以我接受了@Bakuriu 的回答,因为它在技术上是正确的。无法复制/腌制 itertools.cycle 对象。

我已经实现了 itertools.cycle 的一个子类,它可腌制的(带有一些额外的花里胡哨的引导)。

import itertools


class FiniteCycle(itertools.cycle):
    """
    Cycles the given finite iterable indefinitely. 
    Subclasses ``itertools.cycle`` and adds pickle support.
    """
    def __init__(self, finite_iterable):
        self._index = 0
        self._iterable = tuple(finite_iterable)
        self._iterable_len = len(self._iterable)
        itertools.cycle.__init__(self, self._iterable)

    @property
    def index(self):
        return self._index

    @index.setter
    def index(self, index):
        """
        Sets the current index into the iterable. 
        Keeps the underlying cycle in sync.

        Negative indexing supported (will be converted to a positive index).
        """
        index = int(index)
        if index < 0:
            index = self._iterable_len + index
            if index < 0:
                raise ValueError('Negative index is larger than the iterable length.')

        if index > self._iterable_len - 1:
            raise IndexError('Index is too high for the iterable. Tried %s, iterable '
                             'length %s.' % (index, self._iterable_len))

        # calculate the positive number of times the iterable will need to be moved
        # forward to get to the desired index
        delta = (index + self._iterable_len - self.index) % (self._iterable_len)

        # move the finite cycle on ``delta`` times.
        for _ in xrange(delta):
            self.next()

    def next(self):
        self._index += 1
        if self._index >= self._iterable_len:
            self._index = 0
        return itertools.cycle.next(self)

    def peek(self):
        """
        Return the next value in the cycle without moving the iterable forward.
        """
        return self._iterable[self.index]

    def __reduce__(self):
        return (FiniteCycle, (self._iterable, ), 'index': self.index)

    def __setstate__(self, state):
        self.index = state.pop('index')

一些示例用法:

c = FiniteCycle([1, 2, 3])

c.index = -1
print c.next() # prints 3

print [c.next() for _ in xrange(4)] # prints [1, 2, 3, 1]

print c.peek() # prints 2
print c.next() # prints 2

import pickle
import cStringIO
serialised_cycle = pickle.dumps(c)

del c

c = pickle.loads(serialised_cycle)

print c.next() # prints 3
print c.next() # prints 1

欢迎反馈。

谢谢,

【讨论】:

【参考方案4】:

根据您使用cycle 的方式,您甚至可以像这样简单地使用自定义类包装器:

class SmartCycle:
    def __init__(self, x):
        self.cycle = cycle(x)
        self.to_list = x

    def __next__(self):
        return next(self.cycle)

例如

> a = SmartCycle([1, 2, 3])
> for _ in range(4):
>     print(next(a))
1
2
3
1

> a.to_list
[1, 2, 3]

【讨论】:

以上是关于从 itertools.cycle 中提取一个列表的主要内容,如果未能解决你的问题,请参考以下文章

与 itertools.cycle 或类似结构一起使用时的 Python“重启”生成器?

在 Python 中,为啥 itertools.cycle 需要额外的内存? [复制]

python内置函数itertools

itertools模块

Itertools循环方法 - 为什么“while”循环?

python 内置迭代:itertools