Pull to refresh

Comments 48

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

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

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

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

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

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

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

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

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

Стройте lookup

models = repo.list()

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

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

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

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

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

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

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

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

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

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

Затраты на итерацию. Движок должен "прыгать" по коду больше в случае нескольких итераций. Чтобы прочувствовать, можете реализовать while через goto в Pascal

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

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

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

UFO just landed and posted this here

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

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

UFO just landed and posted this here

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

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

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

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

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

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

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

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

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

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

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

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

"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]

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

это почему? Функция мап какраз таки для этого и создана и оптимизированна

это почему? Функция мап какраз таки для этого и создана и оптимизированна

Вот заметка на эту тему:
For Loop vs. List Comprehension

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

Например,

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

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

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

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

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

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

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

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

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

Так и делаю.

Ваш комментарий отправился раньше времени, похоже. :(

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

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

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

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

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

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

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

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

Кажется, всё таки стоит написать большой комментарий.

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

Не забывайте про \ и / в путях.

Не очень понял, что имеется ввиду? Что нужно про них помнить?
import pathlib -- и все, не надо о слешах почти думать. Не представляю, зачем нужно как-то иначе работать с путями. Чтобы потом мучаться с портируемостью?

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

В рамках данной статьи был константа с прямым слэшом /, это теоретически может привести к некорректному поведению, когда его будут комбинировать дальше с путями в ОС Windows, содержащими обратный слэш \

В линейке Windows NT (т.е. последние лет 20, если я не ошибаюсь) можно использовать в путях прямой слэш.

А причем здесь объектная модель?
Хотите корректное мультиплатформенное решение? Используйте pathlib. Собираете пути? Создаете папки, что-то там парсите в названиях? Используйте pathlib.
Мне сложно придумать примеры, где pathlib не нужен при работе с путями. Можете привести такой?

Если вы не используете пути как объекты, а везде вам нужны строки, то все возжности покрываются os.path.

Sign up to leave a comment.

Articles