half-python
Kandinsky 2.1: Умпалумпы программируют python код без yield
Иногда говорят, что код имеет запах. Это относится к стилистике написания, выбору переменных и т.п. Однако, когда речь идет про циклы, я предпочитаю использовать термин «недо-yield», характеризующий стиль работы программиста в циклах и с массивами данных.

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


Disclaimer:

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


Первый признак «недо-yield» — Вальтруизм (Whiletruism).


Ну конечно, кто ж не знает старину «while»!

# Synchronous put. For threads.
    def put(self, item):
        while True:
            fut = self._put(item)
            if not fut:
                break
curio, queue.py
Мне не нравится этот стиль написания, поскольку while по идее должен проверять условие, которого нет. Я предпочитаю использовать более декларативную конструкцию.

Для этого мне понадобится built-in iter из PEP 234 – Iterators:

iter(func, sentinel) 

Когда мы используем функцию iter(func, sentinel), каждое обращение к итератору будет вызывать функцию func() и возвращать её результат, до тех пор, пока он не станет равным sentinel. Зная это — можно легко получить вечный генератор:

iter(int, 1)  # самый известный мне вечный генератор.

C помощью вечного генератора можно написать такой вариант вечного цикла:

infinity = iter(int, 1)  # infinity generator
for _moment_ in infinity:
    ...  # Carpe diem
Лови момент, так сказать.
Все известные мне варианты перебора бесконечностей из itertools проигрывают предложенному варианту, но вы можете использовать:

  • count(start=0, step=1): 0, 1, 2, 3, 4,… раньше был не бесконечный, говорят, уже поправили.
  • cycle(p): p[0], p[1], ..., p[-1], p[0], ...
  • repeat(x, times=∞): x, x, x, x, ...

Если вы не эстет, то пока никакой выгоды от такой замены кода вы не получите.

Следующий признак «недо-yield» — Прерванный Вальтруизм(Breakable Whiletruism).


Вот пример этого родственника бытового вальтруизма:

def build_values(self, args, kwargs):
        values = {}
        if args:
            arg_iter = enumerate(args)
            while True:
                try:
                    i, a = next(arg_iter)
                except StopIteration:
                    break
               arg_name = self.arg_mapping.get(i)
               ....
pydantic, decorators.py
Я предложил бы написать этот код с использованием генераторного выражения. Для этого нам пригодится PEP 289 – Generator Expressions.

def build_values(self, *args, **kwargs):
    finc = self.arg_mapping.get
    gen = (func(idx), arg for idx, arg in enumerate(args))
    for arg_name, arg in gen:
        # do something

С этого момента начинает проявляться важность использования генераторов:

  1. Код с генератором выполняет то же самое, только код короче.
  2. Мы избежали объявления дополнительных переменных.
  3. По моему мнению, когнитивная сложность этой части кода ниже. Я делал доклад на тему сложности кода на PyCon DE 2022, кому сложно понимать мой немглийский, этот же доклад на русском.


Вершина вальтруизма — блок классического For-loop вкупе с break. (Breakable Looping)


Вы можете встретить подобный пугающий код:

host_header = None
for key, value in scope["headers"]:
    if key == b"host":
        host_header = value.decode("latin-1")
        break
starlette, datastructires.py
— А что такого ужасного в моем коде, возмутился Пупа, увидев, что я пишу эту статью.
— Смотри, ты ранее уже положил в цикле информацию в список «headers», и после ищешь ключ host. Что мешает тебе сразу создать словарь, ведь потом все равно список используется как словарь. Значение получить проще: headers.get('host'). Кстати, ты можешь получить host еще на этапе создания «headers»...

Конечно, у такой задачи много решений. Я бы использовал функцию next:

host_header = next((val.decode("latin-1") for key, val in scope["headers"] if key == b"host"), None)

Пупа, скорее всего, покрутит пальцем у виска и побежит за… своим коллегой. А я попробую объяснить, почему наличие break в цикле for — это одно из диких недоразумений, которые могут встретиться в коде. Я считаю, что такой паттерн также характеризует «недо-yield» кода.

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

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

Отвечая на эти вопросы, приходим к следующему признаку «недо-yield»:

Создание List в циклах. Loop for List


Рассмотрим пример:

if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)):
        encoded_list = []
        for item in obj:
            encoded_list.append(jsonable_encoder(item, *args, **kwargs))
        return encoded_list
fastapi, encoders.py
Если смотреть на код fastapi дальше, то видно, что возвращаемое значение encoded_list позже будет проитерировано. В таком случае, действительно, имеет смысл использовать генератор:

if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)):
    return (
        jsonable_encoder(item, *args, ** kwargs) for item in obj
    )

