Python Tips


Гео и язык канала: Россия, Русский
Категория: Технологии


Советы от разработчика по языку программирования Python и его библиотекам.

Связанные каналы

Гео и язык канала
Россия, Русский
Категория
Технологии
Статистика
Фильтр публикаций


Однострочная документация

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

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

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

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

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

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

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

def random():
"""Возвращает случайное число."""


Документирование кода

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

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

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

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


Модуль 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


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


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


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


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 с запросами, в которых рост количества получаемых записей не ограничен, это может привести к обратному эффекту.


Команда allvirtualenv из virtualenvwrapper

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

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

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


Порядок указания декораторов

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

Напомню, что объявление функции вида
@d1
@d2
def f():
pass

аналогично такой записи:
def f():
pass
f = d1(d2(f))

Так вот в зависимости от того, в каком порядке указываются декораторы, результат может быть различным (а может и не быть 🙂). Для примера рассмотрим реализацию обработчика сигнала в Django:

@atomic
@receiver(post_save)
def handler(**kwargs):
....

Вот в каком порядке будет происходить создание функции:

1. Будет создана функция (для условности буду называть её "исходная").
2. В декораторе receiver исходная функция будет добавлена к обработчикам сигнала post_save.
3. Т.к. декоратор receiver возвращает оборачиваемую функцию без изменений, она будет передана в декоратор atomic.
4. atomic создаст новый callable-объект, который будет выполнять исходную функцию внутри транзакции.
5. Этот объект будет сохранен в атрибуте handler модуля, в котором была реализована функция.

В указанном выше примере допущена ошибка, т.к. при срабатывании сигнала будет вызыватся исходная функция, а не функция, обёрнутая декоратором atomic, ведь именно она подключалась к сигналу.

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

@receiver(post_save)
@atomic
def handler(**kwargs):
....


Загрузка данных (fixtures)

До появления в Django встроенных миграций задача внесения в БД изменений решалась с помощью South. У этого инструмента была одна особенность: при каждом запуске команды migrate загружались данные из файла fixtures/initial_data.*. Недостающие записи добавлялись, существующие перезаписывались. При необходимости дополнительной загрузки данных предлагалось использовать команду loaddata. В Django 1.7+ такая особенность использования файла fixtures/initial_data.* не была реализована, а необходимость загрузки данных в БД осталась. Скорее всего по привычке, в сети Интернет стали предлагать в качестве решения использовать команду loaddata. И эти решения стали часто применяться на практике. Но это работает до первого несовместимого изменения в соответствующих моделях Системы. Всё дело в том, что management-команды Django работают с моделями Системы, а не со снимком Системы, который доступен в миграции. Поэтому для загрузки данных в БД рекомендуется использовать свои реализации операций, благо делаются они не сложно.

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

#django


Django ORM: Миграции как "снимки" Системы

(речь пойдет о миграциях в Django 1.7 и выше)

Для начала некоторые факты о миграциях:
Миграции предназначены для программирования изменений в базах данных проекта. Это могут быть как изменения в схеме данных (изменение структуры таблиц, ограничений целостности, индексов и т.п.), так и изменения в самих данных (исправление ошибочных данных, загрузка новых данных и т.п.).
Каждая миграция применяется для каждой БД проекта (баз данных может быть несколько).
● Миграция состоит из т.н. операций. Операции могут быть как встроенные, например, создание таблицы для модели, добавление/изменение/удаление/переименование поля в таблице и т.п., так и реализованные в рамках сторонних библиотек, либо проекта.
● Операция имеет "направление" выполнения. Прямое (forwards) реализуется всегда, обратное (backwards) при наличии такой возможности/необходимости.
● Выполнение или пропуск операции определяется через роутеры в методе allow_migrate.
● Перед и после выполнения миграций каждого django-приложения отправляются сигналы pre_migrate и post_migrate.
● Каждая миграция по умолчанию выполняется в отдельной транзакции. Поэтому надо учитывать некоторые особенности, обусловенные этим. Например, внутри транзакций нельзя выполнять некоторые операции (зависит от СУБД и её версии). Это определяется атрибутом atomic в классе миграции.
● Для удобства в каждом приложении миграции нумеруются. Но порядок применения миграций определяется через зависимости, а не их номерами. Например, зависимости можно описать так, что сначала будет применена миграция 0001, затем 0004, потом 0002 и только после этого 0003.
● Зависимости могут быть определены через атрибуты dependencies и run_before. В них указываются миграции, которые нужно выполнить до/после данной миграции соответственно.

