Подвох в функции sum()
Однострочник из предыдущей заметки заработал у меня не с первого раза. Причина — особенности работы функции
Разобрал все нюансы, пост получился великоват для телеграма, поэтому вынес его в блог: https://antonz.ru/sum-gotcha/
#stdlib
Однострочник из предыдущей заметки заработал у меня не с первого раза. Причина — особенности работы функции
sum()
в питоне.Разобрал все нюансы, пост получился великоват для телеграма, поэтому вынес его в блог: https://antonz.ru/sum-gotcha/
#stdlib
Хранить последние N объектов
Допустим, вы пишете систему учёта посетителей для музея изящных искусств в Хиросиме (не спрашивайте). Одно из требований безопасников — команда tail, которая показывает трёх последних визитёров. Как её реализовать?
Конечно, можно складывать всех прибывших в список и по запросу выдавать из него последние 3 элемента:
Но как-то не очень правильно хранить всех посетителей только ради того, чтобы показывать последних трёх, верно? Нам поможет
deque (double-ended queue) хранит не более maxlen элементов, автоматически «выпихивая» старые при добавлении новых.
А ещё она добавляет элементы в начало и в конец за O(1), в отличие от списка, у которого это O(n). Идеально подходит, если коллекция часто модифицируется, а выбирать элементы по индексу не надо.
#stdlib
Допустим, вы пишете систему учёта посетителей для музея изящных искусств в Хиросиме (не спрашивайте). Одно из требований безопасников — команда 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
Кортеж здорового человека
Наверняка вы сталкивались с ситуацией, когда нужно передать несколько свойств объекта одним куском. Например, информацию о домашнем питомце: тип, кличка и возраст.
Часто создавать отдельный класс под это дело лень, и используют кортежи:
Для большей наглядности подойдёт
Но что делать, если одно из свойств надо изменить? Френк стареет, а кортеж-то неизменяемый. Чтобы не пересоздавать его целиком, придумали метод
А если хотите сделать всю структуру изменяемой —
Удобно!
#stdlib
Наверняка вы сталкивались с ситуацией, когда нужно передать несколько свойств объекта одним куском. Например, информацию о домашнем питомце: тип, кличка и возраст.
Часто создавать отдельный класс под это дело лень, и используют кортежи:
("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
Из десятичной дроби — в обычную
Субботний совет от капитана Очевидность. У класса float есть прекрасный метод
Так вот. Никогда им не пользуйтесь ツ Потому что:
Виной всему стандарт представления дробных чисел IEEE 754, который реализует float.
Используйте
Уверен, вы и так это знаете. Просто на всякий случай ツ
#stdlib
Субботний совет от капитана Очевидность. У класса 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
Кортеж здорового человека: автоматическая замена названий
Допустим, вы импортируете данные из CSV и превращаете каждую строчку в кортеж. Названия полей взяли из заголовка CSV-файла. Но что-то идёт не так:
Решение — аргумент
Ставьте 👍, если хотите ещё про кортежи; или 😴, если хватит уже и поехали дальше.
#stdlib
Допустим, вы импортируете данные из 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
Кортеж здорового человека: значения по умолчанию
Хорошо, продолжим пока про кортежи ツ Если у кортежа куча необязательных полей, всё равно приходится каждый раз перечислять их при создании объекта:
Чтобы этого избежать, укажите в конструкторе аргумент
defaults присваивает умолчательные значения справа налево, с хвоста. Работает в питоне 3.7+
Для старых версий можно более коряво добиться того же результата через протитип:
Но с defaults, конечно, куда приятнее.
#stdlib
Хорошо, продолжим пока про кортежи ツ Если у кортежа куча необязательных полей, всё равно приходится каждый раз перечислять их при создании объекта:
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
Именованный кортеж vs объект
Эта заметка может быть немного тяжеловата для начинающих, так что если что — просто пропустите её. Скоро мы уже закончим с нюансами кортежей.
Одно из преимуществ именованного кортежа — легковесность. Армия из ста тысяч голубей займёт всего 10 мегабайт:
Для сравнения, если Pet сделать обычным классом, аналогичный список займёт уже 19 мегабайт.
Так происходит, потому что обычные объекты в питоне таскают с собой увесистый дандер
Объекты-namedtuple же лишёны этого словаря, а потому занимают меньше памяти:
Но как именованному кортежу удалось избавиться от
#stdlib
Эта заметка может быть немного тяжеловата для начинающих, так что если что — просто пропустите её. Скоро мы уже закончим с нюансами кортежей.
Одно из преимуществ именованного кортежа — легковесность. Армия из ста тысяч голубей займёт всего 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
Что внутри у namedtuple и почему он такой лёгкий
Если вы давно работаете с питоном, то наверняка знаете: легковесный объект можно создать через дандер
У «слотовых» объектов нет словаря с атрибутами, поэтому они занимают мало памяти. «Френк на слотах» такой же лёгкий, как «Френк на кортеже», смотрите:
Если вы решили, что namedtuple тоже использует слоты — вы недалеки от истины ツ
Как вы помните, конкретные классы-кортежи объявляются динамически:
Конструктор namedtuple применяет разную тёмную магию и генерит примерно такой класс (сильно упрощаю):
То есть наш Pet — это на самом деле обычный
— type возвращает нулевой элемент кортежа
— name — первый элемент кортежа
— age — второй элемент кортежа
А
Хитро придумано, а?
#stdlib
Если вы давно работаете с питоном, то наверняка знаете: легковесный объект можно создать через дандер
__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
Чистый код: раздвоение личности у функции
Я решил не терзать вас больше именованными кортежами, поэтому последнюю заметку серии выложил на хабре.
А сегодня очередной выпуск «чистого кода». Есть в модуле
Ничего особо интересного в упорядоченном словаре нет — тем более, что начиная с версии 3.6 обычный словарь тоже сохраняет порядок ключей.
Но есть у него любопытный метод
Всё вроде понятно, метод передвигает указанный ключ в конец словаря. Логично предположить, что должна существовать парная операция — передвинуть в начало. Интересно, как она называется, наверно
То есть, чтобы передвинуть ключ в начало, мы делаем
Много лет назад Роберт Мартин написал в «Чистом коде»: если у функции есть переключатель, который кардинально меняет её поведение — функцию следует разделить на две:
Не вижу причин спорить с Мартином в данном случае.
#код
Я решил не терзать вас больше именованными кортежами, поэтому последнюю заметку серии выложил на хабре.
А сегодня очередной выпуск «чистого кода». Есть в модуле
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("Френк")
Не вижу причин спорить с Мартином в данном случае.
#код
Объединить отсортированные списки в один
Предположим, вы решили провести чемпионат мира по оглаживанию собак. Кто погладит больше всех шерстяных волчар за день, тот и победил. Участники не смогли собраться вместе, поэтому каждый город провёл независимое состязание и прислал результат:
Теперь ваша задача — выбрать трёх призёров. Я знаю как минимум один простой способ:
Если всего
Можно сделать то же самое за константное время и память — благодаря
P.S. Френк, всего лишь третье место. Не ожидал от тебя.
#stdlib
Предположим, вы решили провести чемпионат мира по оглаживанию собак. Кто погладит больше всех шерстяных волчар за день, тот и победил. Участники не смогли собраться вместе, поэтому каждый город провёл независимое состязание и прислал результат:
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
Выбрать топ-k элементов списка
Сегодня новое соревнование — граждане города выбирают самое наглое животное. Результаты опроса поступили в виде неупорядоченного списка пар «количество голосов — участник»:
Осталось, как обычно, выбрать трёх победителей. Как насчёт такого:
Неплохо. Но, как вы помните, сортировка списка занимает
Вот альтернатива через
Такой вариант использует только
Ну а если k = 1 (выбираем одного победителя), то так:
Я даже знаю, как его зовут ツ
#stdlib
Сегодня новое соревнование — граждане города выбирают самое наглое животное. Результаты опроса поступили в виде неупорядоченного списка пар «количество голосов — участник»:
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
Обработать заявки с учётом приоритетов
Если система обрабатывает заявки, редко бывает, что все они одинакового веса. Чаще встречаются разные приоритеты: клиенты бывают обычные и VIP, баги бывают минорные и критические, заказы бывают «до 1000 ₽» и «10000+ ₽».
Если приоритетов нет, обслуживать заявки просто: кто раньше пришёл, того раньше и обслужили (first in, first out — FIFO). С приоритетами сложнее: более важные заявки должны идти вперёд, но среди заявок с одинаковым приоритетом по-прежнему должен действовать принцип FIFO.
Допустим, была у нас система без приоритетов:
Обработка по порядку, всё честно. А теперь допустим, что у заявки появился вес:
→ Лукас, вес 1
→ Зоя, вес 1
→ Френк, вес 10
Френк с весом 10 должен пойти первым. А Зоя и Лукас — после него, в порядке поступления: сначала Лукас, потом Зоя.
Реализовать эту логику поможет модуль
Здесь первым аргументом мы передаём вес заявки.
Вторым аргументом передаём текущее время в наносекундах, чтобы заявки с одинаковым весом разрешались в порядке поступления.
Проверим результат:
Порядок!
#stdlib
Если система обрабатывает заявки, редко бывает, что все они одинакового веса. Чаще встречаются разные приоритеты: клиенты бывают обычные и 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
Сегодня == сейчас
В каждом языке есть участки, которые не особо удались создателям. Для большинства языков, созданных до двухтысячных годов, камнем преткновения стала работа со временем.
Питон — не исключение. Возьмём функцию, которая сравнивает дату-время с точностью до минуты:
И сравним «сегодня» и «сейчас»:
Оказывается, это одно и то же ツ Метод
А что насчёт времени по UTC?
Неожиданно,
Если работать с датой-временем средствами стандартной библиотеки, лучше всегда использовать или только наивные объекты (без часового пояса), или только осведомлённые (с часовым поясом). Если их смешать — гарантированно будет беда.
#stdlib
В каждом языке есть участки, которые не особо удались создателям. Для большинства языков, созданных до двухтысячных годов, камнем преткновения стала работа со временем.
Питон — не исключение. Возьмём функцию, которая сравнивает дату-время с точностью до минуты:
from datetime import datetime, timezone
def equal(dt1, dt2):
return dt1.replace(second=0, microsecond=0) == dt2.replace(second=0, microsecond=0)
И сравним «сегодня» и «сейчас»:
>>> equal(datetime.today(), datetime.now())
True
Оказывается, это одно и то же ツ Метод
today()
возвращает не начало дня, как можно было бы ожидать, а текущий момент времени.А что насчёт времени по UTC?
>>> equal(datetime.now(timezone.utc), datetime.utcnow())
False
Неожиданно,
now()
с часовым поясом UTC и utcnow()
возвращают разные значения. Это потому, что now()
возвращает объект с часовым поясом, а utcnow()
— без. Хотя дата-время у них и совпадают.Если работать с датой-временем средствами стандартной библиотеки, лучше всегда использовать или только наивные объекты (без часового пояса), или только осведомлённые (с часовым поясом). Если их смешать — гарантированно будет беда.
#stdlib
Дата из строки
До некоторых пор в питоне не было простого способа создать дату из строки.
Либо так:
Либо так:
Либо сторонние библиотеки вроде
Но с версии 3.7 делать это стало легко и приятно, если строковые даты вы храните в формате ISO 8601 (что в любом случае хорошая идея):
Для даты-времени тоже работает:
🐥
#stdlib
До некоторых пор в питоне не было простого способа создать дату из строки.
Либо так:
import time
from datetime import date
>>> date_struct = time.strptime("2019-02-20", "%Y-%m-%d")
>>> date(*date_struct[:3])
datetime.date(2019, 2, 20)
Либо так:
from datetime import date, datetime
>>> dt = datetime.strptime("2019-02-20", "%Y-%m-%d")
>>> dt.date()
datetime.date(2019, 2, 20)
Либо сторонние библиотеки вроде
dateutil
или arrow
.Но с версии 3.7 делать это стало легко и приятно, если строковые даты вы храните в формате ISO 8601 (что в любом случае хорошая идея):
>>> date.fromisoformat("2019-02-20")
datetime.date(2019, 2, 20)
Для даты-времени тоже работает:
>>> datetime.fromisoformat("2019-02-20T14:30:15")
datetime.datetime(2019, 2, 20, 14, 30, 15)
🐥
#stdlib
Очень противоречивый день недели
Помните, я писал, что работа с датой и временем в Питоне не очень удалась? Проявилось это и в нумерации дней недели.
Нашлось место аж трём вариантам, бережно размазанным по трём модулям:
Судя по всему, сначала борьба шла между
#stdlib
Помните, я писал, что работа с датой и временем в Питоне не очень удалась? Проявилось это и в нумерации дней недели.
Нашлось место аж трём вариантам, бережно размазанным по трём модулям:
time.strftime("%w")
интервал [0, 6], вс = 0
time.strftime("%u")
интервал [1, 7], пн = 1
time.struct_time.tm_wday
интервал [0, 6], пн = 0
datetime.date.weekday()
интервал [0, 6], пн = 0
datetime.date.isoweekday()
интервал [1, 7], пн = 1
calendar.weekday()
интервал [0, 6], пн = 0
Судя по всему, сначала борьба шла между
вс = 0
и пн = 0
, а потом пришёл ISO 8601 и вообще всё испортил ツ#stdlib
Очередь с приоритетами
Некоторое время назад мы с вами сделали систему обработки заявок с приоритетами. Правила простые: более важные заявки идут вперёд, но среди заявок с одинаковым приоритетом действовует принцип очерёдности (FIFO).
Помог нам в этом модуль
Для ленивых любителей ООП в питоне есть класс
Сделаем простую заявку из двух полей — заказчик и вес:
Если не понимаете, что это за
А очередь возьмём готовую:
Теперь проверим. Ожидаем, что первым будет Френк (у него больше вес), вторым Лукас (пришёл раньше), а третьей — Зоя.
Так и вышло ツ
Есть неприятный нюанс. Модуль
#stdlib
Некоторое время назад мы с вами сделали систему обработки заявок с приоритетами. Правила простые: более важные заявки идут вперёд, но среди заявок с одинаковым приоритетом действовует принцип очерёдности (FIFO).
Помог нам в этом модуль
heapq
. Он всем хорош, кроме того, что исполнен в архитектурном стиле «потроха наружу» — это не всегда удобно.Для ленивых любителей ООП в питоне есть класс
queue.PriorityQueue
, который реализует ровно то, что нам надо — очередь с приоритетами.Сделаем простую заявку из двух полей — заказчик и вес:
import time
from functools import total_ordering
@total_ordering
class Request:
def __init__(self, name, weight):
self.name = name
self.weight = -weight
self._timestamp = time.time_ns()
def __str__(self):
return self.name
def __eq__(self, other):
return (self.weight, self._timestamp) == (other.weight, other._timestamp)
def __gt__(self, other):
return (self.weight, self._timestamp) > (other.weight, other._timestamp)
Если не понимаете, что это за
@total_ordering
, странные методы с кучей подчёркиваний и зачем тут _timestamp
— не переживайте, разберём отдельно.А очередь возьмём готовую:
from queue import PriorityQueue
q = PriorityQueue()
q.put(Request(name="Лукас", weight=1))
q.put(Request(name="Зоя", weight=1))
q.put(Request(name="Френк", weight=10))
Теперь проверим. Ожидаем, что первым будет Френк (у него больше вес), вторым Лукас (пришёл раньше), а третьей — Зоя.
while not q.empty():
print(q.get())
Френк
Лукас
Зоя
Так и вышло ツ
Есть неприятный нюанс. Модуль
queue
вообще-то создан для многотопочной работы, поэтому если вызвать у очереди get()
, когда она пустая — поток выполнения заблокируется на веки вечные. Так что придётся со всех сторон обкладываться проверками empty()
и full()
(если решите ограничить максимальный размер очереди).#stdlib
Некоторые животные равнее: сравниваем объекты в питоне
Британские учёные, известные своими исследованиями, решили составить общемировой рейтинг животных. Согласно методике, каждая зверюга описывается четырьмя атрибутами:
С методикой разобрались, осталось всех сравнить и построить итоговый рейтинг:
Вот досада, по умолчанию питон не знает, как сравнивать зверей. Дальше к нашим услугам куча способов, как это сделать.
Если хотим просто отсортировать список, подойдёт аргумент
Если сортировать только по ЧСВ, можно даже свою функцию не писать — в модуле
Френк ожидаемо победил в обеих номинациях. Чёртова птица.
#stdlib
Британские учёные, известные своими исследованиями, решили составить общемировой рейтинг животных. Согласно методике, каждая зверюга описывается четырьмя атрибутами:
class Pet:
def __init__(self, type, name, weight, importance):
self.type = type
self.name = name
self.weight = weight
self.importance = importance
def __repr__(self):
return self.name
weight
— это вес в килограммах, а importance
— чувство собственной важности (единица измерения не сообщается). Общий ранг рассчитывается как (weight + importance)
.С методикой разобрались, осталось всех сравнить и построить итоговый рейтинг:
frank = Pet(type="голубь", name="Френк", weight=1, importance=100)
claire = Pet(type="лиса", name="Клер", weight=5, importance=90)
zoe = Pet(type="свинка", name="Зоя", weight=90, importance=10)
pets = [claire, frank, zoe]
sorted(pets)
TypeError: '<' not supported between instances of 'Pet' and 'Pet'
Вот досада, по умолчанию питон не знает, как сравнивать зверей. Дальше к нашим услугам куча способов, как это сделать.
Если хотим просто отсортировать список, подойдёт аргумент
key
в функции sorted
:
key = lambda x: x.weight + x.importance
sorted_pets = sorted(
pets,
key=key,
reverse=True
)
print("Рейтинг по методике:")
print(sorted_pets)
Рейтинг по методике:
[Френк, Зоя, Клер]
key
— это функция, которая принимает наш объект, а возвращает число. Функция sorted
упорядочивает список объектов именно по этим числам.Если сортировать только по ЧСВ, можно даже свою функцию не писать — в модуле
operator
есть готовая:
import operator
key = operator.attrgetter("importance")
sorted_pets = sorted(
pets,
key=key,
reverse=True
)
print("Рейтинг по ЧСВ:")
print(sorted_pets)
Рейтинг по ЧСВ:
[Френк, Клер, Зоя]
Френк ожидаемо победил в обеих номинациях. Чёртова птица.
#stdlib
Сравнение объектов: кортежи
Опубликованный рейтинг зверей подвергся разгромной критике Фонда защиты дикой природы. Недопустимо складывать ЧСВ с килограммами, возмущаются эксперты.
Британских учёных такими мелочами не смутишь. Они выпустили новую редакцию методики. Теперь, уважая право животных на самоопределение, мы должны прежде всего сравнивать их ЧСВ. И только если ЧСВ одинаковое, сравнивать вес.
Вопрос, как это реализовать. Раньше у нас была простая функция:
Как поменять её на «сначала ЧСВ, затем вес»? Очень просто — с помощью кортежа:
Питон сравнивает кортежи поэлементно, причём переходит к следующему элементу, только если предыдущие одинаковые:
Именно то, что нам надо! А что, если сделать весь объект
Теперь не приходится указывать
#stdlib
Опубликованный рейтинг зверей подвергся разгромной критике Фонда защиты дикой природы. Недопустимо складывать ЧСВ с килограммами, возмущаются эксперты.
Британских учёных такими мелочами не смутишь. Они выпустили новую редакцию методики. Теперь, уважая право животных на самоопределение, мы должны прежде всего сравнивать их ЧСВ. И только если ЧСВ одинаковое, сравнивать вес.
Вопрос, как это реализовать. Раньше у нас была простая функция:
key = lambda x: x.weight + x.importance
Как поменять её на «сначала ЧСВ, затем вес»? Очень просто — с помощью кортежа:
key = lambda x: (x.importance, x.weight)
Питон сравнивает кортежи поэлементно, причём переходит к следующему элементу, только если предыдущие одинаковые:
(1, 10) < (2, 1)
True
(1, 10) > (1, 1)
True
Именно то, что нам надо! А что, если сделать весь объект
Pet
кортежем?
from collections import namedtuple
Pet = namedtuple("Pet", ("importance", "weight", "name", "type"))
frank = Pet(...)
claire = Pet(...)
zoe = Pet(...)
pets = [claire, frank, zoe]
sorted_pets = sorted(pets, reverse=True)
print([p.name for p in sorted_pets])
['Френк', 'Клер', 'Зоя']
Теперь не приходится указывать
key
— питон сортирует зверей как кортежи. Бонусом получили возможность сравнивать объекты напрямую:
claire > zoe
True
#stdlib
Сравнение объектов: дандеры
На презентации второй версии отчёта Френк нагадил на научного руководителя проекта. Пребывая в крайне дурном настроении, тот зачем-то полез в исходники и обнаружил там нашу реализацию через
Брызжа слюной, научрук отверг её как «необъектную». Попытки объяснить ему, что в питоне всё — объект не увенчались успехом. Придётся переделывать на явное использование
Окей, вот наш класс:
Сравнение на нём, естественно, не работает:
А чтобы заработало — придётся перекрыть специальные методы (они же дандеры), которые отвечают за сравнение:
Поскольку перекрывать все шесть методов лениво, можно воспользоваться декоратором
Для краткости я опустил проверку, что
Теперь будет работать и обычное сравнение, и сортировка:
Такой способ создания «сравнибельных» объектов всем хорош. Проверен временем, его ещё Рюрик в битве при Фермопилах с ацтеками использовал. Но есть и более молодёжный. О нём в следующий раз.
#stdlib
На презентации второй версии отчёта Френк нагадил на научного руководителя проекта. Пребывая в крайне дурном настроении, тот зачем-то полез в исходники и обнаружил там нашу реализацию через
namedtuple
.Брызжа слюной, научрук отверг её как «необъектную». Попытки объяснить ему, что в питоне всё — объект не увенчались успехом. Придётся переделывать на явное использование
class
, а то не отстанет.Окей, вот наш класс:
class Pet:
def __init__(self, name, weight, importance):
self.name = name
self.weight = weight
self.importance = importance
Сравнение на нём, естественно, не работает:
frank = Pet(...)
claire = Pet(...)
zoe = Pet(...)
claire > zoe
TypeError: '>' not supported between instances of 'Pet' and 'Pet'
А чтобы заработало — придётся перекрыть специальные методы (они же дандеры), которые отвечают за сравнение:
__lt__ (<)
__le__ (<=)
__eq__ (==)
__ne__ (!=)
__gt__ (>)
__ge__ (>=)
Поскольку перекрывать все шесть методов лениво, можно воспользоваться декоратором
@functools.total_ordering
и перекрыть только два — eq
и ещё один (например, lt
). Остальное декоратор сделает сам.
@functools.total_ordering
class Pet:
...
@property
def rank(self):
return (self.importance, self.weight)
def __eq__(self, other):
return self.rank == other.rank
def __lt__(self, other):
return self.rank < other.rank
Для краткости я опустил проверку, что
other
обладает нужными свойствами.Теперь будет работать и обычное сравнение, и сортировка:
claire > zoe
True
sorted_pets = sorted(pets, reverse=True)
print(sorted_pets)
[Френк, Клер, Зоя]
Такой способ создания «сравнибельных» объектов всем хорош. Проверен временем, его ещё Рюрик в битве при Фермопилах с ацтеками использовал. Но есть и более молодёжный. О нём в следующий раз.
#stdlib
Сравнение объектов: финал
Плохие новости: британские учёные не угомонились. Теперь каждый релиз должен проходить согласование в архитектурном комитете. И наш последний вариант с total_ordering и дандерами комитет забраковал.
Обоснование, цитирую: «решение недостаточно инновационное» (если когда-нибудь наблюдали работу архитектурного комитета в жизни, вас такой дичью не удивить). В приватной беседе архитекторы намекнули, что следует использовать новые фичи языка. Ну что ж, расчехляем датаклассы.
Датаклассы! Эта абсолютно анти-pythonic штука, которая нарушает сразу несколько постулатов питонячьего дзена. Но реализация с ними будет более компактной, тут не поспоришь. Вот она:
А вот как это работает:
— декоратор
— параметр
— сравнение производится по полям в том порядке, как они перечислены в классе (importance, weight, name)
—
Дальше можно сравнивать объекты как обычно:
Инновационнее некуда. Надеюсь, архитекторы будут довольны. Хотя лично мне этот вариант нравится меньше всех предыдущих.
Теперь вы знаете о сравнении объектов больше, чем 90% питонистов. Осталась ещё пара нюансов — они выйдут в режиссёрской версии от Френка за 99₽ в семечковом эквиваленте.
#stdlib
Плохие новости: британские учёные не угомонились. Теперь каждый релиз должен проходить согласование в архитектурном комитете. И наш последний вариант с total_ordering и дандерами комитет забраковал.
Обоснование, цитирую: «решение недостаточно инновационное» (если когда-нибудь наблюдали работу архитектурного комитета в жизни, вас такой дичью не удивить). В приватной беседе архитекторы намекнули, что следует использовать новые фичи языка. Ну что ж, расчехляем датаклассы.
Датаклассы! Эта абсолютно анти-pythonic штука, которая нарушает сразу несколько постулатов питонячьего дзена. Но реализация с ними будет более компактной, тут не поспоришь. Вот она:
from dataclasses import dataclass, field
@dataclass(order=True)
class Pet:
importance: int
weight: int
name: str = field(compare=False)
А вот как это работает:
— декоратор
dataclass
генерит кучу дандеров для класса, включая eq— параметр
order=True
заставляет его дополнительно сгенерить дандеры lt, le, gt, ge— сравнение производится по полям в том порядке, как они перечислены в классе (importance, weight, name)
—
field(compare=False)
исключает поле name из сравнения, так что сравнивается только (importance, weight)Дальше можно сравнивать объекты как обычно:
frank = Pet(...)
claire = Pet(...)
zoe = Pet(...)
frank > claire > zoe
True
Инновационнее некуда. Надеюсь, архитекторы будут довольны. Хотя лично мне этот вариант нравится меньше всех предыдущих.
Теперь вы знаете о сравнении объектов больше, чем 90% питонистов. Осталась ещё пара нюансов — они выйдут в режиссёрской версии от Френка за 99₽ в семечковом эквиваленте.
#stdlib