Как стать автором
Обновить

Комментарии 18

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

Код только для примера, разумеется.

Вот вполне себе production код, чтобы сделать "плоским" irregular list произвольной вложенности:


def flatten(l):
    for el in l:
        if isinstance(el, Iterable):
            for sub in flatten(el):
                yield sub
        else:
            yield el

И он не работает со строками если не добавить условие проверки на строку:


if isinstance(el, Iterable) and not isinstance(el, (str, bytes)):
    ...

Так работает.

произвольной вложенности

Это не так. Попробуйте сделать со своим кодом следующее (в CPython):


nested = reduce(lambda acc, _: [acc], range(sys.getrecursionlimit()), 0)
tuple(flatten(nested))

Получите RuntimeError: maximum recursion depth.


Если действительно нужно обрабатывать коллекции произвольной вложенности, можно использовать нерекурсивный подход (использовать свой стек).




А ещё в Python 3 можно использовать yield from:


def flatten(l):
    for el in l:
        if isinstance(el, Iterable):
            yield from flatten(el)
        else:
            yield el

Очевидно, что любая рекурсивная функция когда-то упрётся в recursion limit. Произвольная вложенность, конечно, в разумных пределах.


Про yield from понятно, но у меня подобный код используется в AsyncGenerator, где нет поддержки yield from.

Это не только неочевидно, но ещё и неверно.
Например алгоритм Евклида для поиска наибольшего общего делителя хорошо работает и в рекурсивном виде для обычных чисел. При этом именно такая реализация является самой наивной, лёгкой для восприятия.

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

Возможно, это будет уже придиркой, но вы писали не просто про корректный, а про production код, а к нему более жёсткие требования. Вряд ли функция, которая ломается от массива с ~тысячей элементов может им удовлетворять.
В питоне просто нет отдельного типа данных для хранения одного символа (юникодный аналог char из C). Поэтому даже один символ — это строка. И хоть __iter__ и итерируется по «символам», но возвращает он всётаки строки.
Совсем другое дело с типом bytes, для его элементов в питоне есть специальный тип — byte. И для него всё как бы нормально — итератор возвращает «числа», а не bytes с длиной в один байт.

Вообще‐то, итератор по bytes возвращает int. Может, где‐то внутри и есть тип byte, но пока что я вижу, что type(next(iter(bytes(b'abc')))) is int, а обращение к byte вызывает NameError. А в Python2 вообще bytes is str.

Да, с byte это я нагнал конечно, не проверил.

ну это Вы конечно дали гари. видимо впервые столкнулись с некоторыми странностями в питоне и «пригорело»)) Вас ждёт множество удивительных открытий
Упс, не угадали.
тогда я не понимаю чему Вы так удивляетесь в поведени строк. Да это странно, но так было как минимум с версии 2.2 (как было раньше не знаю). И как бы печально это не звучало, но всегда приходится писать отдельный «if» для строк в подобных универсальных методах для рекурсивных обходов. Да прочая добрая половина стандартных типов требуют повышенного внимания в рекурсивных алгоритмах
Если в апи есть проблема, её надо обсуждать и находить пути решения. Главным образом этой статьёй я хотел узнать, один я ли вижу проблему или она действительно есть.
сложно не согласиться. проблемы надо выводить на чистую воду. Однако давай рассудим логически: за 12 лет в апи строк в этом направлении изменений не было (если моя память не спит с другим). были ли вопросы подобные Вашему — конечно да и не одна сотня, а то и не одна тысяча. Я лично не встречал в рассылках упоминаний, что тут планируется что-то менять, сообщество это просто приняло и не бунтует (возможно зря, кто знает). И чтобы добраться до истины существует только один правильный путь — спросить автора))

В ruby 1.8.х у строки был метод each (практически аналог __iter__ в руби) https://ruby-doc.org/core-1.8.7/String.html#method-i-each, который итерировался по "линиям" (и был алиасом к each_line). Это лучше, чем по символам, но всё равно мешал.
В 1.9.1 его выкинули. Оставили each_line, each_byte и each_char.

Что можно сделать?

В подобном коде нам остаётся только добавлять условие для проверки строк:


if isinstance(foo, Iterable) and not isinstance(foo, (str, bytes)):
    ...
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.