Como usar uma fila de prioridades no Python
Filas, Pilhas e Filas de Prioridade em Python
Python oferece alguns flavors embutidos de filas que você verá em ação neste tutorial. Você também terá uma breve introdução sobre a teoria das filas e seus tipos. Por fim, você vai dar uma olhada em algumas bibliotecas externas para se conectar a corretores de mensagens populares disponíveis nas principais provedoras de plataformas em nuvem.
Neste tutorial, você aprenderá a:
- Diferenciar entre vários tipos de filas
- Implementar o tipo de dados fila em Python
- Resolver problemas práticos aplicando a fila correta
- Usar as filas à prova de thread, assíncronas e interprocessos do Python
- Integrar o Python a corretores de filas de mensagens distribuídas por meio de bibliotecas
Aprendendo sobre os tipos de filas
As filas podem ser classificadas em vários tipos, cada uma com suas próprias características e aplicações. Nesta seção, discutiremos os principais tipos de filas:
Fila: Primeiro a entrar, primeiro a sair (FIFO)
A fila FIFO é um tipo básico de fila em que os elementos são inseridos no final da fila e retirados do início da fila. É como uma fila em que a primeira pessoa a entrar é a primeira a sair. A fila FIFO é adequada para situações em que a ordem de chegada é importante e onde precisamos processar os elementos na mesma ordem em que eles chegaram.
Pilha: Último a entrar, primeiro a sair (LIFO)
A pilha LIFO (Last-In, First-Out) é um tipo de fila em que os elementos são inseridos e retirados do mesmo lado. É como uma pilha de pratos, onde o último prato colocado é o primeiro a ser retirado. A pilha LIFO é adequada para situações em que queremos processar os elementos na ordem inversa à ordem de chegada.
Deque: Fila com duas pontas
Um deque (double-ended queue) é uma fila com duas pontas, o que significa que podemos inserir e retirar elementos de ambos os lados da fila. Ele combina as características da fila FIFO e da pilha LIFO, oferecendo flexibilidade na manipulação dos elementos.
Fila de Prioridade: Ordenada do maior para o menor
Uma fila de prioridade é uma fila em que cada elemento tem uma prioridade associada a ele. Os elementos são organizados na fila de acordo com suas prioridades, que podem ser números, strings ou qualquer outro critério de comparação. Quando um elemento é adicionado à fila de prioridade, ele é colocado em uma posição apropriada com base em sua prioridade. Os elementos com prioridade mais alta são colocados na frente da fila e são os primeiros a serem retirados.
Implementando filas em Python
Em Python, existem várias maneiras de implementar filas. Nesta seção, você aprenderá como implementar filas FIFO, LIFO e de prioridade em Python.
Representando filas FIFO e LIFO com um deque
O módulo collections
do Python fornece a classe deque
, que é uma implementação de fila duplamente terminada que pode ser usada para representar tanto filas FIFO quanto filas LIFO. Com um deque
, você pode inserir elementos no final da fila usando o método append()
e remover elementos do início da fila usando o método popleft()
.
Aqui está um exemplo de como implementar uma fila FIFO usando um deque
:
E aqui está um exemplo de como implementar uma pilha LIFO usando um deque
:
Construindo um tipo de dados fila
Se você precisa de uma implementação personalizada de uma fila, pode criar seu próprio tipo de dados fila em Python. Uma maneira de fazer isso é usar uma lista como base para o tipo de dados fila e adicionar métodos para inserção e remoção de elementos.
Aqui está um exemplo de como criar um tipo de dados fila em Python:
Você pode usar esse tipo de dados fila da seguinte maneira:
Representando filas de prioridade com um heap
Uma fila de prioridade pode ser implementada em Python usando um heap. Um heap é uma estrutura de dados que mantém os elementos em uma determinada ordem, permitindo que os elementos de maior prioridade sejam acessados mais rapidamente. O módulo heapq
do Python fornece a função heappush()
para inserir elementos em um heap e a função heappop()
para remover e retornar o elemento de maior prioridade.
Aqui está um exemplo de como implementar uma fila de prioridade usando um heap:
Construindo um tipo de dados fila de prioridade
Se você precisa de uma implementação personalizada de uma fila de prioridade, pode criar seu próprio tipo de dados fila em Python. Você pode combinar um heap com uma lista para armazenar os elementos da fila de prioridade e adicionar métodos para inserção e remoção de elementos.
Aqui está um exemplo de como criar um tipo de dados fila de prioridade em Python:
Você pode usar esse tipo de dados fila de prioridade da seguinte maneira:
Lidando com casos especiais na sua fila de prioridade
Ao implementar uma fila de prioridade personalizada, é importante lidar com os casos especiais para garantir que a fila funcione corretamente. Alguns casos especiais incluem:
- Tratar um empate na prioridade
- Lidar com a remoção de um elemento específico da fila
- Verificar se a fila está vazia
Aqui está um exemplo de como lidar com esses casos especiais em um tipo de dados fila de prioridade:
Neste exemplo, adicionamos os métodos remover_elemento()
e esta_vazia()
para lidar com a remoção de um elemento específico da fila e verificar se a fila está vazia.
Refatorando o código usando uma classe mixin
Se você estiver implementando várias filas em seu código, pode ser útil criar uma classe mixin para evitar a duplicação de código. Uma classe mixin é uma classe que fornece métodos que podem ser usados por outras classes, permitindo que você compartilhe comportamentos comuns entre várias classes.
Aqui está um exemplo de como refatorar o código das filas FIFO, LIFO e de prioridade usando uma classe mixin:
Neste exemplo, criamos a classe mixin MixinFila
que fornece os métodos comuns às filas FIFO, LIFO e de prioridade. Em seguida, definimos subclasses para cada tipo de fila, onde implementamos o método remover
especificamente para cada tipo de fila. Dessa forma, você pode reutilizar o código comum e manter a implementação de cada fila separada.
Usando filas na prática
Agora que você aprendeu como implementar filas em Python, é hora de aplicar esse conhecimento em cenários práticos. Nesta seção, você encontrará exemplos de como usar filas em problemas reais.
Dados de exemplo: Mapa Rodoviário do Reino Unido
Para ilustrar o uso de filas na prática, vamos considerar um problema que envolve encontrar o caminho mais curto em um mapa rodoviário. Suponha que temos os seguintes dados de exemplo:
Esses dados de exemplo representam um mapa rodoviário fictício do Reino Unido, onde as chaves do dicionário cidades
são as cidades e os valores são as cidades vizinhas. O dicionário distancias
armazena as distâncias entre as cidades vizinhas.
Representação de objeto das Cidades e Rodovias
Uma maneira de representar os dados de exemplo acima em Python é usando objetos da seguinte maneira:
Neste exemplo, criamos a classe Cidade
para representar uma cidade no mapa rodoviário. A classe Cidade
tem um atributo nome
para representar o nome da cidade e uma lista vizinhos
para armazenar as cidades vizinhas e suas respectivas distâncias. Usamos o dicionário cidades
para mapear os nomes das cidades aos objetos Cidade
correspondentes. Em seguida, percorremos o dicionário distancias
e adicionamos cada cidade vizinha às listas vizinhos
dos objetos Cidade
adequados.
Busca em largura usando uma fila FIFO
A busca em largura é um algoritmo de busca que explora todos os nós vizinhos de um nó antes de explorar os nós mais distantes. Com uma fila FIFO, podemos implementar a busca em largura de forma elegante.
Aqui está um exemplo de como usar uma fila FIFO para encontrar o menor caminho entre duas cidades no mapa rodoviário:
Neste exemplo, usamos uma fila FIFO do módulo collections
para armazenar as cidades a serem visitadas. Começamos inserindo a cidade_origem
na fila. Em seguida, usamos um conjunto visitados
para rastrear as cidades que já foram visitadas. Dentro do loop while
, removemos a cidade atual da fila e verificamos se é a cidade_destino
. Se for, retornamos True
. Caso contrário, adicionamos todos os vizinhos da cidade atual à fila, desde que eles não tenham sido visitados antes.
Caminho mais curto usando a travessia em largura
Usando a busca em largura, podemos encontrar o caminho mais curto entre duas cidades no mapa rodoviário. Para isso, precisamos rastrear o caminho percorrido durante a busca em largura.
Aqui está um exemplo de como encontrar o caminho mais curto entre duas cidades usando a busca em largura:
Neste exemplo, usamos uma fila FIFO para armazenar os caminhos percorridos durante a busca em largura. Começamos inserindo [cidades[cidade_origem]]
na fila, onde cidades[cidade_origem]
é uma lista com um único elemento contendo a cidade_origem
. Dentro do loop while
, removemos o caminho atual da fila e obtemos a cidade atual (o último elemento do caminho). Se a cidade atual for a cidade_destino
, retornamos o caminho completo. Caso contrário, adicionamos todos os vizinhos da cidade atual ao caminho atual, desde que eles ainda não façam parte do caminho.
Busca em profundidade usando uma pilha LIFO
A busca em profundidade é um algoritmo de busca que explora os ramos de um nó até que alcance um nó folha. Com uma pilha LIFO, podemos implementar a busca em profundidade de forma simples.
Aqui está um exemplo de como usar uma pilha LIFO para realizar uma busca em profundidade no mapa rodoviário:
Neste exemplo, usamos uma pilha LIFO (uma lista) para armazenar as cidades a serem visitadas. Começamos inserindo a cidade_origem
na pilha. Em seguida, usamos um conjunto visitados
para rastrear as cidades que já foram visitadas. Dentro do loop while
, removemos a cidade atual da pilha e verificamos se é a cidade_destino
. Se for, retornamos True
. Caso contrário, adicionamos todos os vizinhos da cidade atual à pilha, desde que eles não tenham sido visitados antes.
Algoritmo de Dijkstra usando uma fila de prioridade
O algoritmo de Dijkstra é um algoritmo de busca de caminho mais curto que encontra o caminho mais curto entre um nó de origem e todos os outros nós em um gráfico ponderado e direcionado. Podemos usar uma fila de prioridade para implementar o algoritmo de Dijkstra de forma eficiente.
Aqui está um exemplo de como usar uma fila de prioridade para encontrar o caminho mais curto entre duas cidades usando o algoritmo de Dijkstra:
Neste exemplo, usamos uma fila de prioridade do módulo heapq
para armazenar os caminhos percorridos durante o algoritmo de Dijkstra. Começamos inserindo (0, cidades[cidade_origem], [cidades[cidade_origem]])
na fila de prioridade, onde cidades[cidade_origem]
é uma lista com um único elemento contendo a cidade_origem
. Em seguida, inicializamos o dicionário distancias
com a distância de cada cidade em relação à cidade_origem
definida como math.inf
(infinito), exceto para a cidade_origem
em que definimos a distância como 0. Dentro do loop while
, removemos o caminho atual da fila de prioridade e obtemos a cidade atual. Se a cidade atual for a cidade_destino
, retornamos o caminho completo. Caso contrário, atualizamos as distâncias para cada vizinho da cidade atual caso sua distância acumulada seja menor do que a distância conhecida anteriormente. Em seguida, adicionamos os caminhos atualizados à fila de prioridade.
Usando filas seguras para threads
Em ambientes concorrentes com várias threads, é importante garantir que as filas sejam seguras e evitem condições de corrida. O módulo queue
do Python fornece três classes de filas seguras para threads: queue.Queue
, queue.LifoQueue
e queue.PriorityQueue
.
Aqui está um exemplo de como usar essas filas seguras para threads:
Neste exemplo, criamos uma fila segura para threads usando queue.Queue
. Em seguida, definimos uma função de trabalhador que retira itens da fila e realiza alguma tarefa com eles. Usamos o método fila.get()
para retirar um item da fila e fila.task_done()
para indicar que a tarefa com o item foi concluída. Em seguida, inicializamos três threads para executar a função de trabalhador e adicionamos dez itens à fila usando o método fila.put()
. Por fim, usamos o método fila.join()
para esperar até que todos os itens tenham sido processados.
As filas queue.LifoQueue
e queue.PriorityQueue
Além da classe queue.Queue
, o módulo queue
do Python também fornece as classes queue.LifoQueue
e queue.PriorityQueue
. A queue.LifoQueue
é uma fila segura para threads que implementa uma pilha LIFO (último a entrar, primeiro a sair). A queue.PriorityQueue
é uma fila segura para threads que implementa uma fila de prioridade.
Aqui está um exemplo de como usar as filas queue.LifoQueue
e queue.PriorityQueue
:
Neste exemplo, criamos duas filas seguras para threads usando queue.LifoQueue
e queue.PriorityQueue
. Em seguida, definimos duas funções de trabalhador que retiram itens das filas e realizam alguma tarefa com eles. Usamos o método fila.get()
para retirar um item da fila e fila.task_done()
para indicar que a tarefa com o item foi concluída. Em seguida, inicializamos duas threads para executar as funções de trabalhador e adicionamos dez itens às filas usando os métodos fila_lifo.put()
e fila_priority.put()
. Por fim, usamos os métodos fila_lifo.join()
e fila_priority.join()
para esperar até que todos os itens tenham sido processados.
Usando filas assíncronas
Em programas assíncronos, é necessário usar filas que sejam compatíveis com o modelo assíncrono. O módulo asyncio
do Python fornece três classes de filas assíncronas: asyncio.Queue
, asyncio.LifoQueue
e asyncio.PriorityQueue
.
Aqui está um exemplo de como usar essas filas assíncronas:
Neste exemplo, criamos uma fila assíncrona usando asyncio.Queue
. Em seguida, definimos uma função de worker assíncrona que retira itens da fila e realiza alguma tarefa com eles. Usamos o método await queue.get()
para retirar um item da fila e queue.task_done()
para indicar que a tarefa com o item foi concluída. Em seguida, iniciamos a tarefa do worker usando asyncio.create_task()
e adicionamos dez itens à fila usando o método await queue.put()
. Por fim, usamos await queue.join()
para esperar até que todos os itens tenham sido processados.
As filas asyncio.LifoQueue
e asyncio.PriorityQueue
Além da classe asyncio.Queue
, o módulo asyncio
do Python também fornece as classes asyncio.LifoQueue
e asyncio.PriorityQueue
. A asyncio.LifoQueue
é uma fila assíncrona que implementa uma pilha LIFO (último a entrar, primeiro a sair) e a asyncio.PriorityQueue
é uma fila assíncrona que implementa uma fila de prioridade.
Aqui está um exemplo de como usar as filas asyncio.LifoQueue
e asyncio.PriorityQueue
:
Neste exemplo, criamos duas filas assíncronas usando asyncio.LifoQueue
e asyncio.PriorityQueue
. Em seguida, definimos duas funções de worker assíncronas que retiram itens das filas e realizam alguma tarefa com eles. Usamos o método await queue.get()
para retirar um item da fila e queue.task_done()
para indicar que a tarefa com o item foi concluída. Em seguida, iniciamos as tarefas dos workers usando asyncio.create_task()
e adicionamos dez itens às filas usando os métodos await queue_lifo.put()
e await queue_priority.put()
. Por fim, usamos await queue_lifo.join()
e await queue_priority.join()
para esperar até que todos os itens tenham sido processados.
Usando multiprocessing.Queue
para comunicação entre processos
Em programas que executam em processos separados, é necessário usar filas que sejam compatíveis com a comunicação entre processos. O módulo multiprocessing
do Python fornece a classe multiprocessing.Queue
para comunicação segura entre processos usando filas.
Aqui está um exemplo de como usar multiprocessing.Queue
para comunicação entre processos:
Neste exemplo, criamos uma fila segura para processos usando multiprocessing.Queue
. Em seguida, definimos uma função de worker que retira itens da fila e realiza alguma tarefa com eles. Usamos o método queue.get()
para retirar um item da fila e queue.task_done()
para indicar que a tarefa com o item foi concluída. Em seguida, inicializamos um processo para executar a função de worker usando multiprocessing.Process
e adicionamos dez itens à fila usando o método queue.put()
. Por fim, usamos queue.join()
para esperar até que todos os itens tenham sido processados e process.terminate()
para encerrar o processo.
Integrando Python com Corretores de Filas de Mensagens Distribuídas
Python pode ser integrado com corretores de filas de mensagens distribuídas para facilitar a comunicação entre sistemas distribuídos. Alguns populares corretores de filas de mensagens distribuídas e suas bibliotecas Python correspondentes incluem:
- RabbitMQ:
pika
- Redis:
redis
- Apache Kafka:
kafka-python3
Aqui está um exemplo de como usar a biblioteca pika
para integrar Python com o RabbitMQ:
Neste exemplo, usamos a biblioteca pika
para estabelecer uma conexão com o RabbitMQ. Em seguida, declaramos uma fila chamada 'fila'
usando o método canal.queue_declare()
. Usamos o método canal.basic_consume()
para consumir mensages da fila e fornecemos uma função de callback
que será chamada sempre que uma mensagem for recebida. Por fim, usamos o método canal.start_consuming()
para iniciar o consumo de mensagens.
Conclusão
Neste tutorial, você aprendeu sobre os diferentes tipos de filas em Python e como implementá-los usando as bibliotecas embutidas, como collections
e heapq
. Você também viu como usar filas em cenários práticos, como encontrar o caminho mais curto em um mapa rodoviário e realizar a comunicação entre threads, processos e sistemas distribuídos. Espero que este tutorial tenha fornecido uma visão aprofundada sobre como usar filas em Python e como elas podem ser aplicadas em diversos contextos.