Ну как же, начнет возмущаться Лупа, увидев мой код. — У тебя же не так очевидно, как в моем варианте.
В принципе да, если нет опыта работы с генераторами, очевидность теряется, отвечаю я.
Но подождите, это же еще и тестировать невозможно! — восклицает Пупа за… своим другом.

И здесь я соглашусь:

В Python не так много средств для отладки генераторных выражений


  • Для отладки можно написать простую обертку-генератор:

def genreport(gen):
    return ((print(item), item)[-1] for item in gen)  # это может быть и logging.log


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

  • Кроме того, есть библиотека itertools

itertools.tee(iterable, n=2)
# Return n independent iterators from a single iterable.


Превращает ваш итератор в два независимых итератора. Один можно использовать для тестирования, второй — для продолжения выполнения. Ознакомьтесь с ограничениями использования.


more_itertools.spy(iterable, n=1)
# Return a 2-tuple with a list containing the first n elements of iterable, and an iterator with the same items as iterable.
#This allows you to “look ahead” at the items in the iterable without advancing it.
Позволяет «взглянуть вперед» на элементы итерируемого объекта, не «продвигая» его. Читайте ограничения использования.
Про more_itertools я узнал от глубоко почитаемого мной kesn, хотя фраза в его недавней статье «Генераторы всем хороши, кроме одного: они откладывают выполнение кода, и в реальности узнать, когда ваш код выполнится, бывает затруднительно...» показывает неприятие величайшей сути генератора: выполняться только когда нужно, а когда не нужно — не выполняться.

У меня есть на это пример с множественным continue в цикле, конечно же, как признак «недо-yield»:

Продолжающая Форлупнутость. Continued Forlooperty


Представим себе код:

def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
    """Unsets a cookie by name, by default over all domains and paths. Wraps CookieJar.clear(), is O(n)."""
    clearables = []
    for cookie in cookiejar:
        if cookie.name != name:
            continue
        if domain is not None and domain != cookie.domain:
            continue
        if path is not None and path != cookie.path:
            continue
        clearables.append((cookie.domain, cookie.path, cookie.name))
        ... 
request, cookies.py
Тут мне кажется странным следующее: если при создании множественного списка элементов позже многие из них будут пропущены, то зачем было создавать такой список?

Помочь нам в этой ситуации может David Beazley с презентацией coroutines, начинаем читать со слайда 34 про генераторы в цепочке generators pipeline:

def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
    """Unsets a cookie by name, by default over all domains and paths """
    clearables = (cookie for cookie in cookiejar if cookie.name == name)
    if domain is not None:
        clearables = (cookie for cookie in clearables if domain == cookie.domain)
    if path is not None:
        clearables = (cookie for cookie in clearables if path == cookie.path)
   ...

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

Еще один пример для тренировки, как работать с генераторами в цепочке, мне, кстати, кажется, что в исходнике есть небольшая ошибка:

    def _read_file(self, file_name):
        file_values = {}
        with open(file_name) as input_file:
            for line in input_file.readlines():
                line = line.strip()
                if "=" in line and not line.startswith("#"):
                    key, value = line.split("=", 1)
                    key = key.strip()
                    value = value.strip().strip("\"'")
                    file_values[key] = value
        return file_values
starlette, config.py
Моя рекомендация по улучшению этого кода остается неизменной: строим трубопровод (pipeline):

def _read_file(self, file_name):
        with open(file_name) as input_file:
            lines = (line for line in input_file.readlines())
            lines = (line.strip() for line in lines if line or lines.close())  # спасибо Пупе и Лупе
            lines = (line.partition("=") for line in lines if "=" in line and not line.startswith("#"))
            return {key.rstrip() : value.lstrip().strip("\"'") for key, __, val in lines}

В ранних версиях Python этот код выглядит более элегантным, но Пупа и Лупа быстро подсуетились и внесли PEP 479 – Change StopIteration handling inside generators.
Именно поэтому мне приходится использовать generator.close() из PEP 342 – Coroutines via Enhanced Generators , чтобы остановить работу генератора внутри генератора.

Завершим наше исследование финальным признаком «недо-yield»:

Любовь к изменению списков. List-changes Love.


Эта часть — мой основной аргумент для всех пупалупов. Замена генерации списков/словарей итераторами не спасет вас. Многократное использование одного и того же списка — это тот самый супер-пупер-лупер паттерн, способный убить производительность даже самой отлаженной и отрефакторенной библиотеки!

