Como fazer loop até o EOF em Python?
Pergunta
Eu preciso fazer um loop até chegar ao fim de um objeto semelhante a um arquivo, mas não estou encontrando uma "maneira óbvia de fazê-lo", o que me faz suspeitar que estou negligenciando algo, bem, óbvio. :-)
Eu tenho um fluxo (neste caso, é um objeto Stringio, mas também estou curioso sobre o caso geral) que armazena um número desconhecido de registros em "u003Clength>u003Cdata> "Formato, por exemplo:
data = StringIO("\x07\x00\x00\x00foobar\x00\x04\x00\x00\x00baz\x00")
Agora, a única maneira clara de imaginar ler isso está usando (o que eu acho como) um loop inicializado, que parece um pouco pitônico:
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)
Em uma linguagem do tipo C, eu apenas enfia o read(4)
no while
Cláusula de teste, mas é claro que isso não funcionará para o Python. Alguma opinião sobre uma maneira melhor de conseguir isso?
Solução
Você pode combinar iteração através iter () com um sentinela:
for block in iter(lambda: file_obj.read(4), ""):
use(block)
Outras dicas
Você viu como iterar sobre linhas em um arquivo de texto?
for line in file_obj:
use(line)
Você pode fazer a mesma coisa com seu próprio gerador:
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)
Veja também:
Prefiro a solução já mencionada baseada em iterador para transformar isso em um loop for. Outra solução escrita diretamente é o "loop e meio" de Knuth
while 1:
len_name = data.read(4)
if not len_name:
break
names.append(data.read(len_name))
Você pode ver, em comparação, como isso é facilmente içado em seu próprio gerador e usado como um loop for.
Vejo, como previsto, que a resposta típica e mais popular está usando geradores muito especializados para "ler 4 bytes de cada vez". Às vezes, a generalidade não é mais difícil (e muito mais gratificante ;-), então, sugeri a seguinte solução muito geral:
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
Agora o cabeçalho do loop desejado é apenas: for len_name in funlooper(data.read, 4):
.
Editar: tornado muito mais geral pelo wearedone
idioma desde que um comentário acusou minha versão anterior um pouco menos geral (codificando o teste de saída como if not data:
) de ter "uma dependência oculta", de todas as coisas!-)
A usual faca do exército suíço de loop, itertools
, está bem também, é claro, como sempre:
import itertools as it
for len_name in it.takewhile(bool, it.imap(data.read, it.repeat(4))): ...
ou, de maneira bastante equivalente:
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): ...
O marcador EOF no Python é uma corda vazia, então o que você tem é bem próximo do melhor que você obterá sem escrever uma função para envolver isso em um iterador. Eu poderia ser escrito de maneira um pouco mais pitônica, mudando o while
Curti:
while len_name:
len_name = struct.unpack("<I", len_name)[0]
names.append(data.read(len_name))
len_name = data.read(4)
Eu iria com a sugestão de tendayi e a função e o iterador para legibilidade:
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)