Коробка с питоном
539 subscribers
45 photos
119 links
Заметки от Python-разработчика: сниппеты, обзоры пакетов, новости и другая полезная информация.

Автор: @kiriharu
Download Telegram
На второе октября намечен релиз Python 3.12, поэтому Никита Соболев (opensource разработчик и контрибьютор в сpython) рассказывает про новинки в новой версии и чуть затрагивает то, что ожидает нас в 3.13.

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

#посмотреть
Недавно пришлось писать конвертор который транслировал Markdown в довольно специфичный формат.

Была следующая идея - мне нужен парсер, который перегонит Markdown в синтаксическое дерево, которое я буду обходить.
С обычным python-markdown в этом плане банально неудобно работать - у него нет простого и расширяемого API (как минимум), который бы позволял решить мою задачу.

А вот у mistletoe такое API есть. Например, вот так выглядит рендерер markdown в jira:

class JiraRenderer(BaseRenderer):

def render_strong(self, token):
template = '*{}*'
return template.format(self.render_inner(token))

def render_emphasis(self, token):
template = '_{}_'
return template.format(self.render_inner(token))

Ещё mistletoe можно использовать как утилиту. Например, вот так markdown-файл можно перевести в тот же формат Jira:

mistletoe foo.md --renderer mistletoe.contrib.jira_renderer.JiraRenderer

Поэтому, если вам нужна небольшая, но расширяемая библиотека для работы с markdown - берите её, не прогадаете.

#библиотека
Пока я спал, руководящий совет языка принял PEP 703 (Making the Global Interpreter Lock Optional in CPython).

Кратко о том, о чём говорится в посте:

1. Руководящему совету ясно, что несмотря на все проблемы и недостатки потоков, nogil будет полезен для Python, так как позволит находить более масштабируемые решения.
2. В то же время, они не уверены, получится ли убрать GIL не сломав при этом обратную совместимость - всё же не хотелось бы терять десятилетия развития базы пакетов. Существующая пакетная экосистема - это одна из сильных сторон языка, как и простая интеграция библиотек на C c CPython.
3. Оценить влияние nogil без реализации сложно, поэтому nogil должен выпускаться в составе регулярных релизов и не обязательно он там должен быть по-умолчанию.
4. Это всё ещё не гарантированная история. Если что-то пойдет не так - от изменений откажутся. Развёртывание должно быть постепенным и наиболее плавным.
5. Выкатка будет происходить в 3 фазы, которые возможно изменятся:
- В первой фазе nogil сделают возможным таргетом при сборке, чтобы разработчики могли тестировать свои пакеты.
- Во второй фазе, когда изменения в API и ABI будут сформированы, а поддержка nogil от сообщества будет достаточной, nogil-сборку добавлят как "поддерживаемую, но не по умолчанию".
- В третьей фазе nogil-сборку сделают сборкой "по-умолчанию", а от gil-сборки будут отказываться.
6. При успешной реализации nogil, ожидается падение производительности на 10-15% в худшем случае.

#pep
Коробка с питоном
Если обычного itertools вам мало, то можно использовать more-itertools. Эта библиотека добавляет огромное количество функций для работы с итераторами. На практике всеми ими не всегда пользуются, поэтому я выделю некоторые из тех, которые сам использую часто.…
На сегодня расскажу ещё пару рецептов с more_itertools.

1) map_if работает как обычный map, но применяет функцию на элемент только если оно попадает под условие. Например, вот так мы можем возвести в квадрат только те числа, которые делятся на 2 нацело:

example = [1, 2, 3, 4, 5, 6, 7, 8]
list(map_if(example, lambda x: x % 2 == 0, lambda x: x * x)) # [1, 4, 3, 16, 5, 36, 7, 64]


2) Получить последний элемент можно при помощи last. Возникает вопрос а зачем он существует, если можно указать sequence[-1]? Ответом является то, что last позволяет указать, что ему возвращать, если элементов в коллекции нет:

last([1, 2, 3]) # Очевидно получим 3 
last([], 0) # Список пустой, но получим 0
[][-1] # Получим IndexError


Ещё есть first - как понятно из названия, он получает первый элемент.

3) map_except тоже работает как map, но умеет игнорировать ошибки. Например, мы хотим получить только те элементы, которые получилось привести к целому числу:

