Oh My Py
2.72K subscribers
21 photos
55 links
Все о чистом коде на Python // antonz.ru
Download Telegram
Новости стандартной библиотеки

Когда выходит очередная версия Python, все внимание достается новым фичам языка: моржовому оператору, слиянию словарей, паттерн-матчингу. Еще много пишут об изменениях в асинхронной работе (модуль asyncio) и типизации (модуль typing) — эти модули на виду и бурно развиваются.

Остальным модулям стандартной библиотеки достается незаслуженно мало внимания. Хочу это исправить и рассказать, что интересного появилось в версиях 3.8–3.10.

Планировал небольшую заметку, но не преуспел: получилась здоровенная статья. Старался выбрать только самое интересное, но все равно в обзор попали аж 17 модулей. Питон, он такой 😁

Читайте на хабре:
https://habr.com/ru/post/665020/

P.S. Для каждого описанного новшества в статье есть ссылка на интерактивную песочницу (например, graphlib)! Спасибо Степику за нее ❤️
Компактные объекты

Питон — объектный язык. Это здорово и удобно, пока не придется создать 10 млн объектов в памяти, которые благополучно ее и съедят. Поговорим о том, как уменьшить аппетит.

Допустим, есть у вас простенький объект «питомец» с атрибутами «имя» (строка) и «стоимость» (целое). Интуитивно кажется, что самое компактное предоставление — в виде кортежа:

("Frank the Pigeon", 50000)


Замерим, сколько займет в памяти один такой красавчик:

def fields():
# генерит name длины 10
# и price до 100К
# ...
return (name, price)

n = 10_000
pets = [fields() for _ in range(n)]
size = round(asizeof(pets) / n)
print(f"Pet size (tuple) = {size} bytes")


Pet size (tuple) = 161 bytes


161 байт. Будем использовать как основу для сравнения.

С чистыми кортежами, конечно, работать неудобно. Наверняка вы используете датакласс:

@dataclass
class PetData:
name: str
price: int


А что у него с размером?

Pet size (dataclass) = 257 bytes
x1.60 to baseline


Ого, какой толстенький!

Попробуем использовать именованный кортеж:

class PetTuple(NamedTuple):
name: str
price: int


Pet size (named tuple) = 161 bytes
x1.00 to baseline


Теперь вы понимаете, за что я его так люблю. Удобный интерфейс как у датакласса — а вес как у кортежа. Идеально.

Или нет? В Python 3.10 приехали датаклассы со слотами:

@dataclass(slots=True)
class PetData:
name: str
price: int


Pet size (slots dataclass) = 153 bytes
x0.95 to baseline


Ого! Магия слотов создает специальные худощавые объекты, у которых внутри нет словаря, в отличие от обычных питонячих объектов. И такой датакласс ничуть не уступает кортежу.

Что делать, если 3.10 вам еще не завезли? Использовать NamedTuple. Или прописывать слоты вручную:

@dataclass
class PetData:
__slots__ = ("name", "price")
name: str
price: int


У слотовых объектов есть свои недостатки. Но они отлично подходят для простых случаев (без наследования и прочих наворотов).

P.S. Конечно, настоящий победитель — numpy-массив. Но с ним неинтересно соревноваться ツ

песочница

#stdlib
Компактные (и не очень) объекты в Python
Великий Рандом

Все знают про random.randint(a, b), который возвращает случайное число в указанном диапазоне:

random.randint(10, 99)
# 59


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

Например, можно выбрать из диапазона с шагом:

random.randrange(10, 99, 3)
# 91


Или случайный элемент последовательности:

numbers = [7, 9, 13, 42, 64, 99]
random.choice(numbers)
# 42


А то и несколько элементов:

numbers = range(99, 10, -1)
random.choices(numbers, k=3)
# [32, 62, 76]


Можно еще и веса элементам назначить — чтобы одни выбирались чаще других:

numbers = [7, 9, 13, 42, 64, 99]
weights = [10, 1, 1, 1, 1, 1]

