Как зациклить до EOF в Python?

Мне нужно зацикливаться, пока я не достигну конца файла-подобного объекта, но я не нахожу "очевидного способа сделать это", что заставляет меня подозревать, что я что-то упускаю, ну, очевидно:-)

У меня есть поток (в данном случае это объект StringIO, но мне также интересен общий случай), который хранит неизвестное количество записей в формате "<длина><данные>", например:

data = StringIO("\x07\x00\x00\x00foobar\x00\x04\x00\x00\x00baz\x00")

Теперь, единственный ясный способ, которым я могу представить, чтобы прочитать это, - это использовать (как я думаю, что) инициализированный цикл, который кажется немного не-Pythonic:

len_name = data.read(4)

while len_name != "":
    len_name = struct.unpack("<I", len_name)[0]
    names.append(data.read(len_name))

    len_name = data.read(4)

На C-подобном языке я бы просто read(4) в whileЭто тестовое предложение, но, конечно, это не сработает для Python. Есть мысли о лучшем способе сделать это?

6 ответов

Решение

Вы можете комбинировать итерацию через iter() с часовым:

for block in iter(lambda: file_obj.read(4), ""):
  use(block)

Вы видели, как перебирать строки в текстовом файле?

for line in file_obj:
  use(line)

Вы можете сделать то же самое с вашим собственным генератором:

def read_blocks(file_obj, size):
  while True:
    data = file_obj.read(size)
    if not data:
      break
    yield data

for block in read_blocks(file_obj, 4):
  use(block)

Смотрите также:

Я предпочитаю уже упомянутое решение на основе итераторов, чтобы превратить это в цикл for. Другое решение, написанное непосредственно, является "петлей с половиной" Кнута

while 1:
    len_name = data.read(4)
    if not len_name:
        break
    names.append(data.read(len_name))

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

Как и предполагалось, я вижу, что типичный и самый популярный ответ - использование очень специализированных генераторов для "чтения 4 байтов за раз". Иногда общность не сложнее (и гораздо полезнее;-), поэтому вместо этого я предложил следующее очень общее решение:

import operator
def funlooper(afun, *a, **k):
  wearedone = k.pop('wearedone', operator.not_)
  while True:
    data = afun(*a, **k)
    if wearedone(data): break
    yield data

Теперь желаемый заголовок цикла просто: for len_name in funlooper(data.read, 4):,

Редактировать: сделано гораздо более общим wearedone идиома, поскольку комментарий обвинил мою чуть менее общую предыдущую версию (жесткое кодирование выходного теста как if not data:) наличия "скрытой зависимости", всего!-)

Обычный швейцарский армейский нож зацикливания, itertools тоже нормально, конечно, как обычно

import itertools as it

for len_name in it.takewhile(bool, it.imap(data.read, it.repeat(4))): ...

или, что вполне эквивалентно:

import itertools as it

def loop(pred, fun, *args):
  return it.takewhile(pred, it.starmap(fun, it.repeat(args)))

for len_name in loop(bool, data.read, 4): ...

Маркер EOF в python - это пустая строка, поэтому то, что у вас есть, очень близко к лучшему, что вы получите, не написав функцию для ее обертывания в итераторе. Я мог бы быть написан немного более питонически, изменив while лайк:

while len_name:
    len_name = struct.unpack("<I", len_name)[0]
    names.append(data.read(len_name))
    len_name = data.read(4)

Я бы пошел с предложением Tendayi и функцией повторения для удобства чтения:

def read4():
    len_name = data.read(4)
    if len_name:
        len_name = struct.unpack("<I", len_name)[0]
        return data.read(len_name)
    else:
        raise StopIteration

for d in iter(read4, ''):
    names.append(d)
Другие вопросы по тегам