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

Автор: @kiriharu
Download Telegram
Разговоры шли давно, но 9ого числа представили PEP 703, который предлагает сделать GIL опциональным. Прототип опубликован в репозитории nogil.

В качестве основания используется то, что Python используют для ML, а GIL - это серьезное препятствие для параллельных вычислений.
В этом PEP так же привели работу с PEP 684, который предлагает прекратить использовать GIL между несколькими интерпретаторами в процессе.

Как думаете, могут ли действительно убрать/сделать опциональным GIL?

#pep
В Python 3.12 планируют переделать f-строки в рамках PEP 701 – Syntactic formalization of f-strings.

Дело в том, что f-строки в язык занесли в версии 3.6, когда балом правил еще старый LL(1) парсер, который было сложно расширять, поэтому для f-строк был написан отдельный маленький парсер. Из-за этого они получились слегка неполноценными, а еще в этом отдельном парсере, само собой, были отдельные новые баги. Самое яркое ограничение — это то, что внутри f-строк нельзя использовать те же кавычки, в которых лежит сама строка. Это ж капец как неудобно, что приходится другие кавычки использовать, чтобы просто достать что-то из словаря. У меня линтер на это ругается, и приходится ему каждый раз объяснять, что тут красиво никак не сделать.

Вот, в 3.12 разбор f-строк доверят основному PEG-парсеру, а сам синтаксис f-строк станет частью грамматики языка. Можно будет использовать любые кавычки, которые нравятся. А еще бесконечно вложенные f-строки. Не знаю, зачем нам это, но хорошо, что станет можно. Жду 3.12.
Когда-то давно, когда я только изучал Django, мне хотелось избавиться от бойлерплейта в темплейтах и вынести некоторые части шаблонов (например кнопки) в какие-то отдельные сущности которые можно переиспользовать - например в виде компонентов.

К счастью, в Django есть решение для этого и оно называется template tags. Но у него есть несколько проблем:
1) Необходимо пробрасывать руками js и css зависимости для конкретного тега там, где он используется.
2) Теги плохо кастомизируются, например нет возможности изменить поведение тега, обязательно нужно его переписывать.

Решением этих вопросов занимается пакет с названием django-components. Он предоставляет возможность делать простые, но в то же время мощные переиспользуемые компоненты. А как он справляется с проблемами выше?
1) При объявлении компонента будут грузиться только те js и css, которые указаны в классе компонента. Класс компонента выглядит как-то так:

from django_components import component

@component.register("calendar")
class Calendar(component.Component):
template_name = "calendar/calendar.html"

def get_context_data(self, date):
return {
"date": date,
}

class Media:
css = "calendar/calendar.css"
js = "calendar/calendar.js"

Сам js/css рендерится только там, где указаны теги component_js_dependencies и component_css_dependencies.

2) Для изменения поведения компонента можно использовать слоты - это что-то вроде django-блоков внутри компонента. Например, мы можем сделать блок body внутри компонента и изменять его вид тогда, когда нам нужно:

<div class="calendar-component">
<div class="header">
{% slot "header" %}Заголовок календаря{% endslot %}
</div>
<div class="body">
{% slot "body" %}Сегодня <span>{{ date }}</span>{% endslot %}
</div>
</div>

А теперь импортируем компонент и меняем его body:
{% component_block "calendar" date="2020-06-06" %}
{% slot "body" %}А сегодня точно <span>{{ date }}</span>?{% endslot %}
{% endcomponent_block %}

Github | PyPi

#django #библиотека
Если вы хоть раз задумывались о том, можно ли фичи FastAPI (автогенерация OpenAPI, интеграция с Pydantic, поддержка асинхронности и т.д.) добавить в Django, то я пришел вас обрадовать - есть такой проект под названием django-ninja.

Из приятных фич, которые можно встретить здесь:
1) Версионирование и возможность создания нескольких API инстансов со своей авторизацией и т.д.
2) Класс схемы интегрирован с модельками Django, поэтому можно писать что-то вроде такого:

@api.get("/tasks", response=List[TaskSchema])
def tasks(request):
return Task.objects.all()

