Oh My Py
2.47K subscribers
21 photos
55 links
Все о чистом коде на Python // antonz.ru
Download Telegram
Разбить строку на слова с учётом кавычек

Предположим, вы собираете архив статей, и хотите для каждой автоматически определять теги — по ним можно будет моментально найти статью в архиве. В качестве тегов решили брать топ-3 слова из текста.

Например, такая статья:

text = """Голубь Френк прибыл в отель "Четыре сезона" с дружеским визитом. По сообщениям очевидцев, он сожрал в ресторане киноа прямо из тарелки гостя, а затем клюнул в глаз прибежавшего на шум официанта.

Френк прилетает в "Четыре сезона" каждый год. В прошлый раз мерзкая птица нагадила в ванну с шампанским в королевском люксе, лишив кого-то романтического вечера."""


Вы чистите текст от пунктуации, бьёте по пробелам и считаете слова. Вот топ-3:

[(‘френк', 2),
('четыре', 2),
('сезона', 2)]


Но погодите, разве правильно считать «четыре» и «сезона» разными тегами? Это ведь название отеля, лучше учитывать их как одно словосочетание. Тут-то и пригодится функция shlex.split() — она трактует словосочетания в кавычках как один токен:

import shlex
from collections import Counter

# слегка чистим text, для краткости опускаю
words = shlex.split(text)
words = [word for word in words if len(word) > 3]
common = Counter(words).most_common(3)
print(common)

[('френк', 2),
('четыре сезона', 2),
('голубь', 1)]


Вот теперь теги что надо!

P.S. Вообще, shlex предназначен для разбора shell-подобных строк, так что если злая судьба заставит вас парсить bash-скрипты — вы знаете, куда смотреть.

#stdlib
👍3
Шаблонизатор для бедных

Мантра «There should be one — and preferably only one — obvious way to do it» из Zen of Python далека от реальности.

Все мы знаем, что в Питоне за долгие годы собрали аж три способа подстановки переменных в строку:

who = "Голубь Френк"
"%s постучался в стеклянные двери" % who
"{} постучался в стеклянные двери".format(who)
f"{who} постучался в стеклянные двери"


Но не все знают, что есть ещё и четвёртый способ — string.Template. Больше того, он ещё и может быть полезен иногда.

Например, если вам не нужны расширенные возможности вроде форматирования чисел или обращения к атрибутам внутри шаблона, а нужно тупо заменять строковые переменные на их значения. Да ещё и синтаксис подстановки отличается от стандартного:

CHANGEME:who постучался в стеклянные двери


Тут и пригодится string.Template:

import string
class OmskTemplate(string.Template):
delimiter = "CHANGEME:"

>>> template = OmskTemplate("CHANGEME:who постучался в стеклянные двери")
>>> template.substitute({ "who": "Кот Джарвис"})

'Кот Джарвис постучался в стеклянные двери'


Если нужен ещё более извращённый синтаксис — достаточно перекрыть атрибут класса pattern, указав в нём подходящее регулярное выражение.

#stdlib
👍2
Чистый код: единообразие в именах

Всякая книга про хороший код начинается с главы об именах переменных и функций. Но каждый, кто работал с большим проектом, знает — хороших имён недостаточно. Важно ещё, чтобы они были единообразными во всём проекте.

Посмотрим на питоновский модуль difflib, который помогал нам сравнивать строки:

find_longest_match() находит самый длинный совпадающий кусок между двуми последовательностями и возвращает match — объект с совпадением и дополнительной информацией.

get_matching_blocks() находит все совпадения между двумя последовательностями и возвращает список из match.

get_close_matches() находит слова, сильнее всего похожие на переданное слово, возвращает список строк.

По отдельности вроде все названия хороши и понятны. Но я утверждаю, что это — плохой код:

1️⃣ find_longest_match вовращает объект-match, как и следует из названия; и get_matching_blocks возвращает такие же объекты, хотя название намекает, что должны возвращаться какие-то blocks

2️⃣ get_close_matches, судя по названию, должен возвращать match, как find_longest_match — но возвращает строки

3️⃣ одна и та же по сути операция (поиск совпадений) в одном случае называется find, а в двух других — get

В результате уже на следующий день не вспомнить, кто как называется, без обращения к документации.

Уж на уровне одного модуля можно напрячься и сохранить единообразие? Я предложил бы такие имена:

find_longest_match()
find_all_matches()
find_similar_words()

#код
👍3
Исходники стандартной библиотеки

У большинства питонячих модулей хорошее описание: написано доходчиво, часто есть примеры. Но ничто не заменит исходного кода, если что-то непонятно или хочется понять, как та или иная штука реализована.

Core-разработчик Питона Реймонд Хеттингер тоже это заметил, и поэтому в документации к каждому модулю стандартной библиотеки первым делом идёт ссылка на исходники этого самого модуля на гитхабе.

Если вы прочитали описание функции или класса, а вопросы остались — не стесняйтесь пойти в исходный код и посмотреть, как оно там устроено. Большинство модулей отлично написаны, код понятный, в меру откомментирован.
👍1
Enum здорового человека

Если программист привык писать код, как это делали наши пращуры со времён аграрной революции, то перечисления у него выглядят как-то так:

class PigeonState:
eating = 0
sleeping = 1
flying = 2

>>> PigeonState.sleeping
1


Конечно, у наших современников есть способ получше — enum:

import enum
class PigeonState(enum.Enum):
eating = 0
sleeping = 1
flying = 2

>>> PigeonState.sleeping.value
1


Это не просто более многословный способ сделать то же самое. У енумов есть вагон плюшек, недоступных староверам. Например, можно делать синонимы состояний:

class PigeonState(enum.Enum):
eating = 0
sleeping = 1
flying = 2

# There is no way Frank
# is really doing that
thinking = 1

>>> PigeonState.thinking
<PigeonState.sleeping: 1>


Или добавлять свои атрибуты:

class PigeonState(enum.Enum):
eating = (0, "Ест")
sleeping = (1, "Спит")
flying = (2, "Парит в небесах")

def __init__(self, id, title):
self.id = id
self.title = title


>>> PigeonState.flying.id
2

>>> PigeonState.flying.title
'Парит в небесах'


А ещё можно: сортировать, сравнивать по is вместо ==, итерировать по значениям и создавать динамически. В общем, енумы — однозначное добро.

#stdlib
👍2
Умолчательные значения настроек

Если в программе есть настройки, хорошо предусмотреть для них умолчательные значения. Так всё будет работать «из коробки», а в конфиг полезут только те, кому это действительно надо.

Допустим, настройки по умолчанию мы сложили в словарь:

DEFAULTS = {
"name": "Frank",
"species": "pigeon",
"age": 42,
}


А пользовательские настройки лежат в settings.ini. Их можно считать функцией load_settings(), которая тоже возвращает словарь.

Вопрос: как получить актуальное значение того или иного свойства?

Так себе способ:

custom = load_settings()

def get_setting_value(name):
if name in custom:
return custom[name]
else:
return DEFAULTS[name]


Способ лучше — воспользоваться collections.ChainMap:

from collections import ChainMap

# пусть custom == { "species": "human" }
custom = load_settings()
settings = ChainMap(custom, DEFAULTS)

def get_setting_value(name):
return settings[name]

>>> get_setting_value("name")
'Frank'

>>> get_setting_value("species")
'human'


В ChainMap можно запихать сколько угодно словарей, поиск по ним производится последовательно. Присваивание тоже работает:

>>> settings["age"] = 33
>>> custom
{'species': 'human', 'age': 33}


#stdlib
👍1
Посчитать количество объектов каждого типа

Допустим, вы пишете программу, которая обрабатывает заявки разных типов — идеи, вопросы и проблемы:

from collections import namedtuple
Request = namedtuple("Request", ("type", "text"))

requests = [
Request(type="question", text="Как пасти котов?"),
Request(type="problem", text="Бакланы портят стадион"),
Request(type="idea", text="Переводчик с лисьего на русский"),
Request(type="problem", text="Кот крадёт электричество"),
Request(type="problem", text="Мыши похитили 540 кг марихуаны"),
Request(type="idea", text="Холодильник с таймером"),
]


Предположим, требуется посчитать количество заявок каждого типа. Если в прошлой жизни человек писал на джаваскрипте, код может получиться таким:

stats = {}
for req in requests:
if req.type in stats:
stats[req.type] += 1
else:
stats[req.type] = 1

>>> stats
{'question': 1, 'problem': 3, 'idea': 2}


Прямо больно смотреть на этот if, верно? Лучше воспользоваться методом dict.setdefault(). Но как по мне, он тоже уродливый, поэтому ещё лучше — collections.defaultdict:

from collections import defaultdict
stats = defaultdict(lambda: 0)
for req in requests:
stats[req.type] += 1

>>> dict(stats)
{'question': 1, 'problem': 3, 'idea': 2}


А совсем хорошо — collections.Counter:

from collections import Counter
stats = Counter(req.type for req in requests)

>>> dict(stats)
{'question': 1, 'problem': 3, 'idea': 2}


У счётчиков есть ещё пара полезных особенностей, но о них в другой раз.

#stdlib
👍1
Операции со статистикой

Вернёмся к нашему примеру со статистикой по заявкам разных типов. Вот данные за три дня:

monday = {"question": 1, "problem": 3, "idea": 2}
tuesday = {"problem": 5, "idea": 1}
wednesday = {"question": 2, "problem": 2}


Как бы посчитать агрегированную статистику? Можно так, конечно:

def add_day(day_stats, stats):
for key, value in day_stats.items():
stats[key] += value
return stats

stats = {"question": 0, "problem": 0, "idea": 0}
stats = add_day(monday, stats)
stats = add_day(tuesday, stats)
stats = add_day(wednesday, stats)

>>> stats
{'question': 3, 'problem': 10, 'idea': 3}


Но вы наверняка догадываетесь, что это не наш метод. Поможет арифметика со счётчиками:

from collections import Counter
monday = Counter(monday)
tuesday = Counter(tuesday)
wednesday = Counter(wednesday)
stats = monday + tuesday + wednesday

>>> stats
Counter({'problem': 10, 'question': 3, 'idea': 3})


Что насчёт самого популярного типа заявок?

>>> stats.most_common(1)
[('problem', 10)]


А какие типы заявок встречались во вторник и в среду?

>>> (tuesday | wednesday).keys()
dict_keys(['problem', 'idea', 'question'])


А сколько проблем было за все дни, кроме понедельника?

>>> (stats - monday)["problem"]
7


Думаю, вы уловили идею ツ

P.S. Хотите реально злую штуку? Вот как посчитать агрегированную статистику в одну строчку:

sum(map(Counter, [monday, tuesday, wednesday]), Counter())


#stdlib
👍2
Подвох в функции sum()

Однострочник из предыдущей заметки заработал у меня не с первого раза. Причина — особенности работы функции sum() в питоне.

Разобрал все нюансы, пост получился великоват для телеграма, поэтому вынес его в блог: https://antonz.ru/sum-gotcha/

#stdlib
Хранить последние N объектов

Допустим, вы пишете систему учёта посетителей для музея изящных искусств в Хиросиме (не спрашивайте). Одно из требований безопасников — команда tail, которая показывает трёх последних визитёров. Как её реализовать?

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

TAIL_COUNT = 3
visitors = []

def handle(visitor):
visitors.append(visitor)

def tail():
return visitors[-TAIL_COUNT:]

handle("Питер")
handle("Клер")
handle("Френк")
handle("Кен Чан")
handle("Гоу Чан")

>>> visitors
['Питер', 'Клер', 'Френк', 'Кен Чан', 'Гоу Чан']

>>> tail()
['Френк', 'Кен Чан', 'Гоу Чан']


Но как-то не очень правильно хранить всех посетителей только ради того, чтобы показывать последних трёх, верно? Нам поможет collections.deque:

from collections import deque

visitors = deque(maxlen=3)

def handle(visitor):
visitors.append(visitor)

def tail():
return list(visitors)

handle("Питер")
handle("Клер")
handle("Френк")
handle("Кен Чан")
handle("Гоу Чан")

>>> visitors
deque(['Френк', 'Кен Чан', 'Гоу Чан'], maxlen=3)

>>> tail()
['Френк', 'Кен Чан', 'Гоу Чан']


deque (double-ended queue) хранит не более maxlen элементов, автоматически «выпихивая» старые при добавлении новых.

А ещё она добавляет элементы в начало и в конец за O(1), в отличие от списка, у которого это O(n). Идеально подходит, если коллекция часто модифицируется, а выбирать элементы по индексу не надо.

#stdlib
👍1
Кортеж здорового человека

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

Часто создавать отдельный класс под это дело лень, и используют кортежи:

("pigeon", "Френк", 3)
("fox", "Клер", 7)
("parrot", "Питер", 1)


Для большей наглядности подойдёт collections.namedtuple:

from collections import namedtuple

Pet = namedtuple("Pet", "type name age")
frank = Pet(type="pigeon", name="Френк", age=3)

>>> frank.age
3


Но что делать, если одно из свойств надо изменить? Френк стареет, а кортеж-то неизменяемый. Чтобы не пересоздавать его целиком, придумали метод _replace():

>>> frank._replace(age=4)
Pet(type='pigeon', name='Френк', age=4)


А если хотите сделать всю структуру изменяемой — _asdict():

>>> dict(frank._asdict())
{'type': 'pigeon', 'name': 'Френк', 'age': 3}


Удобно!

#stdlib
👍1
Из десятичной дроби — в обычную

Субботний совет от капитана Очевидность. У класса float есть прекрасный метод as_integer_ratio(), который представляет десятичную дробь в виде обычной — пары «числитель, знаменатель»:

>>> (0.25).as_integer_ratio()
(1, 4)

>>> (0.5).as_integer_ratio()
(1, 2)

>>> (0.75).as_integer_ratio()
(3, 4)


Так вот. Никогда им не пользуйтесь ツ Потому что:

>>> (0.2).as_integer_ratio()
(3602879701896397, 18014398509481984)


Виной всему стандарт представления дробных чисел IEEE 754, который реализует float.

Используйте Decimal:

>>> from decimal import Decimal
>>> Decimal("0.2").as_integer_ratio()
(1, 5)


Уверен, вы и так это знаете. Просто на всякий случай ツ

#stdlib
👍1
Кортеж здорового человека: автоматическая замена названий

Допустим, вы импортируете данные из CSV и превращаете каждую строчку в кортеж. Названия полей взяли из заголовка CSV-файла. Но что-то идёт не так:

>>> headers = ("name", "age", "with")
>>> Pet = namedtuple("Pet", headers)
ValueError: Type names and field names cannot be a keyword: 'with'

>>> headers = ("name", "age", "name")
>>> Pet = namedtuple("Pet", headers)
ValueError: Encountered duplicate field name: 'name'


Решение — аргумент rename=True в конструкторе:

headers = ("name", "age", "with", "color", "name", "food")
Pet = namedtuple("Pet", headers, rename=True)

>>> Pet._fields
('name', 'age', '_2', 'color', '_4', 'food')


Ставьте 👍, если хотите ещё про кортежи; или 😴, если хватит уже и поехали дальше.

#stdlib
👍1
Кортеж здорового человека: значения по умолчанию

Хорошо, продолжим пока про кортежи ツ Если у кортежа куча необязательных полей, всё равно приходится каждый раз перечислять их при создании объекта:

Pet = namedtuple("Pet", "type name alt_name")

>>> Pet("pigeon", "Френк")
TypeError: __new__() missing 1 required positional argument: 'alt_name'

>>> Pet("pigeon", "Френк", None)
Pet(type='pigeon', name='Френк', alt_name=None)


Чтобы этого избежать, укажите в конструкторе аргумент defaults:

Pet = namedtuple("Pet", "type name alt_name", defaults=("нет",))

>>> Pet("pigeon", "Френк")
Pet(type='pigeon', name='Френк', alt_name='нет')


defaults присваивает умолчательные значения справа налево, с хвоста. Работает в питоне 3.7+

Для старых версий можно более коряво добиться того же результата через протитип:

Pet = namedtuple("Pet", "type name alt_name")
default_pet = Pet(None, None, "нет")

>>> default_pet._replace(type="pigeon", name="Френк")
Pet(type='pigeon', name='Френк', alt_name='нет')

>>> default_pet._replace(type="fox", name="Клер")
Pet(type='fox', name='Клер', alt_name='нет')


Но с defaults, конечно, куда приятнее.

#stdlib
👍1
Именованный кортеж vs объект

Эта заметка может быть немного тяжеловата для начинающих, так что если что — просто пропустите её. Скоро мы уже закончим с нюансами кортежей.

Одно из преимуществ именованного кортежа — легковесность. Армия из ста тысяч голубей займёт всего 10 мегабайт:

from collections import namedtuple
import objsize # non-standard

Pet = namedtuple("Pet", "type name age")
frank = Pet(type="pigeon", name="Френк", age=None)

pigeons = [frank._replace(age=idx) for idx in range(100000)]


>>> round(objsize.get_deep_size(pigeons)/(1024**2), 2)
10.3


Для сравнения, если Pet сделать обычным классом, аналогичный список займёт уже 19 мегабайт.

Так происходит, потому что обычные объекты в питоне таскают с собой увесистый дандер __dict__, в котором лежат названия и значения всех атрибутов объекта:

class PetObj:
def __init__(self, type, name, age):
self.type = type
self.name = name
self.age = age

frank_obj = PetObj(type="pigeon", name="Френк", age=3)


>>> frank_obj.__dict__
{'type': 'pigeon', 'name': 'Френк', 'age': 3}


Объекты-namedtuple же лишёны этого словаря, а потому занимают меньше памяти:

>>> frank.__dict__
AttributeError: 'Pet' object has no attribute '__dict__'


>>> objsize.get_deep_size(frank_obj)
335


>>> objsize.get_deep_size(frank)
227


Но как именованному кортежу удалось избавиться от __dict__? Поговорим об этом в следующий раз ツ

#stdlib
👍1
Что внутри у namedtuple и почему он такой лёгкий

Если вы давно работаете с питоном, то наверняка знаете: легковесный объект можно создать через дандер __slots__:

class PetSlots:
__slots__= ("type", "name", "age")

def __init__(self, type, name, age):
self.type = type
self.name = name
self.age = age

frank_slots = PetSlots(type="pigeon", name="Френк", age=3)


У «слотовых» объектов нет словаря с атрибутами, поэтому они занимают мало памяти. «Френк на слотах» такой же лёгкий, как «Френк на кортеже», смотрите:

>>> objsize.get_deep_size(frank)
239

>>> objsize.get_deep_size(frank_slots)
231


Если вы решили, что namedtuple тоже использует слоты — вы недалеки от истины ツ

Как вы помните, конкретные классы-кортежи объявляются динамически:

Pet = namedtuple("Pet", "type name age")


Конструктор namedtuple применяет разную тёмную магию и генерит примерно такой класс (сильно упрощаю):

class Pet(tuple):
__slots__= ()

type = property(operator.itemgetter(0))
name = property(operator.itemgetter(1))
age = property(operator.itemgetter(2))

def __new__(cls, type, name, age):
return tuple.__new__(cls, (type, name, age))


То есть наш Pet — это на самом деле обычный tuple, к которому гвоздями приколотили три метода-свойства:

— type возвращает нулевой элемент кортежа
— name — первый элемент кортежа
— age — второй элемент кортежа

А __slots__ нужен только для того, чтобы объекты получились лёгкими. В результате Pet и занимает мало места, и может использоваться как обычный кортеж:

>>> frank.index("Френк")
1

>>> type, _, _ = frank
>>> type
'pigeon'


Хитро придумано, а?

#stdlib
👍1
Чистый код: раздвоение личности у функции

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

А сегодня очередной выпуск «чистого кода». Есть в модуле collections класс OrderedDict. Это обычный словарь, только помнит порядок, в котором добавлялись ключи:

from collections import OrderedDict

d = OrderedDict()
d["Френк"] = "наглый"
d["Клер"] = "хитрая"
d["Питер"] = "тупой"

>>> d.keys()
odict_keys(['Френк', 'Клер', 'Питер'])


Ничего особо интересного в упорядоченном словаре нет — тем более, что начиная с версии 3.6 обычный словарь тоже сохраняет порядок ключей.

Но есть у него любопытный метод move_to_end():

>>> d.move_to_end("Френк")
>>> d.keys()
odict_keys(['Клер', 'Питер', 'Френк'])


Всё вроде понятно, метод передвигает указанный ключ в конец словаря. Логично предположить, что должна существовать парная операция — передвинуть в начало. Интересно, как она называется, наверно move_to_start() или что-то в этом роде. А вот и нет:

>>> d.move_to_end("Френк", False)
>>> d.keys()
odict_keys(['Френк', 'Клер', 'Питер'])


То есть, чтобы передвинуть ключ в начало, мы делаем move_to_end(False). Это как если бы login(False) выполнял logout(). Это как если бы left(False) выполнял right(). Настолько хрестоматийно плохо, что я не понимаю, как это оказалось в стандартной библиотеке.

Много лет назад Роберт Мартин написал в «Чистом коде»: если у функции есть переключатель, который кардинально меняет её поведение — функцию следует разделить на две:

d.move_to_end("Френк")
d.move_to_start("Френк")


Не вижу причин спорить с Мартином в данном случае.

#код
👍1
Объединить отсортированные списки в один

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

washington = [
(99, "Френк"),
(80, "Клер"),
(73, "Зоя")
]

moscow = [
(90, "Валера"),
(88, "Мария"),
(50, "Анатолий")
]

beijing = [
(123, "Чан"),
(109, "Пинг"),
(70, "Ки"),
]


Теперь ваша задача — выбрать трёх призёров. Я знаю как минимум один простой способ:

all = sorted(washington + moscow + beijing)
winners = all[-3:]

>>> winners
[(99, 'Френк'), (109, 'Пинг'), (123, 'Чан')]


Если всего n участников, такая реализация займёт 2n памяти и потребует O(n log n) операций. Довольно расточительно.

Можно сделать то же самое за константное время и память — благодаря heapq.merge():

import heapq

all = heapq.merge(washington, moscow, beijing, reverse=True)

>>> next(all)
(123, 'Чан')

>>> next(all)
(109, 'Пинг')

>>> next(all)
(99, 'Френк')


heapq.merge() возвращает генератор, который работает поверх исходных коллекций — поэтому не расходует лишнюю память. И он учитывает, что исходные списки уже отсортированы — поэтому не выполняет лишних действий.

P.S. Френк, всего лишь третье место. Не ожидал от тебя.

#stdlib
👍1
Выбрать топ-k элементов списка

Сегодня новое соревнование — граждане города выбирают самое наглое животное. Результаты опроса поступили в виде неупорядоченного списка пар «количество голосов — участник»:

contenders = [
(31, "индюк"),
(22, "крыса"),
(79, "кот"),
(98, "голубь"),
(13, "собака"),
(95, "енот"),
(15, "хомяк"),
]


Осталось, как обычно, выбрать трёх победителей. Как насчёт такого:

>>> sorted(contenders)[-3:]
[(79, 'кот'), (95, 'енот'), (98, 'голубь')]


Неплохо. Но, как вы помните, сортировка списка занимает O(n log n) операций. Жирновато, чтобы просто выбрать топ-3 элемента.

Вот альтернатива через heapq.nlargest():

>>> import heapq
>>> heapq.nlargest(3, contenders)
[(98, 'голубь'), (95, 'енот'), (79, 'кот')]


Такой вариант использует только O(n) операций — при небольшом k (в данном случае k = 3). Для больших k вариант с sorted() эффективнее.

Ну а если k = 1 (выбираем одного победителя), то так:

>>> max(contenders)
(98, 'голубь')


Я даже знаю, как его зовут ツ

#stdlib
👍1
Обработать заявки с учётом приоритетов

Если система обрабатывает заявки, редко бывает, что все они одинакового веса. Чаще встречаются разные приоритеты: клиенты бывают обычные и VIP, баги бывают минорные и критические, заказы бывают «до 1000 ₽» и «10000+ ₽».

Если приоритетов нет, обслуживать заявки просто: кто раньше пришёл, того раньше и обслужили (first in, first out — FIFO). С приоритетами сложнее: более важные заявки должны идти вперёд, но среди заявок с одинаковым приоритетом по-прежнему должен действовать принцип FIFO.

Допустим, была у нас система без приоритетов:

from collections import deque

def process(requests):
while requests:
client, task = requests.pop()
print(f"{client}: {task}")

requests = deque()
requests.appendleft(
("Лукас", "нарвать бананов"))
requests.appendleft(
("Зоя", "почесать спинку"))
requests.appendleft(
("Френк", "насыпать зёрен"))

>>> process(requests)
Лукас: нарвать бананов
Зоя: почесать спинку
Френк: насыпать зёрен


Обработка по порядку, всё честно. А теперь допустим, что у заявки появился вес:

→ Лукас, вес 1
→ Зоя, вес 1
→ Френк, вес 10

Френк с весом 10 должен пойти первым. А Зоя и Лукас — после него, в порядке поступления: сначала Лукас, потом Зоя.

Реализовать эту логику поможет модуль heapq:

import heapq
import time
requests = []

heapq.heappush(requests,
(-1, time.time_ns(), "Лукас"))
heapq.heappush(requests,
(-1, time.time_ns(), "Зоя"))
heapq.heappush(requests,
(-10, time.time_ns(), "Френк"))


Здесь первым аргументом мы передаём вес заявки. heapq.heappush() ставит первыми элементы с меньшим значением, там что берём вес со знаком минус.

Вторым аргументом передаём текущее время в наносекундах, чтобы заявки с одинаковым весом разрешались в порядке поступления.

Проверим результат:

def process(requests):
while requests:
_, _, client = heapq.heappop(requests)
print(f"{client}")

>>> process(requests)
Френк
Лукас
Зоя


Порядок!

#stdlib