Mamy pewną listę. Mamy jakąś funkcję testującą. Chcemy sprawdzić, czy dla jakiegokolwiek elementu
listy funkcja testująca zwraca True
.
To proste zadanko - bardzo częste w realnym kodzie - można wykonać na wiele sposobów.
By przykłady były konkretne, zdefiniujmy sobie funkcję testującą i dane:
LIST = [7, 3, 4, 8, 2, 9]
def is_even(n):
print "Testing", n
return n % 2 == 0
Zapis bezpośredni
Zapis bezpośredni jest trywialny ale długi. I kłopotliwy zwłaszcza, gdy opisany test chcielibyśmy
włączyć w jakieś wyrażenie (np. if
czy while
) albo wykonać kilka takich sprawdzeń obok siebie:
found = False
for x in LIST:
if is_even(x):
found = True
break
print found
Działa to dobrze i testuje tylko tyle elementów, ile musi:
Testing 7
Testing 3
Testing 4
True
Pouczające złe przykłady
OK, spróbujmy krócej. Przypominamy sobie o funkcji any
:
print any( [is_even(x) for x in LIST] )
Działa dobrze, jest zwarte ale... niepotrzebnie testuje wszystkie elementy listy.
Testing 7
Testing 3
Testing 4
Testing 8
Testing 2
Testing 9
True
Ten sam problem ma kilka innych zbliżonych podejść (z których każde bywa zresztą - w innych sytuacjach - przydatne):
print any(filter(is_even, LIST))
albo:
print True in [is_even(x) for x in LIST]
albo:
print bool( [x for x in LIST if is_even(x)] )
albo:
import operator
print reduce(operator.or_, [is_even(x) for x in LIST] )
W każdym wypadku przetestujemy wszystkie elementy listy a dopiero potem zwrócimy odpowiedź. Bo zawsze tworzymy najpierw listę wyników testów.
Dobrze
W pierwszym z złych przykładów wystarczy zmienić nawiasy:
print any( (is_even(x) for x in LIST) )
Efekt:
Testing 7
Testing 3
Testing 4
True
Nieco nieczytelna składnia (wartość for x in iterator)
oznacza
stworzenie generatora, czyli obiektu leniwie wyliczającego kolejne
wartości.
O kilka znaków dłuższy zapis ilustrujący użycie if
w tak robionym generatorze:
print any( (True for x in LIST if is_even(x)) )
Czytelniejszy zapis:
import itertools
print any(itertools.imap(is_even, LIST))
Można też poprawić drugi zły przykład:
import itertools
print any(itertools.ifilter(is_even, LIST))
Generator oznacza pewien dodatkowy narzut, dla krótkich list powyższy kod nie musi być szybszy od przeszukujących całą listę rozwiązań. Ale ... jest bardziej elegancki.
Sprawdzanie czy wszystkie
Sprawdzanie, czy wszystkie elementy spełniają warunek robimy analogicznie:
print all( (is_even(x) for x in LIST) )
albo
import itertools
print all(itertools.imap(is_even, LIST))
Czemu o tym piszę
W kilku dotychczasowych notkach o Pythonie pisałem raczej o rzeczach nietrywialnych - choćby tutaj. Powyższe to przypomnienie elementarza (choć o module itertools
nieoczekiwanie często zdarza się nawet biegłym
programistom zapominać).
Po prostu: duży program składa się z mnóstwa małych fragmentów. Zapisy funkcyjne podobne do powyższych bardzo pomagają wiele z tych małych fragmentów skracać. Trzy linijki pętli zamiast kilkunastu i ... długa wcześniej funkcja ładnie mieści się na ekranie i daje ogarnąć jednym spojrzeniem.