example = [1, "1", "2", "test", "three", object, 4.0]
list(map_except(int, example, ValueError, TypeError)) # [1, 1, 2, 4]


4) Ну и в конце про take - он просто берет N элементов из итерируемого объекта:

example = range(10)
take(3, example) # [0, 1, 2]
take(20, example) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - если больше, возьмет доступные


#itertools #more_itertools #библиотека #рецепт
В одном чатике встретил проект fakeredis, реализующий Redis-сервер на питоне, который можно использовать для тестов. Очевидно, чтобы не таскать для тестов настоящий Redis.

Поддерживаются практически все стандартные команды, которые я встречал для работы с типами/коллекциями (список здесь). Есть не полная поддержка JSON, поиска, time series.
Если каких-то команд не хватает, автор оставил гайд, как её сделать. Ну а если сделаете - законтрибутьте в опенсорс, полезное дело всё таки!

#библиотека
PSF и JetBrains запустили своё ежегодное исследование Python Developers Survey 2023. Погнали заполнять! 👇

https://survey.alchemer.com/s3/7554174/python-developers-survey-2023

Через год, когда подведут результаты, будет повод написать пост, типа такого 😅
Да, у нас есть тесты. А толку?

Супер-мега-гига экслюзивный доступ к записи моего доклада с прошедшего PiterPy. Организаторы разрешили выложить не дожидаясь следующего года. Получилось как всегда холиварно, не всё всем может понравиться, но как минимум советую присмотреться к библиотечкам и статьям/докладам из полезных ссылок. Там немного, но самое годное из того что знаю по этой теме. Куда кидать помидоры и звездочки вы знаете 🙂🙃

https://youtu.be/liECQCFGfkE
Решил расширить канал ещё одной тематикой - занимательными задачками.
Пока что буду писать про те, которые встречались на тех. собеседованиях. Они не всегда будут адекватные, но что уж есть :)

А начнём, как полагается с классики. Надо объяснить следующее поведение:

>>> a = 256
>>> b = 256
>>> a is b
True # ???

>>> a = 257
>>> b = 257
>>> a is b
False # ???


Вопрос в том, что здесь творится с ссылками. Разберём самую первую часть. Пробуем получить id объектов:

>>> a = 256
>>> b = 256
>>> id(a), id(b)
(2214170730704, 2214170730704)


На вопрос, почему у них одинаковые идентификаторы ответит деталь реализации PyLong_FromLong (для искушенных читать можно читать отсюда), которая указывает, что интерпретатор хранит массив целочисленных объектов для всех чисел в диапазоне от -5 до 256. Поэтому, когда мы создаем переменную с числом в этом диапазоне он просто отдает ссылку на уже существующий объект.
Микрооптимизация, при чём очень важная - так уж получилось что числа из этого диапазона используются чаще всего.
В Java есть похожая оптимизация, там такой диапазон составляет от -128 до 127, но есть нюансы.

Второй вопрос отпадает сам собой (будут разные ссылки), но что будет если мы создадим файл с следующим содержимым и запустим его:

a = 257
b = 257
print(a is b) # True


А вот это уже нюанс работы нашего REPL.
Каждая написанная нами строка в нём разбирается отдельно. Но при запуске через файл Python имеет возможность применить дополнительные оптимизации для констант, так как он видит сразу весь код - в этом и различие.

А какие ещё неочевидные моменты вы знаете с REPL или int'ами? Пишите в комменты, обсудим :)

#std #задачки
Сегодня у нас простенькая задачка, а то пятница, все отдыхать хотят, я понимаю.

Есть следующий код:

def test():
try:
return 1
finally:
return 2


Вопрос - что вернется при вызове test()? Все и так на этом моменте понимают, что вернётся 2 (ну не просто так же мы собрались, верно?), но почему?

Ответ, как обычно, есть в документации. Возвращаемое функцией значение определяется последним выполненным return.
Вторым важным аспектом является то, что finally исполняется всегда, поэтому мы и получаем его return.

raise, кстати, тоже работать не будет:

def test():
try:
raise ValueError()
finally:
return 3

test() # 3


#std #задачки
Автор Pydantic совершенно недавно начал разрабатывать ещё один проект... И на этот раз он решил сделать UI фронтенд-фреимворк который назвал FastUI.

