你称之为具有两种不同“完成”状态的迭代器是什么?

时间:2012-03-10 22:10:19

标签: python naming-conventions

在查询具有未知长度的分页列表的API时,我发现自己基本上在做

def fetch_one(self, n):
    data = json.load(urlopen(url_template % n))
    if data is None:
        self.finished = True
        return
    for row in data:
        if row_is_weird(row):
            self.finished = True
            return
        yield prepare(row)

def work(self):
    n = 1
    self.finished = False
    while not self.finished:
        consume(self.fetch_one(n))
        n += 1

workfetch_one之间的分割使得测试变得非常容易,但是通过实例变量的信令意味着我不能同时进行多个work ,这很糟糕。我提出了我认为更清洁的解决方案,但它涉及一个具有两个“完成”状态的迭代器,我不知道该怎么称呼它。我确信这种模式存在于其他地方,所以我很欣赏指针(或者说这是愚蠢的原因):

class Thing(object):
    def __init__(self, gen):
        self.gen = gen
        self.finished = False

    def __iter__(self):
        return self

    def __next__(self):
        try:
            v = next(self.gen)
        except StopThisThing:
            self.finished = True
            raise StopIteration
        else:
            return v
    next = __next__

然后我会像

那样使用
@thinged
def fetch_one(self, n):
    data = json.load(urlopen(url_template % n))
    if data is None:
        raise StopThisThing()
    for row in data:
        if row_is_weird(row):
            raise StopThisThing()
        yield prepare(row)

def work(self):
    n = 1
    while True:
        one = self.fetch_one(n)
        consume(one)
        if one.finished:
            break
        n += 1

那么我创造的是什么?

4 个答案:

答案 0 :(得分:2)

我认为你可以通过产生一些特别的东西来避免这种情况。

我必须建立自己的可运行示例,以显示我的意思:

def fetch_one(n):
    lst = [[1,2,3], [4,5,6], [7,8,9]][n]
    for x in lst:
        if x == 6:
            yield 'StopAll'
            return
        yield x

def work():
    n = 0
    in_progress = True
    while in_progress:
        numbers_iterator = fetch_one(n)
        for x in numbers_iterator:
            if x == 'StopAll':
                in_progress = False
                break
            print('x =', x)
        n += 1

work()

输出:

x = 1
x = 2
x = 3
x = 4
x = 5

我比self.finished或者像你建造的装饰者更喜欢这个,但我认为仍然可以找到更好的东西。 (也许这个答案可以帮助你)。

更新:更简单的解决方案可能是将fetch_one转换为带有自己的finised标志的类。

此解决方案的装饰器方法可能是:

class stopper(object):
    def __init__(self, func):
        self.func = func
        self.finished = False

    def __call__(self, *args, **kwargs):
        for x in self.func(*args, **kwargs):
            if x == 6:
                self.finished = True
                raise StopIteration
            yield x
        else:
            self.finished = True

基本上你不再关心fetch_one如何运作,只有当收益率合适时才会这样。

用法示例:

@stopper
def fetch_one(n):
    lst = [[1,2,3], [4,5,6], [7,8,9]][n]
    #lst = [[1,2,3], [], [4,5,6], [7,8,9]][n]   # uncomment to test for/else
    for x in lst:
        yield x

def work():
    n = 0
    while not fetch_one.finished:
        for x in fetch_one(n):
            print('x =', x)
        n += 1

答案 1 :(得分:1)

有一种更清晰的方法来处理您的情况:您有一个由分页数据组成的数据源,但可以通过检查各个行来检测终止条件。所以我会使用一个逐行取数据的迭代器,并在它应该的时候停止。没有特殊值(在带外),没有双向通信。

编辑:我刚刚发现您实际上并不关心页面边界。在这种情况下,您应该只使用它:

def linegetter(url_template):
    """
    Return the data line by line. Stop when end of input is detected.
    """
    n=0
    while True:
        n += 1
        data = json.load(urlopen(url_template % n))
        if data is None:
            return
        for row in data:
            if row_is_weird(row):
                return
            yield row

它逐行返回数据,您可以以任何方式准备和使用它。完成!

这似乎应该是整个答案。但是假设您需要按页面处理数据(正如您的代码现在所做的那样)。只需将第一个迭代器的输出分组到每个页面的子迭代器中。代码更复杂,因为我粘贴在完全通用的解决方案中;但使用它非常简单。

def linegetter(source, terminate=lambda x: False):
    """
    Return the data line by line, in a tuple with the page number.
    Stop when end of input is detected.
    """
    for n, data in enumerate(source):
        if data is None:
            return
        for row in data:
            if terminate(row):
                return
            yield (n, row)

