在查询具有未知长度的分页列表的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
work
和fetch_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
那么我创造的是什么?
答案 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()
可能只是一行,但为了便于阅读,我将其保留为多行。