Если отвечать на вопрос "зачем", то в readme изложены вот такие поинты:
1) Если не хочется разбираться в JS/фронтенд фреимворках и всей их экосистеме;
2) Если хочется декларативно писать фронтенд на питоне, с переиспользуемыми компонентами;
3) FastUI - противоположность GraphQL, но у них одинаковые цели: когда GraphQL позволяет расширять фронтенд без расширения бекенда, FastUI предлагает расширять бекенд без разработки фронтенда;

Под капотом у нас:
- Сам пакет FastUI, который содержит утилиты и сами компоненты. Cами компоненты описаны с использованием Pydantic.
- Так как это всё это работает на react, есть npm пакет @pydantic/fastui, который позволяет повторно использовать механизмы и типы FastUI при реализации собственных компонентов.
- Bootstrap компоненты для FastUI.
- @pydantic/fastui-prebuilt который предоставляет пребилд основной части фронтенда.

Вкратце, как это работает - при помощи Pydantic моделей описываются интерфейсы React компонентов, а дальше их описания отправляются на фронтенд, где по ним отрисовываются компоненты.

Само описание небольшой страницы выглядит как-то так:

c.Page(
components=[
c.Heading(text=user.name, level=2),
c.Link(components=[c.Text(text='Back')], on_click=BackEvent()),
c.Details(data=user),
]
)


Простенько, лаконично, декларативно. Но такой же подход использует Flet, Dash и ещё куча других фреимворков. Очень интересно, взлетит ли это или нет?

Забавно, что мы не кодим на Rust, но всё у нас "fast" - FastAPI, FastUI, FastStream :D

#библиотека
С наступившим 2024 вас! Сегодня расскажу про dirty-equals.

Этот пакет используется для реализации более читаемых и декларативных проверок на равенство/соответствие, но в полной мере он себя раскрывает при написании тестов.
Как обычно, приведу несколько примеров:

from dirty_equals import *

>>> assert 1 == IsPositive # Всё ок, число положительное
>>> assert -2 == IsPositive # AssertionError
>>> # Проверки можно комбинировать при помощи булевой логики
>>> assert ['a', 'b', 'c'] == HasLen(3) & Contains('a') # Ок, список на 3 элемента, содержит 'a'
>>> # Есть проверка словарей
>>> assert {'a': 1, 'b': 2} == IsDict(a=1, b=2)
>>> # Словари можно проверять частично
>>> assert {'a': 1, 'b': 2, 'c': 3} == IsDict(a=1, b=2).settings(partial=True)
>>> # Или вот так
>>> assert {'a': 1, 'b': 2, 'c': 3} == IsPartialDict(a=1, b=2)
>>> Можно проверять соответствие регуляркам
>>> assert 'test@kiriha.ru' == IsStr(regex=r'^[^@]+@[^@]+\.[^@]+$')
>>> # И так далее...


Рекомендую заглянуть в документацию, там таких удобных штук ещё очень много! Ну и изучите исходники - написано не сложно, под капотом используется чуток метаклассов, для изучения темы - самое то!

#библиотека
Тренажер по тайпингу в Python для самых маленьких (и не только)

Python Type Challenges, очень прикольный сайт-тренажер для изучения типизации. Выбираешь тему, дописываешь необходимые куски кода и запускаешь проверку линтером. Почти как олимпиадные задачки, только полезные😁

Для некоторых задач доступны подсказки со ссылками на документацию.

Проверить свои знания по типам сюда
Посмотреть исходный код сюда
Совсем скоро (релиз запланирован на конец января - начало февраля) выйдет NumPy 2. Список изменений можно прочитать здесь, а информацию по миграции - здесь. Статус и анонсы можно читать здесь.

Изменений в API очень много, поэтому рекомендую зайти в свои проекты и зафиксировать зависимости.
По большей части релиз выглядит как рефакторинг API.

Некоторые функции, модули, константы переместили или удалили, чтобы сделать неимспейсы более удобными и чистыми. Про это можно прочитать в NEP 52.
Релиз так же затронет юзеров с Виндой - теперь int в numpy 64х-разрядный (для 32х систем - 32х-разрядный), хотя ранее, по историческим причинам, связанных Python 2, был эквивалентен типу long в C.
Подписчиков набежало, а постов всё так же не было. Буду исправляться!