def _giverow(source):
    "Yield page contents line by line, discarding page number"
    for page, row in source:
        yield row

def pagegetter(source):
    """Return an iterator for each page of incoming data.
    """
    import itertools
    for it in itertools.groupby(source, lambda x : x[0]):
        yield _giverow(it[1])

演示:每个“行”都是一个数字,每个页面都是一个子列表。我们看到“b”时停止。您的主循环现在有终止检查:

incoming = iter([[1,2,3], [4,5,6, "b", 7], [7,8,9]])
def row_is_weird(r): 
    return r == "b"

for page in pagegetter(linegetter(incoming, row_is_weird)):
    print list(page)

如您所见,代码完全通用。您可以将它与获取json页面的迭代器一起使用,如下所示:

from itertools import imap, count
jsonsource = imap(lambda n: json.load(urlopen(url_template % n)), count(1))
for page in pagegetter(linegetter(jsonsource, row_is_weird)):
    consume(page)

答案 2 :(得分:0)

你发明的名字是“穷人的迭代器版本”。你的work函数正在花费精力重新实现python已经在for循环中提供的内容。你有一系列可以在任何时候停止的值,这正是python的迭代器提供的原因。我们最好把一些逻辑转移到一个单独的函数中。像这样:

def fetch_all(self):
    for n in itertools.count():
        data = json.load(urlopen(url_template % n))
        if data is None:
            return

        for row in data:
             if row_is_wierd(row):
                  return

        yield itertools.imap(prepare, data)

或者,您可以使用例外

def fetch_all(self):
    for n in itertools.count():
        data = json.load(urlopen(url_template % n)
        if data is None:
             return

        try:
             yield map(prepare, data)
        except WierdRowError:
             return

实际上,我质疑以这种方式处理奇怪行的逻辑。是什么让一行变得奇怪?我们为什么要停在那里?这行真的很奇怪吗?

无论如何,您的工作职能变为

def work():
    for item in fetch_all():
        consume(item)

编辑

有了额外的信息,我会做一些像

这样的事情
def fetch_rows():
    for n in itertools.count():
        data = json.load(urlopen(url_template % n))
        for row in data:

            if row_is_wierd(row):
                return
            yield row

此函数生成行序列

def work():
    for row in fetch_all_rows():
         consume(row)

此函数实际处理行。

其中一些或全部可以用来自itertools的迭代器对象替换。

答案 3 :(得分:0)

我最初给出了错误的答案;这是一个更好的。

你有几个序列(JSON文件)可以正常或突然结束(如果 row_is_weird)。如果序列正常结束,则必须采用下一个序列。当您获得None而不是JSON文件时,此序列序列结束。</sanity-check>

您使用实例变量来指示突然和正常结束。这有助于您的代码破坏深层嵌套循环,但它也会引入不需要的非本地状态。

删除共享状态的最简单方法是将其作为结果或参数的一部分传递。让我们传递每一行的“古怪”。实际上,如果一行很奇怪,我们不需要传递行值,我们只传递一个值'从现在开始,结果无效'。它有助于在正确的位置停止迭代。

基本上它看起来像是接受的答案,但在内部你可以将其视为an application of Maybe and List monads。额外的好处是,您永远不会将序列结束标记误认为序列标记。

# preparations and mockups

input = [ # imitates rows or parsed JSON
  ['apple', 'orange', 'peach'], # entirely good rows
  ['meat', 'fowl', 'ROTTEN', 'unicorn'], # some good rows, then a bad one
  ['unicorn2', 'unicorn3'], # good rows we should never see
  None, # sentinel imitating 'no data' from JSON parser
]

def prepare(x): 
  print "%s is prepared" % x
  return 'prepared %s' %x

consume = lambda x: "%s is consumed" % x

row_is_weird = lambda x: x is 'ROTTEN'

# the solution

def maybe_prepare(row):
  if row_is_weird(row):
    return (False, None) # Nothing
  else:
    return (True, prepare(row)) # Just prepare(row)

def fetch_one(n):
  data = input[n-1] # instead of json.load(template % n)
  if data is None:
    return iter([(False, None)])
  else:
    return (maybe_prepare(row) for row in data)

# chain_all iterates over all items of all sequences in seqs 
chain_all = lambda seqs: (item for seq in seqs for item in seq)

from itertools import count
def work():
  for is_ok, prepared_row in chain_all(fetch_one(n) for n in count(1)):
    if not is_ok:
      break
    print consume(prepared_row)

此代码仍然很容易测试,但fetch_one()的测试稍微有点棘手:您必须在第一个(False, None)之前迭代值。这可以通过itertools.takewhile()轻松完成。

函数maybe_prepare()可能只是一行,但为了便于阅读,我将其保留为多行。