我可以从实例方法中产生吗

Can I yield from an instance method

在 class 的实例方法中使用 yield 语句可以吗?例如,

# Similar to itertools.islice
class Nth(object):
    def __init__(self, n):
        self.n = n
        self.i = 0
        self.nout = 0

    def itervalues(self, x):
        for xi in x:
            self.i += 1
            if self.i == self.n:
                self.i = 0
                self.nout += 1
                yield self.nout, xi

Python 对此没有抱怨,简单的案例似乎也行得通。但是,我只看到了常规函数产生收益的例子。

当我尝试将它与 itertools 函数一起使用时,我开始遇到问题。例如,假设我有两个大数据流 X 和 Y,它们存储在多个文件中,我想通过一次数据循环来计算它们的和与差。我可以像下图那样使用 itertools.teeitertools.izip

在代码中应该是这样的(抱歉,太长了)

from itertools import izip_longest, izip, tee
import random

def add(x,y):
    for xi,yi in izip(x,y):
        yield xi + yi

def sub(x,y):
    for xi,yi in izip(x,y):
        yield xi - yi

class NthSumDiff(object):
    def __init__(self, n):
        self.nthsum = Nth(n)
        self.nthdiff = Nth(n)

    def itervalues(self, x, y):
        xadd, xsub = tee(x)
        yadd, ysub = tee(y)
        gen_sum = self.nthsum.itervalues(add(xadd, yadd))
        gen_diff = self.nthdiff.itervalues(sub(xsub, ysub))
        # Have to use izip_longest here, but why?
        #for (i,nthsum), (j,nthdiff) in izip_longest(gen_sum, gen_diff):
        for (i,nthsum), (j,nthdiff) in izip(gen_sum, gen_diff):
            assert i==j, "sum row %d != diff row %d" % (i,j)
            yield nthsum, nthdiff

nskip = 12
ns = Nth(nskip)
nd = Nth(nskip)
nsd = NthSumDiff(nskip)
nfiles = 10
for i in range(nfiles):
    # Generate some data.
    # If the block length is a multiple of nskip there's no problem.
    #n = random.randint(5000, 10000) * nskip
    n = random.randint(50000, 100000)
    print 'file %d n=%d' % (i, n)
    x = range(n)
    y = range(100,n+100)
    # Independent processing is no problem but requires two loops.
    for i, nthsum in ns.itervalues(add(x,y)):
        pass
    for j, nthdiff in nd.itervalues(sub(x,y)):
        pass
    assert i==j
    # Trying to do both with one loops causes problems.
    for nthsum, nthdiff in nsd.itervalues(x,y):
        # If izip_longest is necessary, why don't I ever get a fillvalue?
        assert nthsum is not None
        assert nthdiff is not None
    # After each block of data the two iterators should have the same state.
    assert nsd.nthsum.nout == nsd.nthdiff.nout, \
           "sum nout %d != diff nout %d" % (nsd.nthsum.nout, nsd.nthdiff.nout)

但这会失败,除非我将 itertools.izip 换成 itertools.izip_longest,即使迭代器具有相同的长度。这是最后一个 assert 被击中,输出类似于

file 0 n=58581
file 1 n=87978
Traceback (most recent call last):
  File "test.py", line 71, in <module>
    "sum nout %d != diff nout %d" % (nsd.nthsum.nout, nsd.nthdiff.nout)
AssertionError: sum nout 12213 != diff nout 12212 

编辑:我想从我写的例子来看并不明显,但是输入数据X和Y只能以块的形式使用(在我的实际问题中,它们被分块文件)。这很重要,因为我需要维护块之间的状态。在上面的玩具示例中,这意味着 Nth 需要产生等同于

>>> x1 = range(0,10)
>>> x2 = range(10,20)
>>> (x1 + x2)[::3]
[0, 3, 6, 9, 12, 15, 18]

不等同于

>>> x1[::3] + x2[::3]
[0, 3, 6, 9, 10, 13, 16, 19]