Вот вам пример кода, написанного Пупой за… его напарника, который имеет огромное количество положительных оценок на stackowerflow, попал в документацию DRF и уже опубликован на страницах HABR.

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        # Don't pass the 'fields' arg up to the superclass
        fields = kwargs.pop('fields', None)
        # Instantiate the superclass normally
        super().__init__(*args, **kwargs)
        if fields is not None:
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields)
            for field_name in existing - allowed:
                self.fields.pop(field_name)

И ни одна Пупа или Лупа, а с ними и множество других разработчиков не видят проблемы, которая, на поверку, оказывается довольно ощутимой в production.

Расскажу вам, в чем тут косяк. Я обнаружил его в 2017 году, когда заметил, что мой сериализатор dynamicFieldsModel для «толстой» модели работал медленнее обычного при сериализации только трех запрошенных полей. В приведенном выше примере происходят изменения сериализатора в коде после super().__init_(), и, возможно, причина замедления именно в этом коде.

И, да, так оно и оказалось — причиной было пупалупное решение сначала создать ВСЕ поля модели, а затем выполнить проход по списку полей и удалить ненужные. Там вообще-то dict-like object, но не будем портить такую хорошую притчу. Ад многократных проходов по спискам внутри самой DRF я еще упомяну.

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

Оказывается, что создание полей после инициализации сериализатора происходит в ленивом property fields, которое в первую очередь вызывает get_field_names:

# Methods for determining the set of field names to include...
    def get_field_names(self, declared_fields, info):
        """ Returns the list of all field names that should be created when
        instantiating this serializer class. This is based on the default
        set of fields, but also takes into account the `Meta.fields` or
        `Meta.exclude` options if they have been specified. """
        fields = getattr(self.Meta, 'fields', None)
        exclude = getattr(self.Meta, 'exclude', None)
        ...
rest_framework, serializers.py
Как видим, информацию о полях get_field_names берет из self.Meta.

Только не поддавайтесь первому пупалупному желанию переопределить self.Meta.fields/self.Meta.extra. Этим вы сломаете все и сразу: Meta — это синглтон для всех объектов этого класса.

А вот так — уже можно:
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
    """A ModelSerializer that takes an additional `fields` argument that controls which fields should be displayed."""
    def __init__(self, *args, **kwargs):
        self.Meta = type('Meta', (self.Meta,) {'fields' : kwargs.pop('fields', self.Meta.fields)})
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

Разумеется, переданные fields проверены на наличие в модели, и в декларации класса сериализатора определены Meta.fields. Попробуйте, возможно, результат вас удивит.

В этом примере я хочу выразить еще один признак «недо-yield». Создание списка объектов и его изменение в дальнейшем — это ужасное программное решение. Хуже может быть только многократное изменение этого же списка.

Подводя итог моим размышлениям над «пупалупским» кодом, соберу все воедино:

