In [1]:
# setup
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

6. Zaawansowane operacje na sekwencjach

Podstawą działania języka Python są sekwencyjne struktury danych: listy, krotki i łancuchy znaków. Dotychczas poznane metody pracy nie należą do najbardziej wydajnych oraz generują dużo niepotrzbnego kodu. Jedną z zasad pracy w języku Python jest tworzenie jak najmiejszej ilości kodu niezbędnego do rozwiązania problemu. Oczywiście kod bardziej kompaktowy jest mniej czytelny niemniej jednak w praktyce dużo łatwiej się nim zarządza oraz - przede wszyskim jest szybszy. Każdy język programowania jest optymalizowany do pracy z pewnymi strukturami danych - a strukturami danych Pythona są sekwencje.

Listy zasięgu (comprehension list)

Angielski termin (comprehension list) nie ma dobrego tłumaczenia na język polski. My będziemy stosować semantycznie najbliższy mu termin listy zasięgu. Listy zasięgu mają za zadanie utworzyć listę lub krotkę bez konieczności dodawania elementów do listy. Porównajmy wykorzystanie list zasięgu z klasycznym podjeściem strukturalnym.

Aby utworzyć listę przy pomocy pętli należy w pierwszej kolejności utworzyć pustą listę do której będą dodawane wyniki przetwarzania, a następnie dodawać do niej kolejne elementy tworzone w ramach każdej iteracji. Do dodawania elementów do listy służy funkcja append(). Funkcja ta jest metodą obiektu lista, jej argumentem może być pojedycza wartość lub inna lista. W tym przykładzie przefiltrujemy listę od 0 do 10 dodając do nowej listy wyłącznie elementy nieparzyste. Do tego działania wykorzystamy operator modulo %. Jeżeli operator zwraca jakąś wartość to jest ona konwetowana niejawnie do True w obrębie klauzuli if i jeżeli jest to prawda, element jest dodawany do listy.

In [2]:
list1 = range(10)
list2 = []
for i in list1:
    if i%2:
        list2.append(i)
        
list2
Out[2]:
[1, 3, 5, 7, 9]

Funkcja append() niestety nie jest bardzo wydajna. Powyższą operację można znacząco uprościć i przyspieszyć stosując listy zasięgu. Listy zasiegu stosuje się do tworzenia list, krotek i słowników, zagnieżdzając polecenia wewnątrz defnicji listy. Poniższy przykład buduje taką siamą listę skłądającą się z elementów nieparzystych.

In [3]:
list2=[i for i in range(10) if i%2]
list2
Out[3]:
[1, 3, 5, 7, 9]

Jak widać kod jest dużo prostrzy, niestety składnia wydaje się skomplikowana. Przestanie taką być, jeżeli przeczytamy to wyrażenie w języku polskim:

i dla każdego i w zasięgu 10 jeżeli modulo z i jest prawdą

Listy zasięgu posługują się składnią zbliżoną do naturalnego angielskiego oraz, co najważniejsze pozwalają zrezygnować ze stosowania funkcji append.

W przypadku jeżeli chcemy utworzyć krotkę, musimy jawnie wywołać funkcje tuple(), ponieważ same nawiasy są zarezerwowane dla obiektu generator, który zostanie omówiony w daleszej części modułu.

In [4]:
ctuple=tuple(i for i in range(10) if i%2)
print(ctuple)
(1, 3, 5, 7, 9)

Słowniki można również tworzyć przy pomocy listy zasięgu, definiując ją w nawiasach klamrowowych. Słowniki wymagają dodatkowo szerszego zrozumienia wspomnianego już zagadnienia jakim jest rozwijanie krotek.

Note: rozwijanie krotek

Pętle for w Pythonie jak i w niektórych innych językach mogą pracować na kilku zmiennych równocześnie. Również funkcje w Pythonie mogą "zwracać" więcej niż jedną wartość. Nie jest to jednak zgodne z matematyczną definicją funkcji, która wymaga, aby funkcja zwracała jedną i tylko jedną wartość. Python omija to ograniczenie w prosty sposób: funkcja zwraca pojedynczy obiekt złożony - krotkę, a poszczególne elementy ktorki przypisywane są do kolejnych zmiennych. Na przykład poniższa funkcja "zwraca" dwie wartości. W praktyce:

  1. najpierw zwija (ang. pack) dwie zmienne do krotki
  2. zwraca pojedynczy obiekt złożony 3.rozwija (ang. unpack) krotkę do dwóch zmiennych
In [5]:
def dwie_wartosci():
    x = 1
    y = 2 
    return x,y
# zwraca krotkę
a = dwie_wartosci()
a