3) Можно делать схемы из моделей, прям как в DRF.
4) Пагинация! При чем в документации показано, как можно сделать собственный кастомный класс для неё.
5) Поддержка кастомных рендереров ответов, что позволяет перевести сериализацию на ORJSON или отдавать ответы в XML, например.
6) Работа с аутентификацией из под коробки.
7) Ну и асинхронность, тем более разработчики Django работают над ней.
А в остальном он очень похож на FastAPI, особенно когда речь идет о работе с аргументами, схемами и OpenAPI.

Как по мне, выглядит как достойная замена DRF, при чем довольно простая. И об этом я говорю не просто так - в последнее время я делал проекты на FastAPI и возвращаться на Django и тем более DRF было очень непривычно.

Github | Документация

#django #библиотека
Немного про роутинг в FastAPI

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

Скорее всего первая мысль которая придет вам в голову будет звучать как-то так - если у нас есть I/O-bound задачи (например работа с БД), то надо использовать асинхронщину, если всё остальное - потоки, процессы и так далее. Но тут есть несколько нюансов:

1) Под капотом FastAPI отлично справляется с обработкой как синхронных, так и асинхронных роутов. Если роут асинхронный, то задача по его обработке запустится в event loop, если синхронный - то в thread pool.
2) Так как синхронные роуты запускаются в thread pool, иногда просто нет вообще никакого смысла тащить в проект асинхронную ORM, так как всё и так будет работать не блокируя основное приложение.

Возьмем вот такой роут:
@router.get("/nonblocking-sync-operation")
def nonblocking_sync_operation():
time.sleep(10)
return {"test": "test"}

После того как мы перейдем по этому роуту, мы будем ждать 10 секунд и в конце получим ответ. При этом сам FastAPI не заблокируется, и сможет обрабатывать другие подключения - потому что функция запустилась в отдельном потоке.

А теперь возьмем вот такой роут:
@router.get("/blocking-sync-operation")
async def blocking_sync_operation():
time.sleep(10)
return {"test": "test"}

Здесь после перехода по роуту функция запустится в event loop и sleep заблокирует всё приложение до тех пор, пока он не пройдет. То есть, FastAPI вообще перестанет принимать подключения до тех пор, пока функция не выполнится.

Поэтому, если вам нужно написать на FastAPI небольшой CRUD и вы думаете тащить асинхронную ORM - задумайтесь, а надо ли она вам там вообще?

Ссылки:
- Path operation functions

#fastapi
В PEP 695 предлагают наконец переделать синтаксис для указания дженериков и ввести новый оператор для указания алиасов.

1) Что там с дженериками?
Например, если раньше было так:
_T_co = TypeVar("_T_co", covariant=True, bound=str)

class ClassA(Generic[_T_co]):
def method1(self) -> _T_co:
...

То сейчас предлагают сделать так:
class ClassA[T: str]:
def method1(self) -> T:
...

2) А что с алиасами?
Раньше алиасы типов записывались вот так:
_T = TypeVar("_T")
ListOrSet: TypeAlias = list[_T] | set[_T]
Сейчас предлагают сделать вот так:
type ListOrSet[T] = list[T] | set[T]


Если не ошибаюсь, то ждем в 3.12, PEP уже приняли.

#pep
Оказывается в конце апреля urllib3 обновили до 2.0.0, который делали аж с 2020 года. Из очень годного:

1) Сделали функцию urllib3.request(), прям как в всеми любимом requests. Теперь работать с ним можно следующим образом:

response = urllib3.request("GET", "https://example.com")
И никаких тебе больше urlopen.

2) Упростили работу с json, теперь все выглядит следующим образом:

resp = urllib3.request(
"POST", "https://httpbin.org/anything",
# Параметр json здесь заэнкодит json в body запроса
# и установит 'Content-Type' в 'application/json'.
json={"key": "value"}
)
# а вот так теперь можно получить наш жсончк c ответа
print(resp.json())

Всё как у requests, всё как у людей.

3) Тайпинги! Хотя, было бы странно видеть такого вида библиотеку без тайпингов в 2023 году.
Плюс ко всему команда описала, с какими проблемами они столкнулись когда добавляли тайпинги, почитать здесь.

