70 subscribers
14 links
Советы от разработчика по языку программирования Python и его библиотекам.
Download Telegram
Команда allvirtualenv из virtualenvwrapper

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

Например, обновить pip:
allvirtualenv pip install pip -U

Или посмотреть версии интерпретатора во всех окружениях:
allvirtualenv python -V
👍1
Django: select_related и prefetch_related

Часто приходится слышать о том, что select_related предназначен для связей <один/многие>-к-одному (ForeignKey, OneToOneField), а prefetch_relatedтолько для связей <один/многие>-ко-многим (ManyToOneRel, ManyToManyField). Вторая часть этого утверждения верная, но не полная, т.к. prefetch_related может использоваться для тех же связей, что и select_related, только на уровне SQL подгрузка связанных объектов будет выглядеть иначе.

Если при использовании select_related для загрузки связанных объектов используются SELECT-запросы с JOIN-ами, то prefetch_related в случае связей *-к-одному будет также формировать отдельные SELECT-запросы вида SELECT ... FROM ... WHERE id in (1, 4, 6, 8, ...). Отсюда следуют две особенности:

1. Список идентификаторов в условии напрямую зависит от количества записей в таблице БД (актуально и связей *-к-одному). Из-за этого по мере наполнения БД такие SQL-запросы могут содержать очень много идентификаторов в условии WHERE id in (десятки, сотни тысяч и более). Такие SQL-запросы будут работать тем медленнее, чем больше идентификаторов в них.

2. Для связей многие-к-одному в тех случаях, когда на один и тот же объект приходится более одной ссылки, prefetch_related создаст по одному экземпляру модели, а select_related создаст дубликаты.

Совет 1: используйте prefetch_related вместо select_related со связями *-к-одному в тех случаях, когда на одну запись в связанной таблице приходится несколько ссылок из основной таблицы — это более оптимально за счет отсутствия дубликатов. При этом нужно помнить о том, что prefetch_related не будет работать совместно с методом iterator ☝🏻

Совет 2: не используйте prefetch_related с запросами, в которых рост количества получаемых записей не ограничен, это может привести к обратному эффекту.
👍1
Python: функции attrgetter, itemgetter и methodcaller

Часто возникает необходимость применения к последовательности объектов функции, извлекающей атрибут объекта или вызывающей его метод, либо возвращающей элемент массива или словаря. Например, в функциях filter, sorted, map и др.

Почти всегда это делается подобным образом:

map(
lambda day: (day.year, day.month, day.day),
dates
)

В модуле operator стандартной библиотеки Python есть функции attrgetter, itemgetter и methodcaller, с помощью которых можно решать подобные задачи более наглядным и лаконичным образом:

map(
attrgetter('year', 'month', 'day'),
dates
)

Совет: для повышения читаемости кода используйте функции attrgetter, itemgetter, methodcaller. Описание функций здесь.

#python
👍1
Django ORM: проверка на prefetch_related

Метод prefetch_related позволяет загрузить связанные через обратную связь объекты модели.

class Person(models.Model):
name = models.CharField(...)

class Employee(models.Model):
person = models.ForeignKey(
Person,
related_name='employees',
)
job = models.CharField(...)

Для таких моделей при финализации QuerySet-а
persons = Person.objects.prefetch_related(
'employees'
)
будет выполнено два SELECT-запроса.

При обходе в цикле доступ к связанным объектам через all() не потребует выполнения дополнительных SELECT-запросов:
for person in persons:
print(person.employees.all())

Совет: для контроля использования prefetch_related и отсутствия "проблемы N+1" рекомендую использовать assert:
for person in query:
assert (
'employees' in person._prefetched_objects_cache
)
print(person.employees.all())

#django
👍1
itertools.groupby()

В модуле itertools есть полезная в некоторых ситуациях функция groupby(). Она позволяет сгруппировать последовательности элементов по какому-либо признаку (ключу). При этом функция работает как с коллекциями (кортежи, списки и др.), так и с итераторами/генераторами. Следует отметить, что элементы в исходной последовательности должны быть упорядочены по ключу ☝🏻 Для формирования значений ключа в функцию передается callable-объект с одним аргументом, возвращающий значение ключа.

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

