Как определить, является ли контейнер бесконечно рекурсивным и найти его самый маленький уникальный контейнер?

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

def flat(iterable):
    try:
        iter(iterable)
    except TypeError:
        yield iterable
    else:
        for item in iterable:
            yield from flatten(item)

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

def flatter(iterable):
    try:
        iter(iterable)
        if isinstance(iterable, str):
            raise TypeError
    except TypeError:
        yield iterable
    else:
        for item in iterable:
            yield from flatten(item)

Теперь он работает и для строк. Тем не менее, я тогда вспомнил, что a list может содержать ссылки на себя.

>>> lst = []
>>> lst.append(lst)
>>> lst
[[...]]
>>> lst[0][0][0][0] is lst
True

Таким образом, строка не является единственным типом, который может вызвать такую ​​проблему. На этом этапе я начал искать способ защитить эту проблему без явной проверки типов.

Следующее flattener.py последовало. flattish() - это версия, которая просто проверяет строки. flatten_notype() проверяет, соответствует ли первый элемент первого элемента объекта самому себе для определения рекурсии. flatten() делает это, а затем проверяет, является ли объект или первый элемент первого элемента экземпляром другого типа. Класс Fake в основном просто определяет оболочку для последовательностей. Комментарии по строкам, которые проверяют каждую функцию, описывают результаты в форме should be `desired_result` [> `undesired_actual_result`]. Как вы можете видеть, каждый из них терпит неудачу различными способами Fake, обернутыми вокруг строки, Fake, обернутыми вокруг целых чисел list, односимвольных строк и многосимвольных строк.

def flattish(*i):
    for item in i:
        try: iter(item)
        except: yield item
        else:
            if isinstance(item, str): yield item
            else: yield from flattish(*item)

class Fake:
    def __init__(self, l):
        self.l = l
        self.index = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.index >= len(self.l):
            raise StopIteration
        else:
            self.index +=1
            return self.l[self.index-1]
    def __str__(self):
        return str(self.l)

def flatten_notype(*i):
    for item in i:
        try:
            n = next(iter(item))
            try:
                n2 = next(iter(n))
                recur = n == n2
            except TypeError:
                yield from flatten(*item)
            else:
                if recur:
                    yield item
                else:
                    yield from flatten(*item)
        except TypeError:
            yield item

def flatten(*i):
    for item in i:
        try:
            n = next(iter(item))
            try:
                n2 = next(iter(n))
                recur = n == n2
            except TypeError:
                yield from flatten(*item)
            else:
                if recur:
                    yield item if isinstance(n2, type(item)) or isinstance(item, type(n2)) else n2
                else:
                    yield from flatten(*item)
        except TypeError:
            yield item


f = Fake('abc')

print(*flattish(f)) # should be `abc`
print(*flattish((f,))) # should be `abc` > ``
print(*flattish(1, ('a',), ('bc',))) # should be `1 a bc`

f = Fake([1, 2, 3])

print(*flattish(f)) # should be `1 2 3`
print(*flattish((f,))) # should be `1 2 3` > ``
print(*flattish(1, ('a',), ('bc',))) # should be `1 a bc`

f = Fake('abc')
print(*flatten_notype(f)) # should be `abc`
print(*flatten_notype((f,))) # should be `abc` > `c`
print(*flatten_notype(1, ('a',), ('bc',))) # should be `1 a bc` > `1 ('a',) bc`

f = Fake([1, 2, 3])     

print(*flatten_notype(f)) # should be `1 2 3` > `2 3`
print(*flatten_notype((f,))) # should be `1 2 3` > ``
print(*flatten_notype(1, ('a',), ('bc',))) # should be `1 a bc` > `1 ('a',) bc`

f = Fake('abc')
print(*flatten(f)) # should be `abc` > `a`
print(*flatten((f,))) # should be `abc` > `c`
print(*flatten(1, ('a',), ('bc',))) # should be `1 a bc`

f = Fake([1, 2, 3])     

print(*flatten(f)) # should be `1 2 3` > `2 3`
print(*flatten((f,))) # should be `1 2 3` > ``
print(*flatten(1, ('a',), ('bc',))) # should be `1 a bc`

Я также пробовал следующее с рекурсивным lst, определенным выше, и flatten():

>>> print(*flatten(lst))
[[...]]
>>> lst.append(0)
>>> print(*flatten(lst))
[[...], 0]
>>> print(*list(flatten(lst))[0])
[[...], 0] 0

Как вы можете видеть, он работает аналогично 1 ('a',) bc, а также по-своему.