Ссылка на новость
Docker 4.19 теперь поддерживает python в docker init

docker init - это утилита, которая упрощает добавление docker в проект. Она просто пробежит по вопросам, спросит версию проекта, какой командой он запускается и сгенерирует Dockerfile и compose-файл.

Мелочь, а приятно :)

#docker
Попался на глаза отличный перевод статьи "Writing Python like it's Rust", который скорее говорит о возможностях аннотаций типов и структур которые их используют (и еще немного про контекстные менеджеры).

Очень много примеров и очень много полезных советов :)

#статья #хабр
Если обычного itertools вам мало, то можно использовать more-itertools.

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

Например, вот так можно разделить список на 3 части:

data = ["first", "second", "third", "fourth", "fifth", "sixth", "seventh"]
[list(l) for l in divide(3, data)]
# [['first', 'second', 'third'], ['fourth', 'fifth'], ['sixth', 'seventh']]

Вот еще задача. Надо разделить список с элементами по определенному условию. В этом нам поможет bucket:

class Cat:
pass
class Dog:
pass
shapes = [Cat(), Dog(), Cat(), Dog(), Cat(), Cat()]
result = more_itertools.bucket(shapes, key=lambda x: type(x))
len(list(result[Cat])) # 4
len(list(result[Dog])) # 2

По итогу кошки и собаки будут разделены по своему типу на 2 генератора.

Решим самую частую проблему - перевести список с несколькими уровнями вложенностями в "плоский" список:

iterable = [(1, 2), ([3, 4], [[5], [6]])]
list(more_itertools.collapse(iterable)) #[1, 2, 3, 4, 5, 6]

А если в плоский список нам нужно вытащить только элементы с первым уровнем вложенности?

list(more_itertools.collapse(iterable, levels=1)) # [1, 2, [3, 4], [[5], [6]]]

А вот так мы можем посмотреть, все ли элементы в коллекции уникальные:

more_itertools.all_unique([1, 2, 3, 4]) # True
more_itertools.all_unique([1, 2, 1, 4]) # False

Библиотека решает очень много типовых проблем, поэтому если научиться ей пользоваться - она сэкономит очень много времени. Возможно, я еще буду писать какие-то рецепты с ней, но это не точно 🌚...

PyPI | Документация

#more_itertools #itertools #библиотека #рецепт
Как раз сегодня искал фреимворк для организации работы консьюмера RabbitMQ и на глаза попался Propan - декларативный фреимворк для работы с очередями сообщений.

Для чего это нужно? На базе очередей можно построить асинхронную коммуникацию сервисов, а это привет микросервисной архитектуре!

Для сравнения, вот столько кода нам нужно написать, чтобы сделать консьюмер при помощи aio_pika:

import asyncio
import aio_pika
async def main():
connection = await aio_pika.connect_robust(
"amqp://guest:guest@127.0.0.1/"
)
queue_name = "test_queue"
async with connection:
channel = await connection.channel()
queue = await channel.declare_queue(queue_name)
async with queue.iterator() as queue_iter:
async for message in queue_iter:
async with message.process():
print(message.body)
asyncio.run(main())

А вот столько с Propan:

from propan import PropanApp, RabbitBroker
broker = RabbitBroker("amqp://guest:guest@localhost:5672/")
app = PropanApp(broker)
@broker.handle("test_queue")
async def base_handler(body):
print(body)

Чистый кайф, не правда ли? Выглядит просто и понятно.

Что ещё умеет?
1) Кастить типы сообщений в модельки при помощи Pydantic.
2) Умеет работать с зависимостями (привет DI)
3) Имеет CLI утилитку, которая поможет сгенерировать проект, запустить несколько процессов воркеров, запустить хот-релоад для разработки.
4) А ещё есть огромное количество примеров, как им пользоваться.
5) Бонус - похоже у автора в планах прикрутить AsyncAPI (это как OpenAPI, только для очередей).

На данный момент стабильно работает с RabbitMQ, Redis и Nats. Kafka и SQS в бете, а NatsJs, MQTT, Redis Streams и Pulsar в планах.
Ну и накиньте звёзд автору, выглядит как то, что в будущем выстрелит :)

