# ПРИМЕЧАНИЕ: Не забудьте выполнить пересортировку всякий раз,
# когда добавляется новый элемент, либо используйте
# bisect.insort().
q.sort(reverse=True)
while q:
next_item = q.pop()
print(next_item)
# Результат:
# (1, 'есть')
# (2, 'программировать')
# (3, 'спать')
heapq — двоичные кучи на основе списка
Данная реализация двоичной кучи обычно подкрепляется обыкновенным
списком, и она поддерживает вставку и извлечение наименьшего элемента
за O(log n) время
2
.
1
См. документацию Python «bisect.insort»:
https://docs .python .org/3 .6/library/bisect .html
2
См. документацию Python «heapq»:
https://docs .python .org/3 .6/library/heapq .html
5 .7 . Очереди с приоритетом 199
Этот модуль — хороший выбор для реализации очередей с приоритетом
в Python. Поскольку двоичная куча
heapq
технически обеспечивает толь-
ко реализацию min-heap (то есть кучи, где значение в любой вершине не
больше, чем значения ее потомков), должны быть предприняты допол-
нительные шаги, которые обеспечат стабильность сортировки и другие
функциональные возможности, которые, как правило, ожидают от «прак-
тической версии» очереди с приоритетом
1
.
import heapq
q = []
heapq.heappush(q, (2, 'программировать'))
heapq.heappush(q, (1, 'есть'))
heapq.heappush(q, (3, 'спать'))
while q:
next_item = heapq.heappop(q)
print(next_item)
# Результат:
# (1, 'есть')
# (2, 'программировать')
# (3, 'спать')
queue .PriorityQueue — красивые очереди с приоритетом
Данная реализация очереди с приоритетом во внутреннем представлении
использует двоичную кучу
heapq
и имеет одинаковую временную и про-
странственную вычислительную сложность
2
.
Разница состоит в том, что очередь с приоритетом
PriorityQueue
синхро-
низирована и обеспечивает семантику блокирования с целью поддержки
многочисленных параллельных производителей и потребителей.
В зависимости от вашего варианта использования она либо станет по-
лезной, либо слегка замедлит вашу программу. В любом случае вы мо-
1
См. документацию Python «Примечания к реализации очереди с приоритетом»: там же.
2
См. документацию Python «queue.PriorityQueue»:
https://docs .python .org/3 .6/library/queue .
html
200 Глава 5 • Общие структуры данных Python
жете предпочесть интерфейс на основе класса, предлагаемый классом
PriorityQueue
, использованию интерфейса на основе функций, предла-
гаемого модулем heapq.
from queue import PriorityQueue
q = PriorityQueue()
q.put((2, 'программировать'))
q.put((1, 'есть'))
q.put((3, 'спать'))
while not q.empty():
next_item = q.get()
print(next_item)
# Результат:
# (1, 'есть')
# (2, 'программировать')
# (3, 'спать')
Ключевые выводы
Python содержит несколько реализаций очередей с приоритетом, ко-
торые вы можете использовать в своих программах.
Реализация
queue.PriorityQueue
выбивается из общего ряда, отлича-
ясь хорошим объектно-ориентированным интерфейсом и именем, ко-
торое четко указывает на ее направленность. Такая реализация должна
быть предпочтительным вариантом.
Если требуется избежать издержек, связанных с блокировкой очере-
ди
queue.PriorityQueue
, то непосредственное использование моду-
ля
heapq
также будет хорошим выбором.
6
Циклы и итерации
6 .1 . Написание питоновских циклов
Один из самых легких способов отличить разработчика с опытом работы
на C-подобных языках, который совсем недавно перешел на Python, — по-
смотреть, как он пишет циклы.
Например, всякий раз, когда я вижу фрагмент кода, который выглядит,
как показано ниже, сразу понимаю, что тут пытались программировать на
Python так, будто это C или Java:
my_items = ['a', 'b', 'c']
i = 0 while i < len(my_items):
print(my_items[i])
i += 1
Итак, вы спрашиваете, что же такого непитоновского в этом фрагменте
кода?
Две вещи.
Во-первых, в коде вручную отслеживается индекс
i
— его инициализация
нулем, а затем постепенное увеличение после каждой итерации цикла.
И во-вторых, в коде используется функция
len()
, которая получает раз-
мер контейнера
my_items
, чтобы определить количество итераций.
202 Глава 6 • Циклы и итерации
В Python можно писать циклы, которые справляются с этими двумя за-
дачами автоматически. И будет просто замечательно, если вы возьмете это
на вооружение. Например, если вашему коду не придется отслеживать на-
растающий индекс, то будет намного труднее написать непреднамеренный
бесконечный цикл. Это также сделает программный код более сжатым
и поэтому удобочитаемым.
Чтобы рефакторизовать первый пример кода, я начну с того, что удалю
фрагмент, который вручную обновляет индекс. В Python лучше всего для
этого применить цикл
for
. При помощи встроенной фабричной функции
range()
я могу генерировать индексы автоматически:
>>> range(len(my_items))
range(0, 3)
>>> list(range(0, 3))
[0, 1, 2]
Тип
range
представляет неизменяемую последовательность чисел. Его
преимущество перед обычным списком
list
в том, что он всегда занимает
одинаково небольшое количество оперативной памяти. Объекты-диапа-
зоны в действительности не хранят отдельные значения, представляющие
числовую последовательность, вместо этого они функционируют как
итераторы и вычисляют значения последовательности на ходу
1
.
Поэтому, вместо того чтобы на каждой итерации цикла вручную увели-
чивать индекс
i
, я смог воспользоваться функцией
range()
и написать
что-то подобное:
for i in range(len(my_items)):
print(my_items[i])
Уже лучше. Однако этот вариант по-прежнему выглядит не совсем по-
питоновски и ощущается больше как итеративная Java-конструкция, а не
как настоящий цикл Python. Когда вы видите программный код, в кото-
1
Чтобы получить такое экономное для оперативной памяти поведение в Python 2, вам
придется использовать встроенную функцию
xrange()
, так как функция
range()
будет
в действительности конструировать объект-список.
6 .1 . Написание питоновских циклов 203
ром для итеративного обхода контейнера используется
range(len(...))
,
его, как правило, можно еще больше упростить и улучшить.
Как я уже отмечал, циклы
for
в Python в действительности являются
циклами «for each», которые могут выполнять непосредственный пере-
бор элементов контейнера или последовательности без необходимости
искать их по индексу. И этот факт я могу задействовать для дальнейшего
упрощения этого цикла:
for item in my_items:
print(item)
Я считаю такое решение вполне питоновским. В нем применено несколько
продвинутых функциональных средств Python, но при этом оно остается
хорошим и чистым и читается почти как псевдокод из учебника по про-
граммированию. Обратите внимание, что в этом цикле больше не отсле-
живается размер контейнера, а для доступа к элементам не используется
нарастающий индекс.
Теперь контейнер сам занимается раздачей элементов для их обработки.
Если контейнер упорядочен, то и результирующая последовательность
элементов будет такой же. Если контейнер не упорядочен, он будет воз-
вращать свои элементы в произвольном порядке, но цикл по-прежнему
охватит их все полностью.
Нужно сказать, что, конечно, вы не всегда будете в состоянии переписать
свои циклы таким образом. А что, если, например, вам нужен индекс
элемента?
Для таких случаев есть возможность писать циклы, которые поддержива-
ют нарастающий индекс, избегая применения шаблона с
range(len(...))
,
от которого я вас предостерег. Встроенная функция
enumerate()
поможет
вам сделать подобного рода циклы безупречными и питоновскими:
>>> for i, item in enumerate(my_items):
... print(f'{i}: {item}')
0: a
1: b
2: c
204 Глава 6 • Циклы и итерации
Дело в том, что итераторы в Python могут возвращать более одного зна-
чения. Они могут возвращать кортежи с произвольным числом значений,
которые затем могут быть распакованы прямо внутри инструкции
for
.
Это очень мощное средство. Например, тот же самый прием можно исполь-
зовать, чтобы в цикле одновременно перебрать ключи и значения словаря:
>>> emails = {
... 'Боб': 'bob@example.com',
... 'Алиса': 'alice@example.com',
... }
>>> for name, email in emails.items():
... print(f'{name} -> {email}')
Do'stlaringiz bilan baham: |