Мои личные наблюдения и правила
У всех разработчиков со временем нарабатывается опыт, растёт экспертиза. Когда вы много лет занимаетесь разработкой, приходит понимание каких-то общих концепций, вырабатываются правила поведения в конкретных сценариях.
В этой небольшой статье я хочу рассказать про свои Best Practices, которые я применяю и всегда советую другим. Сверху будут наиболее общие замечания, но чем ниже по статье вы будете продвигаться, тем больше будет реальных проблем и советов (с кодом и т. п.).
1) Чаще используйте генераторы
Многие опытные разработчики часто используют генераторы. И делают они это не просто так, генераторы позволяют значительно экономить память и ускорить (хоть и не так заметно) вашу программу. Если вы пишите большой и более-менее сложный код, то такая простая оптимизация лишней точно не будет.
Если сомневаетесь в своих знаниях генераторов, то могу порекомендовать статью: https://habr.com/ru/post/560300/
Если же у вас полно сложных вычислений или математического моделирования, то пожалуйста, не пилите свой велосипед (который ещё и будет страшно медленным и корявым), воспользуйтесь специальными библиотеками (NumPy, SciPy).
2) Соблюдайте правила игры
Говорю банальности, но всё-же. Не нарушайте PEP8, даже если очень хочется, выучите и поймите смысл DRY, KISS. Серьёзно, если вы будете придерживаться всех этих правил, то ваш код станет гораздо более читаемым. Не забывайте, ваш код может читать маньяк, который знает, где вы живёте)
А теперь без шуток. Очень часто разработчики решают опустить какое-нибудь правило из PEP8, или нагородить код с огромным Cognitive Complexity. Как потом поддерживать такой кусок кода? Правильно - никак. Поэтому постарайтесь никогда не нарушать правила Python-игры, иначе расплачиваться будет вся команда.
Кстати, немного облегчить задачу по приведению кода в порядок, смогут плагины по типу Black. Если не знаете что это - рекомендую ознакомиться.
3) Увеличивайте производительность вашего кода
Кто-то скажет: Зачем это делать? Python всё равно медленный, просто забьём. И как вы понимаете, забивать - плохая идея. Я надеюсь, что вам хоть немного жаль пользователей, которые вынуждены ждать несколько секунд, пока ваша программа обработает его запрос.
Если это так, или вам просто дали задание увеличить скорость работы программы, то вот пару моих рекомендаций:
3.1) Многопроцессорность, а не многопоточность. В связи с особенностями работы GIL, вы не сможете в многопоточной системе полностью нагрузить вашу машину, программа будет тратить 5%-10% от максимальной мощности компьютера (возможно, когда-нибудь напишу статью про принцип и логику работы GIL). А вот при многопроцессорности, вы сможете получить максимальную производительность. Поэтому, сложные вычисления/расчёты и т. п. - многопроцессность.
3.2) Используйте слоты. В своей предыдущей статье я уже описывал что такое слоты, и как они работают. Тут только добавлю, что они хоть и не сильно, но всё же влияют на общую скорость работы вашей программы.
Моя предыдущая статья: https://vk.com/@matvey.chekashov-python-ispolzovanie-slots
3.3) Экспериментируйте с интерпретаторами. Многие разработчики, по непонятной причине, бояться касаться интерпретатора и всё с ним связанное. Не будьте такими. Попробуйте применить PyPy, он ускоряет работу вашей программы в десятки раз (https://habr.com/ru/company/otus/blog/349230/)!
Можете «потыкать» Cython, это расширенный Python с возможностью использовать C’шные компоненты (https://habr.com/ru/company/ruvds/blog/462487/).
Главное - экспериментируйте, сделайте свою программу по настоящему быстрой!
4) Тестируйте свой код
Ну вот, я свалился на совсем банальности, но делать нечего. Многие разработчики (и я в их числе) не любят тестироваться свой код.
Писать тесты скучно, долго, муторно, да и просто бесит. Я вас понимаю, сам такой). Но тесты - необходимы. Без них вы не сможете быть уверены, что ваши изменения не обвалили рандомную часть системы. Плюс, тесты, в некотором роде, являются документацией к коду, смотря на них можно понять как работает программный компонент.
Если коротко, тесты - душные, но без них никак. Так что привыкайте тестировать и будет вам счастье.
5) Используйте срезы (слайсы)
Все знают про срезы, но почему-то не так много разработчиков их использует. Люди городят циклы, условия, вместо банального использования a[:3]. Не бойтесь срезов, они работают быстро, выглядят лаконично. Также не забывайте про шаги, которые также можно указать в срезах.
Если не уверены в своих познаниях срезов, рекомендую ознакомиться.
6) Храните данные правильно
Есть несколько разных способов хранить данные:
6.1) Просто передавать данные напрямую:
Способ конечно рабочий, но немного кривой. Например: может поменяться адрес файла, тогда придётся во всём коде искать ссылку на файл и вручную всё это менять.
6.2) Использовать константы:
Такой способ хранения данных гораздо лучше. Если поменяется адрес файла, мы просто изменим одну константу и весь остальной код получит уже новую ссылку.
6.3) Использовать Дата-Классы:
Я считаю это лучшим способом хранения данных. Почему? Например:
Автоматическая проверка типов, мы не сможем случайно записать в нашу переменную адреса файла число, или скажем кортеж;
Мы можем запретить изменение нашего dataclass, для этого в декораторе укажите frozen=True. Тогда у нас действительно получиться неизменяемая константа;
Так как это класс, то мы можем написать методы для удобного взаимодействия с нашими переменными, что может сократить кол-во повторяющегося кода.
Почитать и дата-классах можно тут.
7) Используйте максимум возможностей ООП
Из всех разработчиков пишущих на Python, ООП использует процентов 40, правильно использует ООП процентов 20, а использует все его возможности процентов 5.
Попадите в эти 5%, ООП - это круто, это удобно, это надёжно.
Давайте кратко рассмотрим типичные проблемы в коде таких разработчиков:
7.1) Отсутствие абстракций. Это большая проблема, ведь абстракции позволяют нам писать программы не уходя в сторону и не реализуя бесполезный функционал (принцип YAGNI).
Пример кода с абстракцией:
7.2) Постоянное использование наследования. Если вам нужен кусочек функционала из другого класса, то вы, как нормальный человек, не захотите всё писать заново. Не очень опытные разработчики просто наследуют класс, даже если это никак не сочетающиеся сущности (К примеру: вертолёт и голубь). Чтобы решить эту проблему, используйте композицию или агрегацию, подробнее про них можно прочесть тут.
7.3) Боязнь классов-в-классе и мета-классов. Когда разработчики впервые видят классы в классе или мета-классы (например в Django), у них встают волосы дыбом и начинает дёргаться глаз, но совершенно напрасно, ведь это, на самом деле, позволяет упростить код.
Пример:
Подобные трюки с ООП не совсем типичны, но они позволяют значительно упростить процесс разработки, также такой код интересно писать и поддерживать.
8) Попробуйте кусочек функционального программирования
Как вы наверняка знаете, существует не только ООП, есть и другие парадигмы, среди них и ФП (функциональное программирование). В Python есть несколько функций из данной парадигмы, которые помогут немного упростить и сократить ваш код, рассмотрим на примере. Предположим, вам нужно перевести все данные из списка в строку, в классическом варианте это выглядит так:
Уверен вам знаком такой способ. Он хорош, но зачем если можно проще?
Согласитесь, такой код гораздо проще и понятнее. Функция map лишь одна из представителей ФП, более подробно по этой теме вы можете прочитать здесь.
9) Дескрипторы
Это достаточно сложная тема, но дающая огромную власть и величие :-)
С помощью дескрипторов можно автоматически проводить проверки, логировать, отправлять уведомления, вообщем всё что угодно.
Так что же такое дескрипторы? Дескрипторы позволяют нам добавлять любую логику при создании объекта в классе, путём создания этого объекта через промежуточный класс который и называется дескриптором (вы могли такое видеть например в Django). Вот пример использования и создания дескрипторов:
В этом коде видно использование дескриптора, мы говорим, что атрибуты price и quantity будут проверяться с помощью дескриптора NoNegative. Думаю название говорящие, этот дескриптор не позволит нам создать отрицательное число, что позволяет нам убрать эти проверки из __init__.
А вот и сам дескриптор:
Как видите, ничего сложного тут нет. Если хотите более подробно ознакомиться с этой темой, рекомендую эту статью.
10) Декомпозируйте правильно
Я специально оставил эту тему для последнего, главного пункта.
Как же много разработчиков, знающих все описанные мною ранее темы, но при это всё равно пишущие ужасный код.
Такая грустная ситуация возникает именно из-за декомпозиции.
Для не знающих, декомпозиция - разделение большой задачи на ряд более простых. Такой подход поможет вам писать небольшие, элегантные, читаемые функции/классы/методы. Облегчит процесс тестирования. Упростит процесс сопровождения вашего кода другим разработчикам. Программист не умеющий или неправильно декомпозирующий, обречён писать трудный, монолитный, неподдерживаемый код.
Поэтому, первое чему вы должны научиться - декомпозиции, а потом уже всему описанному выше.
Благодарю за чтение моей небольшой статьи, пишите свои Best Practices, сделаем код чуть чище!
Мой GitHub: https://github.com/Ryize
Комментарии (37)
TiesP
02.09.2022 17:52А про вложенные функции что скажете — это best или не best? (на leetcode иногда встречаю такие решения). Плюсы то понятны — не надо параметры передавать.
Infinitive1 Автор
02.09.2022 19:25Я бы сказал, что это лучшие практики в трёх случаях:
1) Декораторы;
2) Замыкания;
3) Карринг.
Кроме того, иногда хочется создать вложенную функцию, чтобы не захламлять код. В таком случае это также оправдано, но тут тонкая грань, можно сильно попортить код вложенными функциями.
CrocodileRed
03.09.2022 11:21Плюсы, по-моему не в том, что параметры передавать не надо (кстати, надо :-)) Я при помощи вложенных функц разгружаю код основной функции и это хорошо.
TiesP
03.09.2022 11:26+1Ну я имел в виду параметры, которые есть в основной функции (которые нужны для вспомогательной) и которые вложенная «видит». Для плоской структуры функций все эти параметры нужно было бы передавать во вспомогательную.
Tishka17
03.09.2022 22:24+1Вложенные функции имеют смысл только для использования закмыканий при реализации декораторов, колбэков и т.п. (Частичное применение делается через functools.partial). В остальных случаях это ошибка: их использование как минимум затрудняет тестирование.
Вложенные классы имеют смысл когда этого требует фреймворк, в остальных случаях их использование скорее всего не имеет смысла. Как минимум это усложняет испорты
krote
02.09.2022 20:42+4А вот при многопроцессорности, вы сможете получить максимальную производительность. Поэтому, сложные вычисления/расчёты и т. п. - многопроцессорность.
Может "многопроцессность"?
Closius
03.09.2022 10:21+1Попробуйте применить PyPy, он ускоряет работу вашей программы в десятки раз
А где гарантии, что эти ваши модные эксперименты не дадут ошибок в неожиданных местах? Да, можно сказать покрывайте тестами, но не все и не всегда можно покрыть тестами да и реально такова бывает.
Например как pypy работает с тензорфлоу, если там уже numpy приходится другой юзать? Как там с совместимостью версий?
Эти нововведения хороши, когда у вас не сильно сложный (с точки зрения математики, сторонних не чисто питоновских библиотек), или когда у вас много скиловых разрабов, которые могут это дело поддерживать
nikolay_karelin
04.09.2022 11:21TF и другие пакеты для расчётов во многом написаны на C (и CUDA и даже Fortran) и будут работать только с обычной реализацией Python. В этом и состоит основная идея Python для расчётов - язык-клей для оптимизированного кода из других библиотек.
Кстати, многпоточность можно через Cython использовать, если уместно в это вкладываться.
siroBS
03.09.2022 14:03+5"8) Попробуйте кусочек функционального программирования"
Неудачные примеры для этого...
Так никто не делает:И так:
Делают так:
list_with_price = [55, 27.5, 49.99, 85, 99.9, 63, 117] list_with_price = [str(el) for el in list_with_price]
GeorgeII
03.09.2022 17:12+4Ага. В питоне даже самые базовые принципы из ФП по типу применения на списке последовательных map с filter на лямбдах - это боль и очевидная чужеродность. Тот же самый результат, за исключением ленивости, дает list-comprehension с if-guard'ом и такой подход фактически считается pythonic-way
masai
03.09.2022 14:12Генераторы не всегда быстрее.
Например,
sum(i for i in range(10_000))
работает медленнее, чем
sum([i for i in range(10_000)])
По крайней мере я сейчас проверил и получил такие числа:
Tishka17
03.09.2022 22:40Генераторы хорошая вещь, но обращение к ним действительно имеет дополнительные накладные расходы по сравнению со списком. Кроме того, генераторы одноразовые, не могут быть развернуты в обратным порядке. Но они экономят нам память.
Я встречал код, где вовсю использовали генераторы, но затем делали itertools.tee, для создания копий и который в результате разворачивал их в памяти.
Хочется сказать - разберитесь что в вашем случае важно и используйте те конструкции и типы данных, которые действительно помогают это достичь. Либо используйте тот код, который проще для понимания и имеет минимум побочных эффектов
masai
03.09.2022 23:17+1Я думаю, это должен был быть комментарий к самой статье. :) Я лишь отметил, что утверждение автора о быстроте генераторов не совсем корректно. Я вовсе не против генераторов и сам их использую, если этого требует задача.
Хочется сказать - разберитесь что в вашем случае важно и используйте те конструкции и типы данных, которые действительно помогают это достичь.
Так и делаю.
vanyas
03.09.2022 14:58Долго не мог понять, что ещё за "срезы", пока не увидел a[:3]. Это называется Слайсы же, не надо пытаться переводить устоявшиеся термины на Русский, становится непонятно.
Infinitive1 Автор
03.09.2022 15:10+4Понятия срезы уже +- устоялось. Во многих статьях используется именно это название.
Всё равно благодарю за замечание, в заголовке пункта в скобках указал слайсы
Tishka17
03.09.2022 22:27+1"Слайсы" слышал только применительно к языку го, где это отдельный тип объекта. Всегда встречал именно "срезы".
squaremirrow
04.09.2022 00:13В питоне слайсы — тоже объекты. Создаются, внезапно, с помощью команды slice(start, stop, step), либо более привычным способом start:stop:step внутри квадратных скобок.
Tishka17
03.09.2022 22:30+5Использование датакласса для хранения константы - очень плохое и бессмысленное действие.
Во-первых, классы нужны для того чтобы создавать экземпляры. У вас они не создаются
Во-вторых, датакласс нужен для того чтобы сгенерировать классу такие методы как__init__
и__eq__
, что снова не используется.
В-третьих, вы упоминаете frozen, но он влияет именно на проведение экземпляра класса, которого нет. Да и создать можно много экземпляров.В указаннлм примере константа на уровне модуля самый очевидный и хороший способ. При необходимости можно туда и аннотацию типа добавить.
Дополнение про возможность добавить методы имеет смысл, но это совершенно другая ситуация, не относящаяся к строковой константе.
Tishka17
04.09.2022 16:47+4Кажется, всё таки стоит написать большой комментарий.
1) Используйте генераторы
Как уже обсуждали выше это может замедлить программу или внести дополнительные негативные эффекты ввиду того, что это итератор. При этом генератор действительно позволяет сэкономить память и иногда код с ним получается проще, в других местах же они не подходят.3) Увеличивайте производительность вашего кода
Увеличивайте, если это дает что-то бизнесу. Ваше время не бесплатное. Вы можете потратить больше времени и денег, чем сэкономите за счет ускорения кода. Иногда скорость выполнения критчна, иногда нет. Стоит понимать в какой вы ситуации.
Кроме того, прежде чем заниматься оптимизацией неплохо бы понять что мы ускоряем и есть ли тут потенциал. Если у вас 99% времени занимает расчет на numpy или ожидании времени ответа СУБД, то никакие слоты и сабпроцессы не помогут.
Профилируйте код, ищите узкие места, оптимизируйте то, что имеет смысл оптимизировать. При этом, само собой, при наличии двух вариантов реализации не стоит выбирать просто так тот, который медленнее. Если оптимизация дается вам бесплатно (без значительного ухудшнения поддерживаемости кода и сроков), стыдно ей не воспользоваться3.1) Многопроцессность
Использование нескольких процессов по сравнению с потоками может привести к дополнительным накладным расходам на пересылку данных, невозможности использовать общие данные.
При использовании же алгоритмов, реализованных в нативных библиотеках, которые отпускают GIL это просто не имеет смысл.
Зачастую наши приложения деплоятся в одноядерном окружении и масштабируются горизонтально, не увеличивая число ядер на отдельном инстансе.
При выборе подхода учитывайте алгоритмы, реализацию, окружение и обязательно профилируйте код!3.3) Другие интерпретаторы
Это опция интересная, но доступная далеко не всегда. Плюс, как уже выше сказали, надо понимать, что использование альтернативного интерпрrтатора может ограничить вас в использовании библиотек, значительно усложнить процесс деплоя или нарушить стабильность кода.
Иногда так же использование другого интерпретатора приводит к повышению требований к ОЗУ.5) Используйте срезы
Используйте срезы там где вам нужен срез. Не забывайте, что взятие среза - копирование части списка, это легко может превратить ваш алгоритм с O(n) в O(n^2).
Стоит напомнить про существованиеislice
.6) Храните данные правильно
Используйте константы на уровне модуля там где вам нужна просто константа.
Используйте датаклассы, там где вам нужна структура данных, потенциально имеющаяся в нескольких экземплярах.
Используйте словари там, где вам нужны произвольные гомогенные ключи, а классы там где у вас поля имеют разный смысл или тип.
Используйте Enum там где у вас выбор значений из нескольких вариантов и других быть не может.
Не забывайте про\
и/
в путях.7) Используйте максимум возможностей ООП
7.1) Используйте утиную типизацию, но выделяйте абстракции
У нас всё таки Python. Если необходимо - декларируйте абстрактные классы или Protocol7.2) Используйте наследование и композицию.
Запомните, что наследование делает структуру кода более хрупкой и должно применяться с осторожностью. Это полезный инстурмент, но зачастую композиция дает более стабильное и гибкое решение. Почитайте про различные паттеры проектирования, в частности декоратор и стратегию7.3) Не используйте вложенные классы.
Вложенные классы дают примерно ничего, но лишь мешают их импортировать отдельно. В Python вкачестве неймспейсов используются именно модули, а классы нужны чтобы у них создавать экземпляры. Побочным эффектом вложенных классов является доступ к ним и переопределение при наследовании, это потенциально может быть полезно, но скорее всего наоборот навредит.8) Попробуйте кусочек функционального программирования
В целом в Python удобнее НЕ писать в ФП стиле, но есть оговорки.map
удобен когда у вас уже есть функция, но иногда его пытаются использовать с лямбдой, что только замедляет код. Используйте comprehension. Кроме list comprehension есть set comprehension и dict comprehension.Используйте частичное применение функций, это позволит вам избавиться от вложенных функций и повысит тестируемость кода. Но иногда вместо этого стоит создать callable-объект.
Пользуясь случаем, хочу поделиться своим каналом в telegram, где я рассказываю как лучше делать некоторые вещи: https://t.me/advice17
ryo_oh_ki
А как при этом быть с общепрограммным правилом: "лучше пишите реентерабельные функции"? Особенно, учитывая врождённые проблемы с GIL.
danilovmy
одно другому не мешает. И я согласен, что в большинстве Питонисты не умеющие пользоваться генераторами. Вот у тебя в коде создается лист или тупля. Зачем? Ты все равно ее когда нибудь проитерируешь. Тогда создай генератор. Но я хочу использовать лист несколько раз! Ага, ты хочешь пройти по листу несколько раз, вместо того, чтобы написать код который проходит только один раз. Всегда пожалуйста, но можно сделать pipe_generator и не итерироваться многократно.
kesn
Если Максим дважды итерирует по коллекции, он считает день неудачным :D
danilovmy
да, это больно. :*)
masai
А если ещё вспомнить, что списки и словари резервируют места больше, чем хранят элементов... :)
JordanCpp
Разве количество итераций не зависит от требований функционала? К примеру если нам нужно обработать массив пикселей однопроходным алгоритмов, то как бэ будет одна итерация. Если требуется постоянный обход списка чего то, то будет n итераций. Что за проблемы в питоне с итерациями?
danilovmy
для функционала многократного прохода по массиву действительно надо итерироваться.
CrocodileRed
Проблема в голове разработчика обычно )) Можно привести много примеров, когда многократная итерация по одной коллекции вполне норм
ReadOnlySadUser
Мне лично интересно, что делать, если мне не только итерироваться надо, а ещё и по индексам обращаться иногда)
CrocodileRed
Стройте lookup
models = repo.list()
lookup = {x.pk: x for x in models}
dmitrysvd
Как потом при отладке смотреть содержимое генератора?
danilovmy
смотря что надо.
писать простые генераторы однострочники и не тестировать
создавать генератор повторно после print/debug
принт/breackpoint перед yield в многострочнках?
лист превращать в генератор после ручного тестирования, в однострочниках
... предложи что то еще.
dmitrysvd
Почему многократная итерация это плохо? Какая разница, выполнить один цикл по три операции, или три цикла по одной?
danilovmy
если у тебя миллион записей или неизвестно большое количество элементов. то разница есть.