Filas em Python com deque (queue)
Filas em Python podem ser implementadas utilizando a estrutura de
dados deque
(do módulo
collections) para garantirmos complexidade de tempo constante para
inserção e remoção de elementos em
qualquer uma das pontas, cabeça (head) ou cauda (tail).
Deque – abreviação de
Double Ended Queue (ou Fila de duas pontas) – pode
ser usada para implementar filas FIFO (que veremos a seguir) e
pilhas LIFO.
O que são filas?
Quando falamos em filas, estamos tratando de uma estrutura de dados abstrata que se comporta exatamente como uma fila na vida real. Elementos são adicionados à cauda (tail) e removidos da frente (head).
Porém, remover elementos da frente (com índice 0) pode ser problemático do ponto de vista de desempenho do programa. Por exemplo, poderíamos usar listas para criar filas (como fizemos com as pilhas), porém elas não são otimizadas para isso e acabam movendo os índices de todos os elementos posteriores ao elemento removido. Infelizmente, isso nos dá uma complexidade de tempo linear O(n), ou seja, ao remover um item do começo da lista, o interpretador precisa navegar em todos os seus elementos, reajustando os índices dos elementos restantes.
No entanto, isso não ocorre com deque
. Segundo
a
documentação oficial do Python, deque
adiciona e remove em ambas as
direções com complexidade de tempo de
aproximadamente O(1). Isso quer dizer que requer aproximadamente
uma operação para a adição e
remoção de elementos.
De forma resumida, deque
é mais rápido
para inserir e remover elementos da frente da fila (no
índice 0).
Filas são FIFO
Na programação, o comportamento de uma fila é conhecido como FIFO (First In First Out), ou seja, o primeiro que entrar será o primeiro a sair. Isso é exatamente o que acontece em uma fila da vida real, a primeira pessoa a entrar na fila, será a primeira a ser atendida. É claro, se ninguém furar a fila!
Imagine uma fila de letras: A, B, C.
Agora, suponha que eu quero adicionar um novo elemento, o D. Para as filas, esse elemento deve ser adicionado na cauda, como mostra o diagrama a seguir:
Mas, se eu precisasse remover um elemento da fila anterior, o elemento removido deveria sair na cabeça (head).
Este elemento é representado pelo A na imagem a seguir:
Além disso, perceba que agora, além da minha fila
não ter mais o elemento A, os índices foram
reorganizados. B passou a ter o índice 0, C, índice
1 e D, índice 2. Lembre-se que deque
é
otimizada para isso.
Outra informação que você deve ter percebido, é que a cabeça (head) e a cauda (tail) da minha fila também mudaram. B é a cabeça e D a cauda.
Filas em Python com deque
Se fossemos representar a fila apresentada nas imagens anteriores com os elementos A, B e C, em seguida acrescentar o elemento D, o código ficaria da seguinte maneira:
from collections import deque fila = deque(['A', 'B', 'C']) # Inserindo o elemento D fila.append('D') # deque(['A', 'B', 'C', 'D']) print(fila)
Perceba que deque
tem quase os mesmos métodos
de uma lista normal no Python (por isso podemos usá-las
como
pilhas
se quisermos). Então, para inserir um novo elemento na
cauda da fila, basta usarmos o método append
.
Para a remoção do elemento A (como fizemos na
seção anterior desse post), poderíamos
utilizar o método popleft
(remover da
esquerda).
from collections import deque fila = deque(['A', 'B', 'C']) # Inserindo o elemento D fila.append('D') # Removendo A fila.popleft() # deque(['B', 'C', 'D']) print(fila)
É bem simples, não é mesmo?
popleft retorna o elemento removido
Um comportamento padrão sempre que usamos métodos
“pop
” (de remoção) é
que o elemento removido é retornado por essa
função. Isso não é diferente como
popleft
.
Por exemplo, se eu estou removendo um elemento da minha fila, é natural querer usá-lo em algum momento do meu código. Então, eu poderia usar uma variável para isso.
from collections import deque fila = deque(['A', 'B', 'C']) # Inserindo o elemento D fila.append('D') # Removendo A e usando seu valor head = fila.popleft() # A print(head) # deque(['B', 'C', 'D']) print(fila)
Perceba que no código acima (linha 9),
popleft
faz duas coisas:
- Remove o primeiro elemento da fila (cabeça);
-
Retorna o valor do elemento removido para a variável
head
(A).
Assim, é possível utilizar o valor removido conforme preferir no seu código.
Criando uma filas em Python
Assim como fizemos com as pilhas (no
artigo anterior), podemos encapsular deque
em uma classe para
manter o controle sobre os métodos que deverão ser
utilizados para manter o comportamento FIFO das
filas. Afinal, deque
não tem apenas os
métodos append
e popleft
. De
fato, se você conferir a
documentação oficial, vai ver que existem vários outros métodos
possíveis para deque
, incluindo os que quebram
o princípio FIFO, removendo elementos da
cauda ou inserindo na cabeça, por exemplo.
from typing import Deque, Any from collections import deque class Queue: """Uma classe representando uma fila""" def __init__(self, maxlen=None) -> None: # Deque permite enviar maxlen # para criar um tamanho máximo para # a fila self.__items: Deque[Any] = deque(maxlen=maxlen) def enqueue(self, *items: Any) -> None: """Enqueue (enfileirar) é o mesmo que append""" for item in items: self.__items.append(item) def dequeue(self) -> Any: """Dequeue (desenfileirar) é o mesmo que popleft""" if not self: raise IndexError('pop from empty queue') return self.__items.popleft() def __repr__(self) -> str: return str(self.__items) def __bool__(self) -> bool: return bool(self.__items) def __len__(self) -> int: return len(self.__items)
Na classe anterior, criei apenas os métodos que precisava
para minha fila. Nesse sentido, também mudei o nome de
algumas coisas: append
passou a ser
enqueue
(enfileirar); popleft
passou a
ser dequeue
(desenfileirar). Contudo, eles continuam
fazendo o mesmo trabalho.
Também permiti adicionar o argumento
“maxlen
” de deque
, para
fixar um tamanho máximo para nossa fila.
Além disso, nós precisamos saber de três coisas muito importantes:
-
Quais valores estão na fila? Por isso o método
__repr__
; -
Existem valores na minha fila? Por isso o método
__bool__
; -
Quantos valores tem na minha fila? Por isso o método
__len__
.
Você pode enfeitar essa classe com os métodos que preferir. Aqui eu quis manter as coisas o mais simples que foi possível.
Agora podemos usar a classe:
if __name__ == "__main__": # Instanciando fila = Queue() # Enfileirando A, B e C fila.enqueue('A', 'B', 'C') # Enfileirando D fila.enqueue('D') # Recuperando A head = fila.dequeue() print(head) # Mostrando a fila print(fila) """ Resultado: A deque(['B', 'C', 'D']) """
Indo além do básico
Em algum momento, você pode querer iterar sobre a sua fila
ou então obter um item diretamente. Pra isso podemos
repassar o método __iter__
da nossa classe
para o método __iter__
do
deque
da mesma (__items
). Veja a
implementação do método
__iter__
por si só.
Este método deve estar dentro da classe, é claro (não se preocupe, vou mostrar o código completo logo abaixo).
def __iter__(self) -> Iterator: return self.__items.__iter__()
Por fim, se você quiser permitir que um item da fila seja obtido pelo seu índice, também pode implementar o método __getitem__, dessa maneira:
def __getitem__(self, index: int) -> Any: return self.__items[index]
Veja como ficou nossa classe completa:
from typing import Deque, Any, Iterator from collections import deque class Queue: """Uma classe representando uma fila""" def __init__(self, maxlen=None) -> None: # Deque permite enviar maxlen # para criar um tamanho máximo para # a fila self.__items: Deque[Any] = deque(maxlen=maxlen) def enqueue(self, *items: Any) -> None: """Enqueue (enfileirar) é o mesmo que append""" for item in items: self.__items.append(item) def dequeue(self) -> Any: """Dequeue (desenfileirar) é o mesmo que popleft""" if not self: raise IndexError('pop from empty queue') return self.__items.popleft() def __repr__(self) -> str: return str(self.__items) def __bool__(self) -> bool: return bool(self.__items) def __len__(self) -> int: return len(self.__items) def __iter__(self) -> Iterator: return self.__items.__iter__() def __getitem__(self, index: int) -> Any: return self.__items[index]
Perceba que agora temos os métodos __iter__ e __getitem__. Isso nos permite iterar sobre nossa fila e também obter itens por índice, veja:
if __name__ == "__main__": # Instanciando fila = Queue() # Enfileirando A, B, C e D fila.enqueue('A', 'B', 'C', 'D') # Obtendo o elemento com índice 1 (B) print('Item com índice 1:', fila[1], end='\n\n') # Iterando com for em nossa fila for item in fila: print('Iteração:', item) """ Resultado: Item com índice 1: B Iteração: A Iteração: B Iteração: C Iteração: D """
Nossa classe ficou assim, simples mas útil e eficaz. Todavia, você pode adicionar os métodos que preferir. Apenas adicionei métodos que acho que podem ser úteis.
Resumo
Em conclusão, você viu o seguinte ao decorrer desse post:
-
Filas em Python podem ser implementadas utilizando
deque
; - Filas têm o comportamento FIFO (First In First Out), assim como na vida real;
append
adiciona elementos na cauda (tail);-
popleft
remove elementos da cabeça (head); -
deque
é muito genérica, por isso criamos nossa própria classe para permitir apenas os métodos que queremos.
Como resultado, agora você já sabe criar filas em
Python. Além disso, também sabe usar
deque
, que é outra estrutura de dados abstrata
conhecida em programação.
Te vejo no próximo post!