Qual é a maneira mais “pythônico” para iterar sobre uma lista em pedaços?
-
10-07-2019 - |
Pergunta
Eu tenho um script Python que toma como entrada uma lista de inteiros, o que eu preciso trabalhar com quatro inteiros de cada vez. Infelizmente, eu não tenho o controle da entrada, ou eu tê-lo passado como uma lista de tuplas de quatro elementos. Atualmente, estou Iterando sobre ele desta maneira:
for i in xrange(0, len(ints), 4):
# dummy op for example code
foo += ints[i] * ints[i + 1] + ints[i + 2] * ints[i + 3]
Ele se parece muito com "C-pensar", embora, que me faz suspeitar que há uma maneira mais Python de lidar com esta situação. A lista é descartado após a iteração, por isso não precisa ser preservada. Talvez algo como isso seria melhor?
while ints:
foo += ints[0] * ints[1] + ints[2] * ints[3]
ints[0:4] = []
Ainda não bastante "sente" certo, no entanto. : - /
questão relacionada: Como fazer você dividir uma lista em pedaços de tamanho uniforme em Python?
Solução
modificado a partir da href="https://docs.python.org/library/itertools.html#itertools-recipes" rel="noreferrer"> receitas seção de Python itertools docs:
from itertools import zip_longest
def grouper(iterable, n, fillvalue=None):
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)
Exemplo
Em pseudocódigo para manter o exemplo concisa.
grouper('ABCDEFG', 3, 'x') --> 'ABC' 'DEF' 'Gxx'
Nota:. em Python 2 uso izip_longest
vez de zip_longest
Outras dicas
def chunker(seq, size):
return (seq[pos:pos + size] for pos in range(0, len(seq), size))
# (in python 2 use xrange() instead of range() to avoid allocating a list)
Simples. Fácil. Rápido. Funciona com qualquer sequência:
text = "I am a very, very helpful text"
for group in chunker(text, 7):
print repr(group),
# 'I am a ' 'very, v' 'ery hel' 'pful te' 'xt'
print '|'.join(chunker(text, 10))
# I am a ver|y, very he|lpful text
animals = ['cat', 'dog', 'rabbit', 'duck', 'bird', 'cow', 'gnu', 'fish']
for group in chunker(animals, 3):
print group
# ['cat', 'dog', 'rabbit']
# ['duck', 'bird', 'cow']
# ['gnu', 'fish']
Eu sou um fã de
chunk_size= 4
for i in range(0, len(ints), chunk_size):
chunk = ints[i:i+chunk_size]
# process chunk of size <= chunk_size
import itertools
def chunks(iterable,size):
it = iter(iterable)
chunk = tuple(itertools.islice(it,size))
while chunk:
yield chunk
chunk = tuple(itertools.islice(it,size))
# though this will throw ValueError if the length of ints
# isn't a multiple of four:
for x1,x2,x3,x4 in chunks(ints,4):
foo += x1 + x2 + x3 + x4
for chunk in chunks(ints,4):
foo += sum(chunk)
Outra forma:
import itertools
def chunks2(iterable,size,filler=None):
it = itertools.chain(iterable,itertools.repeat(filler,size-1))
chunk = tuple(itertools.islice(it,size))
while len(chunk) == size:
yield chunk
chunk = tuple(itertools.islice(it,size))
# x2, x3 and x4 could get the value 0 if the length is not
# a multiple of 4.
for x1,x2,x3,x4 in chunks2(ints,4,0):
foo += x1 + x2 + x3 + x4
from itertools import izip_longest
def chunker(iterable, chunksize, filler):
return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)
Eu precisava de uma solução que também trabalham com conjuntos e geradores. Eu não conseguia pensar em nada muito curto e muito, mas é bastante legível, pelo menos.
def chunker(seq, size):
res = []
for el in seq:
res.append(el)
if len(res) == size:
yield res
res = []
if res:
yield res
Lista:
>>> list(chunker([i for i in range(10)], 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Set:
>>> list(chunker(set([i for i in range(10)]), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Gerador:
>>> list(chunker((i for i in range(10)), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
Semelhante a outras propostas, mas não exatamente iguais, eu gosto de fazê-lo desta maneira, porque é simples e fácil de ler:
it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9])
for chunk in zip(it, it, it, it):
print chunk
>>> (1, 2, 3, 4)
>>> (5, 6, 7, 8)
Desta forma, você não terá o último pedaço parcial. Se você deseja obter (9, None, None, None)
com a última parcela, basta usar izip_longest
de itertools
.
A solução ideal para este problema trabalha com iteradores (não apenas as sequências). Ele também deve ser rápido.
Esta é a solução fornecida pela documentação para itertools:
def grouper(n, iterable, fillvalue=None):
#"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return itertools.izip_longest(fillvalue=fillvalue, *args)
Usando %timeit
de ipython no meu ar mac book, eu recebo 47,5 nós por loop.
No entanto, isso realmente não funciona para mim desde que os resultados são acolchoada para ser grupos mesmo porte. Uma solução, sem o preenchimento é ligeiramente mais complicado. A solução mais simples pode ser:
def grouper(size, iterable):
i = iter(iterable)
while True:
out = []
try:
for _ in range(size):
out.append(i.next())
except StopIteration:
yield out
break
yield out
Simples, mas muito lento: 693 nós por laço
A melhor solução que eu poderia vir acima com usos islice
para o loop interno:
def grouper(size, iterable):
it = iter(iterable)
while True:
group = tuple(itertools.islice(it, None, size))
if not group:
break
yield group
Com o mesmo conjunto de dados, recebo 305 nós por loop.
Não é possível obter uma solução pura mais rápido do que isso, eu forneço a seguinte solução com uma ressalva importante:. Se os dados de entrada tem instâncias de filldata
nele, você pode obter resposta errada
def grouper(n, iterable, fillvalue=None):
#"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
for i in itertools.izip_longest(fillvalue=fillvalue, *args):
if tuple(i)[-1] == fillvalue:
yield tuple(v for v in i if v != fillvalue)
else:
yield i
Eu realmente não gosto dessa resposta, mas é significativamente mais rápido. 124 nós por laço
Uma vez que ninguém mencionou ainda aqui está uma solução zip()
:
>>> def chunker(iterable, chunksize):
... return zip(*[iter(iterable)]*chunksize)
Ele só funciona se o comprimento de sua seqüência é sempre divisível pelo tamanho do bloco ou você não se preocupam com um pedaço de fuga se não é.
Exemplo:
>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9')]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8')]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]
Ou usando itertools.izip para retornar um iterador em vez de uma lista:
>>> from itertools import izip
>>> def chunker(iterable, chunksize):
... return izip(*[iter(iterable)]*chunksize)
O preenchimento pode ser corrigido usando @ do ??O????? resposta :
>>> from itertools import chain, izip, repeat
>>> def chunker(iterable, chunksize, fillvalue=None):
... it = chain(iterable, repeat(fillvalue, chunksize-1))
... args = [it] * chunksize
... return izip(*args)
Usando o mapa () em vez de zip () corrige o problema de preenchimento na resposta de J.F. Sebastian:
>>> def chunker(iterable, chunksize):
... return map(None,*[iter(iterable)]*chunksize)
Exemplo:
>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9'), ('0', None, None)]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8'), ('9', '0', None, None)]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]
Se você não se importa com um pacote externo que você poderia usar iteration_utilities.grouper
de iteration_utilties
1 . Ele suporta todos os iterables (e não apenas as sequências):
from iteration_utilities import grouper
seq = list(range(20))
for group in grouper(seq, 4):
print(group)
que impressões:
(0, 1, 2, 3)
(4, 5, 6, 7)
(8, 9, 10, 11)
(12, 13, 14, 15)
(16, 17, 18, 19)
No caso do comprimento não é um múltiplo do groupsize que também suporta o enchimento (o último grupo incompleto) ou truncando (descartando o último grupo incompleta) a última:
from iteration_utilities import grouper
seq = list(range(17))
for group in grouper(seq, 4):
print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16,)
for group in grouper(seq, 4, fillvalue=None):
print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16, None, None, None)
for group in grouper(seq, 4, truncate=True):
print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
1 Disclaimer:. Eu sou o autor desse pacote
Se a lista é grande, o caminho mais alto desempenho para fazer isso é usar um gerador:
def get_chunk(iterable, chunk_size):
result = []
for item in iterable:
result.append(item)
if len(result) == chunk_size:
yield tuple(result)
result = []
if len(result) > 0:
yield tuple(result)
for x in get_chunk([1,2,3,4,5,6,7,8,9,10], 3):
print x
(1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10,)
Usando pequenas funções e as coisas realmente não apelar para mim; Eu prefiro fatias usar apenas:
data = [...]
chunk_size = 10000 # or whatever
chunks = [data[i:i+chunk_size] for i in xrange(0,len(data),chunk_size)]
for chunk in chunks:
...
Outra abordagem seria usar a forma de dois argumentos de iter
:
from itertools import islice
def group(it, size):
it = iter(it)
return iter(lambda: tuple(islice(it, size)), ())
Isto pode ser facilmente adaptado para usar preenchimento (isto é semelhante à resposta Markus Jarderot 's):
from itertools import islice, chain, repeat
def group_pad(it, size, pad=None):
it = chain(iter(it), repeat(pad))
return iter(lambda: tuple(islice(it, size)), (pad,) * size)
Estes podem até ser combinados para preenchimento opcional:
_no_pad = object()
def group(it, size, pad=_no_pad):
if pad == _no_pad:
it = iter(it)
sentinel = ()
else:
it = chain(iter(it), repeat(pad))
sentinel = (pad,) * size
return iter(lambda: tuple(islice(it, size)), sentinel)
Com o NumPy-lo simples:
ints = array([1, 2, 3, 4, 5, 6, 7, 8])
for int1, int2 in ints.reshape(-1, 2):
print(int1, int2)
saída:
1 2
3 4
5 6
7 8
A menos que eu sente falta de alguma coisa, a seguinte solução simples com gerador de expressões não foi mencionado. Assume-se que tanto o tamanho eo número de pedaços são conhecidos (que é frequentemente o caso), e que não é necessária qualquer padding:
def chunks(it, n, m):
"""Make an iterator over m first chunks of size n.
"""
it = iter(it)
# Chunks are presented as tuples.
return (tuple(next(it) for _ in range(n)) for _ in range(m))
Em seu segundo método, gostaria de avançar para o próximo grupo de 4 por fazer isso:
ints = ints[4:]
No entanto, eu não fiz qualquer medição de desempenho, então eu não sei qual deles pode ser mais eficiente.
Dito isto, eu normalmente escolher o primeiro método. Não é bonito, mas é muitas vezes consequência de interface com o mundo exterior.
No entanto, outra resposta, cujas vantagens são:
1) facilmente compreensível
2) funciona em qualquer iteráveis, não apenas seqüências (algumas das respostas acima vai engasgar com filehandles)
3) Não carregar o trecho na memória de uma só vez
4) Não faça uma lista pedaço de comprimento de referências ao mesmo iterador na memória
5) No preenchimento dos valores de preenchimento no final da lista ??p>
Dito isto, eu não programado ele para que ele possa ser mais lento do que alguns dos métodos mais inteligentes, e algumas das vantagens pode ser irrelevante dado o caso de uso.
def chunkiter(iterable, size):
def inneriter(first, iterator, size):
yield first
for _ in xrange(size - 1):
yield iterator.next()
it = iter(iterable)
while True:
yield inneriter(it.next(), it, size)
In [2]: i = chunkiter('abcdefgh', 3)
In [3]: for ii in i:
for c in ii:
print c,
print ''
...:
a b c
d e f
g h
Update:
Um par de desvantagens devido ao facto das espiras internas e externas estão puxando os valores a partir do mesmo iteração:
1) continuar não funciona como esperado no loop externo - ele só continua para o próximo item, em vez de pular um pedaço. No entanto, este não parece ser um problema, pois não há nada para teste no circuito exterior.
2) pausa não funciona como esperado no laço interno - controle vai acabar no circuito interno novamente com o próximo item no iterador. Para passar pedaços inteiros, quer envolva a iteração interior (ii acima) em um tuplo, v.g. for c in tuple(ii)
, ou definir um sinalizador e esgotar o iterador.
def group_by(iterable, size):
"""Group an iterable into lists that don't exceed the size given.
>>> group_by([1,2,3,4,5], 2)
[[1, 2], [3, 4], [5]]
"""
sublist = []
for index, item in enumerate(iterable):
if index > 0 and index % size == 0:
yield sublist
sublist = []
sublist.append(item)
if sublist:
yield sublist
Você pode usar partição ou pedaços função de funcy biblioteca:
from funcy import partition
for a, b, c, d in partition(4, ints):
foo += a * b * c * d
Estas funções também tem iteradoras versões ipartition
e ichunks
, que será mais eficiente neste caso.
Você também pode espreitar sua implementação .
Para evitar todas as conversões a uma lista import itertools
e:
>>> for k, g in itertools.groupby(xrange(35), lambda x: x/10):
... list(g)
Produz:
...
0 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
2 [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
3 [30, 31, 32, 33, 34]
>>>
Eu verifiquei groupby
e não converter a lista ou o uso len
então eu (acho que) isso vai atrasar resolução de cada valor até que seja realmente utilizado. Infelizmente nenhuma das respostas disponíveis (neste momento) parecia oferecer esta variação.
Obviamente, se você precisa lidar com cada item, por sua vez um ninho para varrer g:
for k,g in itertools.groupby(xrange(35), lambda x: x/10):
for i in g:
# do what you need to do with individual items
# now do what you need to do with the whole group
Meu interesse específico nesta era a necessidade de consumir um gerador para enviar alterações em lotes de até 1000 para a API do Gmail:
messages = a_generator_which_would_not_be_smart_as_a_list
for idx, batch in groupby(messages, lambda x: x/1000):
batch_request = BatchHttpRequest()
for message in batch:
batch_request.add(self.service.users().messages().modify(userId='me', id=message['id'], body=msg_labels))
http = httplib2.Http()
self.credentials.authorize(http)
batch_request.execute(http=http)
Sobre a solução deu por J.F. Sebastian
aqui :
def chunker(iterable, chunksize):
return zip(*[iter(iterable)]*chunksize)
É inteligente, mas tem uma desvantagem - sempre voltar tupla. Como chegar cadeia em vez
Claro que você pode escrever ''.join(chunker(...))
, mas a tupla temporária é construída de qualquer maneira.
Você pode se livrar da tupla temporária escrevendo própria zip
, como este:
class IteratorExhausted(Exception):
pass
def translate_StopIteration(iterable, to=IteratorExhausted):
for i in iterable:
yield i
raise to # StopIteration would get ignored because this is generator,
# but custom exception can leave the generator.
def custom_zip(*iterables, reductor=tuple):
iterators = tuple(map(translate_StopIteration, iterables))
while True:
try:
yield reductor(next(i) for i in iterators)
except IteratorExhausted: # when any of iterators get exhausted.
break
Em seguida
def chunker(data, size, reductor=tuple):
return custom_zip(*[iter(data)]*size, reductor=reductor)
Exemplo de utilização:
>>> for i in chunker('12345', 2):
... print(repr(i))
...
('1', '2')
('3', '4')
>>> for i in chunker('12345', 2, ''.join):
... print(repr(i))
...
'12'
'34'
Eu gosto desta abordagem. Parece simples e não mágico e suporta todos os tipos iteráveis ??e não requer importações.
def chunk_iter(iterable, chunk_size):
it = iter(iterable)
while True:
chunk = tuple(next(it) for _ in range(chunk_size))
if not chunk:
break
yield chunk
Eu nunca quero que meus pedaços acolchoado, para que exigência é essencial. Acho que a capacidade de trabalhar em qualquer iterable também é exigência. Dado que, eu decidi estender na resposta aceita, https://stackoverflow.com/a/434411/1074659 .
Performance leva uma ligeira sucesso nesta abordagem, se o preenchimento não é desejado, devido à necessidade de comparar e filtrar os valores acolchoados. No entanto, para grandes tamanhos do pedaço, este utilitário é muito alto desempenho.
#!/usr/bin/env python3
from itertools import zip_longest
_UNDEFINED = object()
def chunker(iterable, chunksize, fillvalue=_UNDEFINED):
"""
Collect data into chunks and optionally pad it.
Performance worsens as `chunksize` approaches 1.
Inspired by:
https://docs.python.org/3/library/itertools.html#itertools-recipes
"""
args = [iter(iterable)] * chunksize
chunks = zip_longest(*args, fillvalue=fillvalue)
yield from (
filter(lambda val: val is not _UNDEFINED, chunk)
if chunk[-1] is _UNDEFINED
else chunk
for chunk in chunks
) if fillvalue is _UNDEFINED else chunks
def chunker(iterable, n):
"""Yield iterable in chunk sizes.
>>> chunks = chunker('ABCDEF', n=4)
>>> chunks.next()
['A', 'B', 'C', 'D']
>>> chunks.next()
['E', 'F']
"""
it = iter(iterable)
while True:
chunk = []
for i in range(n):
try:
chunk.append(next(it))
except StopIteration:
yield chunk
raise StopIteration
yield chunk
if __name__ == '__main__':
import doctest
doctest.testmod()
Aqui está uma chunker sem importações que os geradores suporta:
def chunks(seq, size):
it = iter(seq)
while True:
ret = tuple(next(it) for _ in range(size))
if len(ret) == size:
yield ret
else:
raise StopIteration()
Exemplo de utilização:
>>> def foo():
... i = 0
... while True:
... i += 1
... yield i
...
>>> c = chunks(foo(), 3)
>>> c.next()
(1, 2, 3)
>>> c.next()
(4, 5, 6)
>>> list(chunks('abcdefg', 2))
[('a', 'b'), ('c', 'd'), ('e', 'f')]
Não parece ser uma maneira bonita de fazer isso. Aqui é uma página que tem uma série de métodos, incluindo:
def split_seq(seq, size):
newseq = []
splitsize = 1.0/size*len(seq)
for i in range(size):
newseq.append(seq[int(round(i*splitsize)):int(round((i+1)*splitsize))])
return newseq
Se as listas são do mesmo tamanho, você pode combiná-los em listas de 4-tuplas com zip()
. Por exemplo:
# Four lists of four elements each.
l1 = range(0, 4)
l2 = range(4, 8)
l3 = range(8, 12)
l4 = range(12, 16)
for i1, i2, i3, i4 in zip(l1, l2, l3, l4):
...
Aqui está o que a função zip()
produz:
>>> print l1
[0, 1, 2, 3]
>>> print l2
[4, 5, 6, 7]
>>> print l3
[8, 9, 10, 11]
>>> print l4
[12, 13, 14, 15]
>>> print zip(l1, l2, l3, l4)
[(0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15)]
Se as listas são grandes, e você não quer combiná-los em uma lista maior, uso itertools.izip()
, que produz um iterador, em vez de uma lista.
from itertools import izip
for i1, i2, i3, i4 in izip(l1, l2, l3, l4):
...
One-liner, solução ad hoc para iterar sobre uma lista x
em pedaços de tamanho 4
-
for a, b, c, d in zip(x[0::4], x[1::4], x[2::4], x[3::4]):
... do something with a, b, c and d ...
Em primeiro lugar, eu projetei-lo para cordas divididas em substrings para analisar string contendo hex.
Hoje eu transformou-o em complexo, mas ainda gerador simples.
def chunker(iterable, size, reductor, condition):
it = iter(iterable)
def chunk_generator():
return (next(it) for _ in range(size))
chunk = reductor(chunk_generator())
while condition(chunk):
yield chunk
chunk = reductor(chunk_generator())
Argumentos:
óbvios
-
iterable
é qualquer iteráveis ??/ iteração / gerador containg / gerador / iteração sobre os dados de entrada, -
size
é, naturalmente, o tamanho do pedaço que você quer obter,
Mais interessante
-
reductor
é um exigível, que recebe a iteração gerador sobre o conteúdo do pedaço.
Eu esperaria que ele retorne seqüência ou string, mas eu não exigem isso.Você pode passar como este argumento, por exemplo
list
,tuple
,set
,frozenset
,
ou mais extravagante nada. Eu passaria esta função, retornar string
(Desde que contémiterable
/ gera itera sobre cordas /):def concatenate(iterable): return ''.join(iterable)
Note que
reductor
pode causar fechamento gerador elevando exceção. -
condition
é um exigível que recebe tudo o quereductor
retornado.
Ele decide aprovar e deu-lo (por retornando nada avaliando aTrue
),
ou recusá-la e trabalho de gerador de acabamento (retornando outra coisa ou levantar exceção).Quando o número de elementos em
iterable
não é divisível porsize
, quandoit
fica exausto,reductor
receberá gerador gerando menos elementos do quesize
.
Vamos chamar esses elementos dura elementos .Convidei duas funções para passar como este argumento:
-
lambda x:x
-. dura elementos serão cedidos -
lambda x: len(x)==<size>
- a dura elementos será rejeitada
. substituir<size>
usando número igual aosize
-