У вас лютый «недо-yield» в коде, если часто встречаются следующие признаки:


  1. Основной «недо-yield» в проекте характеризуется соотношением количества yield с количеством циклов на количество файлов проекта.
  2. Whiletruism. Измеряется количеством бесконечных циклов в проекте.
  3. Breakable Whiletruism. Измеряется количеством бесконечных циклов с break в проекте.
  4. Breakable Looping. Измеряется количеством for циклов с break в проекте.
  5. Continuable Looping. Измеряется количеством for циклов с continue в проекте.
  6. Loop for List. Измеряется количеством объявлений "= [ ]" перед циклом с .append в проекте.
  7. Looped List. Измеряется количеством «For… in [...» в проекте.
  8. List-Change Love. Измеряется количеством .extend/.pop/.append и т.п. в циклах в проекте.

Использование вышеупомянутых паттернов в коде не обязательно ошибочно, и надежнее сделать замеры времени. Мой способ позволяет мне предположить наличие «пупалупных» кусков кода еще до запуска. Когда полученные цифры выглядят странно, становится ясно, что Пупа и Лупа где-то рядом.

Давайте проверим несколько библиотек:

  • DRF оказалась рекордсменом «пупалупия»: 590 циклов, 8 yield, 72 файла, 5 while, 23 break, 24 continue.
  • Pydantic, та еще «пупалупа»: 436 циклов и 107 yield на 26 файлов, и 13 while, 6 break, 38 continue.
  • Сравните с fastapi: 70 циклов и 38 yield на 42 файла, и ни одного while или break, только 8 continue. Я раньше реально недооценивал качество кода этой библиотеки.

Моя любимая библиотека django.contrib.admin показала: 326 циклов и 38 yield на 29 файлов.
Об этих и других моих исследованиях Django.admin я докладывал на Django Con EU 2022 и после на Django Con US 2022. Это был интересный опыт, не уверен только, что после моих заявлений о ежегодном многокилометровом недоелде Django.admin меня позовут выступить там еще раз.

В завершение предлагаю вам попробовать посмотреть характеристики «недо-yield» вашего проекта, и, может быть, вы захотите поделиться своими результатами в комментариях. Также буду рад услышать истории о том, как вы используете и тестируете генераторы в коде.


P.S. Те, кто еще не знаком с Пупой и Лупой, это два умпалумпа и про их приключения есть множество историй.

P.P.S. На вопрос, откуда такие галимые примеры — все блоки кода для статьи взяты из публичных репозиториев. В каждом примере указан источник.

Могу добавить, что в приватных проектах ситуация не лучше. С 2017 года я в роли Code-Ментора для Python-разработчиков повидал множество репозиториев. Если проект большой и сменил несколько разработчиков, то каждая доработка наслаивается на предыдущий код и, например, паттерн looped-list мог объединять десятки повторов.

P.P.P.S. Для генерации картинок в статью я использовал рекламируемый сейчас на Habr Kandinsky 2.1. Генератор так себе, но иногда он попадает в цель. Смотрите, как и выглядит и пахнет Python-код без генераторов.

cacaha
Kandinsky 2.1: Пупа и Лупа пишут код без yield
Хочу больше Кандинского!
lutiy
Kandinsky 2.1: Лютый недо-yield в программном коде Python без генераторов

malevich
Kandinsky 2.1: Недо-yield в программном коде Python, --малевич

kilometr
Kandinsky 2.1: Многокилометровый недо yield программного кода

putin?
Kandinsky 2.1: недо yield программного кода Python надо устранять!

Lupa
Kandinsky 2.1: Пупа и Лупа программируют недо-yield

Middleage
Kandinsky 2.1: бытовой вальтруизм не так ужасен, как его близкий родственник прерванный вальтруизм(Breakable Whiletruism)

udavinchi
Kandinsky 2.1: пупа и лупа пишут код -ренессанс

frogs
Kandinsky 2.1: пупа и лупа пишут код на Python

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


  1. HemulGM
    12.04.2023 04:40
    +13

    Правильно ли я понимаю, что автор, в большинстве примеров, заменяет большую, но легкочитаемую конструкцию, на короткую, но трудночитаемую, без какого-то профита?


    1. remzalp
      12.04.2023 04:40
      +1

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


      1. tuupic
        12.04.2023 04:40

        Помню, коллега-аналитик, как-то спрашивал совета, что лучше сделать(просто потому что я под рукой оказался). Что во время его функций обработки данных из базы, СУБД коннект закрывает. Через пол-суток.

        На вопрос, какой объём данных он считывает из базы, он ответил, что мегабайт 100-200, не больше. А функции обработки дёргали ещё апишки всякие и т.д., вот и занимало много времени.

        Вот вам и ленивая обработка, с yield


    1. danilovmy Автор
      12.04.2023 04:40
      +2

      Согласен, что стилистика кода меняется. Становится ли труднее читать — не уверен. Предлагаю рассматривать мой вариант написания, как некий DSL. Поначалу нечитаемо, после привыкаешь.
      По поводу профита — генератор позволяет отложить вычисление на неопределённый срок и не занимать все это время память. Вот и весь профит.


      1. HemulGM
        12.04.2023 04:40

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


        1. danilovmy Автор
          12.04.2023 04:40

          Я сам из Мира Ассемблера пришел, и, поначалу, году эдак в 2012-2014, генераторы мне казались одной из бесполезных штук в Python. После я подсел на Dabeaz, и его гениальные презентации о генераторах, корутинах, корпоративную многозадачность и т.п. Конечно, я фанат его Curio, поскольку понял, насколько это круче asyncio. Так вот, если полагаться на его замеры из 2009, то генератор не сильно проигрывает циклу. Презентация coroutines, слайд 51. На следующих слайдах он почти сокращает отставание.
          Выигрыш проявится совсем не в этом месте. В REST представлении, например, надо сереализовать полмиллиона точек и отдать пользователю. Я могу их подготовить заранее в list/tuple. В случае с генератором — генерация точек запланирована, но не выполнена. И тут, если что то после пойдет не так, я выброшу 427 exception. В итоге ответ 427 пришел пользователю быстрее в случае с генератором.


          1. whoisking
            12.04.2023 04:40
            +1

            Можно подробнее про пример с полмиллионом точек?
            1) Это стриминг или классический ответ с данными на запрос?
            2) Что такое 427? В HTTP статус-кодах гуглится как unassigned.
            3) Не понял, почему мы в цикле не можем сразу рейзить ошибку? (хотя, конечно, try except вставлять в итерацию в любом случае не лучшее решение) Или что тут имеется ввиду?


            1. danilovmy Автор
              12.04.2023 04:40
              +1

              1. REST. вопрос-ответ, в ответе schape(фигура) для карты высокой точности. про полмиллиона приврал, максимум 17 тысяч точек.
              2. 427 — unprocessable entity. Это когда все верно на входе, например запрошено пересечение искомого шейпа с шейпом, Пересечение ищет Postgis. нет ошибки расчета, но ответ не соответствует ожидаемому. например: шейпы пересекаются в одной точке. точка это не плоскость пересечения. Для плоскости минимум три точки надо. Потому не получается вернуть фигуру, а только ее частный случай прямая или точка. Мы маркируем такой ответ как 427. Хотя можно было бы писать, не найдено пересечений, но найдены граничные области. Вопрос вкуса.
              3. В случае обсчета объемов/плоскостей невозможно по одной точке понять что есть ошибка. А так то — Exception хорошее решение, EAFP в питоне это норм практика.


              1. whoisking
                12.04.2023 04:40
                +1

                2) 422
                2.5) Стало очень интересно, что за задача на входе и что за тип данных, где так принципиально иметь больше 1 точки в качестве пересечения, просто представил полигоны и там обычно в пересечении тоже полигон ... (без снобизма, реально интересно, для общего развития, так сказать)
                3) Я всё-таки не понял, в чём тут разница между примененияем генератора и применением цикла? Если и там и там нам надо сперва посчитать, почему пользователь ошибку увидит раньше?


                1. danilovmy Автор
                  12.04.2023 04:40
                  +1

                  1. Принимается, 422. Попутал
                  2. Все верно. Линия и точка это полигон нулевой площади. Можно вернуть пользователю полигон нулевой площади. И отрисовать его. Или обработать на клиенте и выдать ошибку. А можно вернуть ошибку с сервера. Задача то простая для менеджера. Нарисовал виноградник на карте, порверил, что DOC совпадает. не совпала — выдал ошибку, совпала — выдал DOC.
                  3. Вероятно решить можно проще и без генераторов. Надо еще подумать.


  1. nimishin
    12.04.2023 04:40
    +3

    Отличный стиль, очень увлекательно, но с читаемостью проблемы. А стоит ли улучшать не критичный код?


    1. danilovmy Автор
      12.04.2023 04:40

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


  1. whoisking
    12.04.2023 04:40

    Мама, я в телевизоре


  1. Nikola2222
    12.04.2023 04:40
    +1

    В примере где используется generator.close() в последней строке я не нашел закрывающую фигурную скобку, или так надо?


    1. danilovmy Автор
      12.04.2023 04:40

      спасибо, не та скобка стояла, исправил.


  1. Dirlandets
    12.04.2023 04:40
    +2

    Конечно в статье есть много спорных утверждений, но хотел сказать другое!

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

    На счет нечитаемости генераторов Мне кажется по началу только конструкция с or может вызвать проблемы, а с pipelines так вообще только лучше становится.


    1. danilovmy Автор
      12.04.2023 04:40
      +2

      Спасибо. Такие отзывы мотивируют писать еще.


  1. evgenyk
    12.04.2023 04:40
    +6

    Статья хорошая, поднимает интересную проблему. Как писать код на питоне, когда есть варианты.

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

    Я выучил новую языковую конструкцию, буду теперь пихать ее везде и всюду.

    Я использовал языковую конструкцию, которая позволяет запихнуть в одну строку то, что обычный цикл делает в несколько строк. Ура! Теперь мой код чище.

    Моя точк зрения по этому вопросу. Главное в коде читаемость. И одна из главных фишек питона, это отступы, которые вместе с циклами гарантируют хороший уровень читабельности кода. Вот например:

    while True:
      if a:
        break

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

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


    1. tuupic
      12.04.2023 04:40
      +1

      Тоже не понимаю, зачем сокращать количество строк, ради "чистоты". KISS же

      На днях как раз колупался в коде, где в строку было map/filter/reduce, приправленное join(tuple(filter(lambda

      Причём, timeit показал, что оно ещё и работает медленнее, написанного в человекочитаемом виде.Хотя, может, timeit врёт, конечно.


      1. danilovmy Автор
        12.04.2023 04:40
        +1

        Проверил, сработает без tuple


        template.join(filter(lambda

        И сработает так же без filter


        template.join(bit for bit in iterable if lambda(bit))
        

        Теперь должно работать быстро.


        Человекочитаемость у всех своя, так тоже работает:


        template.join(
            bit for bit in iterable 
                 if lambda(bit)
        )