我可以使用 itertools.chain 提前加入块,然后调用 Nth.itervalues,但我想了解在 [=21= 中维护状态有什么问题] class 调用之间(我的真实应用是涉及更多保存状态的图像处理,不简单 Nth/add/subtract)。

我不明白我的 Nth 实例在长度相同时如何以不同的状态结束。例如,如果我给 izip 两个等长的字符串

>>> [''.join(x) for x in izip('ABCD','abcd')]
['Aa', 'Bb', 'Cc', 'Dd']

我得到了相同长度的结果;为什么我的 Nth.itervalues 生成器似乎得到了不等数量的 next() 调用,即使每个生成器产生相同数量的结果?

Gist repo with revisions | Quick link to solution

快速回答

您从未在 class Nth 中重置 self.iself.nout。另外,你应该使用这样的东西:

# Similar to itertools.islice
class Nth(object):
    def __init__(self, n):
        self.n = n

    def itervalues(self, x):
        for a,b in enumerate(islice(x, self.n - 1, None, self.n)):
            self.nout = a
            yield a,b

但是因为你甚至不需要 nout,你应该使用这个:

def Nth(iterable, step):
    return enumerate(itertools.islice(iterable, step - 1, None, step)) 

长答案

你的代码有一股异味,导致我在 NthSumDiff.itervalues():

中看到这一行
for (i,nthsum), (j,nthdiff) in izip(gen_sum, gen_diff):

如果交换 gen_sumgen_diff,您会发现 gen_diff 始终是 nout 大一的那个。这是因为 izip() 在从 gen_diff 拉取之前从 gen_sum 拉取。 gen_sumgen_diff 甚至在最后一次迭代中尝试之前引发 StopIteration 异常。

例如,假设您选择了 N 个样本,其中 N % step == 7。在每次迭代结束时,第 N 个实例的 self.i 应等于 0。但是在最后一次迭代中,[= gen_sum 中的 18=] 将增加到 7,然后 x 中将不再有元素。它将引发 StopIteration。不过,gen_diff 仍然位于 self.i 等于 0。

如果将 self.i = 0self.nout = 0 添加到 Nth.itervalues() 的开头,问题就会消失。

课程

你遇到这个问题只是因为你的代码太复杂而不是Pythonic。如果您发现自己在循环中使用大量计数器和索引,这是一个好兆头(在 Python 中)退后一步,看看您是否可以简化您的代码。我有很长的 C 编程历史,因此,我仍然不时发现自己在 Python 中做同样的事情。

实施更简单

言出必行...

from itertools import izip, islice
import random

def sumdiff(x,y,step):
    # filter for the Nth values of x and y now
    x = islice(x, step-1, None, step)
    y = islice(y, step-1, None, step)
    return ((xi + yi, xi - yi) for xi, yi in izip(x,y))

nskip = 12
nfiles = 10
for i in range(nfiles):
    # Generate some data.
    n = random.randint(50000, 100000)
    print 'file %d n=%d' % (i, n)
    x = range(n)
    y = range(100,n+100)
    for nthsum, nthdiff in sumdiff(x,y,nskip):
        assert nthsum is not None
        assert nthdiff is not None
    assert len(list(sumdiff(x,y,nskip))) == n/nskip

问题的更多解释

回复 Brian 的评论:

This doesn't do the same thing. Not resetting i and nout is intentional. I've basically got a continuous data stream X that's split across several files. Slicing the blocks gives a different result than slicing the concatenated stream (I commented earlier about possibly using itertools.chain). Also my actual program is more complicated than mere slicing; it's just a working example. I don't understand the explanation about the order of StopIteration. If izip('ABCD','abcd') --> Aa Bb Cc Dd then it seems like equal-length generators should get an equal number of next calls, no? – Brian Hawkins 6 hours ago

你的问题太长了,我错过了关于来自多个文件的流的部分。让我们看看代码本身。首先,我们需要真正清楚 itervalues(x) 的实际工作原理。