Предположим, например, что нужно подсчитать сумму окладов сотрудников по отделам какой-либо организации. Данные указаны в csv-файле в формате ("Наименование подразделения","ФИО сотрудника",Оклад) и упорядочены по наименованию отдела:

with open('filename.csv') as csvfile:
groupped_data = groupby(
csv.reader(csvfile), itemgetter(0)
)
salaries_by_department = {
department: sum(int(row[2]) for row in rows)
for department, rows in groupped_data
}

Использование итераторов позволяет оптимизировать потребление памяти, т.к. не требуется хранения промежуточных результатов (map() из Python 2 уже не учитываем). Еще это даёт возможность работать с потоками данных (файлы, курсоры БД, сокеты и т.п.).

Совет: используйте функцию groupby() в сочетании с итераторами для получения оптимальных результатов.

Ссылки:
- функция groupby.

#python
👍1
Модуль textwrap

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

wrap(text, width=70, **kwargs)
Разбивает строку text так, чтобы в полученном в результате списке строк длина каждой из них не превышала width символов.

fill(text, width=70, **kwargs)
Делает то же самое, что и wrap, только результат возвращается в виде строки, а не списка строк.

shorten(text, width, **kwargs)
Сокращает строку text до width символов, попутно удаляя лишние пробельные символы (пробел, табуляция, перевод строки и т.п.).

>>> shorten('Beautiful   is better than ugly.', 20)
'Beautiful is [...]'

indent(text, prefix, predicate=None)
Добавляет prefix слева в каждую строку параграфа.

>>> print(indent(json.dumps({'qwe': 1}, indent=4), '.'*8))
........{
........ "qwe": 1
........}

dedent(text)
Удаляет пробельные символы в начале строк параграфа.

Также в модуле есть класс TextWrapper, объединяющий в себе перечисленные выше возможности. Его методы wrap и fill обрабатывают текст согласно настроек экземпляра класса и возвращают результат обработки в виде списка или строки соответственно.

Совет: используйте инструментарий из модуля textwrap для форматирования текстовых строк. В консольных приложениях, в лог-файлах и т.п. это повысит читаемость вывода приложения. Также это повысит читаемость кода, т.к. он не будет перегружен конструкциями, форматирующими текст.

Ссылки:
- Модуль textwrap.

#python
👍1
Документирование кода

Многим python-разработчикам знакомо соглашение о стиле кодирования PEP-8. Для проверки кода на соответствие этому соглашению созданы статические анализаторы (например, pycodestyle). Также есть и ПО для автоматического форматирования кода (black и др.).

О написании строк документации в PEP-8 сказано лишь то, что строки документации нужно писать для всех публичных модулей, функций, классов и методов, а также об использовании тройных кавычек """. Остальные соглашения о стиле оформления строк документации описаны в PEP-257.

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

Ну а совет очевиден: документируйте свой код — это сэкономит время другим разработчикам, а значит и повысит эффективность команды в целом, ведь код читается чаще, чем пишется.
👍1
Python Tips
Документирование кода Многим python-разработчикам знакомо соглашение о стиле кодирования PEP-8. Для проверки кода на соответствие этому соглашению созданы статические анализаторы (например, pycodestyle). Также есть и ПО для автоматического форматирования…
Однострочная документация

К однострочной документации предъявляются следующие требования:

1. Должна находиться на одной строке, без пустых строк до и после текста.
2. Нужно использовать тройные кавычки (""").
3. Должна быть фраза c точкой в конце.
4. Не надо дублировать объявление функции/класса/метода.

Приведу примеры документирования, нарушающие эти соглашения:

1. Лишние переводы строк:
def random():
"""
Возвращает случайное число.
"""

2. Нет точки в конце + лишние пробелы:
def random():
""" Возвращает случайное число """

3. Дублирование объявления функции + одинарные кавычки:
def random():
"random() -> int"

Правильный вариант:

def random():
"""Возвращает случайное число."""
👍1
Многострочная документация

1. Первая строка содержит краткое описание модуля, класса, функции и др., начинается сразу после """, заканчивается точкой.
2. Отделена от следующих абзацев пустой строкой.
3. Закрывающие кавычки на отдельной строке.

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

Например, генератор документации Sphinx (CPython, Django, Celery) при генерации справочников по API извлекает необходимую информацию из строк документации в форматах reStructuredText или Markdown, в которых используется такой синтаксис:

def get_files(day: date) -> list[str]:
"""Возвращает имена файлов на указанную дату.