Сегодня расскажу про deptry - тулзу, которая позволяет находить неиспользованные, транзитивные или отсутствующие зависимости в проекте на Python.
Работает это следующим образом - запускаем утилиту в каталоге с проектом, она сканит все импортированные модули и сравнивает их с тем, что описано в зависимостях. Умеет работать с любым пакетным менеджером который строго следует PEP 621, а так же pyproject.toml от Poetry и PDM. Ну и обычный requirements.txt тоже поддерживается!

Где это может быть полезно? Для себя выявил несколько сценариев:
1. Получили старый проект без зависимостей и пытаемся собрать либы, которые он использует (много раз такое было на фрилансе).
2. Проводим глубокий рефакторинг, трекаем ненужные зависимости и таким образом избавляемся от них.
3. Если кто-то напихал нормальные зависимости в качестве dev зависимостей, утилита тоже это покажет.
4. Отлично детектит использование транзитивных зависимостей - это такие пакеты, которые мы не устанавливаем напрямую, но которые устанавливаются при установке другого пакета как его зависимость. Самый простой пример - установка sqlparse при установке Django. Такие зависимости должны быть указаны явно.
5. Добавляем в CI и время от времени смотрим отчеты, таким образом поддерживаем зависимости в нормальном виде.

Настраивается достаточно гибко, документация лежит здесь.

#утилита
Dishka - IoC-контейнер для Python

Когда мы следуем подходу Dependency Injection, а особенно - слоистой архитектуре, у нас образуется отдельная группа функций и классов, выполняющих только одну задачу - создание других объектов. Такой код лучше держать поближе к main, так как он связывает воедино разные части приложения и связан с конфигурацией запуска.

В сложном приложении такой компонент может содержать большое количество функций, контролировать как создание, так и корректную очистку объектов и, что самое главное, их взаимосвязь. Для упрощения работы с такими фабриками придумали отдельный тип библиотек - IoC-контейнеры (DI-фреймворки).

В Python меня долго не устраивали существующие контейнеры и я решил сделать свой:

Хочу представить вам Dishka 1.0

Цель этого проекта - предоставить простой и удобный IoC-контейнер, который сможет забрать всю работу с зависимостями. Мне кажется, на текущий момент это самый функциональный вариант контейнера, имеющий при этом самое простое API.

• Вы можете использовать его с любым фреймворком, но для некоторых мы уже подготовили хелперы
• Для создания зависимости можно указать отдельную функцию или использовать __init__ класса
• Зависимости имеют ограниченное время жизни (скоуп) и вы сами управляете им
• Зависимости кэшируются, поэтому один и тот же объект может быть переиспользован пока он жив. Так можно передать одно соединение с БД в несколько гейтвеев
• Фабрики зависимостей можно группировать в классы и компоненты, что позволяет делать контейнер модульным
• Можно декорировать объекты, использовать один объект для нескольких типов
• При старте проверяется корректность конфигурации контейнера, что позволяет исключить многие ошибки

Что значит версия 1.0?

У библиотеки было 9 промежуточных релизов, мы ради объявить, что закончена вся работа по стабилизации её интерфейса и исправлению ошибок. И у нас есть планы по развитию, уникальные фичи сами себя не напишут.

Будем рады новым пользователям, багрепортам, запросам фич и звездам на гитхабе

Github
Pypi
Документация
Там в Python 3.12 добавили нашумевший PEP 659, а у меня пет-проект один давно не обновлялся, и так уж звёзды сошлись, что я сижу второй день обновляю его на 3.12

Задача - есть функционал, который под капотом имеет некоторый класс следующего вида:

class BaseFunction:
serialize_to: None

def serialize(self, data: dict) -> serialize_to:
pass # тут мы используем наш serialize_to

@dataclass
class ModelA:
x: str

class FunctionA(BaseFunction):
serialize_to: ModelA


Мы определяем новые классы наследуясь от BaseFunction, переопределяем в них serialize_to и вызываем serialize который делает нам инстанс serialize_to.

Ну прямо дженерик напрашивается! Тем более в 3.12 их завезли, красивые:

class BaseFunction[T]:
def serialize(self, data: dict) -> T:
pass # тут мы используем наш serialize_to

class FunctionA(BaseFunction[ModelA]):
pass


Встаёт вопрос, а как нам получить наш тип из дженерика?

Для начала, получим __orig_bases__[0] - он вернёт нам классы, от которых мы наследовались. Так как нам нужен только наш первый класс, мы указываем [0]:

>>> FunctionA.__orig_bases__
__main__.BaseFunction[__main__.ModelA]


Ещё можно это сделать с помощью get_original_bases из types, но его добавили только в 3.12 (почему я об этом сказал - узнаете ниже).

Теперь надо получить получить сам тип в дженерике. В этом нам поможет typing.get_args, который получает все аргументы типа. Дополнительно укажем, что нам нужен первый тип:

>>> get_args(FunctionA.__orig_bases__[0])[0]
<class '__main__.ModelA'>


Теперь в методе serialize класса BaseFunction[T] можно написать штуку, которая автоматически сериализует наши данные:

def serialize(self, data: dict) -> T:
type_from_generic = get_args(self.__class__.__orig_bases__[0])[0]
return type_from_generic(**data)


Проверяем:

>>> f = FunctionA()
>>> f.serialize(data={"x": 1})
ModelA(x=1)

Вы восхитительны!

Кстати, эта же штука должна работать ещё вроде как аж с 3.8, так как в нём именно был добавлен __orig_bases__ (PEP 560), ну и под капотом у новых дженериков используется...

>>> FunctionA.__mro__
(<class '__main__.FunctionA'>, <class '__main__.BaseFunction'>, <class 'typing.Generic'>, <class 'object'>)


Ага, typing.Generic :)

#рецепт #std
Да, оно существует.

Под капотом низкоуровневая магия, из самого понятного - оно позволяет создавать поддельные объекты на куче. Можно создать байтовый массив с базовым адресом 0 и длинной SSIZE_MAX, что даст доступ к сырой памяти, на чтение и запись.

Такая техника, кстати, используется в эксплоитах движков javascript. Парочкой похожих эксплоитов уже давно хакают PS4.
Итак, ко мне пришли ребята из Podlodka Python Crew и предложили разыграть один бесплатный билет на новый сезон их конференции, где участников с 3 по 7 июня будет ждать целая неделя онлаин-воркшопов и много докладов по теме инфраструктуры.

Из интересного:
— К ним придёт разработчик FastStream, про который я неоднократно рассказывал здесь, покажет насколько удобно можно работать с Kafka, RabbitMQ, NATS и другими брокерами.
— Для енжоеров микросервисной архитектуры расскажут и покажут, как гарантировать консистентную работу всей системы при помощи паттерна "transactional outbox". Будет демо-стенд и реализация на практике.
— Приложение без метрик - плохое приложение. Покажут как собирать и анализировать данные, чтобы всегда быть в курсе состояния системы.
— Так же будут истории от экспертов про фейлы с инфраструктурой и как их можно избежать.

А так же нетворкинг, много практических примеров и дискуссии!

Чтобы попытать счастье, постите под этим постом любой мем про наш любимый язык до 02.06.24. Самый залайканный станет победителем - ему и отправится билет.
А если пытать счастье не хочется - вот вам скидка в 500 рублей по промокоду `podlodka_boxwithpython`

Ждем всех, тыкай чтобы попасть 😎
Сегодня расскажу про модуль, который поможет остановить время в ваших тестах и не только - FreezeGun

Сначала история из жизни.
Недавно мне принесли скрипт, который скрапил некоторую информацию с онлаин-каталога на текущий момент. Задачей было заставить его скрапить данные до определенного момента.
В нём было очень много вызовов datetime.now() (пагинация по датам, ага), перепиливать все это мне не очень хотелось, а результат хотели вчера.

Поэтому я такой хоп:
from freezegun import freeze_time

with freeze_time("01-08-24"):
result = call_api()

И всё работает!

Но вы так не делайте, конечно, это самый настоящий подпор костылём, мне вообще повезло, что оно заработало.
Или делайте если надо, разрешаю.

В основном этот пакет используют для тестов и как было понятно с примера выше - он позволяет "заморозить" время на определенном моменте.
Проверить истечение подписки, корректность фильтров, расчёт на определенное время - этот пакет позволит сделать всё это всего в пару строк кода!

Есть возможность использовать декоратор вместо контекстного менеджера:

@freeze_time("2012-01-14")
def test():
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)


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

freezer = freeze_time("2012-01-14 12:00:01")
freezer.start()
assert datetime.datetime.now() == datetime.datetime(2012, 1, 14, 12, 0, 1)
freezer.stop()


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