# Similar to itertools.islice
class Nth(object):
    def __init__(self, n):
        self.n = n
        self.i = 0
        self.nout = 0

    def itervalues(self, x):
        for xi in x:
            # We increment self.i by self.n on every next()
            # call to this generator method unless the
            # number of objects remaining in x is less than
            # self.n. In that case, we increment by that amount
            # before the for loop exits normally.
            self.i += 1
            if self.i == self.n:
                self.i = 0
                self.nout += 1
                # We're yielding, so we're a generator
                yield self.nout, xi
        # Python helpfully raises StopIteration to fulfill the 
        # contract of an iterable. That's how for loops and
        # others know when to stop.

在上面的 itervalues(x) 中,对于每个 next() 调用,它在内部将 self.i 递增 self.n 然后产生或它递增 self.i 数字剩余在 x 中的对象,然后退出 for 循环,然后退出生成器(itervalues() 是一个生成器,因为它产生)。当 itervalues() 生成器退出时,Python 引发 StopIteration 异常。

因此,对于每个用 N 初始化的 class Nth 实例,在用尽 itervalues(X) 中的所有元素后 self.i 的值将是:

self.i = value_of_self_i_before_itervalues(X) + len(X) % N

现在当你遍历 izip(Nth_1, Nth_2) 时,它会做这样的事情:

def izip(A, B):
    try:
        while True:
            a = A.next()
            b = B.next()
            yield a,b
    except StopIteration:
        pass

所以,想象一下 N=10len(X)=13。在最后一次 next() 调用 izip() 时, A 和 B 的状态都是 self.i==0A.next() 被调用,递增 self.i += 3,用完 X 中的元素,退出 for 循环,returns,然后 Python 引发 StopIteration。现在,在 izip() 中,我们直接进入异常块,完全跳过 B.next()。所以,A.i==3B.i==0 最后。

第二次尝试简化(要求正确)

这是将所有文件数据视为一个连续流的另一个简化版本。它使用链式、小型、可重复使用的生成器。我非常非常推荐观看这个 PyCon '14 talk about generators by David Beazley。看你的问题描述,应该是100%适用。

from itertools import izip, islice
import random

def sumdiff(data):
    return ((x + y, x - y) for x, y in data)

def combined_file_data(files):
    for i,n in files:
        # Generate some data.
        x = range(n)
        y = range(100,n+100)
        for data in izip(x,y):
            yield data

def filelist(nfiles):
    for i in range(nfiles):
        # Generate some data.
        n = random.randint(50000, 100000)
        print 'file %d n=%d' % (i, n)
        yield i, n

def Nth(iterable, step):
    return islice(iterable, step-1, None, step)

nskip = 12
nfiles = 10
filedata = combined_file_data(filelist(nfiles))
nth_data = Nth(filedata, nskip)
for nthsum, nthdiff in sumdiff(nth_data):
    assert nthsum is not None
    assert nthdiff is not None

浓缩讨论,在实例方法 本身 中使用 yield 并没有错。如果实例状态在最后一个 yield 之后发生变化,您会遇到 izip 的麻烦,因为一旦其中任何一个停止产生结果,izip 就会停止对其参数调用 next()。一个更清楚的例子可能是

from itertools import izip

class Three(object):
    def __init__(self):
        self.status = 'init'

    def run(self):
        self.status = 'running'
        yield 1
        yield 2
        yield 3
        self.status = 'done'
        raise StopIteration()

it = Three()
for x in it.run():
    assert it.status == 'running'
assert it.status == 'done'

it1, it2 = Three(), Three()
for x, y in izip(it1.run(), it2.run()):
    pass
assert it1.status == 'done'
assert it2.status == 'done', "Expected status=done, got status=%s." % it2.status

命中最后一个断言,

AssertionError: Expected status=done, got status=running.

在原始问题中,Nth class 可以在其最后一个 yield 之后消耗输入数据,因此和流和差流可能与 [=13= 不同步].使用 izip_longest 会起作用,因为它会尝试耗尽每个迭代器。一个更清晰的解决方案可能是重构以避免在最后一次 yield 之后改变状态。