#rozwija krotkę na dwie zmienne
a,b = dwie_wartosci()
a
b
Out[5]:
(1, 2)
Out[5]:
1
Out[5]:
2

W sytuacji, gdy rozumiemy już jak działa mechanizm przekazywania wielu zmiennych przez funkcję możemy przystąpić do tworzenia słownika przy pomocy list (tu właściwie słowników) zasięgu. Funkcja zip jest generatorem, króry zwraca kolejne krotki, rozwijane do dwóch zmiennych k (key) an v(value), które następnie pozwalają zbudować słownik.

In [6]:
mce = ['styczen', 'luty', 'marzec', 'kwiecien', 'maj', 'czerwiec', 'lipiec', 'sierpien', 'wrzesien', 'pazdziernik', 'listopad', 'grudzien'] 
dny=[31,28,31,30,31,30,31,31,30,31,30,31]
slownik = {k:v for k,v in zip(mce,dny)}
slownik
Out[6]:
{'czerwiec': 30,
 'grudzien': 31,
 'kwiecien': 30,
 'lipiec': 31,
 'listopad': 30,
 'luty': 28,
 'maj': 31,
 'marzec': 31,
 'pazdziernik': 31,
 'sierpien': 31,
 'styczen': 31,
 'wrzesien': 30}

Note Funkcje jako generatory wyrażeń w Python 3.0

W przypadku funkcji zip() jej działanie zmieniło się pomiędzy linią 2.x i 3.x. W wersji 2.x funkcja zip() zwracała listę krotek, co w przypadku dużej listy mogło prowadzić do znaczącego obciążenia pamięci. Aby wyeliminować ten mankament w wersji 3.x zip() nie buduje już nowego obiektu a jedynie "wyraża gotowość do przetwarzania, linia po linii istniejących obiektów, które są jej argumentami. Podobną zmianę przeszła funkcja range(). w wersji 2.x zwracała listę, co było nieco kłopotliwe w sytuacji, gdy liczba iteracji sięgała milionów kroków. W takiej sytuacji dodano funkcję xrange(), która była generatorem. W wersji 3.x range() jest generatorem, jeżeli chcemy zamienić go w listę należy użyć polecenia:

In [7]:
range(10)
list(range(10))
Out[7]:
range(0, 10)
Out[7]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Oczywiście przetworzenie dwóch list do słownika lepiej wykonać przypomocy funkcji dict(zip()) bezpośrednio.

Generatory wyrażeń

Generatory

Generartory to funkcje, które zamiast zwracać duże struktury danych (zagnieżdzone listy) zwracają jedynie niewielki fragment całości, tak aby niepoptrzebnie nie obciążąć pamięci. Korzystanie z generatorów powoduje że w każdym kroku iteracji namy dostęp tylko określonej pozycji generatora. Do określonego miejsca obiektu tworzonego przez generator nie można się dostać przez indeks a jedynie iterując to danego miejsca, lub wywołując metodę next(). Różnica pomiędzy generatorem wyrażeń a listą zasięgu jest taka, że wynik działania generatora nie jest przechowywany w pamięci w postacie kompletnej, generator zapamiętuje jedynie swój stan z ostatniego wywołania. Generator można zdefiniować jako generator wyrażenia (poprzez użycie zwykłych nawiasów) lub jako funkcję, gdzie, wyrażenie return, zastępujemy wyrażeniem yield. Jako przykład generatora wyrażeń użyjemy mechanizmu przetwarzania tekstu na duże litery:

In [8]:
mce = ['styczen', 'luty', 'marzec', 'kwiecien', 'maj', 'czerwiec', 'lipiec', 'sierpien', 'wrzesien', 'pazdziernik', 'listopad', 'grudzien'] 
gen = (mc.upper() for mc in mce)
next(gen)
next(gen)
Out[8]:
'STYCZEN'
Out[8]:
'LUTY'
In [9]:
def genmc(mce):
    for i in mce:
        yield i.upper()

gen = genmc(mce)
next(gen)
next(gen)
Out[9]:
'STYCZEN'
Out[9]:
'LUTY'

Note: Funkcja Enumerate

Funkcja enumerate jest rodzajem generatora i służy do przechodzenia po obiekcie iterowalnym (lista, krotka, generator itp) z jednoczesnym zwracaniem indeksu takiego obiektu. Funkcja enumerate zwraca krotkę, której pierwszym elementem jest index (numer porządkowy) elementu z obiektu iterowalnego (listy/krotki itp), a drugim właściwy element.

In [10]:
gen = (1,2,3,4,5,6)

for i,g in enumerate(gen):
    print(i,g)
0 1
1 2
2 3
3 4
4 5
5 6