Миграции — это снимки (snapshot) Системы

При
необходимости выполнения в операциях миграций действий с моделями (CRUD) в Django используются т.н. исторические модели. Иными словами, это снимки моделей Системы на момент реализации миграции. Это обусловлено тем, что модели Системы не только могут быть изменены в процессе её развития, но и вовсе удалены, однако операция должна всё равно выполняться при применении миграций с нуля (например, при запуске автотестов).

Отсюда следует другой не всегда очевидный факт: помимо моделей в Системе могут меняться и другие её составные части, например, константы, функции, классы и т.п. Поэтому при написании нестандартных миграций целесообразно также делать "снимок" используемых в миграции объектов, которые в будущем могут быть изменены, т.е. копипастить. Пожалуй, это единственный случай, когда программирование копипастом не является антипаттерном 🙂

Исторические модели — это самостоятельные классы!

При написании миграций следует помнить, что историческая модель не имеет ничего общего с моделью Системы, кроме, разве что, имени. Это означает, что атрибуты и методы, имеющиеся в моделях Системы, отсутствуют в исторических моделях. Обработчики сигналов, подключенные с параметром sender также не будут срабатывать на исторические модели.


Django ORM: on_commit

Иногда появляется необходимость выполнить какие-то действия сразу после коммита. Одним из примеров такой ситуации является удаление записей с полями на основе FileField (в т.ч. ImageField). Эти поля предназначены для хранения файлов, но в БД хранится только путь к файлу, а сам файл хранится в файловой системе или другом хранилище. При удалении такой записи Django ORM НЕ удаляет файл. Поэтому при интенсивном добавлении/удалении записей в такой модели будет как минимум нерационально использоваться дисковое пространство.

В документации Django сказано, что разработчикам нужно самостоятельно позаботиться об удалении файлов. Также там предлагается использовать для этих целей management-команды и их периодический запуск (например, через cron). Начиная с Django 1.9 и выше доступен более оптимальный способ для удаления файлов, на которые уже нет ссылок в БД. Речь о функции on_commit в модуле django.db.transaction. Её первый аргумент — callable-объект, которая будет вызвана сразу после успешного коммита текущей транзакции.

@receiver(post_delete)
def delete_files(sender, instance, using, **kwargs):
for field in sender._meta.get_fields():
if isinstance(field, FileField):
file = getattr(instance, field.name)
if file:
on_commit(file.delete, using)

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

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

Совет: при взаимодействии с внешними относительно СУБД системами (файловые системы, почтовые серверы и т.п.) помните о возможности отката транзакции и откладывайте все действия до её подтверждения с помощью on_commit.

#django

Ссылки:
- функция on_commit: https://docs.djangoproject.com/en/2.0/topics/db/transactions/#django.db.transaction.on_commit


Метод iterator() в Django ORM

В Django ORM загрузка объектов модели из БД осуществляется через один из её менеджеров. У менеджера, а также у QuerySet, есть метод iterator(). О нем и пойдет речь.

При выполнении SELECT-запроса QuerySet по умолчанию загружает ВСЕ записи (строки) и для КАЖДОЙ из них создает экземпляр модели (кеширует). В большинстве случаев результаты выполнения запроса обрабатываются в цикле и каждый из созданных объектов используется только на одной итерации цикла. Очевидно, что при этом необходимости хранить в памяти экземпляры модели для всех записей нет. Это особенно актуально, когда запрос возвращает много записей, каждая из которых занимает память.

Метод iterator() позволяет отключить такое кэширование и оптимизировать использование памяти при обработке данных. А в Django 1.11+ для Oracle и PostgreSQL также используются серверные курсоры, что позволяет еще лучше оптимизировать использование памяти приложением, т.к. данные с сервера БД будут загружаться порциями по 100 записей (см. GET_ITERATOR_CHUNK_SIZE).

for obj in TestModel.objects.filter(...).iterator():
...

Для values() и values_list() также можно отключать кеширование.

Примечание 1: при использовании iterator() игнорируется prefetch_related().
Примечание 2: при использовании пулов соединений с БД серверные курсоры нужно отключать (см. параметр DISABLE_SERVER_SIDE_CURSORS).

