Классы LifoQueue и PriorityQueue модуля queue

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

На прошлом занятии мы с вами подробно разобрали работу класса queue.Queue, реализующий потокобезопасную очередь типа FIFO. Продолжим эту тему и рассмотрим еще два аналогичных класса:

  • queue.LifoQueue – потокобезопасная реализация очереди типа LIFO;
  • queue.PriorityQueue – потокобезопасная реализация очереди с приоритетами.

Очередь queue.LifoQueue типа LIFO

Класс LifoQueue работает по аналогии с классом Queue с тем же набором методов, отличаясь лишь порядком извлечения элементов: они выбираются в обратном порядке, то есть, последний добавленный удаляется первым. Простая визуализация этого типа очереди демонстрирует стопка тарелок, когда новая кладется сверху и берется тоже сверху:

Это принцип добавления/удаления элементов также известен под названием стек (stack).

Давайте для примера реализуем стек с помощью класса LifoQueue. Вначале нужно создать объект этой очереди:

import queue
 
stack = queue.LifoQueue() # LIFO без ограничений длины

Затем, добавим несколько элементов в этот стек так, как это мы делали на прошлом занятии:

data = ['one', 2, 'three', 4, 5, 6]
 
for d in data:
    try:
        stack.put(d, block=False) # в многопоточных приложениях block=True
    except queue.Full as e:
        print("Full: " + str(e))
        break
 
print("Число элементов в очереди:", stack.qsize())

И извлечем добавленные элементы:

while not stack.empty():
    try:
        d = stack.get(block=False) # в многопоточных приложениях block=True
        print(d)
    except queue.Empty as e:
        print("Empty: " + str(e))
        break

Обратите внимание, что данные извлекаются в обратном порядке: сначала значение 6, потом 5 и последним ‘one’.

Очередь queue.PriorityQueue

Рассматриваемые до сих пор очереди имели жесткий приоритет (порядок) добавления/извлечения элементов. Однако на практике существуют задачи, когда элементы следует извлекать согласно их приоритетам. Причем, чем ниже значение, указанное в приоритете, тем раньше элемент должен быть извлечен из очереди.

Примерами классических задач, где имеет смысл использовать очереди с приоритетами, являются следующие:

  • Планировщик заданий с приоритетами, где каждому заданию назначается определенный уровень важности (приоритет). Необходимо обеспечить выполнение наиболее важных задач первыми.
  • Обработка заказов в службе доставки. Высокоприоритетные заказы должны доставляться быстрее низкоприоритетных.
  • Графический интерфейс с отложенными событиями. Здесь разные события имеют разную важность. Например, обновление интерфейса должно происходить чаще, чем обработка фоновых процессов.
  • Оптимизация серверных ресурсов. Допустим, сервер обрабатывает запросы пользователей. Запросы делятся на две категории: критические и обычные. Важно сначала обработать критически важные запросы.

Работа с очередью PriorityQueue выполняется так же, как и с двумя другими классами: Queue и LifoQueue. Единственное ключевое отличие, что метод put() ожидает объект, который можно сравнивать на меньше/больше. В самом простом варианте можно использовать кортеж в формате:

(приоритет, данные)

Например, создадим очередь со следующими данными:

import queue
 
tasks = queue.PriorityQueue()
 
lst_t = [(1, 'варим пельмешки'), (7, 'программируем'), (2, 'едим пельмешки'),
         (2, 'пьем кофе'), (4, 'принимаем лабы'), (3, 'читаем лекции')]
 
for d in lst_t:
    tasks.put(d)
 
print("Число элементов в очереди:", tasks.qsize())

Каждый элемент (задача) имеет свой числовой приоритет, причем приоритеты могут совпадать. В этом случае они будут извлекаться по порядку их записи в очередь.

Извлечем элементы из этой очереди и посмотрим на порядок их следования:

while not tasks.empty():
    try:
        d = tasks.get(block=False) # в многопоточных приложениях block=True
        print(d)
    except queue.Empty as e:
        print("Empty: " + str(e))
        break

Увидим, что элементы с наименьшим значением приоритета выбираются в первую очередь:

Число элементов в очереди: 6
(1, 'варим пельмешки')
(2, 'едим пельмешки')
(2, 'пьем кофе')
(3, 'читаем лекции')
(4, 'принимаем лабы')
(7, 'программируем')

Пользовательский класс для объектов очереди queue.PriorityQueue

Конечно, кортеж удобен для простых реализаций элементов очереди с приоритетом. Однако на практике не редко для этих целей объявляют свой собственный класс, в котором определяют операции сравнения: меньше, больше. Например, это можно сделать так:

class Task:
    def __init__(self, data, priority):
        self.data = data
        self.priority = priority
 
    def __lt__(self, other):
        return self.priority < other.priority
 
    def __repr__(self):
        return f"Task: {self.data}, {self.priority}"

Обратите внимание, в этом классе определен магический метод __lt__, который вызывается для операции сравнения на меньше. Соответственно, он возвращает булево значение при сравнении соответствующих приоритетов двух объектов класса Task. Например:

a = Task('a', 1)
b = Task('b', 2)
a < b # True

И, кроме того, автоматически реализуется метод сравнения на больше, как отрицание результата метода __lt__:

a > b # False

Те из вас, кто изучал курс по ООП Python, все это прекрасно уже знают.

Итак, после объявления класса, можно создать список из этих объектов и добавить их в очередь:

tasks = queue.PriorityQueue()
 
lst_t = [Task('варим пельмешки', 1), Task('программируем', 7), Task('едим пельмешки', 2),
         Task('пьем кофе', 2), Task('принимаем лабы', 4), Task('читаем лекции', 3)]
 
for d in lst_t:
    tasks.put(d)

Программа здесь выглядит абсолютно также, только вместо кортежей появляются объекты класса Task. После выбора этих объектов из очереди, получим результат:

Число элементов в очереди: 6
Task: варим пельмешки, 1
Task: едим пельмешки, 2
Task: пьем кофе, 2
Task: читаем лекции, 3
Task: принимаем лабы, 4
Task: программируем, 7

Как видите, все работает прежним образом, элементы выбираются в порядке указанных приоритетов, начиная с наименьшего.

Таким образом, класс PriorityQueue хорошо подходит для ситуаций, когда важно обрабатывать события или задачи в строго определенном порядке приоритетов. Это особенно полезно в потоковых сценариях и многозадачных системах, где важно гарантировать соблюдение строгого порядка обработки запросов.

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме