2016-01-25 10 views
10

Czytałem Flatten (an irregular) list of lists i zdecydowałem się przyjąć go jako ćwiczenie Python - małą funkcję, którą będę czasem przepisywać bez odwoływania się do oryginału, tylko po to, aby ćwiczyć. Po raz pierwszy próbowałem to miałem coś jak następuje:Jak określić, czy kontener jest nieskończenie rekurencyjny i czy znajduje najmniejszy unikalny kontener?

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

Działa to dobrze dla podstawowych struktur jak zagnieżdżonych list s zawierających cyfry, ale struny katastrofy, ponieważ pierwszym elementem łańcucha jest pojedynczym znaków ciąg, którego pierwszym elementem jest sam, którego pierwszy element jest ponownie, i tak dalej. Sprawdzając powyższe pytanie, zdałem sobie sprawę, że to wyjaśnia sprawdzanie ciągów. To dało mi następujące:

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) 

Teraz działa również na strunach. Jednak przypomniałem sobie, że list może zawierać odniesienia do siebie.

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

Tak więc łańcuch nie jest jedynym rodzajem, który może powodować tego rodzaju problemy. W tym momencie zacząłem szukać sposobu na zabezpieczenie się przed tym problemem bez wyraźnego sprawdzania typu.

Nastąpił następujący . flattish() to wersja, która sprawdza tylko ciągi. flatten_notype() sprawdza, czy pierwszy element obiektu pierwszego przedmiotu jest równy samemu sobie w celu określenia rekursji. flatten() robi to, a następnie sprawdza, czy pierwszy obiekt lub pierwszy element tego elementu jest instancją typu drugiego. Klasa Fake po prostu definiuje opakowanie dla sekwencji. Komentarze na liniach testujących każdą funkcję opisują wyniki w postaci should be `desired_result` [> `undesired_actual_result`]. Jak widać, każda z nich kończy się na różne sposoby Fake owiniętym wokół sznurka, Fake owiniętym wokół list liczb całkowitych, ciągów jednoznakowych i ciągów zawierających wiele znaków.

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` 

Próbowałem zostały również następujące z rekurencyjnym lst zdefiniowano powyżej i flatten():

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

Jak widać, nie jest on podobny do 1 ('a',) bc jak również we własnym specjalny sposób.

czytam how can python function access its own attributes? myśleć, że być może funkcja mogła śledzić każdy obiekt to widział, ale to nie działa albo ponieważ nasz lst zawiera obiekt z pasującymi tożsamość i równości, struny zawierać obiekty, które mogą mieć tylko dopasowanie równości , a równość nie jest wystarczająca ze względu na możliwość czegoś takiego jak flatten([1, 2], [1, 2]).

Czy istnieje jakiś niezawodny sposób (tj. Nie sprawdza się po prostu znanych typów, nie wymaga, aby pojemnik rekursywny i jego pojemniki były tego samego typu itp.), Aby sprawdzić, czy pojemnik zawiera obiekty z możliwością iteracji nieskończona rekurencja i niezawodnie określić najmniejszy unikalny pojemnik? Jeśli tak, proszę wyjaśnić, jak to zrobić, dlaczego jest wiarygodny i jak radzi sobie z różnymi okolicznościami rekurencyjnymi. Jeśli nie, proszę wyjaśnić, dlaczego jest to logicznie niemożliwe.

+0

Myślę, że można to zredukować do wykrywania cykli w ukierunkowanych wykresach. Więc wszystkie rozwiązania, które mają tam zastosowanie, powinny mieć zastosowanie tutaj: http://stackoverflow.com/questions/261573/best-algorithm-for-detecting-cycles-in-a-directed-graph –

Odpowiedz

0

Nie sądzę, że istnieje wiarygodny sposób sprawdzenia, czy dowolna iteracja jest nieskończona.Najlepiej jak potrafimy to uzyskując nieskończenie prymitywów z takiego iterable bez wyczerpania stosu, na przykład:

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 

Powyższa definicja „prymitywny” jest dobrze, prymitywny, ale na pewno można poprawić.

0

Co o coś takiego:

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 

po skończonej liczbie kroków (rekurencja) wykaz będzie zawierał co najwyżej sama jak iterowalny elementu (Ponieważ musimy wygenerować go w skończonych wielu etapach). Właśnie to testujemy z obj==old gdzie obj w elemencie old.

Lista used śledzi wszystkie elementy, ponieważ chcemy, aby każdy element był tylko jeden raz. Moglibyśmy go usunąć, ale uzyskalibyśmy brzydkie (a co ważniejsze nieokreślone) zachowanie, na którym elementy zarabiają jak często. Wadą jest to, że możemy przechowywać całą listę w końcu na liście used ...

Testowanie to z niektórych listach wydaje się działać:

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

Uwaga: To rzeczywiście ma sens, że nieskończone listy [[...], (1, 'ab'), [1, [...]], 3] i [1, [...]] są uważane za elementy, ponieważ faktycznie zawierają się same, ale jeśli to nie jest pożądane, można skomentować pierwszy kod yield w powyższym kodzie.

0

Wystąpił problem z kodem testowym, który nie jest związany z problemem pojemnika rekurencyjnego, który próbujesz rozwiązać. Problem polega na tym, że twoja klasa Fake jest iteratorem i może być używana tylko raz. Po wykonaniu iteracji wszystkich wartości, zawsze będzie podnosić StopIteration przy próbie iterowania na nim ponownie.

Jeśli wykonujesz wiele operacji na tej samej instancji Fake, nie powinieneś oczekiwać, że cokolwiek będzie pustym wyjściem po pierwszej operacji, która zużyła iterator. Jeśli ponownie utworzysz iterator przed każdą operacją, nie będziesz miał tego problemu (i faktycznie możesz spróbować rozwiązać problem rekursji).

A więc do tego problemu. Jednym ze sposobów uniknięcia nieskończonej rekurencji jest utrzymanie stosu z obiektami, w których aktualnie się znajdujesz. Jeśli następna wartość, którą widzisz, jest już gdzieś na stosie, wiesz, że jest rekurencyjna i może ją pominąć. Oto realizacja tego na podstawie listy jako stosu:

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() 

Należy pamiętać, że może to nadal uzyskując wartości z tego samego kontenera więcej niż jeden raz, tak długo jak nie jest zagnieżdżone w sobie (np x=[1, 2]; y=[x, 3, x]; print(*flatten(y)) wypisze 1 2 3 1 2) .

też nie rekursja do strun, ale będzie to zrobić tylko na jednym tylko poziomie, więc flatten("foo") przyniesie litery 'f', 'o' i 'o' po kolei. Jeśli chcesz tego uniknąć, prawdopodobnie potrzebujesz funkcji, aby być świadomym typu, ponieważ z perspektywy protokołu iteracji łańcuch nie różni się niczym od iteracyjnego kontenera swoich liter. To tylko pojedyncze ciągi znaków, które rekursywnie zawierają się same.

0

Scenariusz, o który pytasz, jest bardzo luźno zdefiniowany. Jak zdefiniowano w twoim pytaniu, logicznie niemożliwe jest sprawdzenie, czy kontener zawiera obiekty iterowalne z potencjalną nieskończoną rekurencją [.] "Jedynym ograniczeniem zakresu twojego pytania jest obiekt" iterowalny ".Oficjalna dokumentacja w języku Python definiuje "iterable" w następujący sposób:

Obiekt zdolny do zwrócenia swoich członków po jednym na raz. Przykłady iterables obejmują wszystkie typy sekwencji (takie jak list, str i tuple) i niektóre typy niesekwencyjne, takie jak dyktatura, obiekty plików i obiekty dowolnych klas zdefiniowanych za pomocą metody __iter__() lub __getitem__(). [...]

Kluczem jest tu wyrażenie "wszelkie zajęcia [zdefiniowane] z __iter__() lub __getitem__() metody." Pozwala to na "iterowalne" obiekty z członkami generowanymi na żądanie. Załóżmy na przykład, że ktoś stara się użyć grupy obiektów typu string, które automatycznie sortują i porównują w porządku chronologicznym w oparciu o czas utworzenia danego ciągu. Podklasy te podklasują lub ponownie implementują jego funkcjonalność, dodając znacznik czasu powiązany z każdym wskaźnikiem do obiektu timestampedString() i odpowiednio dostosowują metody porównywania.

Dostęp podciąg według lokalizacji indeksu jest sposobem tworzenia nowy ciąg, więc timestampedString() z len() == 1 mógł zasadnie zwróci timestampedString() z len() == 1 z tego samego znaku, ale nowego znacznika czasu podczas korzystania timestampedString()[0:1]. Ponieważ znacznik czasu jest częścią konkretnej instancji obiektu, nie ma żadnego testu identyczności, który by powiedział, że oba obiekty są takie same, chyba że dwa ciągi składające się z tego samego znaku są uważane za takie same. W swoim pytaniu stwierdzasz, że tak nie powinno być.

Aby wykryć nieskończoną rekursję, najpierw należy dodać ograniczenie do zakresu pytania, aby kontener zawierał tylko statyczne, tj. Wstępnie wygenerowane obiekty. Dzięki temu ograniczeniu każdy legalny obiekt w kontenerze może zostać przekonwertowany na reprezentację ciągów bitowych obiektu. Prostym sposobem na zrobienie tego będzie pobranie wszystkich obiektów w kontenerze po ich dotarciu i zachowanie stosu reprezentacji ciągów bajtowych, które powstają w wyniku trawienia. Jeśli pozwolisz dowolnemu dowolnemu statycznemu obiektowi, zadziała nic innego jak surrealistyczna interpretacja obiektów.

Jednak algorytmiczne egzekwowanie ograniczenia, które kontener zawiera tylko obiekty statyczne, stanowi kolejny problem: wymaga sprawdzenia typu przed niektórymi wstępnie zatwierdzonymi listami typów, takimi jak niektóre pojęcia prymitywów. Można wówczas zastosować dwie kategorie obiektów: pojedyncze obiekty znanego typu statycznego (na przykład prymitywy) i pojemniki, dla których można wcześniej ustalić liczbę zawartych elementów. Ta ostatnia kategoria może być wtedy skończona, gdy wiele zawartych obiektów zostało poddanych iteracji i wszystkie okazały się skończone. Pojemniki w pojemniku mogą być obsługiwane rekurencyjnie. Obiekty pojedyncze typu znanego-statycznego są rekursywną podstawową obudową.

Jeśli kontener wytwarza więcej obiektów, wówczas narusza definicję tej kategorii obiektów. Problem z zezwoleniem na dowolne obiekty w Pythonie polega na tym, że obiekty te można zdefiniować w kodzie Pythona, który może wykorzystywać komponenty napisane w kodzie C i każdy inny język, z którym można połączyć C. Nie ma możliwości oceny tego kodu w celu ustalenia, czy faktycznie spełnia on wymagania statyczne.

0

Po prostu unikaj spłaszczania powtarzających się pojemników . W poniższym przykładzie keepobj śledzi je i keepcls ignoruje kontenery określonego typu. Wierzę, że działa to do Pythona 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 

To może spłaszczyć okrągłe list jak lst = [1, 2, [5, 6, {'a': 1, 'b': 2}, 7, 'string'], [...]] i zachować jakieś pojemniki jak smyczki i dicts un-spłaszczone.

>>> 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'], [...]]] 

Działa również z następującym przypadku:

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

Możesz trzymać kilka klas domyślnych w keepcls dokonać dzwoniąc funkcja bardziej lakoniczne.

Powiązane problemy