Github | Документация

#библиотека #propan
Я знаю на опыте, как иногда не удобно работать с датами и временем в Питоне.

В стандартной библиотеке есть как и большое количество типов данных (date, time, tzinfo, timedelta, relativedelta, ...) так и куча модулей (datetime, time, calendar, dateutil, ...) которые помогают работать с ними. Но содержать в голове, да еще и уметь работать с ними иногда проблематично.

Одна из библиотек, которая облегчает работу с сабжем - arrow. Давайте покажу на практике, насколько с ней удобно работать:

1) Получение времени
Начнём с легкого - получим время по MSK и UTC:
>>> arrow.now() # получаем текущее время по MSK
2023-06-02T18:46:55.188882+03:00
>>> arrow.utcnow() # а теперь текущее, но в UTC
2023-06-02T15:46:55.188882+00:00

А если нам надо текущее время с другой таимзоной?
>>> arrow.get().to("US/Pacific")
2023-06-02T08:49:39.794355-07:00

Это было просто. А что если нам нужно спарсить время из шаблона?
>>> arrow.get('2023/06-02|19:00:{00}', 'YYYY/MM-DD|HH:mm:{ss}') #обратите внимание на шаблон!
2023-06-02T19:00:00+00:00

Ещё arrow позволяет искать даты в строке по шаблону:
>>>arrow.get('Он родился 16 Мая 2020 года', 'DD MMMM YYYY', locale="ru")
2020-05-16T00:00:00+00:00

2) Работа со временем
Например, вот так можно заменить час в исходной дате:
>>>arrow.now().replace(hour=12)
2023-06-02T12:00:29.180755+03:00

Или добавить к текущей дате +5 недель:
>>>arrow.now().shift(weeks=+5)
2023-07-07T19:02:27.174819+03:00

Ну и в конце, давайте отформатируем текущее время в строку по шаблону:
>>>arrow.now().format('YYYY HH:mm:ss MM/DD')
2023 19:05:04 06/02

3) Очеловечивание времени

Возьмем задачу. Нам нужно сказать, через сколько мы прибудем домой. Делается это так:
>>> start_time = arrow.now()
>>> arrive_time = start_time.shift(hours=+3, minutes=10, seconds=12)
>>> humanized = arrive_time.humanize(locale="ru", granularity=["hour", "minute", "second"])
>>> print(f"Вы прибудете домой {humanized}")
Вы прибудете домой через 3 часа 10 минут 12 секунд

А если мы хотим из человеческого формата перевести в машинночитаемый?
>>> start_time.dehumanize(humanized, locale="ru")
2023-06-02T22:28:08.141525+03:00

4) Работа с диапазонами
Как раз недавно решал следующую задачку: дается временной промежуток, для него нужно сделать почасовые интервалы. С arrow всё это делается вот так:

start = datetime(2023, 1, 1, 00, 00)
end = datetime(2023, 1, 1, 23, 00)
for r in arrow.Arrow.span_range('hour', start, end):
print(r)

(<Arrow [2023-01-01T00:00:00+00:00]>, <Arrow [2023-01-01T00:59:59.999999+00:00]>)
....
(<Arrow [2023-01-01T23:00:00+00:00]>, <Arrow [2023-01-01T23:59:59.999999+00:00]>)

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

GitHub | Документация

#библиотека
Если у вас есть желание понять как работает asyncio, threading или multiprocessing, либо же появились вопросы - рекомендую обратить внимание на superfastpython.com

Автор понятным языком рассказывает про доступные формы параллелизма и где какую можно применить. Для совсем начинающих он сделал "пути обучения", где раскиданы темы по каждой из форм.

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

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

#asyncio #threading #multiprocessing #статья
Создание временных файлов

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

Для решения этих проблем в Python есть модуль tempfile. Нас интересует 2 функции - это TemporaryFile и NamedTemporaryFile.

TemporaryFile позволяет создать безымянный временный файл. Вот так можно создать временный текстовой файл, открыть его на запись и чтение (за это отвечает первый аргумент "w+t", подробнее можно прочитать здесь):

