Мои личные наблюдения и правила

У всех разработчиков со временем нарабатывается опыт, растёт экспертиза. Когда вы много лет занимаетесь разработкой, приходит понимание каких-то общих концепций, вырабатываются правила поведения в конкретных сценариях.

В этой небольшой статье я хочу рассказать про свои 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()
Использование map()

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

9) Дескрипторы

Это достаточно сложная тема, но дающая огромную власть и величие :-)

С помощью дескрипторов можно автоматически проводить проверки, логировать, отправлять уведомления, вообщем всё что угодно. 

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

Пример использования дескрипторов
Пример использования дескрипторов

В этом коде видно использование дескриптора, мы говорим, что атрибуты price и quantity будут проверяться с помощью дескриптора NoNegative. Думаю название говорящие, этот дескриптор не позволит нам создать отрицательное число, что позволяет нам убрать эти проверки из __init__. 

А вот и сам дескриптор: 

Пример дескриптора
Пример дескриптора

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

10) Декомпозируйте правильно

Я специально оставил эту тему для последнего, главного пункта.

Как же много разработчиков, знающих все описанные мною ранее темы, но при это всё равно пишущие ужасный код. 

Такая грустная ситуация возникает именно из-за декомпозиции. 

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

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


Благодарю за чтение моей небольшой статьи, пишите свои Best Practices, сделаем код чуть чище!

Мой GitHub: https://github.com/Ryize