random.choices(numbers, weights, k=3)
# [42, 13, 7]

random.choices(numbers, weights, k=3)
# [7, 7, 7]

random.choices(numbers, weights, k=3)
# [13, 7, 7]


Хотите выборку без повторений? Нет проблем:

numbers = [7, 9, 13, 42, 64, 99]
random.sample(numbers, k=3)
# [42, 99, 7]


Или можно всю последовательность перемешать:

numbers = [1, 2, 3, 4, 5]
random.shuffle(numbers)
# [3, 2, 1, 5, 4]


И напоследок. Если используете случайные числа в тестах, всегда инициализируйте генератор константой, чтобы он давал воспроизводимые результаты:

random.seed(42)


А в продакшене, наоборот, вызывайте seed() без параметров — так питон использует генератор шума операционной системы (или текущее время, если его нет).

#stdlib
Задачка: летающая свинья

Допустим, вы написали утилиту, которая отправляет что угодно в полет:

def launch(thing):
thing.fly()


Ну то есть не прям все что угодно, а любую штуку с методом .fly(). Очень удобно: одной функцией запускаем и голубя Френка, и самолет, и даже Супермена:

class Frank:
def fly(self):
print("💩")


class Plane:
def fly(self):
print("Рейс задержан")


class Superman:
def fly(self):
print("ε===(っ≧ω≦)っ")


Вжух:

f = Frank()
launch(f)
# 💩

p = Plane()
launch(p)
# Рейс задержан

s = Superman()
launch(s)
# ε===(っ≧ω≦)っ


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

Работает, и ладно. Но иногда (особенно если программа разрастается) разработчику хочется добавить немного строгости. Дать понять, что параметр thing в launch() — это не любой объект, а обязательно летающая хреновина с методом fly(). Как лучше это сделать?

Опрос следует.

песочница

#задачка
Вариант «и так сойдёт» не требует дополнительных пояснений :–) А по остальным напишу отдельно.

В комментарии

Если вы привыкли избегать типов, то скорее всего обойдетесь именем переменной или комментарием к функции:

def launch(flyer):
"""Launces a flyer (an object with a `fly()` method)"""
flyer.fly()

Почему бы и нет. Беда в том, что чем сложнее код, тем чаще сбоит «описательный» подход.
Через базовый класс

Если расчехлить навыки программирования на джаве из 1990-х годов, получится небольшая иерархия:

class Flyer:
def fly():
...

class Frank(Flyer):
# ...

class Plane(Flyer):
# ...

class Superman(Flyer):
# ...


def launch(flyer: Flyer):
flyer.fly()


Подход рабочий:

$ mypy flyer.py
Success: no issues found in 1 source file


Но, как говорят авторы Python, ужасно «непитоничный»:

The problem is that a class has to be explicitly marked, which is unpythonic and unlike what one would normally do in idiomatic dynamically typed Python code.

Действительно. Мало того, что нам пришлось модифицировать три класса вместо одной функции. Мало того, что у нас в коде завелась иерархия наследования.

Так еще и Френк, самолет и Супермен теперь объединены общим знанием о том, что они Летающие Объекты. Им прекрасно жилось и без этого, знаете ли.
Через протокол

Цитата выше взята из PEP 544 (Python Enhancement Proposal, предложение о доработке), который был реализован в Python 3.8. Начиная с этой версии в питоне появились протоколы.

Протокол описывает поведение без реализации (в других языках такие штуки обычно называют интерфейсами). Вот наш Летающий Объект:

from typing import Protocol

class Flyer(Protocol):
def fly(self):
...


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

def launch(thing: Flyer):
thing.fly()


Причем самим объектам не надо знать о протоколе. Достаточно обладать нужным поведением:

class Frank:
def fly(self):
# ...

class Plane:
def fly(self):
# ...

class Superman:
def fly(self):
# ...


Протокол — это статическая утиная типизация:

— интерфейс явно описан в протоколе: летающий объект обладает методом fly();
— но реализуется он неявно, по «утиному» принципу: у Супермена есть метод fly() — значит он летающий объект.

Проверим:

$ mypy flyer.py
Success: no issues found in 1 source file


Идеально!
Многозначительное многоточие

Не знаю, заметили вы или нет в посте о протоколах. Не самая известная штука:

class Flyer:
def fly(self):
...


Это вполне рабочий код. В питоне ... (он же Ellipsis) — это реальный объект, который можно использовать в коде.

Ellipsis — единственный экземпляр типа EllipsisType (аналогично тому, как None — единственный экземпляр типа NoneType):

>>> ... is Ellipsis
True
>>> Ellipsis is ...
True


Авторы Python в основном используют ..., чтобы показать, что у типа, метода или функции отсутствует реализация — как в примере с fly().

И в тайп-хинтах:

Tuple[str, ...]
Callable[..., str]


Ну а обычные разработчики... Кто во что горазд ツ

#stdlib
Попробуйте Go

Чем больше я узнаю Python, тем больше мне нравится Go.
Альберт Эйнштейн

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

Попробуйте освоить Go в дополнение к Python, если у вас отзывается что-то из перечисленного:

— Хотите поработать с компактным языком без фичеризма и горы легаси.

— Посмотреть, как может выглядеть современная статическая типизация.

— Писать код, который действительно работает быстро (без привычных для питонистов скидок из серии «а мне и так хорошо»).

— Попробовать многозадачность здорового человека.

— Деплоить докер-контейнеры размером в 10 Мб вместо 500 Мб.

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

(минутка саморекламы)

Если вы уверенно пишете на питоне, осваивать го удобно с моим новым курсом Go на практике. Он как раз для опытных программистов, да и отсылок к питону там хватает.

Для всех питонистов на этой неделе скидка 50% по промокоду OHMYPY.

Всем Go 🐾
JSON Lines

На днях узнал про формат JSON Lines. Это такой CSV на стероидах:

— каждая запись идет отдельной строкой, как в CSV;
— но при этом представляет собой полноценный JSON.

Например:

{ "id":11, "name":"Дарья" }
{ "id":12, "name":"Борис" }
{ "id":21, "name":"Елена" }


Очень классная штука:

— Подходит для объектов сложной структуры (в отличие от csv);

— Легко потоково читать, не загружая файл целиком в память (в отличие от json);

— Легко дописывать новые записи к существующему файлу (в отличие от json).

JSON, в принципе, тоже можно читать потоково. Но посмотрите, насколько это проще с JSON Lines:

https://replit.com/@antonz/json-lines#main.py
ChatGPT-бот на Python

Последние месяцы поставили рекорд по количеству программ, интегрированных с OpenAI. Я, конечно, тоже не смог остаться в стороне.

В результате появился проект pokitoki — это чат-бот, который работает через официальное API OpenAI.

Чем (на мой глубоко субъективный взгляд) он может вам пригодиться:

— Демонстрирует, как писать несложных телеграм-ботов на питоне.

— Демонстрирует, как интегрироваться с OpenAI.

— Демонстрирует, как запускать питон-приложения в докере.

— Сознательно сделан максимально простым, чтобы можно было быстро разобраться в коде.

Ну и конечно, вы можете подключить его себе в телеграм (если сумеете зарегистрироваться в OpenAI).
ChatGPT-бот: группы, внешние ссылки, шорткаты

Сделать GPT-бота легко, а вот удобного GPT-бота — намного сложнее. Последние недели я посвятил именно этому.

Поэтому теперь pokitoki умеет вот что:

— Работает не только в личных чатах, но и в группах.
— Читает внешние ссылки (статьи, код, данные).
— Поддерживает шорткаты (команды для нейросети).
— Экономит время и действия в других частых сценариях.

Код при этом старался сохранять понятным. Есть и комментарии, и тесты.

Подробности на хабре: https://habr.com/ru/post/726692/