from tempfile import TemporaryFile
with TemporaryFile("w+t") as t:
t.write("Hello, boxwithpython!")
t.seek(0)
data = t.read()

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

from tempfile import 
NamedTemporaryFile
with NamedTemporaryFile("w+t") as t:
t.write("Hello, boxwithpython!")
print(t.name) # /tmp/tmpljhsktjt

#std
Shielded execution в asyncio

Допустим, есть следующий обработчик, который производит оплату:

async def handler(request):
await service.pay(request.user)
return web.Response(text="payed")

Если соединение отвалится то обработчик упадет с ошибкой, так как серверу будет некуда отправлять ответ. Задача должна отмениться, но что если мы хотим, чтобы она выполнилась наверняка?

Поможет нам в этом asycio.shield(). Он защищает задачу от отмены, даже в случае возникновения ошибки. Выглядит это следующим образом:

async def handler(request):
await asyncio.shield(service.pay(request.user))
return web.Response(text="payed")

#asyncio #std
Про __slots__

Python, аналогично другим динамическим языкам, таким как JavaScript, предоставляет возможность манипулирования объектами в рантайме, в том числе позволяет добавлять, изменять и удалять атрибуты. Цена этого – понижение скорости доступа к атрибутам и дополнительные расходы памяти.

Такое поведение нужно не всегда. Бывают случаи, когда мы точно знаем, какие атрибуты будут у наших экземпляров классов. Или же мы хотим ограничить добавление новых атрибутов. Именно для этого и существует __slots__.

Слоты задаются через атрибут __slots__ в классе:

class SlotsClass:
slots = ('foo', 'bar')

>>> obj = SlotsClass()
>>> obj.foo = 5
>>> obj.foo
# 5
>>> obj.another_attribute = 'test'
Traceback (most recent call last):
File "python", line 5, in <module>
AttributeError: 'SlotsClass' object has no attribute 'another_attribute'

Теперь мы не можем добавлять новые атрибуты к нашим объектам. Скорость доступа к атрибутам повышается на 25-30%, потому что при доступе к ним их больше не надо вычислять.
В свою очередь, память экономится из-за того, что у класса не создается __dict__, который как раз хранил атрибуты.

#std #slots
Коробка с питоном
Про __slots__ Python, аналогично другим динамическим языкам, таким как JavaScript, предоставляет возможность манипулирования объектами в рантайме, в том числе позволяет добавлять, изменять и удалять атрибуты. Цена этого – понижение скорости доступа к атрибутам…
__slots__ и наследование

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

class SlotsClass:
__slots__ = ('foo', 'bar')

class ChildSlotsClass(SlotsClass):
pass

>>> obj = ChildSlotsClass()
>>> obj.__slots__
# ('foo', 'bar')
>>> obj.foo = 5
>>> obj.test = 3
>>> obj.__dict__
# {'test': 3}

Это стандартное и понятное поведение. Чтобы избежать создания __dict__, можно снова переопределить __slots__ в подклассе:

class SlotsClass:
__slots__ = ('foo', 'bar')

class ChildSlotsClass(SlotsClass):
__slots__ = ('baz',)

>>> obj = ChildSlotsClass()
>>> obj.foo = 5
>>> obj.baz = 6
>>> obj.something_new = 3

AttributeError: 'ChildSlotsClass' object has no attribute 'something_new'

А что с множественным наследованием?

class ClassA:
__slots__ = ('foo', 'bar',)

class ClassB:
__slots__ = ('baz',)

class C(ClassA, ClassB):
pass

TypeError: multiple bases have instance lay-out conflict

Оно не работает. Потому-что каждый класс может иметь свои собственные __slots__, которые могут пересекаться с другими классами, а это может привести к тому, что объекты могут быть созданы неправильно или будут иметь непредсказуемое поведение.
Из-за этого возникает неоднозначность, какой именно слот использовать в результирующем классе.

#std #slots
А я к вам с новостями.

FastAPI в версии 0.100.0-beta1 поддерживает Pydantic v2 в бета-режиме. Да-да, это тот самый Pydantic, внутренности которого написаны на Rust. Гайд по миграции можно почитать здесь, а релиз тут.

#fastapi #pydantic