Что вы называете итератором с двумя разными состояниями «сделано»? - PullRequest
6 голосов
/ 11 марта 2012

При запросе 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

, такчто это за вещь, которую я создал?

Ответы [ 4 ]

2 голосов
/ 11 марта 2012

Я думаю, что вы можете избежать этого, принося что-то особенное.

Я должен был создать свой собственный работающий пример, чтобы показать, что я имею в виду:

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 голос
/ 12 марта 2012

Существует гораздо более понятный способ справиться с вашей ситуацией: у вас есть источник данных, состоящий из выгружаемых данных, но условие завершения может быть обнаружено путем изучения отдельных строк.Поэтому я бы использовал итератор, который выбирает данные построчно и останавливается, когда это необходимо.Никаких специальных значений (в или вне диапазона), нет двусторонней связи.

Редактировать: Я только что обнаружил, что на самом деле вам все равноо границах страницы.В этом случае вы должны просто использовать это:

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])

Демонстрация: каждая «строка» - это цифра, каждая страница - это подсписок.Мы останавливаемся, когда видим «б».Ваш основной цикл теперь имеет нет проверок завершения:

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)
0 голосов
/ 26 марта 2012

Я изначально дал неправильный ответ;вот лучший вариант.

У вас есть несколько последовательностей (файлов JSON), которые могут нормально или внезапно заканчиваться (если row_is_weird).Если последовательность заканчивается нормально, следующая последовательность должна быть взята.Эта последовательность последовательностей заканчивается, когда вы получаете None вместо файла JSON. </sanity-check>

Вы используете переменную экземпляра, чтобы сигнализировать как о внезапном, так и о нормальном окончании.Это помогает вашему коду разрывать глубоко вложенные циклы, но также вводит нежелательное нелокальное состояние.

Самый простой способ удалить общее состояние - передать его как часть результата или параметров.Давайте передадим «странность» каждого ряда вместе с ним.На самом деле, если строка странная, нам не нужно передавать значение строки, мы просто передаем значение, говорящее «отныне результаты недопустимы».Это помогает остановить итерации в нужном месте.

По сути, это выглядит как принятый ответ, но внутренне вы можете просмотреть его как приложение монад Возможно и Список .Дополнительным преимуществом является то, что вы никогда не сможете принять свой токен конца последовательности за токен последовательности.

# 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() может быть однострочной, но я оставил ее многострочной для удобства чтения.

0 голосов
/ 11 марта 2012

Имя, которое вы изобрели, - «версия итератора для бедняков». Ваша 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.

...