Iterador de lista circular em Python

99

Preciso iterar em uma lista circular, possivelmente muitas vezes, sempre começando com o último item visitado.

O caso de uso é um pool de conexão. Um cliente pede uma conexão, um iterador verifica se a conexão apontada está disponível e a retorna, caso contrário, faz um loop até encontrar uma disponível.

Existe uma maneira legal de fazer isso em Python?

user443854
fonte

Respostas:

159

Use itertools.cycle, esse é o seu propósito exato:

from itertools import cycle

lst = ['a', 'b', 'c']

pool = cycle(lst)

for item in pool:
    print item,

Resultado:

a b c a b c ...

(Loops para sempre, obviamente)


Para avançar manualmente o iterador e extrair valores dele um por um, basta chamar next(pool):

>>> next(pool)
'a'
>>> next(pool)
'b'
Lukas Graf
fonte
1
Você está imprimindo itens em um loop. O que eu quero sair do loop e voltar depois? (Quero começar de onde parei).
user443854
7
@ user443854 use pool.next()para obter o próximo item único do ciclo
Jacob Krall
4
@ user443854 FWIW, esta é uma resposta muito melhor do que a minha. Não há razão para reimplementar as funções da biblioteca!
Jacob Krall
5
pool.next () não funcionou para mim, apenas próximo (pool). Provavelmente por causa do Python 3?
fjsj de
6
@fjsj isso é correto, no Python 3 você precisa usar next(iterator)(que BTW também funciona bem no Python 2.x e, portanto, é a forma canônica que deve ser usada). Consulte O generator.next () é visível no python 3.0? para uma explicação mais aprofundada. Atualizei minha resposta em conformidade.
Lukas Graf de
54

A resposta correta é usar itertools.cycle . Mas, vamos supor que a função de biblioteca não exista. Como você o implementaria?

Use um gerador :

def circular():
    while True:
        for connection in ['a', 'b', 'c']:
            yield connection

Em seguida, você pode usar uma forinstrução para iterar infinitamente ou chamar next()para obter o próximo valor único do iterador gerador:

connections = circular()
next(connections) # 'a'
next(connections) # 'b'
next(connections) # 'c'
next(connections) # 'a'
next(connections) # 'b'
next(connections) # 'c'
next(connections) # 'a'
#....
Jacob Krall
fonte
Agradável! Como ele sabe começar de novo quando a lista se esgota?
user443854
1
@ user443854 o while Truemeio de repetir para sempre
Jacob Krall
2
@juanchopanza: Sim; itertools.cycleé uma resposta melhor. Isso mostra como você pode escrever a mesma funcionalidade se itertoolsnão estiver disponível :)
Jacob Krall
O gerador simples também salva uma cópia de cada elemento como itertools.cyclefaz? Ou o gerador simples teria um design mais eficiente em termos de memória? De acordo com os cycledocumentos :Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).
dthor
2
@dthor este gerador cria uma lista com três elementos e alfabetiza sobre ela, então destrói a lista e cria uma nova, para sempre. Essa documentação para cycleimplica que o iterável de entrada é convertido para listantes de seu gerador iniciar, uma vez que iterablesó é "bom para uma passagem sobre o conjunto de valores".
Jacob Krall
9

Ou você pode fazer assim:

conn = ['a', 'b', 'c', 'd', 'e', 'f']
conn_len = len(conn)
index = 0
while True:
    print(conn[index])
    index = (index + 1) % conn_len

imprime abcdefab c ... para sempre

viky.pat
fonte
3

você pode fazer isso com o append(pop())loop:

l = ['a','b','c','d']
while 1:
    print l[0]
    l.append(l.pop(0))

ou for i in range()loop:

l = ['a','b','c','d']
ll = len(l)
while 1:
    for i in range(ll):
       print l[i]

ou simplesmente:

l = ['a','b','c','d']

while 1:
    for i in l:
       print i

todas impressas:

>>>
a
b
c
d
a
b
c
d
...etc.

dos três, estaria propenso à abordagem append (pop ()) como uma função

servers = ['a','b','c','d']

def rotate_servers(servers):
    servers.append(servers.pop(0))
    return servers

while 1:
    servers = rotate_servers(servers)
    print servers[0]
litepresença
fonte
Votar positivamente nisso porque me ajudou em um caso de uso completamente diferente, em que eu simplesmente quero iterar em uma lista várias vezes, cada vez com o elemento inicial avançando uma etapa. Meu caso de uso é iterar os jogadores em um jogo de pôquer, fazendo com que o disco do dealer avance um jogador a cada rodada.
Johan
2

Você precisa de um iterador personalizado - adaptarei o iterador a partir desta resposta .

from itertools import cycle

class ConnectionPool():
    def __init__(self, ...):
        # whatever is appropriate here to initilize
        # your data
        self.pool = cycle([blah, blah, etc])
    def __iter__(self):
        return self
    def __next__(self):
        for connection in self.pool:
            if connection.is_available:  # or however you spell it
                return connection
Ethan Furman
fonte
2

Se você deseja ciclos de ntempos, implemente a ncycles receita de itertools :

from itertools import chain, repeat


def ncycles(iterable, n):
    "Returns the sequence elements n times"
    return chain.from_iterable(repeat(tuple(iterable), n))


list(ncycles(["a", "b", "c"], 3))
# ['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c']
pilang
fonte