Zastosowanie funkcji do każdego elementu w liście

Petle stosuje się bardzo często tylko po to, aby przetworzyć każdy element listy Aby zastosować funkcję do każdego elementu listy możemy użyć pętli for lub funkcji map() Funkcja map() wykonuje podaną funkcję dla każdego elementu listy/krotki/słownika:

Składnia funkcji map:

map(nazwa_funkcji, lista/tuplet/słownik)

Efektem działania funkcji map w wersji 3.x jest generator, natomiast w wersji 2.x jest lista. Funkcję map można stosować zarówno do funkcji wbudowanych jak i samodzielnie pisanych funkcji:

In [11]:
import math
lista = list(range(10))
list(map(math.sqrt,lista))
Out[11]:
[0.0,
 1.0,
 1.4142135623730951,
 1.7320508075688772,
 2.0,
 2.23606797749979,
 2.449489742783178,
 2.6457513110645907,
 2.8284271247461903,
 3.0]
In [12]:
mce = ['styczen', 'luty', 'marzec', 'kwiecien', 'maj', 'czerwiec', 'lipiec', 'sierpien', 'wrzesien', 'pazdziernik', 'listopad', 'grudzien'] 

def print_msc(msc):
    return 'Miesiąc: '+ msc
    
x = map(print_msc,mce)
list(x)
Out[12]:
['Miesiąc: styczen',
 'Miesiąc: luty',
 'Miesiąc: marzec',
 'Miesiąc: kwiecien',
 'Miesiąc: maj',
 'Miesiąc: czerwiec',
 'Miesiąc: lipiec',
 'Miesiąc: sierpien',
 'Miesiąc: wrzesien',
 'Miesiąc: pazdziernik',
 'Miesiąc: listopad',
 'Miesiąc: grudzien']

Funkcje anonimowe

W przypadku, gdy proces przetwarzania można zapisać w postaci prostej funkcji, można zastosować operator lambda, który tworzy nam tzw. funkcję anonimową, o zasięgu funkcji map(), lub innej fnkcji w której jest wywoływana. Funkcje anonimowe możemy stosować w obrębie funcji mapujących bez konieczności ich wcześniejszego defniowania

Składnia:

lambda x: `<tu cos robimy z x>`

# odpowiednik w postaci zwykłej funkcji
def fun(x):
    `<tu cos robimy z x>`
In [13]:
miesiace = map(lambda x: 'miesiac: ' + x, mce)
list(miesiace)
Out[13]:
['miesiac: styczen',
 'miesiac: luty',
 'miesiac: marzec',
 'miesiac: kwiecien',
 'miesiac: maj',
 'miesiac: czerwiec',
 'miesiac: lipiec',
 'miesiac: sierpien',
 'miesiac: wrzesien',
 'miesiac: pazdziernik',
 'miesiac: listopad',
 'miesiac: grudzien']

Zasadność stosowania funkcji anonimowych jest często kwestionowna, jako zbędna komplikacja. W praktyce w przykładach takich jak pokazane powyżej, nie ma potrzeby ich stosowania, jednakże w licznych sytuacjach, w bibliotekach zewnętrznych, stosowanie funkcji lambda jest niezbędne, na przyklad w sytuacji, gdy trzeba przekazać funkcję jako argument do innej funkcji. Zagaadnienia te będą się pojawiały w zaawansowanej części kursu.

Funkcje filter i reduce

Te dwie funkcje działają w sposób podobny do map(), ale pozwalają na wykonanie dodatkowych operacji.

Funkcja filter() filtruje listę, zwracając tylko te elementy, które spełniają warunek (zwracają true). Z funkcją fiter z reguły stosuje się funkcje anonimowe. Jako przykład zastosujemy funkcję filter() do realizacji tego samego zadania, które zrealizowaliśmy na początku modułu.

In [14]:
list2 = [i for i in range(10) if i%2]
list2

list3 = filter(lambda x: x%2, range(10))
list(list3)
Out[14]:
[1, 3, 5, 7, 9]
Out[14]:
[1, 3, 5, 7, 9]

Funkcja reduce() służy do wykonywania obliczeń, które wykonywane są na dwóch elementach listy, uprzednim i następnym. Pomijając skomplikowaną terminologię, reduce() stosujemy, gdy chcemy skumulować wyniki działania funkcji. Jako przykład zastosujemy fukcję kumulującą wartości z listy (podobny przykład pojawił się wcześniej, przy okazji stosowania list argumentów).

In [15]:
a = 0
for i in range(10):
    a+=i
a

from functools import reduce
reduce(lambda a,b: a+b, range(10))
Out[15]:
45
Out[15]:
45