Комментарии (37)


  1. ryo_oh_ki
    02.09.2022 17:35

    Чаще используйте генераторы

    А как при этом быть с общепрограммным правилом: "лучше пишите реентерабельные функции"? Особенно, учитывая врождённые проблемы с GIL.


    1. danilovmy
      02.09.2022 21:28
      +1

      одно другому не мешает. И я согласен, что в большинстве Питонисты не умеющие пользоваться генераторами. Вот у тебя в коде создается лист или тупля. Зачем? Ты все равно ее когда нибудь проитерируешь. Тогда создай генератор. Но я хочу использовать лист несколько раз! Ага, ты хочешь пройти по листу несколько раз, вместо того, чтобы написать код который проходит только один раз. Всегда пожалуйста, но можно сделать pipe_generator и не итерироваться многократно.


      1. kesn
        02.09.2022 21:58
        +2

        Если Максим дважды итерирует по коллекции, он считает день неудачным :D


        1. danilovmy
          02.09.2022 22:26

          да, это больно. :*)


        1. masai
          03.09.2022 21:49

          А если ещё вспомнить, что списки и словари резервируют места больше, чем хранят элементов... :)


      1. JordanCpp
        02.09.2022 23:26
        +2

        Разве количество итераций не зависит от требований функционала? К примеру если нам нужно обработать массив пикселей однопроходным алгоритмов, то как бэ будет одна итерация. Если требуется постоянный обход списка чего то, то будет n итераций. Что за проблемы в питоне с итерациями?


        1. danilovmy
          03.09.2022 00:10

          для функционала многократного прохода по массиву действительно надо итерироваться.


        1. CrocodileRed
          03.09.2022 11:14
          +1

          Проблема в голове разработчика обычно )) Можно привести много примеров, когда многократная итерация по одной коллекции вполне норм


          1. ReadOnlySadUser
            03.09.2022 13:19

            Мне лично интересно, что делать, если мне не только итерироваться надо, а ещё и по индексам обращаться иногда)


            1. CrocodileRed
              03.09.2022 13:22

              Стройте lookup

              models = repo.list()

              lookup = {x.pk: x for x in models}


      1. dmitrysvd
        03.09.2022 21:27
        +1

        Как потом при отладке смотреть содержимое генератора?


        1. danilovmy
          03.09.2022 23:03

          смотря что надо.

          1. писать простые генераторы однострочники и не тестировать

          2. создавать генератор повторно после print/debug

          3. принт/breackpoint перед yield в многострочнках?

          4. лист превращать в генератор после ручного тестирования, в однострочниках

          5. ... предложи что то еще.


      1. dmitrysvd
        03.09.2022 21:33

        Почему многократная итерация это плохо? Какая разница, выполнить один цикл по три операции, или три цикла по одной?


        1. danilovmy
          03.09.2022 22:58

          если у тебя миллион записей или неизвестно большое количество элементов. то разница есть.


  1. TiesP
    02.09.2022 17:52

    А про вложенные функции что скажете — это best или не best? (на leetcode иногда встречаю такие решения). Плюсы то понятны — не надо параметры передавать.


    1. Infinitive1 Автор
      02.09.2022 19:25

      Я бы сказал, что это лучшие практики в трёх случаях:
      1) Декораторы;
      2) Замыкания;
      3) Карринг.
      Кроме того, иногда хочется создать вложенную функцию, чтобы не захламлять код. В таком случае это также оправдано, но тут тонкая грань, можно сильно попортить код вложенными функциями.


    1. CrocodileRed
      03.09.2022 11:21

      Плюсы, по-моему не в том, что параметры передавать не надо (кстати, надо :-)) Я при помощи вложенных функц разгружаю код основной функции и это хорошо.


      1. TiesP
        03.09.2022 11:26
        +1

        Ну я имел в виду параметры, которые есть в основной функции (которые нужны для вспомогательной) и которые вложенная «видит». Для плоской структуры функций все эти параметры нужно было бы передавать во вспомогательную.


    1. Tishka17
      03.09.2022 22:24
      +1

      Вложенные функции имеют смысл только для использования закмыканий при реализации декораторов, колбэков и т.п. (Частичное применение делается через functools.partial). В остальных случаях это ошибка: их использование как минимум затрудняет тестирование.

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


  1. krote
    02.09.2022 20:42
    +4

    А вот при многопроцессорности, вы сможете получить максимальную производительность. Поэтому, сложные вычисления/расчёты и т. п. - многопроцессорность.

    Может "многопроцессность"?


    1. Infinitive1 Автор
      03.09.2022 15:07

      Благодарю, исправил


      1. Tishka17
        04.09.2022 16:07
        +1

        не исправил


  1. JordanCpp
    02.09.2022 22:52
    +2

    Мне понравилась шутка, про оптимизацию кода на питоне:)


    1. Infinitive1 Автор
      03.09.2022 15:53

      :)


  1. Closius
    03.09.2022 10:21
    +1

    Попробуйте применить PyPy, он ускоряет работу вашей программы в десятки раз 

    А где гарантии, что эти ваши модные эксперименты не дадут ошибок в неожиданных местах? Да, можно сказать покрывайте тестами, но не все и не всегда можно покрыть тестами да и реально такова бывает.

    Например как pypy работает с тензорфлоу, если там уже numpy приходится другой юзать? Как там с совместимостью версий?

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


    1. nikolay_karelin
      04.09.2022 11:21

      TF и другие пакеты для расчётов во многом написаны на C (и CUDA и даже Fortran) и будут работать только с обычной реализацией Python. В этом и состоит основная идея Python для расчётов - язык-клей для оптимизированного кода из других библиотек.

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


  1. 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]
    


    1. GeorgeII
      03.09.2022 17:12
      +4

      Ага. В питоне даже самые базовые принципы из ФП по типу применения на списке последовательных map с filter на лямбдах - это боль и очевидная чужеродность. Тот же самый результат, за исключением ленивости, дает list-comprehension с if-guard'ом и такой подход фактически считается pythonic-way


  1. masai
    03.09.2022 14:12

    Генераторы не всегда быстрее.

    Например,

    sum(i for i in range(10_000))

    работает медленнее, чем

    sum([i for i in range(10_000)])

    По крайней мере я сейчас проверил и получил такие числа:


    1. Tishka17
      03.09.2022 22:40

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

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

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


      1. masai
        03.09.2022 23:17
        +1

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

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

        Так и делаю.


  1. vanyas
    03.09.2022 14:58

    Долго не мог понять, что ещё за "срезы", пока не увидел a[:3]. Это называется Слайсы же, не надо пытаться переводить устоявшиеся термины на Русский, становится непонятно.


    1. Infinitive1 Автор
      03.09.2022 15:10
      +4

      Понятия срезы уже +- устоялось. Во многих статьях используется именно это название.
      Всё равно благодарю за замечание, в заголовке пункта в скобках указал слайсы


    1. Tishka17
      03.09.2022 22:27
      +1

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


      1. squaremirrow
        04.09.2022 00:13

        В питоне слайсы — тоже объекты. Создаются, внезапно, с помощью команды slice(start, stop, step), либо более привычным способом start:stop:step внутри квадратных скобок.


  1. Tishka17
    03.09.2022 22:30
    +5

    Использование датакласса для хранения константы - очень плохое и бессмысленное действие.

    Во-первых, классы нужны для того чтобы создавать экземпляры. У вас они не создаются
    Во-вторых, датакласс нужен для того чтобы сгенерировать классу такие методы как __init__ и __eq__, что снова не используется.
    В-третьих, вы упоминаете frozen, но он влияет именно на проведение экземпляра класса, которого нет. Да и создать можно много экземпляров.

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

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


  1. 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. Если необходимо - декларируйте абстрактные классы или Protocol

    7.2) Используйте наследование и композицию.
    Запомните, что наследование делает структуру кода более хрупкой и должно применяться с осторожностью. Это полезный инстурмент, но зачастую композиция дает более стабильное и гибкое решение. Почитайте про различные паттеры проектирования, в частности декоратор и стратегию

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

    8) Попробуйте кусочек функционального программирования
    В целом в Python удобнее НЕ писать в ФП стиле, но есть оговорки.

    map удобен когда у вас уже есть функция, но иногда его пытаются использовать с лямбдой, что только замедляет код. Используйте comprehension. Кроме list comprehension есть set comprehension и dict comprehension.

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

    Пользуясь случаем, хочу поделиться своим каналом в telegram, где я рассказываю как лучше делать некоторые вещи: https://t.me/advice17