2012-01-24 18 views
8

W Pythonie 3, jak mogę sprawdzić, czy obiekt jest kontenerem (zamiast iteratora, który może zezwalać tylko na jedno przejście)?jak sprawdzić, czy iteracja dopuszcza więcej niż jeden przebieg?

Oto przykład:

def renormalize(cont): 
    ''' 
    each value from the original container is scaled by the same factor 
    such that their total becomes 1.0 
    ''' 
    total = sum(cont) 
    for v in cont: 
     yield v/total 

list(renormalize(range(5))) # [0.0, 0.1, 0.2, 0.3, 0.4] 
list(renormalize(k for k in range(5))) # [] - a bug! 

Oczywiście, gdy funkcja renormalize odbiera wyrażenie generator, to nie działa zgodnie z przeznaczeniem. Zakłada on, że może on iterować przez kontener wiele razy, podczas gdy generator umożliwia tylko jedno przejście przez niego.

Idealnie, chciałbym to zrobić:

def renormalize(cont): 
    if not is_container(cont): 
     raise ContainerExpectedException 
    # ... 

Jak mogę wdrożyć is_container?

Przypuszczam, że mógłbym sprawdzić, czy argument jest pusty, gdy zaczynamy robić drugie przejście przez to. Ale to podejście nie działa dla bardziej skomplikowanych funkcji, w których nie jest oczywiste, kiedy dokładnie rozpoczyna się drugie przejście. Co więcej, wolę raczej sprawdzanie poprawności przy wejściu do funkcji niż w głąb funkcji (i przesuwanie jej, gdy tylko funkcja jest modyfikowana).

Mogę oczywiście przepisać funkcję renormalize, aby działała poprawnie z iteratorem jednoprzebiegowym. Ale to wymaga skopiowania danych wejściowych do kontenera. Wpływ na wydajność kopiowania milionów dużych list "na wypadek, gdyby nie były listami" jest śmieszny.

EDIT: Mój oryginalny przykład użył weighted_average funkcję:

def weighted_average(c): 
    ''' 
    returns weighted average of a container c 
    c contains values and weights in tuples 
    weights don't need to sum up 1 (automatically renormalized) 
    ''' 
    return sum((v * w for v, w in c))/sum((w for v, w in c)) 

weighted_average([(0,1), (1,1)]) #0.5 
weighted_average([(k, 1) for k in range(2)]) #0.5 
weighted_average((k, 1) for k in range(2)) #mistake 

Ale to nie był najlepszy przykład, ponieważ wersja weighted_average przepisany do korzystania z pojedynczej podanie jest zapewne lepiej i tak:

def weighted_average(it): 
    ''' 
    returns weighted average of an iterator it 
    it yields values and weights in tuples 
    weights don't need to sum up 1 (automatically renormalized) 
    ''' 
    total_value = 0 
    total_weight = 0 
    for v, w in it: 
     total_value += v 
     total_weight += w 
    return total_value/total_weight 
+0

Nie widzę problemu z ogólną wersją, czy profilowałeś ją? A co rozumiesz przez złożoność wizualną? – LBarret

+0

"gdzie nie jest oczywiste, kiedy dokładnie rozpoczyna się drugi przebieg"? Co to może oznaczać? Możesz użyć 'itertools.tee() 'bezwarunkowo gwarantuje, że możesz iterować tyle razy, ile to konieczne. Jak to może nie być oczywiste, kiedy projektujesz algorytmy? –

+0

@LionelBarret: Zgadzam się, nie ma powodu, aby nie używać ogólnego "weighted_average". Zaktualizowałem pytanie, aby podać inny przykład. – max

Odpowiedz

4

Chociaż wszystkie iterables powinny podklasy collections.Iterable, nie wszystkie z nich zrobić, niestety. Oto odpowiedź na podstawie tego, jaki interfejs implementują obiekty, zamiast tego, co "deklarują".

Krótka odpowiedź:

A „pojemnik”, jak to nazwać, czyli listy/krotki, które można iterować na więcej niż jeden raz, w przeciwieństwie do bycia generator, który zostanie wyczerpany, zazwyczaj wdrożyć zarówno __iter__ i __getitem__.Stąd można to zrobić:

>>> def is_container_iterable(o): 
...  return hasattr(o, '__iter__') and hasattr(o, '__getitem__') 
... 
>>> is_container_iterable([]) 
True 
>>> is_container_iterable(()) 
True 
>>> is_container_iterable({}) 
True 
>>> is_container_iterable(range(5)) 
True 
>>> is_container_iterable(iter([])) 
False 

długa odpowiedź:

Można jednak dokonania iterable, które nie zostaną wyczerpane i nie obsługują GetItem. Na przykład funkcja generująca liczby pierwsze. Możesz powtarzać pokolenie wiele razy, jeśli chcesz, ale posiadanie funkcji odzyskiwania 1065. liczby pierwszej wymagałoby wielu obliczeń, więc możesz tego nie chcieć wspierać. :-)

Czy istnieje jakiś bardziej "niezawodny" sposób?

Cóż, wszystkie iteracje zaimplementują funkcję __iter__, która zwróci iterator. Iteratory będą miały funkcję __next__. To jest to, czego używa się podczas iteracji. Wywoływanie __next__ będzie wielokrotnie kończyło się iteratorem.

Jeśli więc ma funkcję __next__, jest to iterator i zostanie wyczerpany.

>>> def foo(): 
... for x in range(5): 
...  yield x 
... 
>>> f = foo() 
>>> f.__next__ 
<method-wrapper '__next__' of generator object at 0xb73c02d4> 

Iterables, które nie są jeszcze iteratory nie będzie miał __next__ funkcję, ale wdroży __iter__ funkcję, która zwróci iterable:

>>> r = range(5) 
>>> r.__next__ 
Traceback (most recent call last): 
    File "<stdin>", line 1, in <module> 
AttributeError: 'range' object has no attribute '__next__' 
>>> ri = iter(r) 
>>> ri.__next__ 
<method-wrapper '__next__' of range_iterator object at 0xb73bef80> 

Więc można sprawdzić, czy obiekt ma __iter__ ale że nie ma __next__.

>>> def is_container_iterable(o): 
...  return hasattr(o, '__iter__') and not hasattr(o, '__next__') 
... 
>>> is_container_iterable(()) 
True 
>>> is_container_iterable([]) 
True 
>>> is_container_iterable({}) 
True 
>>> is_container_iterable(range(5)) 
True 
>>> is_container_iterable(iter(range(5))) 
False 

Iteratory mają również funkcję __iter__, która zwróci samo siebie.

>>> iter(f) is f 
True 
>>> iter(r) is r 
False 
>>> iter(ri) is ri 
True 

Stąd można zrobić te odmiany kontroli:

>>> def is_container_iterable(o): 
...  return iter(o) is not o 
... 
>>> is_container_iterable([]) 
True 
>>> is_container_iterable(()) 
True 
>>> is_container_iterable({}) 
True 
>>> is_container_iterable(range(5)) 
True 
>>> is_container_iterable(iter([])) 
False 

To zawiedzie jeśli wdrożenie obiekt, który zwraca złamaną iterator, jeden, który nie nie powrotny siebie kiedy zadzwonić iter() ponownie. Ale wtedy twój (lub moduły stron trzecich) faktycznie robi coś złego.

To zależy od wykonania iteratora, a więc wywoływania obiektów __iter__, które teoretycznie mogą mieć efekty uboczne, podczas gdy powyższe wywołania hasattr nie powinny wywoływać efektów ubocznych. OK, więc wywołuje getattribute, które mogło mieć. Ale można ustalić, że wygląda następująco:

>>> def is_container_iterable(o): 
...  try: 
...   object.__getattribute__(o, '__iter__') 
...  except AttributeError: 
...   return False 
...  try: 
...   object.__getattribute__(o, '__next__') 
...  except AttributeError: 
...   return True 
...  return False 
... 
>>> is_container_iterable([]) 
True 
>>> is_container_iterable(()) 
True 
>>> is_container_iterable({}) 
True 
>>> is_container_iterable(range(5)) 
True 
>>> is_container_iterable(iter(range(5))) 
False 

Ten jest dość bezpieczny i powinien działać we wszystkich przypadkach z wyjątkiem jeśli obiekt generuje __next__ lub __iter__ dynamicznie na __getattribute__ rozmów, ale jeśli robisz, że jesteś szalony. :-)

Instynktownie moja preferowana wersja to iter(o) is o, ale nigdy nie musiałem tego robić, więc nie jest to oparte na doświadczeniu.

+0

+1: Nie spodziewałem się, że będzie sposób, aby to zrobić, jeśli klasa nie zajmie się podklasą z 'collections.Iterable'. (BTW, czy klasa może znacząco wywodzić się z "Iterable" i "Iterator"?) – max

+0

Iterable wywodzi się z Iteratora, więc nie. –

3

Można użyć abstrakcyjnych klas podstawowych zdefiniowanych w module collections, aby sprawdzić i sprawdzić, czy it jest instancją kolekcji.Iterator.

if isinstance(it, collections.Iterator): 
    # handle the iterator case 

Osobiście jednak uważam swoją iterator przyjazna wersja średnia ważona znacznie łatwiejsze do odczytania niż stwardnienie wersji listowego/SUM. :-)

+0

Tak, zgadzam się. Zaktualizowałem moje pytanie, aby pokazać przykład, w którym nie wydaje się możliwe użycie jednego przejścia. – max

+0

Nice! Wygląda na to, że istnieje standardowy sposób w 3.x. –

+0

To wydaje się działać, nawet dla obiektów takich jak "zakres wirtualny" 'zakres (5)'. Wygląda świetnie! – max

1

Najlepszym sposobem byłoby wykorzystanie infrastruktury abstrakcyjnej klasy bazowej:

def weighted_average(c): 
    if not isinstance(c, collections.Sequence): 
     raise ContainerExpectedException 
Powiązane problemy