:param day: дата, на основе которой формируется путь для поиска файлов.
:type day: date

:raises FileNotFoundError: если ни одного файла не найдено.
"""

Система генерации документации MkDocs (FastAPI, Pydantic), помимо упомянутого выше Sphinx-style, поддерживает другие форматы.

Google-style:

def get_files(day: date) -> list[str]:
"""Возвращает имена файлов на указанную дату.

Args:
day (date): дата, на основе которой формируется путь для поиска
файлов.

Raises:
FileNotFoundError: если ни одного файла не найдено.
"""

Numpydoc:

def get_files(day: date) -> list[str]:
"""Возвращает имена файлов на указанную дату.

Parameters
----------
day : date
Дата, на основе которой формируется путь для поиска
файлов.

Raises
------
FileNotFoundError
Если ни одного файла не найдено.
"""

Совет: придерживайтесь правил стилевого оформления строк документации (PEP-8, PEP-257) и используйте единый формат, даже если не используете системы генерации документации, т.к. IDE тоже умеют их распознавать и отображать во всплывающих подсказках. Единообразие стилевого оформления кода делает его чтение проще, а значит и работу ваших коллег, в т.ч. и будущих, легче 😎
👍5
Функция iter

Встроенная функция iter() используется для создания итераторов и всем знакома. Под капотом она вызывает метод __iter__() объекта и возвращает результат.

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

Это может быть удобно тогда, когда объект не поддерживает итерирование, например queue.Queue:
from queue import Queue

queue = Queue()
for n in (1, 2, 3, 4, 5, None):
queue.put(n)

Обычно для получения данных из очереди используется цикл while:
items = []
while (item := queue.get()) is not None:
items.append(item)

С помощью iter() то же самое можно сделать так:
items = list(iter(queue.get, None))


#python
👍7
Когда много менеджеров контекста

(Под "менеджерами контекста" здесь подразумеваются экземпляры классов с реализованными методами __enter__ и __exit__)

Бывает так, что нужно использовать несколько менеджеров контекста, например, открыть несколько файлов, подключений к БД и курсоров. Не всегда их можно создать в одном блоке with и приходится делать вложенные:
with open('config.json', 'r') as file:
config = json.load(file)
with (
psycopg2.connect(dbname=config['database']) as dbc,
dbc.cursor() as cursor
):
cursor.execute('INSERT INTO ...')

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

В модуле contextlib стандартной библиотеки есть класс ExitStack, с помощью которого можно объединять несколько менеджеров контекста:
with ExitStack() as es:
file = es.enter_context(open('config.json', 'r'))
config = json.load(file)
dbc = es.enter_context(psycopg2.connect(dbname=config['database']))
cursor = es.enter_context(dbc.cursor())
cursor.execute('INSERT INTO ...')

Также в ExitStack есть метод callback(), который позволяет добавить вызов функции на выходе из контекста (из блока with).
with ExitStack() as es:
connection_pool = redis.ConnectionPool.from_url(
'redis://localhost:6379/0'
)
es.callback(collection_pool.close)

ExitStack может быть полезен и тогда, когда количество менеджеров контекста не известно заранее. В этом примере кода все открытые файлы будут закрыты при выходе из блока with:
with ExitStack() as es:
files = [
es.enter_context(open(file_path, 'r'))
for file_path in file_paths
]

Асинхронная версия: AsyncExitStack.

Советы:
- всегда используйте менеджеры контекста для корректного освобождения ресурсов, т.к. они будут работать даже при возникновении непредвиденных ошибок;
- ознакомьтесь с инструментами модуля contextlib и используйте функцию closing() / aclosing() для объектов, имеющих метод close(), но не поддерживающих протокол менеджера контекста;
- создавайте свои менеджеры контекста с помощью декораторов @contextmanager / @asynccontextmanager и базового класса ContextDecorator / AsyncContextDecorator.

Ссылки:
- описание протокола менеджеров контекста;
- документация модуля contextlib;
- документация ExitStack;
- примеры использования.

#python
👍11