Совет: при обходе QuerySet-ов в циклах, где не нужно повторное использование экземпляров моделей, отключайте их кэширование с помощью метода iterator().

#django #оптимизация


Все имена в Python являются ссылками на объекты. Имена переменных, классов, функций и т.д. — это только адрес объекта в памяти, но не сам объект. Убедиться в этом можно с помощью функции id, которая возвращает уникальный идентификатор объекта (в CPython -- адрес объекта в памяти):

>>> import datetime
>>> id(datetime)
139662440951544
>>> a = datetime
>>> id(a)
139662440951544

Ссылаться на объект можно не только из переменных, но и из элементов коллекций (tuple, 'list и т.д.), атрибутов классов. При этом у каждого объекта есть счетчик ссылок. При создании ссылки на объект счетчик ссылок увеличивается, а при удалении ссылки — уменьшается. Когда счетчик ссылок равен 0, объект удаляется из памяти.

Однако в Python есть возможность ссылаться на объекты не увеличивая счетчик ссылок. Делается это с помощью слабых ссылок (weak references). Инструментарий для работы со слабыми ссылками расположен в модуле стандартной библиотеки weakref:
- класс ref для создания слабой ссылки на объект;
- словарь WeakKeyDictionary с ключами, являющимися слабыми ссылками;
- словарь WeakValueDictionary со значениями, являющимися слабыми ссылками;
- множество WeakSet
и другое.

В коллекциях WeakKeyDictionary, WeakValueDictionary и WeakSet элементы существуют до тех пор, пока сущестует объект, на который ссылается слабая ссылка:

>>> d = WeakKeyDictionary()
>>> class C:
pass
>>> c = C()
>>> d[c] = 'qwerty'
>>> len(d)
1
>>> del c
>>> len(d)
0

Например, недавно я использовал WeakKeyDictionary для оптимизации работы функции get_original_object, которая по объекту модели Django возвращает его исходное состояние, заново загружая его из БД. Это нужно для того, чтобы сравнить, какие поля в объекте изменились и выполнить соответствующие действия. Особенность использования функции была в том, что она могла вызываться для одного и того же экземпляра модели много раз в разных частях системы. Для того, чтобы предотвратить повторную загрузку объектов в этой функции, был задействован словарь WeakKeyDictionary для организации кэша исходных объектов:

_get_original_object_cache = WeakKeyDictionary()

def get_original_object(obj):
if obj in _get_original_object_cache:
return _get_original_object_cache[obj]
...

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

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

#python

Ссылки:
- модуль weakref: https://docs.python.org/3.6/library/weakref.html


В таких языках, как C++, Java, C#, PHP и др. реализованы т.н. модификаторы доступа: public, protected и private. С их помощью можно указать степень сокрытия членов класса. Если коротко, то означают они следующее: public — члены класса доступны отовсюду, protected — из методов самого класса и его потомков, private — только из методов класса.

В Python таких модификаторов доступа нет и все члены класса доступны отовсюду. При этом есть соглашение (https://docs.python.org/3/tutorial/classes.html#private-variables) по именованию членов класса, согласно которому они разделяются на три типа, у которых имя:

1. НЕ начинается с символа подчеркивания (foo);
2. начинается с одного символа подчеркивания (_bar);
2. начинается с двух символов подчеркивания (__baz).

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

Аналогия между модификаторами доступа и именами в Python следующая:

1. public — нет подчеркивания, можно использовать отовсюду;
2. protected — один символ подчеркивания, можно использовать только в методах класса и его потомках;
3. private — два символа подчеркивания, можно использовать только в методах класса.

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

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

#python #ООП


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

Обычные классы предоставляют доступ к своему функционалу через свои экземпляры.

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

Классы-примеси (mixins) содержат только часть реализации, "подмешиваемой" к другим классам.

Рассмотрим вышесказанное в контексте MRO (Method Resolution Order). При использовании в классах-потомках доступ к методам базовых классов осуществляется через super. Если иерархия наследования включает несколько уровней, то цепочка вызовов super пройдет как раз в соответствии с MRO. Если коротко, то последовательность вызовов в иерархии будет идти слева направо, снизу вверх. Также стоит отметить, что если в методе не вызывается super, то цепочка вызовов завершается на таком методе.

Исходя из этого список базовых классов нужно формировать в следующем порядке:

1. Классы-примеси.
Т.к. их смысл в первую очередь в том, чтобы расширять другие классы, они по определению должны содержать вызов super. Это означает, что цепочка вызовов не прервется в классе-примеси и функционал основного класса будет достижим. Если же указать класс-примесь после обычного класса, то при наличии абстрактных классов в иерархии наследования может быть вызван абстрактный метод, что в Python приводит к ошибке.

2. Обычные классы.
В конце иерархии наследования обычных классов всегда есть метод, на котором заканчивается цепочка вызовов. Прежде всего это важно при переопределении абстрактных методов. Именно поэтому наличие перед абстрактными базовыми классами в списке наследования обычного класса гарантирует отсутствие ошибок при вызове методов.

3. Абстрактные базовые классы.
Как было сказано выше, вызов абстрактного метода возможен через super и это приводит к ошибке. Поэтому абстрактные классы в списке классов-предков нужно указывать последними, тогда цепочка вызовов завершится на обычном классе. Но при этом останется доступен функционал абстрактных классов в части контрля обязательности переопределения всех абстрактных методов и свойств (в т.ч. контроль реализации интерфейсов).

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

#python #ООП

Ссылки:
- описание модуля abc https://docs.python.org/3/library/abc.html


В двух предыдущих сообщениях я приводил размеры коллекций в байтах, при этом не делал оговорок по поводу того, какой тип данных хранится в коллекции. Это не ошибка. В коллекциях хранятся только ссылки на объекты (указатели, адреса), а размер указателя определяется разрядностью интерпретатора: в 32-хразрадном — 4 байта, в 64-хразрадном — 8 байт. Поэтому размер коллекции по сути не зависит от типа данных, содержащихся в ней.

#python


В отличие от кортежей и списков, множества (set, frozenset) и словари (dict) занимают больше места в памяти. Например, для множества из двух элементов понадобится 224 байта (кортеж — 64). Для 1000 элементов — 32992 и 8048 соответственно.

Однако у словарей и множеств есть важное преимущество: скорость поиска элемента в коллекции (оператор in). Если для кортежей и списков сложность данной операции равна O(n), то для множеств это O(1). Сравните:

In [2]: a = {randint(1, 10000000) for _ in range(1000000)}

In [3]: len(a)
Out[3]: 951498

In [4]: %timeit randint(1, 10000000) in a
1.86 µs ± 19.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Для множества из почти миллиона целых чисел время поиска случайного числа составляет в среднем 1.86 µs. Теперь сделаем замеры для множества из ста целых чисел, т.е. в 10 000 раз меньше:

In [5]: a = {randint(1, 10000000) for _ in range(100)}

In [6]: len(a)
Out[6]: 100

In [7]: %timeit randint(1, 10000000) in a
1.75 µs ± 13.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Из этих замеров видно, что время поиска элемента практически не зависит от количества элементов в коллекции, т.е. равно O(1). Поиск ключа в словаре работает с аналогичной скоростью.

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

#python #оптимизация


В Python есть такие структуры данных, как кортежи (tuple) и списки (list). В книгах, статьях, в реальных проектах чаще встречается использование списков вместо кортежей там, где в этом нет необходимости:

gender = models.PositiveIntegerField(
'Пол',
choices=[GENDER_MALE, GENDER_FEMALE],
)

При хранении одних и тех же данных списки занимают больше места, чем кортежи. Например, на 64-хразрядной системе для хранения двух элементов в списке нужно 80 байт, а в кортеже — 64 (на 16 байт меньше). Для 1000 элементов отличие будет уже на 1064 байта. Это несущественно на небольших проектах, но становится актуальным для больших информационных систем, работающих с большим количеством данных, особенно в совокупности с другими способами оптимизации.

Совет: там, где нет потребности в изменении содержимого коллекции, используйте кортежи вместо списков.

#python #оптимизация


Периодически на ревью вижу примерно такое использование функций all и any:

if any((
is_check1_passed(),
is_check2_passed(),
...
)):
pass

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

Дело в том, что этом случае будут выполнены все проверки вне зависимости от результатов каждой из них. Также будет создан дополнительный кортеж, который потребует, хоть и несущественного, но всё же выделения памяти и процессорного времени.

Более оптимальный код выглядит так:

if (
is_check1_passed() or
is_check2_passed() or
...
):
pass

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

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

Описание функций all и any: https://docs.python.org/3.6/library/functions.html#all

#python #оптимизация

Показано 20 последних публикаций.

69

подписчиков
Статистика канала