Я читаю как функция python может получить доступ к своим собственным атрибутам? думает, что, возможно, функция может отслеживать каждый объект, который он видел, но это не сработало бы потому что наш lst содержит объект с совпадающим идентификатором и равенством, строки содержат объекты, которые могут иметь только совпадающее равенство, а равенства недостаточно из-за возможности чего-то вроде flatten([1, 2], [1, 2]).

Есть ли какой-либо надежный способ (т.е. не просто проверять известные типы, не требует, чтобы рекурсивный контейнер и его контейнеры были одного типа и т.д.), чтобы проверить, содержит ли контейнер итерируемые объекты с потенциальным бесконечным рекурсии и надежно определить самый маленький уникальный контейнер? Если есть, объясните, как это можно сделать, почему оно надежное и как оно обрабатывает различные рекурсивные обстоятельства. Если нет, объясните, почему это логически невозможно.

Ответ 1

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

from collections import deque

def flat(iterable):

    d = deque([iterable])

    def _primitive(x):
        return type(x) in (int, float, bool, str, unicode)

    def _next():
        x = d.popleft()
        if _primitive(x):
            return True, x
        d.extend(x)
        return False, None

    while d:
        ok, x = _next()
        if ok:
            yield x


xs = [1,[2], 'abc']
xs.insert(0, xs)

for p in flat(xs):
    print p

Вышеприведенное определение "примитив" является, ну, примитивным, но это, безусловно, может быть улучшено.

Ответ 2

Что-то вроде этого:

def flat(obj, used=[], old=None):        
    #This is to get inf. recurrences
    if obj==old:
        if obj not in used:
            used.append(obj)
            yield obj
        raise StopIteration
    try:
        #Get strings
        if isinstance(obj, str):
            raise TypeError
        #Try to iterate the obj
        for item in obj:
            yield from flat(item, used, obj)
    except TypeError:
        #Get non-iterable items
        if obj not in used:
            used.append(obj)
            yield obj

После конечного числа шагов (рекурсии) список будет содержать не более самого себя как итерируемый элемент (так как мы должны генерировать его на конечных множества шагов). То, что мы тестируем с помощью obj==old, где obj в элементе old.

Список used отслеживает все элементы, так как мы хотим, чтобы каждый элемент был только один раз. Мы могли бы удалить его, но мы получили бы уродливое (и, что более важно, не четко определенное) поведение, по которому элементы получают доходность, как часто. Недостатком является то, что мы сохраняем весь список в конце в списке used...

Тестирование этого с некоторыми списками, похоже, работает:

>> lst = [1]
>> lst.append(lst)
>> print('\nList1:   ', lst)       
>> print([x for x in flat(lst)])
List1:     [1, [...]]
Elements: [1, [1, [...]]]

#We'd need to reset the iterator here!
>> lst2 = []
>> lst2.append(lst2)
>> lst2.append((1,'ab'))
>> lst2.append(lst)
>> lst2.append(3)
>> print('\nList2:   ', lst2)       
>> print([x for x in flat(lst2)])
List2:     [[...], (1, 'ab'), [1, [...]], 3]
Elements: [[[...], (1, 'ab'), [1, [...]], 3], 1, 'ab', [1, [...]], 3]

Примечание. На самом деле имеет смысл, что бесконечные списки [[...], (1, 'ab'), [1, [...]], 3] и [1, [...]] считаются элементами, поскольку они фактически содержат себя, но если это не нужно, можно прокомментировать первый yield в приведенном выше коде.

Ответ 3

Есть проблема с вашим тестовым кодом, который не связан с проблемой рекурсивного контейнера, которую вы пытаетесь решить. Проблема в том, что ваш класс Fake является итератором и может использоваться только один раз. После того, как вы повторите все его значения, он всегда будет поднимать StopIteration, когда вы снова попытаетесь повторить его.

Итак, если вы выполняете несколько операций с одним и тем же экземпляром Fake, вы не должны ожидать, чтобы что-то было пустым, после того, как первая операция уничтожила итератор. Если вы воссоздаете итератор перед каждой операцией, у вас не будет этой проблемы (и вы действительно можете попытаться решить проблему рекурсии).

Итак, к этому вопросу. Один из способов избежать бесконечной рекурсии - поддерживать стек с объектами, в которые вы в настоящее время вложены. Если следующее значение, которое вы видите, уже находится в стеке, вы знаете его рекурсивным и можете пропустить его. Вот реализация этого, используя список как стек:

def flatten(obj, stack=None):
    if stack is None:
        stack = []

    if obj in stack:
        yield obj

    try:
        it = iter(obj)
    except TypeError:
        yield obj
    else:
        stack.append(obj)
        for item in it:
            yield from flatten(item, stack)
        stack.pop()

Обратите внимание, что это может по-прежнему выдавать значения из одного и того же контейнера более одного раза, если оно не вложено внутри себя (например, для x=[1, 2]; y=[x, 3, x]; print(*flatten(y)) будет печатать 1 2 3 1 2).

Он также перезаписывает строки, но он будет делать это только для одного уровня, поэтому flatten("foo") будет давать буквы 'f', 'o' и 'o' по очереди. Если вы хотите этого избежать, вам, вероятно, нужна функция, которая будет иметь тип, поскольку с точки зрения протокола итерации строка не отличается от итерабельного контейнера его букв. Это только одиночные символьные строки, которые рекурсивно содержат себя.

Ответ 4

Сценарий, о котором вы просите, очень слабо определен. Как определено в вашем вопросе, логически невозможно "проверить, содержит ли контейнер итерируемые объекты с потенциальной бесконечной рекурсией [.]". Единственным ограничением в области вашего вопроса является "итерируемый" объект. Официальная документация Python определяет " iterable" следующим образом:

Объект, способный возвращать свои элементы по одному. Примеры итераций включают все типы последовательностей (например, список, str и кортеж) и некоторые типы без последовательности, такие как dict, объекты файлов и объекты любых классов, которые вы определяете с помощью метода __iter__() или __getitem__(). [...]

Ключевая фраза здесь - "любые классы [определены] с помощью метода __iter__() или __getitem__()". Это позволяет создавать "итерируемые" объекты с элементами, которые генерируются по требованию. Например, предположим, что кто-то хочет использовать кучу строковых объектов, которые автоматически сортируют и сравнивают в хронологическом порядке в зависимости от времени, в которое была создана конкретная строка. Они либо подкласса str, либо переопределяют его функциональность, добавляя временную метку, связанную с каждым указателем на объект timestampedString( ), и соответственно корректируют методы сравнения.

Доступ к подстроке по расположению индекса - способ создания новой строки, поэтому timestampedString( ) из len( ) == 1 может законно вернуть timestampedString( ) из len( ) == 1 с тем же символом, но с новой меткой времени при доступе timestampedString( )[0:1]. Поскольку временная метка является частью экземпляра конкретного объекта, не существует какого-либо теста идентификации, который бы сказал, что оба объекта одинаковы, если только две строки, состоящие из одного и того же символа, считаются одинаковыми. В своем вопросе вы заявляете, что этого не должно быть.

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

Однако алгоритмическое принуждение к ограничению, что контейнер содержит только статические объекты, представляет собой еще одну проблему: для этого требуется проверка типов с некоторыми предварительно утвержденными списками типов, такими как некоторое понятие примитивов. Затем могут быть размещены две категории объектов: отдельные объекты известного статического типа (например, примитивы) и контейнеры, для которых количество содержащихся элементов может быть определено заранее. Затем можно показать, что последняя категория является конечной, когда многие содержащиеся в ней объекты были повторены и все были показаны как конечные. Контейнеры в контейнере могут обрабатываться рекурсивно. Единичные объекты с известным статическим типом являются рекурсивным базовым регистром.

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

Ответ 5

Просто избегайте сглаживания повторяющихся контейнеров. В приведенном ниже примере keepobj отслеживает их, а keepcls игнорирует контейнеры определенного типа. Я считаю, что это работает на python 2.3.

def flatten(item, keepcls=(), keepobj=()):
    if not hasattr(item, '__iter__') or isinstance(item, keepcls) or item in keepobj:
        yield item
    else:
        for i in item:
            for j in flatten(i, keepcls, keepobj + (item,)):
                yield j

Он может сгладить круговые списки, такие как lst = [1, 2, [5, 6, {'a': 1, 'b': 2}, 7, 'string'], [...]], и сохранить некоторые контейнеры как строки и dicts un-flattened.

>>> list(flatten(l, keepcls=(dict, str)))
[1, 2, 5, 6, {'a': 1, 'b': 2}, 7, 'string', [1, 2, [5, 6, {'a': 1, 'b': 2}, 7, 'string'], [...]]]

Он также работает со следующим случаем:

>>> list(flatten([[1,2],[1,[1,2]],[1,2]]))
[1, 2, 1, 1, 2, 1, 2]

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