Python. Receptury
Python. Receptury
PRZYKADOWY ROZDZIA
SPIS TRECI
KATALOG KSIEK
KATALOG ONLINE
ZAMW DRUKOWANY KATALOG
TWJ KOSZYK
DODAJ DO KOSZYKA
CENNIK I INFORMACJE
ZAMW INFORMACJE
O NOWOCIACH
ZAMW CENNIK
CZYTELNIA
FRAGMENTY KSIEK ONLINE
Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
e-mail: [email protected]
Python. Receptury
Autorzy: Alex Martelli, Anna Martelli
Ravenscroft, David Ascher
Tumaczenie: Wojciech Moch, Marek Ptlicki
ISBN: 83-246-0214-3
Tytu oryginau: Python Cookbook
Format: B5, stron: 848
Python zosta opracowany na pocztku lat "90 i szybko zyska uznanie programistw.
Elastyczny i uniwersalny, pozwala na stosowanie zasad programowania obiektowego,
strukturalnego i funkcyjnego. By i nadal jest wykorzystywany nie tylko do tworzenia
skryptw, ale rwnie przy duych projektach, takich jak na przykad serwer aplikacji
Zope. Decydujc si na korzystanie z Pythona, stajemy si czci niezwykej
spoecznoci programistw, chtnie pomagajcej kademu, kto chce doskonali
umiejtno posugiwania si tym jzykiem.
Ksika Python. Receptury to zbir rozwiza problemw, z jakimi w codziennej pracy
borykaj si programici korzystajcy z tego jzyka. Materiay do niej przygotowao
ponad 300 czonkw spoecznoci Pythona odpowiadajcych na pytania zadawane
na forum internetowym. Rozwizania zostay przetestowane w praktyce, co uatwia ich
zaimplementowanie we wasnych projektach.
W ksice umwiono m.in.:
Przetwarzanie tekstw
Operacje na plikach
Programowanie obiektowe
Przeszukiwanie i sortowanie
czenie skryptw z bazami danych
Testowanie i usuwanie bdw
Programowanie wielowtkowe
Realizacj zada administracyjnych
Obsug interfejsw uytkownika
Tworzenie aplikacji sieciowych
Przetwarzanie dokumentw XML
Kady programista Pythona, niezalenie od umiejtnoci,
znajdzie w tej ksice co dla siebie
Wstp ........................................................................................................................................15
Rozdzia 1. Tekst .......................................................................................................................31
1.1. Przetwarzanie tekstu po jednym znaku
1.2. Konwersja pomidzy znakami a kodami numerycznymi
1.3. Sprawdzanie, czy obiekt jest podobny do cigu znakw
1.4. Wyrwnywanie cigw znakw
1.5. Usuwanie spacji z kocw cigu znakw
1.6. czenie cigw znakw
1.7. Odwracanie kolejnoci sw lub znakw w cigu znakw
1.8. Sprawdzanie, czy cig znakw zawiera pewien zestaw znakw
1.9. Upraszczanie uycia metody translate
1.10. Filtrowanie cigu znakw na podstawie znakw z okrelonego zbioru
1.11. Sprawdzanie, czy cig znakw jest tekstowy czy binarny
1.12. Kontrolowanie wielkoci znakw
1.13. Odczytywanie podcigw
1.14. Zmiany wci w wielowierszowym cigu znakw
1.15. Rozszerzanie i zawanie tabulacji
1.16. Wstawianie zmiennych do cigu znakw
1.17. Wstawianie zmiennych do cigu znakw w Pythonie 2.4
1.18. Podmiana wielu wzorcw w jednym przebiegu
1.19. Sprawdzanie kocwek w cigu znakw
1.20. Obsuga tekstw midzynarodowych za pomoc Unikodu
1.21. Konwertowanie pomidzy Unikodem i prostym tekstem
1.22. Wypisywanie znakw Unikodu na standardowe wyjcie
1.23. Kodowanie danych w formatach XML i HTML
1.24. Przygotowanie cigu znakw nierozrniajcego wielkoci liter
1.25. Konwertowanie dokumentw HTML na zwyky tekst na terminalu uniksowym
37
38
39
41
42
42
45
47
50
52
55
57
58
61
63
65
67
69
72
73
76
78
79
82
85
93
97
98
99
100
103
105
106
108
110
111
113
114
115
118
119
121
122
123
124
125
127
129
130
131
132
133
134
136
138
Spis treci
147
149
151
152
153
155
158
159
160
162
163
165
167
170
173
174
179
182
184
185
186
188
191
192
194
196
197
199
201
203
204
206
208
210
212
214
215
217
219
Spis treci
226
227
229
231
234
235
237
239
241
243
246
249
251
253
257
266
268
270
272
274
277
280
282
284
286
289
292
295
299
301
302
307
310
312
315
Spis treci
320
322
325
326
329
331
334
336
339
341
342
344
345
347
349
352
354
356
358
362
363
365
366
368
371
374
375
377
378
380
Spis treci
387
390
392
394
397
399
402
405
407
409
412
413
416
Spis treci
451
453
454
455
457
459
460
463
465
469
471
474
476
479
480
481
482
484
486
491
492
494
495
497
499
500
502
503
505
508
Spis treci
513
515
516
517
518
521
523
524
526
528
531
535
537
540
543
546
547
550
553
555
556
558
559
560
563
564
566
567
570
573
576
582
583
585
587
588
589
592
594
597
599
602
10
Spis treci
611
612
613
615
618
619
622
624
627
629
631
633
640
643
645
648
650
651
655
658
659
660
Spis treci
667
669
673
674
675
677
679
681
684
687
690
692
694
11
696
698
700
703
711
713
715
716
718
720
722
725
728
731
733
734
735
737
740
742
744
747
750
751
753
12
Spis treci
759
762
764
766
768
770
773
775
777
779
781
782
784
786
789
793
797
Spis treci
13
ROZDZIA 5.
5.0. Wprowadzenie
Pomysodawca: Tim Peters, PythonLabs
W latach 60. producenci komputerw szacowali, e 25% czasu pracy wszystkich sprzedanych
przez nich urzdze przeznaczane jest na zadania zwizane z sortowaniem. W rzeczywistoci byo wiele takich instalacji, w ktrych zadanie sortowania zajmowao ponad poow czasu pracy komputerw. Z tych danych mona wywnioskowa, e: a) istnieje wiele
bardzo wanych powodw sortowania, b) wielokrotnie sortuje si dane bez potrzeby lub c)
powszechnie stosowane byy nieefektywne algorytmy sortowania.
Donald Knuth
The Art of Computer Programming, tom 3,
Sorting and Searching, strona 3.
Wspaniaa praca profesora Knutha na temat sortowania i wyszukiwania ma niemal 800 stron
zoonego tekstu technicznego. W praktyce Pythona ca t prac redukuje si do dwch imperatyww (kto inny przeczyta to opase tomisko, dlatego Czytelnik zostanie zwolniony
z tego obowizku):
Jeeli musimy sortowa dane, to najlepiej bdzie znale sposb na wykorzystanie wbu-
221
Podany kod poszukuje pierwszej pary nierwnych sobie elementw. Po znalezieniu takiej pary
na jej podstawie okrelany jest wynik porwnania. W przeciwnym przypadku, jeeli jedna
z sekwencji jest dokadnym przedrostkiem drugiej, to taki przedrostek uznawany jest za mniejsz
sekwencj. W kocu, jeeli nie obowizuje aden z wymienionych wyej przypadkw, znaczy
to, e sekwencje s identyczne i w zwizku z tym uznawane s za rwne. Oto kilka przykadw:
>>>
0
>>>
1
>>>
-1
>>>
-1
# identyczne
222
def sorted_2_3(iterable):
alist = list(iterable)
alist.sort()
return alist
223
Algorytm samplesort mona traktowa jak wariant algorytmu quicksort, w ktrym uywane s
bardzo due prbki do wybierania elementu rozdzielajcego (metoda ta rekursywnie sortuje
algorytmem samplesort duy losowy podzbir elementw i wybiera z nich median). Taki
wariant sprawia, e prawie nie jest moliwy wariant z czasem sortowania proporcjonalnym do
kwadratu liczby elementw, a liczba porwna w typowych przypadkach jest zdecydowanie
blisza teoretycznemu minimum.
Niestety, algorytm samplesort jest na tyle skomplikowany, e jego administracja danymi okazuje si zdecydowanie zbyt rozbudowana przy pracach z niewielkimi listami. Z tego wanie
powodu mae listy (a take niewielkie wykrojenia powstajce w wyniku podziaw dokonywanych przez ten algorytm) obsugiwane s za pomoc algorytmu insertionsort (jest to zwyczajny algorytm sortowania przez wstawianie, ale do okrelania pozycji kadego z elementw korzysta on z mechanizmw szukania binarnego). W wikszoci tekstw na temat sortowania
zaznaczane jest, e takie podziay nie s warte naszej uwagi, ale wynika to z faktu, e w tekstach
tych uznaje si, e operacja porwnania elementw jest mniej czasochonna od operacji zamiany
tych elementw w pamici, co nie jest prawd w algorytmach sortowania stosowanych w Pythonie. Przeniesienie obiektu jest operacj bardzo szybk, poniewa kopiowana jest tylko referencja tego obiektu. Z kolei porwnanie dwch obiektw jest operacj kosztown, poniewa
za kadym razem uruchamiany jest kod przeznaczony do wyszukiwania obiektw w pamici
oraz kod odpowiedni do wykonania porwnania danych obiektw. Jak si okazao, z tego wanie powodu w Pythonie najlepszym rozwizaniem jest sortowanie binarne.
To hybrydowe rozwizanie uzupenione zostao jeszcze o obsug kilku typowych przypadkw
ukierunkowan na popraw prdkoci dziaania. Po pierwsze, wykrywane s listy ju posortowane lub posortowane w odwrotnej kolejnoci i obsugiwane w czasie liniowym. W pewnych aplikacjach takie sytuacje zdarzaj si bardzo czsto. Po drugie, dla tablicy w wikszoci posortowanej, w ktrej nieposortowanych jest tylko kilka ostatnich elementw, ca prac wykonuje
algorytm sortowania binarnego. Takie rozwizanie jest znacznie szybsze od sortowania takich
list algorytmem samplesort, a przedstawiona sytuacja bardzo czsto pojawia si w aplikacjach,
ktre naprzemiennie sortuj list, dodaj do niej nowe elementy, znowu sortuj itd. W kocu
specjalny kod w algorytmie samplesort wyszukuje cigi jednakowych elementw i zajmowan
przez nie cz listy od razu oznacza jako posortowan.
W efekcie takie sortowanie w miejscu odbywa si z doskona wydajnoci we wszystkich znanych typowych przypadkach i osiga nienaturalnie dobr wydajno w pewnych typowych
przypadkach specjalnych. Caa implementacja zapisana zostaa w ponad 500 wierszach skomplikowanego kodu w jzyku C bardzo podobnego do tego prezentowanego w recepturze 5.11.
Przez lata, w ktrych uywany by algorytm samplesort, cay czas oferowaem obiad temu, kto
przygotuje szybszy algorytm sortujcy dla Pythona. Przez cay ten czas musiaem jada sam.
Mimo to cigle ledz pojawiajc si literatur, poniewa pewne aspekty stosowanej w Pythonie hybrydy algorytmw sortujcych s nieco irytujce:
Co prawda w rzeczywistych zastosowaniach nie pojawiaj si przypadki sortowania tablicy
224 |
nych z ca pewnoci byy nieocenion pomoc w normalnej praktyce, ale w czasie moich
prac czsto spotykaem si z innymi rodzajami losowych ukadw danych, ktre mona by
byo obsuy w podobny sposb. Doszedem do wniosku, e w przypadkach rzeczywistych
praktycznie nigdy nie wystpuj cakowicie losowo uoone elementy list wejciowych
(a szczeglnie poza rodowiskami przygotowanymi do testowania algorytmw sortujcych).
Nie istnieje praktyczny sposb przygotowania stabilnego algorytmu samplesort bez jed-
5.0. Wprowadzenie
225
poczenie kilku posortowanych list w Pythonie jest teraz zczenie ich w jedn wielk list
i wywoanie na jej rzecz funkcji list.sort.
Dane wejciowe s w wikszoci posortowane, ale pewne elementy nie s uoone w pra-
widowej kolejnoci. Typowym przykadem takiego stanu jest sytuacja, gdy uytkownicy
rcznie dopisuj nowe dane do bazy danych posortowanej wedug nazwisk. Jak wiemy,
ludzie nie najlepiej radz sobie z takim dopisywaniem danych i utrzymywaniem ich w porzdku alfabetycznym, ale czsto s bliscy wstawienia elementu we waciwe miejsce.
Wrd danych wejciowych znajduje si wiele kluczy o takich samych wartociach. Na
Problem
Chcemy posortowa sownik. Najprawdopodobniej oznacza to, e chcemy posortowa klucze,
a nastpnie pobiera z niego wartoci w tej samej kolejnoci sortowania.
Rozwizanie
Najprostsze rozwizanie tego problemu zostao ju wyraone w opisie problemu: naley posortowa klucze i wybiera powizane z nimi wartoci:
def sortedDictValues(adict):
keys = adict.keys()
keys.sort()
return [adict[key] for key in keys]
226
Analiza
Koncepcja sortowania dotyczy wycznie tych kolekcji, ktrych elementy maj jak kolejno
(czyli sekwencj). Odwzorowania takie jak sowniki nie maj kolejnoci, wobec czego nie mog
by sortowane. Mimo to na listach dyskusyjnych dotyczcych Pythona czsto pojawiaj si
cakowicie bezsensowne pytania Jak mog posortowa sownik?. Najczciej takie pytanie
oznacza jednak, e osoba pytajca chciaa posortowa pewn sekwencj skadajc si z kluczy
i (lub) ich wartoci pobranych ze sownika.
Jeeli chodzi o podan implementacj, to co prawda mona wymyli bardziej zoone rozwizania, ale jak si okazuje (w Pythonie nie powinno by to niespodziank), kod podany
w rozwizaniu jest rozwizaniem najprostszym, a jednoczenie najszybszym. Popraw prdkoci dziaania o mniej wicej 20% mona w Pythonie 2.3 uzyska przez zastpienie w instrukcji
return listy skadanej wywoaniem funkcji map, na przykad:
return map(adict.get, keys)
W Pythonie 2.4 wersja podawana w rozwizaniu jest jednak o wiele szybsza ni ta w Pythonie 2.3, dlatego z takiej zamiany nie zyskamy zbyt wiele. Inne warianty, takie jak na przykad zastpienie metody adict.get metod adict.__getitem__ nie powoduj ju poniesienia
prdkoci dziaania funkcji, a na dodatek mog spowodowa pogorszenie wydajnoci zarwno
w Pythonie 2.3, jak i w Pythonie 2.4.
Zobacz rwnie
Receptur 5.4, w ktrej opisywane s sposoby sortowania sownikw na podstawie przechowywanych wartoci, a nie kluczy.
Problem
Chcemy posortowa list cigw znakw, ignorujc przy tym wszelkie rnice w wielkoci liter. Oznacza to, e chcemy, aby litera a, mimo e jest ma liter, znalaza si przed wielk
liter B. Niestety, domylne porwnywanie cigw znakw uwzgldnia rnice wielkoci liter,
co oznacza, e wszystkie wielkie litery umieszczane s przed maymi literami.
Rozwizanie
Wzorzec DSU (decorate-sort-undecorate) sprawdza si tu doskonale, tworzc szybkie i proste
rozwizanie:
def case_insensitive_sort(string_list):
auxiliary_list = [(x.lower(), x) for x in string_list]
auxiliary_list.sort()
return [x[1] for x in auxiliary_list]
# dekoracja
# sortowanie
# usunicie dekoracji
227
W Pythonie 2.4 wzorzec DSU obsugiwany jest w samym jzyku, dlatego (zakadajc, e obiekty
listy string_list rzeczywicie s cigami znakw, a nie na przykad obiektami cigw znakw Unikodu) mona w nim zastosowa ponisze, jeszcze szybsze i prostsze rozwizanie:
def case_insensitive_sort(string_list):
return sorted(string_list, key=str.lower)
Analiza
Do oczywist alternatyw dla rozwizania podawanego w tej recepturze jest przygotowanie
funkcji porwnujcej i przekazanie jej do metody sort:
def case_insensitive_sort_1(string_list):
def compare(a, b): return cmp(a.lower(), b.lower())
string_list.sort(compare)
Niestety, w ten sposb przy kadym porwnaniu dwukrotnie wywoywana jest metoda lower,
a liczba porwna koniecznych do posortowania listy n-elementowej zazwyczaj jest proporcjonalna do n log(n).
Wzorzec DSU tworzy list pomocnicz, ktrej elementami s krotki, w ktrych kady element
z oryginalnej listy poprzedzany jest specjalnym kluczem. Nastpnie sortowanie odbywa si
wedug tych wanie kluczy, poniewa Python porwnuje krotki leksykograficznie, czyli pierwsze elementy krotek porwnuje w pierwszej kolejnoci. Dziki zastosowaniu wzorca DSU metoda lower wywoywana jest tylko n razy w czasie sortowania listy n cigw znakw, co pozwala oszczdzi na tyle duo czasu, e cakowicie rekompensuje konieczno pocztkowego
udekorowania listy i kocowego zdjcia przygotowanych dekoracji.
Wzorzec DSU czasami znany jest te pod (nie do koca poprawn) nazw transformacji
Schwartza, ktra jest nieprecyzyjn analogi do znanego idiomu jzyka Perl. Poprzez zastosowanie takich porwna wzorzec DSU jest bardziej zbliony do transformacji Guttmana-Roslera
(wicej informacji na stronie http://www.sysarch.com/perl/sort_paper.html).
Wzorzec DSU jest tak wany, e w Pythonie 2.4 wprowadzono jego bezporedni obsug. Do
metody sort mona opcjonalnie przekaza nazywany argument key bdcy elementem wywoywalnym, uywanym w czasie sortowania do uzyskania klucza sortowania kadego elementu listy. Jeeli do funkcji sort zostanie przekazany taki argument, to automatycznie zacznie ona korzysta z wzorca DSU. Oznacza to, e w Pythonie 2.4 wywoanie string_list.sort
(key=str.lower) jest rwnowane z wywoaniem funkcji case_insensitive_sort. Jedyna
rnica polega na tym, e metoda sort sortuje list w miejscu (i zwraca warto None), a funkcja case_insensitive_sort zwraca posortowana kopi listy, nie modyfikujc przy tym oryginau. Jeeli chcielibymy, eby funkcja case_insensitive_sort rwnie sortowaa list w miejscu, to wystarczy wynik jej pracy przypisa do ciaa wejciowej listy:
string_list[:] = [x[1] for x in auxiliary_list]
Z drugiej strony, jeeli w Pythonie 2.4 chcielibymy uzyska posortowana kopi listy, bez modyfikowania oryginau, to moemy skorzysta z wbudowanej funkcji sorted. Na przykad zapis:
for s in sorted(string_list, key=str.lower): print s
w Pythonie 2.4 wypisuje wszystkie cigi znakw zapisane na licie string_list, ktra zostaje
posortowana bez uwzgldniania rnic w wielkoci liter, a jej orygina pozostaje bez zmian.
228
Wykorzystanie w Pythonie 2.4 metody str.lower w argumencie key ogranicza nas do sortowania wycznie cigw znakw (nie da si tak posortowa na przykad cigw znakw
Unikodu). Jeeli wiemy, e bdziemy sortowa obiekty Unikodu, to naley posuy si parametrem key=unicode.lower. Jeeli chcielibymy uzyska funkcj, ktra dziaaaby tak samo
wobec prostych cigw znakw i cigw znakw Unikodu, to mona zaimportowa modu
string i posuy si argumentem key=string.lower. Ewentualnie mona te skorzysta z zapisu key=lambda s: s.lower().
Skoro musimy czasem sortowa listy cigw znakw w sposb nieuwzgldniajcy rnic wielkoci liter, to rwnie dobrze moemy potrzebowa sownikw lub zbiorw stosujcych klucze
nieuwzgldniajce rnic wielkoci liter, a take list, w ktrych podobnie zachowuj si metody
index oraz count i inne. W takich sytuacjach potrzebny jest nam typ wywiedziony z klasy str,
ktry nie uwzgldniaby rnic wielkoci liter w operacjach porwnywania i mieszania (ang.
hashing). Jest to zdecydowanie lepsze rozwizanie w porwnaniu z implementowaniem wielu
rnych typw kontenerowych i funkcji obejmujcych przedstawion funkcj. Sposb implementowania takiego typu podawany by ju w recepturze 1.24.
Zobacz rwnie
Zbir czsto zadawanych pyta (dostpny na stronie http://www.python.org/cgi-bin/faqw.py?req=
show&file=faq04.051.htp. Receptur 5.3. Podrcznik Library Reference Pythona 2.4 w czciach opisujcych wbudowan funkcj sorted oraz parametr key metod sort i sorted. Receptur 1.24.
Problem
Musimy posortowa list obiektw wedug wartoci jednego z atrybutw tych obiektw.
Rozwizanie
Tutaj rwnie doskonale sprawdza si wzorzec DSU:
def sort_by_attr(seq, attr):
intermed = [ (getattr(x, attr), i, x) for i, x in enumerate(seq) ]
intermed.sort()
return [ x[-1] for x in intermed ]
def sort_by_attr_inplace(lst, attr):
lst[:] = sort_by_attr(lst, attr)
W Pythonie 2.4, w ktrym wzorzec DSU zosta wbudowany w jzyk, kod ten moe by jeszcze
krtszy i szybszy:
import operator
def sort_by_attr(seq, attr):
return sorted(seq, key=operator.attrgetter(attr))
def sort_by_attr_inplace(lst, attr):
lst.sort(key=operator.attrgetter(attr))
229
Analiza
Sortowanie listy obiektw wedug okrelonego atrybutu najlepiej wykonuje si za pomoc wzorca DSU omawianego w poprzedniej recepturze 5.2. W Pythonie 2.3 i 2.4 nie jest on ju potrzebny do tworzenia stabilnego sortowania, co byo konieczne w poprzednich wersjach jzyka (od wersji 2.3 algorytm sortowania stosowany w Pythonie zawsze jest stabilny), a mimo
to inne zalety wzorca DSU nadal s niepodwaalne.
W oglnym przypadku i z wykorzystaniem najlepszego algorytmu sortowanie ma zoono
O(n log n) (w formuach matematycznych taki zapis oznacza, e wartoci n i log n s mnoone). Sia wzorca DSU polega na wykorzystaniu wycznie wbudowanych w Pythona (i przez
to najszybszych) mechanizmw porwnania, przez co maksymalnie przyspieszana jest ta cz
wyraenia O(n log n), ktra zabiera najwicej czasu w operacji sortowania sekwencji o bardzo
duej dugoci. Pocztkowy krok dekorowania, w ktrym przygotowywana jest pomocnicza lista
krotek, oraz kocowy krok usuwania dekoracji, w ktrym z posortowanej listy krotek wydobywane s waciwe informacje, oba maj zoono tylko O(n). Oznacza to, e drobne niedocignicia w tych dwch krokach bd miay niky wpyw na sortowanie list z wielk liczb
elementw, a w przypadku niewielkich list wpyw tych krokw rwnie bdzie wzgldnie
niewielki.
Notacja O( )
Najbardziej uyteczny sposb okrelania wydajnoci danego algorytmu polega na wykorzystaniu tak zwanej analizy lub notacji wielkiego O (litera O oznacza po angielsku order, czyli
rzd). Dokadne objanienia dotyczce tej notacji znale mona na stronie http://
pl.wikipedia.org/wiki/Notacja_du%C5%BCego_O. Tutaj podamy tylko krtkie podsumowanie.
Jeeli przyjrzymy si algorytmom stosowanym na danych wejciowych o rozmiarze N, to dla
odpowiednio duych wartoci N (due iloci danych wejciowych sprawiaj, e wydajno
danego rozwizania staje si spraw krytyczn) czas ich dziaania moe by okrelany jako
proporcjonalny do pewnej funkcji wartoci N. Oznacza si to za pomoc notacji takiej jak
O(N) (czas pracy algorytmu jest proporcjonalny do N; przetwarzanie dwukrotnie wikszego
zbioru danych zajmuje dwa razy wicej czasu, a przetwarzanie zbioru dziesiciokrotnie wikszego zajmuje dziesi razy wicej czasu; inna nazwa tej notacji to zoono liniowa), O(N2)
(czas pracy algorytmu jest proporcjonalny do kwadratu N; przetwarzanie dwukrotnie wikszego zbioru danych zajmuje cztery razy wicej czasu, a przetwarzanie zbioru dziesiciokrotnie wikszego zajmuje sto razy wicej czasu; inna nazwa tej notacji to zoono kwadratowa) itd.
Czsto bdziemy natyka si te na zapis O(N log N), ktry oznacza algorytm szybszy ni
O(N2), ale wolniejszy od algorytmu O(N).
Najczciej ignorowane s stae proporcji (przynajmniej w rozwaaniach teoretycznych), poniewa zazwyczaj zale one od takich czynnikw jak czstotliwo zegara w naszym komputerze, a nie od samego algorytmu. Jeeli zastosujemy dwa razy szybszy komputer, to cao bdzie trwaa o poow krcej, ale nie zmieni to wynikw porwna poszczeglnych
algorytmw.
W tej recepturze w kadej krotce bdcej elementem listy intermed umieszczamy indeks i
przed odpowiadajcym mu elementem x (x jest i-tym elementem sekwencji seq). W ten sposb upewniamy si, e dowolne dwa elementy sekwencji seq nie bd porwnywane bezporednio, nawet jeeli maj tak sam warto atrybutu attr, poniewa w takiej sytuacji ich
indeksy bd miay rne wartoci. W ten sposb wykonywane w Pythonie leksykograficzne
230
Nie jest to a tak szybkie jak sortowanie w miejscu, ale ten ostatni kod jest nadal ponad 2,5 razy
szybszy od funkcji przedstawianej w rozwizaniu receptury. Przekazujc opcjonalny argument
nazywany key w Pythonie 2.4, uzyskujemy te pewno, e w czasie sortowania elementy listy
nie zostan porwnane bezporednio ze sob, wic nie musimy tworzy adnych dodatkowych
zabezpiecze. Co wicej, posortowana lista na pewno bdzie stabilna.
Zobacz rwnie
Receptur 5.2. Podrcznik Library Reference z Pythona 2.4 w czci opisujcej wbudowan funkcj sorted, funkcje attrgetter i itemgetter z moduu operator oraz parametr key funkcji
sort i sorted.
Problem
Musimy zliczy wystpienia rnych elementw i zaprezentowa te elementy w kolejnoci czstotliwoci wystpowania na przykad w celu przygotowania histogramu.
231
Rozwizanie
Histogramy niezalenie od swojego zastosowania przy tworzeniu grafiki tworzone s przez
zliczanie wystpie elementw (co nietrudno jest wykona w przypadku list lub sownikw
w Pythonie) i sortowanie list lub indeksw w kolejnoci odpowiadajcej wyznaczonym wartociom. Oto klasa wywiedziona z klasy dict, ktra uzupeniona zostaa o dwie metody:
class hist(dict):
def add(self, item, increment=1):
''' dodaje warto 'increment' do pozycji elementu 'item' '''
self[item] = increment + self.get(item, 0)
def counts(self, reverse=False):
''' zwraca list kluczy posortowan zgodnie z odpowiadajcymi im wartociami '''
aux = [ (self[k], k) for k in self ]
aux.sort()
if reverse: aux.reverse()
return [k for v, k in aux]
Jeeli zliczane elementy mog by modelowane jako niewielkie liczby cakowite z okrelonego zakresu i wyniki zliczania elementw chcemy przechowywa na licie, to mona zastosowa bardzo podobne rozwizanie:
class hist1(list):
def __init__(self, n):
''' inicjalizacja listy do zliczania wystpie n rnych elementw '''
list.__init__(self, n*[0])
def add(self, item, increment=1):
''' dodaje warto 'increment' do pozycji elementu 'item' '''
self[item] += increment
def counts(self, reverse=False):
''' zwraca list indeksw posortowan zgodnie z odpowiadajcymi im wartociami '''
aux = [ (v, k) for k, v in enumerate(self) ]
aux.sort()
if reverse: aux.reverse()
return [k for v, k in aux]
Analiza
Metoda add klasy hist jest przykadem wykorzystania typowego dla Pythona idiomu przeznaczonego do zliczania dowolnych (cho unikalnych) elementw. W klasie hist1, zbudowanej
na podstawie klasy list, stosowane jest inne rozwizanie, polegajce na wpisywaniu w specjalnej metodzie __init__ wartoci 0 do wszystkich elementw listy, dziki czemu metoda add
moe przyj prostsz posta.
Metoda counts tworzy list kluczy lub indeksw posortowanych w kolejnoci wyznaczanej przez
powizane z nimi wartoci. Problem jest bardzo podobny w obu klasach (hist i hist1), dlatego
podane rozwizania s niemal identyczne w obu wykorzystywany jest wzorzec DSU omawiany w recepturach 5.2 i 5.3. Jeeli w naszym programie chcielibymy skorzysta z obu klas,
to moemy wykorzysta ich podobiestwo i wydzieli czci wsplne do pomocniczej funkcji
_sorted_keys:
def _sorted_keys(container, keys, reverse):
''' zwraca list 'keys' posortowan zgodnie z wartociami z parametru 'container' '''
aux = [ (container[k], k) for k in keys ]
aux.sort()
if reverse: aux.reverse()
return [k for v, k in aux]
232
Nastpnie metody counts obu klas mona zaimplementowa jako opakowania zaprezentowanej
funkcji _sorted_keys:
class hist(dict):
## ...
def counts(self, reverse=False):
return _sorted_keys(self, self, reverse)
class hist1(list):
## ...
def counts(self, reverse=False):
return _sorted_keys(self, xrange(len(self)), reverse)
W Pythonie 2.4 wzorzec DSU jest tak wany, e (jak pokazano w recepturach 5.2 i 5.3) metoda
sort z obiektw list oraz nowa, wbudowana funkcja sorted oferuj bardzo szybk implementacj tego wzorca. Dziki temu w Pythonie 2.4 funkcja _sorted_keys moe by jeszcze prostsza
i szybsza:
def _sorted_keys(container, keys, reverse):
return sorted(keys, key=container.__getitem__, reverse=reverse)
Metoda powizana container.__getitem__ wykonuje dokadnie te same operacje co indeksowanie container[k] stosowane w implementacji dla Pythona 2.3, ale jest ona elementem
wywoywalnym, ktry moe by wywoywany na rzecz kadego elementu k z sortowanej sekwencji dokadnie tak warto naley przekazywa w parametrze key do wywoywanej
funkcji sorted. W Pythonie 2.4 udostpniany jest te prosty i bezporedni sposb na odczytanie
listy elementw sownika posortowanych wedug wartoci:
from operator import itemgetter
def dict_items_sorted_by_value(d, reverse=False):
return sorted(d.iteritems(), key=itemgetter(1), reverse=reverse)
Funkcja wysokiego poziomu operator.itemgetter (rwnie wprowadzona zostaa w Pythonie 2.4) jest bardzo wygodnym sposobem dostarczania argumentu key w czasie sortowania
kontenera, ktrego elementy s podkontenerami, a kluczem sortowania ma by okrelony element podkontenera. Dokadnie tak sytuacj mamy w przedstawionym przypadku, poniewa
elementy sownika s sekwencj par (krotek dwuelementowych), a my chcemy posortowa t
sekwencj wedug drugiego elementu kadej krotki.
Wracajc do gwnego tematu tej receptury, oto przykad uycia klasy hist prezentowanej
w rozwizaniu receptury:
sentence = ''' Halo! To jest test. Halo! To by test,
ale ju nim nie jest. '''
words = sentence.split()
c = hist()
for word in words: c.add(word)
print "Rosnco:"
print c.counts()
print "Malejco:"
print c.counts(reverse=True)
233
Zobacz rwnie
Receptur Special Method Names zamieszczon w podrczniku Library Reference oraz rozdzia o programowaniu obiektowym w podrczniku Python in a Nutshell, w czci opisujcej
metod __getitem__. Podrcznik Library Reference Pythona 2.4 w czci opisujcej wbudowan
funkcj sorted oraz parametr key funkcji sort i sorted.
Problem
Musimy tak posortowa list cigw znakw zawierajcych sekwencje cyfr (na przykad list
kodw adresowych), eby wygldaa jak najlepiej. Na przykad tekst 'foo2.txt' powinien
znale si przed tekstem 'foo10.txt'. Niestety, w Pythonie domylnie stosowane jest porwnanie alfabetyczne, wic tekst 'foo10.txt' znajdzie si przed 'foo2.txt'.
Rozwizanie
Musimy podzieli kady cig znakw na sekwencje cyfr i niecyfr, a nastpnie kad sekwencj cyfr zamieni w liczb. W ten sposb uzyskamy list przechowujc waciwe klucze do
sortowania listy. Nastpnie mona skorzysta ze wzorca DSU do wykonania samego sortowania. Jak wida, wystarczy nam przygotowa dwie krciutkie funkcje:
import re
re_digits = re.compile(r'(\d+)')
def embedded_numbers(s):
pieces = re_digits.split(s)
# dzielenie na cyfry/niecyfry
pieces[1::2] = map(int, pieces[1::2])
# zamiana cyfr w liczby
return pieces
def sort_strings_with_embedded_numbers(alist):
aux = [ (embedded_numbers(s), s) for s in alist ]
aux.sort()
return [ s for __, s in aux ]
# konwencja: __ oznacza "ignoruj"
W Pythonie 2.4 mona skorzysta z wbudowanej obsugi wzorca DSU (przyda si te przedstawiona wyej funkcja embedded_numbers) i posortowa list za pomoc poniszej funkcji:
def sort_strings_with_embedded_numbers(alist):
return sorted(alist, key=embedded_numbers)
Analiza
Zamy, e mamy nieposortowan list nazw plikw podobn do poniszej:
files = 'plik3.txt plik11.txt plik7.txt plik4.txt plik15.txt'.split()
Jeeli w Pythonie 2.4 podan list posortujemy i wypiszemy za pomoc instrukcji print '
'.join(sorted(files)), to na ekranie zobaczymy nastpujcy tekst: plik11.txt plik15.txt
plik3.txt plik4.txt plik7.txt. Taka kolejno wynika z faktu, e domylnie cigi znakw
sortowane s alfabetycznie (a mwic bardziej wymylnie sortowane s leksykograficznie).
234 |
Python nie moe si domyla, e chcemy inaczej traktowa cigi znakw, poniewa pewne
czci cigw znakw dziwnym trafem opisuj liczby. Musimy dokadnie okreli, co chcemy
zrobi, i w tej recepturze pokazujemy, jak mona tego dokona:
Korzystajc z tej receptury, mona uzyska znacznie lepiej wygldajce wyniki:
print ' '.join(sort_strings_with_embedded_numbers(files))
Zobacz rwnie
Podrczniki Library Reference i Python in a Nutshell w czciach opisujcych wykrojenia i modu re. Podrcznik Library Reference z Pythona 2.4 w czci opisujcej wbudowan funkcj
sorted i parametr key przekazywany do funkcji sort i sorted. Receptury 5.3 i 5.2.
Problem
Wszystkie elementy dugiej listy musimy obsuy w kolejnoci losowej.
Rozwizanie
Jak to zwykle bywa w Pythonie, najlepszym rozwizaniem jest rozwizanie najprostsze. Jeeli
wolno nam bdzie zmieni kolejno elementw w wejciowej licie, to ponisza funkcja bdzie
zdecydowanie najprostsza i najszybsza:
5.6. Przetwarzanie wszystkich elementw listy w kolejnoci losowej
235
Jeeli wejciowa lista musi zosta niezmieniona lub wejciowe dane zapisane s w elemencie
iterowalnym niebdcym list, to wystarczy dopisa do powyszej funkcji pierwsz instrukcj
w postaci przypisania data = list(data).
Analiza
Powszechnym bdem jest przywizywanie zbytniej wagi do prdkoci dziaania kodu,
ale z drugiej strony nie mona te popenia odwrotnego bdu polegajcego na ignorowaniu rnic wydajnoci poszczeglnych algorytmw. Zamy, e musimy w kolejnoci
losowej i bez powtrze obsuy wszystkie elementy dugiej listy (zakadamy te, e moemy zmodyfikowa, a nawet zniszczy list wejciow). Pierwszym pomysem, jaki zapewne przyszedby nam do gowy, jest losowe wybieranie jednego elementu (za pomoc funkcji
random.choice) i po jego obsueniu usuwanie z listy w celu zapobieenia powstawaniu
powtrze:
import random
def process_random_removing(data, process):
while data:
elem = random.choice(data)
data.remove(elem)
process(elem)
Taka funkcja jest niestety bardzo powolna, nawet w przypadku list z zaledwie kilkuset elementami. Kade wywoanie metody data.remove wymaga liniowego przejrzenia wszystkich
elementw listy w celu odnalezienia tego przeznaczonego do usunicia. Kady taki krok ma
zoono O(n), a zatem cay proces ma zoono O(n2) czas obsugi listy jest proporcjonalny do kwadratu jej dugoci (a zwykle listy nie nale do krtkich).
Kolejne usprawnienia tego pierwszego pomysu mog polega na: wybieraniu losowych indeksw oraz za pomoc metody pop pobieraniu samych elementw i jednoczesnym usuwaniu ich
z listy, niskopoziomowych zabawach z indeksami majcych na celu uniknicie kosztownego
usuwania elementw listy i zastpowaniu wybranego elementu ostatnim jeszcze niewybranym
elementem albo zastpowaniu listy sownikami lub zbiorami. Ten ostatni pomys moe wynika z nadziei, e uda si skorzysta z metody popitem obiektw sownikw (lub rwnowanej
jej metody pop z klasy sets.Set albo wbudowanego w Pythona 2.4 typu set), poniewa na
pierwszy rzut oka wyglda ona na przeznaczon do wybierania i usuwania losowych elementw, ale zgodnie z dokumentacj Pythona metoda dict.popitem suy ma do zwracania
i usuwania dowolnego elementu sownika, ktry zdecydowanie nie jest elementem losowym.
Prosz przyjrze si poniszemu kodowi:
>>> d=dict(enumerate('ciao'))
>>> while d: print d.popitem()
Niektrych moe to zaskoczy, ale w wikszoci implementacji Pythona podany kod wcale nie
wypisze elementw sownika d w kolejnoci losowej. Najczciej zobaczymy najpierw (0, 'c'),
nastpnie (1, 'i') itd. Jeeli w Pythonie potrzebne jest nam zachowanie pseudolosowe, to
obowizkowo musimy skorzysta z moduu random metoda popitem nie jest tutaj adnym
rozwizaniem.
236
Jeeli kto myla o zamianie listy na sownik, to na pewno jest w stanie rozumowa w sposb
pythoniczny, mimo e w tym konkretnym problemie zastosowanie sownikw nie powoduje
znaczcego podniesienia prdkoci dziaania. Jak si okazuje, istnieje jeszcze bardziej pythoniczne rozwizanie od wybrania najwaciwszej struktury danych. Mona je podsumowa nastpujcym zdaniem: niech zrobi to biblioteka standardowa. Biblioteka standardowa Pythona jest
ogromnym, bogatym zbiorem przydatnych, skutecznych i szybkich funkcji oraz klas przeznaczonych do wykonywania najrniejszych operacji. W tym przypadku najwaniejsz rzecz jest
to, eby zaakceptowa fakt, e najprostszym sposobem na obsuenie wszystkich elementw
listy w losowej kolejnoci jest uoenie ich najpierw w kolejnoci losowej (proces ten nazywany
jest tasowaniem sekwencji, przez analogi do tasowania talii kart) i liniowe obsuenie elementw
listy. Takie przetasowanie elementw listy wykonuje funkcja random.shuffle i dlatego wykorzystana zostaa w rozwizaniu tej receptury.
Wydajno danego rozwizania zawsze musi zosta zmierzona; nigdy nie naley polega na
domysach. Do przeprowadzania takich pomiarw najlepiej bdzie wykorzysta modu timeit.
W poczeniu z pust funkcj process i list skadajc si z tysica elementw funkcja process_
all_in_random_order okazaa si by prawie dziesiciokrotnie szybsza od funkcji process_
random_removing. W przypadku listy skadajcej si z dwch tysicy elementw stosunek prdkoci tych dwch funkcji wynosi ju prawie 20. Zwykle popraw wydajnoci o, powiedzmy,
25% albo stay wspczynnik 2 mona uzna za niewart naszej uwagi, to jednak nie mona
tak samo traktowa algorytmu 10 lub 20 razy wolniejszego od swojego konkurenta. Tak wyjtkowo niska wydajno bardzo atwo moe spowodowa, e dana cz programu stanie si
wskim. Co wicej, takie ryzyko wzrasta jeszcze bardziej, gdy zaczynamy porwnywa algorytm o zoonoci O(n2) z algorytmem o zoonoci O(n). Przy takich rnicach zoonoci algorytmw stosunek prdkoci dziaania zego i dobrego algorytmu ronie bez adnych ogranicze wraz ze wzrostem liczby danych wejciowych.
Zobacz rwnie
Dokumentacj moduw random i timeit dostpn w podrcznikach Library Reference i Python
in a Nutshell.
Problem
Chcemy utrzyma w stanie posortowania sekwencj, do ktrej dodawane s nowe elementy,
tak eby w dowolnym momencie mona byo prosto sprawdzi lub usun najmniejszy z zapisanych aktualnie elementw.
Rozwizanie
Zamy, e pocztkowo mamy list nieuporzdkowan, tak jak ponisza:
the_list = [903, 10, 35, 69, 933, 485, 519, 379, 102, 402, 883, 1]
237
Mona teraz wywoa metod the_list.sort(), aby posortowa list, a nastpnie skorzysta
z metody the_list.pop(0), aby pobra i usun najmniejszy element listy. Niestety, pniej
po kadym dodaniu elementu do listy (na przykad metod the_list.append(0)) konieczne
jest ponownie wywoanie metody the_list.sort() w celu utrzymania porzdku na licie.
Inne rozwizanie polega na zastosowaniu moduu heapq pochodzcego ze standardowej biblioteki Pythona:
import heapq
heapq.heapify(the_list)
Tak przeksztacona lista nie musi by koniecznie w peni posortowana, ale spenia waciwo sterty (ang. heap property) (oznacza ona, e jeeli wszystkie indeksy s prawidowe, to
the_list[i]<=the_list[2*i+1] i the_list[i]<=the_list[2*i+2]), przez co w szczeglnoci
element the_list[0] jest zawsze elementem najmniejszym. W celu utrzymania na licie waciwoci sterty, naley uywa metody result=heapq.heappop(the_list), aby pobiera i usuwa najmniejszy element listy, natomiast nowe elementy do listy naley dodawa za pomoc
metody heapq.heappush(the_list, newitem). Jeeli zajdzie potrzeba wykonania obu tych operacji, czyli dodania nowego elementu z jednoczesnym pobraniem i usuniciem najmniejszego
elementu, naley skorzysta z wywoania result=heapq.heapreplace(the_list, newitem).
Analiza
Jeeli musimy odbiera dane w sposb uporzdkowany (przy kadym odczycie pobieramy
najmniejszy spord dostpnych aktualnie elementw), to koszt sortowania danych musimy
ponie albo w momencie odczytywania elementu albo w momencie jego dodawania. Jedno
z rozwiza polega na gromadzeniu danych w ramach listy i sortowaniu caej listy. Dziki
temu odczytywanie danych w kolejnoci od najmniejszego do najwikszego elementu jest bardzo proste. Niestety, jeeli w czasie odczytywania danych dodajemy do listy nowe elementy,
to zmuszeni jestemy do wywoywania metody sort, aby mie pewno, e po dodaniu nowego elementu odczytywa bdziemy najmniejszy element na licie. W Pythonie metoda sort
implementowana jest z wykorzystaniem mao znanego algorytmu Natural Mergesort, ktry minimalizuje koszta sortowania w takim rozwizaniu. Mimo to rozwizanie to moe by mocno
obciajce czas kadego dodania elementu (i sortowania listy), a take kadego odczytania
(i usunicia za pomoc metody pop) jest proporcjonalny do liczby elementw aktualnie znajdujcych si na licie (co oznacza, e algorytm ten ma zoono O(N)).
Rozwizanie alternatywne polega na wykorzystaniu sposobu organizacji danych znanego pod
nazw sterty (ang. heap). Jest to rodzaj kompaktowego drzewa binarnego, ktre zapewnia, e
kady wze-rodzic jest mniejszy od jego wzw-dzieci. Najlepszym sposobem na utworzenie
w Pythonie struktury sterty jest wykorzystanie listy i zarzdzanie ni przez modu heapq
z biblioteki standardowej. Taka lista nie jest sortowana dokadnie, a jedynie w takim stopniu,
eby zyska pewno, e wywoujc funkcj heappop, otrzymamy najmniejszy z dostpnych
aktualnie elementw, a wszystkie pozostae zostan uporzdkowane tak, aby utrzyma struktur sterty. Kade dodanie elementu funkcj heappush, jak rwnie kade usunicie elementu
funkcj heappop zajmuje bardzo mao czasu w stosunku do dugoci caej listy (w oglnym przypadku jest to O(log N)). Niewielkie koszta tych operacji ponosimy w trakcie pracy, a oglny
koszt pracy z tak zarzdzan list rwnie nie jest znaczcy.
238 |
Dobr okazj do zastosowania sterty jest na przykad sytuacja, w ktrej mamy dug kolejk
cyklicznie dopisywanych danych, a chcemy z niej pobiera zawsze najwaniejszy w danym momencie element bez koniecznoci cigego sortowania listy lub wykonywania penego przeszukiwania. Takie rozwizanie nazywane jest kolejk priorytetow (ang. priority queue), a sterta jest
najlepsz metod na jej zaimplementowanie. Trzeba jednak zauway, e modu heapq przy
kadym wywoaniu funkcji heappop bdzie dostarcza nam zawsze najmniejszy element sterty,
dlatego naley uwzgldnia t cech przy ustalaniu priorytetw danych dodawanych do kolejki. Na przykad otrzymywane elementy powizane s z okrelonym kosztem i w zwizku
z tym najwaniejszym elementem jest ten najdroszy spord znajdujcych si aktualnie na
stercie. Co wicej, spord elementw o identycznym koszcie najwaniejszym ma by ten,
ktry zosta dopisany jako pierwszy. Oto sposb na przygotowanie klasy kolejki priorytetowej speniajcej te zaoenia, ktra korzysta z funkcji udostpnianych przez modu heapq:
class prioq(object):
def __init__(self):
self.q = []
self.i = 0
def push(self, item, cost):
heapq.heappush(self.q, (-cost, self.i, item))
self.i += 1
def pop(self):
return heapq.heappop(self.q)[-1]
Najwaniejsz czci powyszego kodu jest zapisywanie na stercie krotek, w ktrych pierwszym elementem jest koszt waciwego elementu ze zmienionym znakiem, dziki czemu elementy
o najwyszym koszcie tworzy bd najmniejsze krotki (tak porwnuje je Python). Zaraz za kosztem w krotce umieszczany jest postpujcy indeks dodawanych elementw, przez co wrd elementw o identycznym koszcie najmniejszym bdzie ten, ktry zosta dopisany jako pierwszy.
W Pythonie 2.4 modu heapq zosta napisany od nowa i poddany wielu optymalizacjom. Wicej informacji na jego temat podanych zostanie w recepturze 5.8.
Zobacz rwnie
Dokumentacj moduu heapq dostpn w podrcznikach Library Reference i Python in a Nutshell.
Wrd rde Pythona plik heapq.py zawiera bardzo ciekaw dyskusj na temat stert. Receptur 5.8, w ktrej podawanych jest wicej informacji o module heapq. Receptur 19.14, w ktrej
opisywany jest sposb czenia posortowanych sekwencji z wykorzystaniem moduu heapq.
Problem
Musimy pobra kilka najmniejszych elementw sekwencji. Mona oczywicie posortowa sekwencj i uy wykrojenia seq[:n], ale moe istnieje jaki lepszy sposb?
239
Rozwizanie
Jeeli n, czyli liczba elementw wybieranych z sekwencji, jest maa w porwnaniu z L, czyli
cakowit dugoci sekwencji, to problem ten da si rozwiza lepiej. Metoda sort dziaa bardzo szybko, a mimo to zabiera O(L log L) czasu, natomiast pobranie z listy n najmniejszych
elementw zajmuje O(n) czasu dla maych n. Oto prosty i bardzo praktyczny generator rozwizujcy przedstawione zadanie, dziaajcy zarwno w Pythonie 2.3, jak i w Pythonie 2.4:
import heapq
def isorted(data):
data = list(data)
heapq.heapify(data)
while data:
yield heapq.heappop(data)
W Pythonie 2.4 mona skorzysta ze znacznie prostszej i szybszej metody pobierania n najmniejszych elementw sekwencji data:
import heapq
def smallest(n, data):
return heapq.nsmallest(n, data)
Analiza
Parametr data moe by dowolnym powizanym elementem iterowalnym. Funkcja isorted
podawana w rozwizaniu receptury rozpoczyna si wywoaniem funkcji list, dziki czemu
warunek ten jest zawsze speniony. Z funkcji tej instrukcj data = list(data) mona usun
dopiero wtedy, gdy spenione s nastpujce warunki: wiemy, e parametr data zawsze bdzie
list, nie przeszkadza nam to, e generator zmieni kolejno jej elementw, a dodatkowo chcemy usun z tej listy pobierane elementy.
Jak pokazalimy w recepturze 5.7, w bibliotece standardowej Pythona dostpny jest modu
heapq, ktry obsuguje struktur danych znan jako sterta. Generator isorted prezentowany
w tej recepturze na pocztku tworzy stert (wywoaniem funkcji heapq.heapify), a nastpnie
oddaje i usuwa w kadym kroku najmniejszy element sterty (za pomoc funkcji heapq.heappop).
W Pythonie 2.4 modu heapq uzupeniony zosta o dwie nowe funkcje. Funkcja heapq.nlargest
(n, data) zwraca list n najwikszych elementw parametru data, natomiast funkcja heapq.
nsmallest(n, data) zwraca list n najmniejszych elementw parametru data. Funkcje te nie
wymagaj, eby parametr data spenia warunki prawidowej sterty; co wicej, nie wymagaj
nawet, eby parametr data by list cakowicie wystarczy dowolny element iterowalny
z elementami pozwalajcymi na porwnywanie. Funkcja smallest prezentowana w rozwizaniu tej receptury cao prac przekazuje zatem do funkcji heapq.nsmallest.
Jeeli chcemy rozmawia na temat prdkoci dziaania funkcji, zawsze musimy j zmierzy
prby zgadywania wzgldnych prdkoci rnych kawakw kodu to naprawd niebezpieczna gra. Jak w takim razie wyglda wydajno funkcji isorted w porwnaniu z funkcj
sorted dostpn w Pythonie 2.4, jeeli interesuje nas tylko kilka (najmniejszych) elementw?
W celu zmierzenia czasw pracy obu tych funkcji napisaem funkcj top10, ktrej mona uy
w poczeniu z obydwoma funkcjami. Poza tym musiaem si upewni, e funkcja sorted dostpna bdzie rwnie w Pythonie 2.3, mimo e nie jest ona w tej wersji funkcj wbudowan:
240 |
try:
sorted
except:
def sorted(data):
data = list(data)
data.sort()
return data
import itertools
def top10(data, howtosort):
return list(itertools.islice(howtosort(data), 10))
Na moim komputerze z Pythonem 2.4 obsuenie listy skadajcej si z tysica dobrze przemieszanych liczb cakowitych funkcji top10 wspomaganej przez funkcj isorted zajmuje 260 mikrosekund, natomiast po zmianie na wspprac z wbudowan funkcj sorted ta sama operacja
trwa 850 mikrosekund. Co wicej, w Pythonie 2.3 funkcje te s o wiele wolniejsze: w poczeniu
z funkcj isorted czas testu wynis 12 milisekund, a z funkcj sorted 2,7 milisekundy. Innymi sowy, funkcja sorted w Pythonie 2.3 jest trzy razy wolniejsza ni w Pythonie 2.4, ale
funkcja isorted jest 50 razy wolniejsza. Wynika z tego nastpujca nauka: przy okazji wprowadzania jakiejkolwiek optymalizacji naley dokonywa pomiarw. Nie powinno si wybiera
optymalizacji na podstawie oglnych przesanek, poniewa wydajno rozwiza moe rni
si znaczco nawet pomidzy bardzo podobnymi do siebie gwnymi wydaniami Pythona.
Trzeba tu zaznaczy jeszcze jedn rzecz: jeeli komu zaley na wydajnoci, powinien jak najszybciej przesi si na Pythona 2.4. Nowsza wersja zostaa w wielu miejscach przyspieszona
i zoptymalizowana w stosunku do wersji 2.3, szczeglnie w zakresie wyszukiwania i sortowania.
Jeeli mamy pewno, e nasz kod bdzie dziaa wycznie w Pythonie 2.4, to tak jak pokazano w rozwizaniu tej receptury, naley skorzysta z funkcji nsmallest z moduu heapq, poniewa jest ona szybsza, a take prostsza od jakiegokolwiek samodzielnie przygotowanego kodu.
W celu zaimplementowania funkcji top10 w Pythonie 2.4 wystarczy tak prosty zapis:
import heapq
def top10(data):
return heapq.nsmallest(10, data)
Podana tu wersja t sam list tysica dokadnie przemieszanych liczb cakowitych przetwarza
dwa razy szybciej od prezentowanej wczeniej wersji korzystajcej z funkcji isorted.
Zobacz rwnie
Podrczniki Library Reference i Python in a Nutshell w czciach opisujcych metod sort i typ
list, a take moduy heapq i timeit. Rozdzia 19., w ktrym podawanych jest wicej informacji na temat iterowania w Pythonie. Podrcznik Python in a Nutshell w czci powiconej
optymalizacji. Plik rdowy heapq.py, w ktrym znale mona interesujc analiz stert. Receptur 5.7, w ktrej podawanych jest wicej informacji na temat moduu heapq.
Problem
W podanej sekwencji musimy odnale wiele elementw.
5.9. Wyszukiwanie elementw w sekwencji posortowanej
241
Rozwizanie
Jeeli lista L jest posortowana, to najprostszym sposobem na sprawdzenie, czy element x znajduje si na licie L jest wykorzystanie moduu bisect bdcego czci standardowej biblioteki
Pythona:
import bisect
x_insert_point = bisect.bisect_right(L, x)
x_is_present = L[x_insert_point-1:x_insert_point] == [x]
Analiza
W Pythonie wyszukiwanie elementu x na licie L jest wyjtkowo proste. Sprawdzenie, czy dany
element jest czci tej listy, wymaga tylko zapisania if x in L, natomiast uzyskanie informacji o dokadnej lokalizacji tego elementu wymaga zastosowania wywoania L.index(x). Jeeli L ma dugo n, to operacje te trwaj proporcjonalnie do dugoci n, co oznacza, e sprawdzaj w ptli wszystkie elementy listy, porwnujc je z elementem x. Jeeli lista L jest
posortowana, to operacje te mona wykona zdecydowanie szybciej.
Klasyczny algorytm wyszukiwania elementu w posortowanej sekwencji znany jest pod nazw
szukania binarnego (ang. binary search). Nazwa ta wynika z tego, e w kadym kroku algorytm
zmniejsza zakres poszukiwa mniej wicej o poow, czyli w oglnym przypadku zajmuje on
log2n krokw. Warto o tym wiedzie szczeglnie wtedy, gdy musimy czsto wyszukiwa elementy w sekwencji, przez co koszt sortowania mona zamortyzowa w czasie wielu wyszukiwa danych. Jeeli zdecydujemy si binarnie szuka elementu x na licie L, to po wywoaniu
L.sort() reszt pracy bardzo uatwi nam modu bisect z biblioteki standardowej Pythona.
W szczeglnoci przyda si nam funkcja bisect.bisect_right, ktra nie zmienia zawartoci
listy, ale zwraca indeks pod ktrym dany element powinien by wstawiony, aby lista pozostaa
posortowana. Co wicej, jeeli szukany element znajduje si ju na licie, to funkcja bisect_
right zwraca indeks elementu znajdujcego si po prawej stronie elementw o tej samej wartoci. Oznacza to, e po uzyskaniu miejsca wstawienia elementu x musimy ju tylko skontrolowa element znajdujcy si tu przed tym miejscem, sprawdzajc, czy element ten jest rwny
elementowi x.
Sposb wyliczenia wartoci zmiennej x_is_present, jaki prezentowany jest w rozwizaniu, moe nie by cakiem zrozumiay. Jeeli wiemy, e lista L nie jest pusta, to moemy skorzysta
ze znacznie prostszego i czytelniejszego rozwizania:
x_is_present = L[x_insert_point-1] == x
Niestety, jeeli lista bdzie pusta, to tak uproszczone indeksowanie spowoduje wyjtek. Wykrawanie dziaa w sposb mniej cisy ni indeksowanie, poniewa w przypadku nieprawidowych granic wykrojenia tworzy tylko puste wykrojenie, ale nie wywouje wyjtkw. Mwic
oglnie, wyraenie somelist[i:i+1] tworzy tak sam list jednoelementow jak wyraenie
[somelist[i]], pod warunkiem, e i jest prawidowym indeksem na licie somelist. W przypadku, w ktrym indeksowanie powoduje wyjtek IndexError, wyrojenie zwraca tylko pust
list []. Przedstawiony w rozwizaniu sposb wyliczania wartoci zmiennej x_is_present
korzysta z tej wanej moliwoci uniknicia obsugi wyjtkw i w jednakowy sposb obsuguje
puste i niepuste listy L. Oto jeszcze inny sposb rozwizania problemu:
242 |
Zobacz rwnie
Dokumentacj moduu bisect w podrcznikach Library Reference i Python in a Nutshell. Receptur 5.12.
Problem
Musimy wybra z sekwencji n-ty element pod wzgldem wielkoci (na przykad element rodkowy zwany median). Jeeli lista byaby posortowana, to mona byoby uy zapisu seq[n],
ale nasza sekwencja nie jest posortowana, wiec zastanawiamy si, czy jest lepszy sposb od jej
posortowania.
Rozwizanie
Oczywicie jest lepsze rozwizanie, sprawdzajce si w sytuacji, gdy sekwencja jest dua, jej
elementy s mocno przemieszane, a porwnanie tych elementw jest kosztown operacj. Sortowanie jest bardzo szybkie, ale dla dobrze przemieszanych sekwencji o n elementach i tak
zajmuje ono O(n log n) czasu, natomiast dostpne s algorytmy pozwalajce na odszukanie
| 243
n-tego najmniejszego elementu w czasie O(n). Oto funkcja bdca solidn implementacj takiego algorytmu:
import random
def select(data, n):
" Wyszukuje n-ty element pod wzgldem wielkoci. "
# tworzy now list, sprawdza indeksy <0, szuka prawidowych indeksw
data = list(data)
if n<0:
n += len(data)
if not 0 <= n < len(data):
raise ValueError, "nie mog pobra elementu %d spord %d" % (n, len(data))
# ptla gwna, podobna do algorytmu quicksort, ale nie potrzebuje rekursji
while True:
pivot = random.choice(data)
pcount = 0
under, over = [], []
uappend, oappend = under.append, over.append
for elem in data:
if elem < pivot:
uappend(elem)
elif elem > pivot:
oappend(elem)
else:
pcount += 1
numunder = len(under)
if n < numunder:
data = under
elif n < numunder + pcount:
return pivot
else:
data = over
n -= numunder + pcount
Analiza
W prezentowanej recepturze chodzi nam o przypadki, w ktrych wane s powtrzenia. Na
przykad mediana z listy [1, 1, 1, 2, 3] wynosi 1, poniewa jest to trzeci element z piciu
w kolejnoci rosncej. Jeeli z jakiego dziwnego powodu chcielibymy nie uwzgldnia powtrze, to tak list trzeba by najpierw zredukowa do jej elementw unikalnych (na przykad
stosujc rozwizania podawane w recepturze 18.1), a dopiero potem wrci do kodu podawanego w tej recepturze.
Wejciowy parametr data moe by dowolnym elementem iterowalnym. Kod tej receptury
rozpoczyna si od wywoania na wszelki wypadek funkcji list. Nastpnie algorytm wchodzi
w ptl, w ktrej przy kadym kroku implementuje kilka wanych operacji: losowo wybiera
element rozdzielajcy (ang. pivot element), dzieli list na dwie czci skadajce si odpowiednio
z elementw poniej i ponad elementem rozdzielajcym, w nastpnym kroku kontynuuje
prac w jednej z czci listy, poniewa na tym etapie wiemy ju, w ktrej czci znajdzie si
szukany n-ty element. Pomys jest bardzo zbliony do klasycznego algorytmu znanego pod
nazw quicksort (rnica polega na tym, e algorytm quicksort musi obsuy obie czci listy
i w zwizku z tym zmuszony jest do korzystania z rekursji lub metod usuwania rekursji takich
jak utrzymywanie wasnego stosu zapewniajcego obsug caoci listy).
244 |
Losowe wybranie elementu rozdzielajcego sprawia, e algorytm lepiej sprawdza si w przypadkach niekorzystnego uoenia danych (takich, ktre siej spustoszenie w naiwnych implementacjach algorytmu quicksort). Podana implementacja mniej wicej log2N razy wywouje
funkcj random.choice. Inn wart wymienienia cech implementacji prezentowanej w rozwizaniu tej receptury jest zliczanie liczby wystpie elementu rozdzielajcego. Takie dziaanie
zapewnia dobr wydajno nawet w niezwykych przypadkach, w ktrych parametr data zawiera wiele powtrze poszczeglnych wartoci.
Wydobywanie z list under oraz over powizanych metod .append i przypisywanie ich do
lokalnych zmiennych uappend i oappend na pierwszy rzut oka moe wydawa si niecelowe,
a na dodatek powodujce pewne komplikacje, ale w rzeczywistoci jest to jedna z najwaniejszych metod optymalizacji w Pythonie. W celu utrzymania prostej, solidnej i pozbawionej niespodzianek struktury kompilatora Python nie przenosi staych wylicze poza ptle, tak jak
i nie buforuje wynikw poszukiwania metod. Jeeli wywoujemy metody under.append
i over.append w ramach ptli, to przy kadej iteracji musimy ponie koszt wyszukania wywoywanej metody. Jeeli chcemy, eby co byo przechowywane, to sami musimy to przechowa. Jeeli zastanawiamy si nad pewn optymalizacj, to zawsze powinnimy zmierzy
wydajno kodu bez tej optymalizacji i z ni. Tylko w ten sposb mona oceni, czy wprowadzona optymalizacja rzeczywicie wprowadza zauwaaln rnic. Zgodnie z moimi pomiarami
usunicie tej jednej optymalizacji powoduje mniej wicej 50% spadek wydajnoci w typowym
zadaniu wybierania piciotysicznego elementu z zakresu range(10000). Uwzgldniajc t
niewielk komplikacj, jak wprowadza stosowany zapis, z ca pewnoci jest on wart dwukrotnego przyspieszenia dziaania tej funkcji.
Do naturalnym pomysem na optymalizacj, z ktrego zrezygnowaem dopiero po wykonaniu dokadnych pomiarw, jest wywoanie w ciele ptli funkcji cmp(elem, pivot), ktre miaoby zastpi osobne porwnania elem < pivot i elem > pivot. Niestety, pomiary wykazay,
e funkcja cmp nie przyspiesza dziaania ptli, ale spowalnia j, przynajmniej w przypadkach,
gdy elementami sekwencji data s typy podstawowe, takie jak liczby lub cigi znakw.
Jak w takim razie wyglda wydajno funkcji select w porwnaniu ze znacznie prostsz funkcj przedstawion poniej?
def selsor(data, n):
data = list(data)
data.sort()
return data[n]
| 245
funkcja select wykonuje O(n) porwna, a funkcja selsor wykonuje ich O(n log n).
Zamy na przykad, e musimy porwna egzemplarze klasy, w ktrej operacje porwnania
s do kosztowne (symuluje ona punkt czterowymiarowy, w ktrym kilka pierwszych wymiarw moe si pokrywa):
class X(object):
def __init__(self):
self.a = self.b = self.c = 23.51
self.d = random.random()
def _dats(self):
return self.a, self.b, self.c, self.d
def __cmp__(self, oth):
return cmp(self._dats, oth._dats)
W takiej sytuacji funkcja select zaczyna dziaa szybciej od funkcji selsor ju w momencie
wyznaczania mediany z wektora skadajcego si z 201 takich egzemplarzy.
Innymi sowy, co prawda funkcja select wykonuje wicej oglnych operacji nadmiarowych
w porwnaniu z niezwykle efektywnym sposobem dziaania metody sort, to jednak w przypadku, gdy n jest odpowiednio due, a kade porwnanie jest wystarczajco kosztowne zastosowanie funkcji select jest warte rozwaenia.
Zobacz rwnie
Podrczniki Library Reference i Python in a Nutshell w czciach opisujcych metod sort,
typ list oraz modu random.
Problem
Musimy wykaza, e obsuga paradygmatu programowania funkcyjnego w Pythonie jest lepsza
ni mona si tego spodziewa na pierwszy rzut oka.
Rozwizanie
Jzyki programowania funkcyjnego, wrd ktrych prym wiedzie jzyk Haskell, to wyjtkowo
udane konstrukcje, ale nawet w takim towarzystwie Python prezentuje si zadziwiajco dobrze:
def qsort(L):
if len(L) <= 1: return L
return qsort([lt for lt in L[1:] if lt < L[0]]) + L[0:1] + \
qsort([ge for ge in L[1:] if ge >= L[0]])
Moim skromnym zdaniem podany kod jest niemal tak pikny jak wersja zapisana w jzyku
Haskell, pobrana ze strony http://www.haskell.org:
qsort [] = []
qsort (x:xs) = qsort elts_lt_x ++ [x] ++ qsort elts_greq_x
where
elts_lt_x = [y | y <- xs, y < x]
elts_greq_x = [y | y <- xs, y >= x]
246 |
Analiza
Ta raczej naiwna implementacja algorytmu quicksort doskonale ilustruje si list skadanych.
Takiego rozwizania nie naley jednak stosowa w kodzie produkcyjnym! W Pythonie listy
uzupeniane s o metod sort, ktra dziaa o wiele szybciej od prezentowanej w tej recepturze. W Pythonie 2.4 nowa wbudowana funkcja sorted dziaa z dowoln skoczon sekwencj i zwraca now, posortowan list elementw tej sekwencji. Jedynym waciwym zastosowaniem kodu z tej receptury jest pokazywanie go znajomym programistom, szczeglnie tym,
ktrzy (co zrozumiae) bardzo entuzjastycznie traktuj jzyki funkcyjne, a przede wszystkim
jzyk Haskell.
Podan funkcj przygotowaem po znalezieniu na stronie http://www.haskell.org/aboutHaskell.html
wspaniaej implementacji algorytmu quicksort w jzyku Haskell (podaem j rwnie w rozwizaniu tej receptury). Po okresie podziwiania elegancji znalezionego kodu uwiadomiem
sobie, e dokadnie takie samo rozwizanie moliwe jest w Pythonie przy zastosowaniu list
skadanych. Nie na darmo zostay one poyczone z jzyka Haskell i lekko spythonizowane,
tak eby moliwe byo wykorzystanie w nich sw kluczowych, a nie tylko operatorw.
Obie implementacje dziel list na jej pierwszym elemencie i dlatego ich wydajno w najgorszym przypadku, czyli w bardzo powszechnym przypadku sortowania posortowanej listy,
wynosi O(n). Z ca pewnoci nie chcielibymy stosowa takiego rozwizania w kodzie produkcyjnym! Omawiana procedura jest jednak czyst propagandwk, wic takie szczegy
nie maj znaczenia.
Mona te zapisa mniej kompaktow wersj o podobnej architekturze, stosujc w niej nazywane zmienne lokalne oraz funkcje poprawiajce czytelno kodu:
def qsort(L):
if not L: return L
pivot = L[0]
def lt(x): return x<pivot
def ge(x): return x>=pivot
return qsort(filter(lt, L[1:]))+[pivot]+qsort(filter(ge, L[1:]))
Skoro weszlimy ju na t ciek, to bardzo atwo moemy podan wersj przeksztaci w wersj nieco mniej naiwn, wykorzystujc losowe wybieranie elementu rozdzielajcego, przez co
zmniejsza si prawdopodobiestwo wystpienia najgorszego przypadku, oraz zliczajc elementy rozdzielajce, przez co poprawia si obsuga przypadkw z wieloma jednakowymi
elementami:
import random
def qsort(L):
if not L: return L
pivot = random.choice(L)
def lt(x): return x<pivot
def gt(x): return x>pivot
return qsort(filter(lt, L))+[pivot]*L.count(pivot)+qsort(filter(gt, L))
247
Mimo takich modyfikacji wersja ta rwnie przeznaczona jest gwnie do zabawy i celw demonstracyjnych. Porzdny kod sortujcy musi wyglda zupenie inaczej: te pereki, nad ktrymi si tak rozwodzimy, nigdy nie osign wydajnoci i skutecznoci rozwiza sortowania
wbudowanych w Pythona.
Zamiast prbowa poprawia czytelno kodu, moemy zacz dziaa w przeciwnym kierunku i prbowa tworzy przede wszystkim skuteczne rozwizanie, pokazujc przy okazji, e
w Pythonie sowo kluczowe lambda pozwala na uzyskanie bardzo zwartego, ale i dziwnego zapisu:
q=lambda x:(lambda o=lambda s:[i for i in x if cmp(i,x[0])==s]:
len(x)>1 and q(o(-1))+o(0)+q(o(1)) or x)()
W przypadku tego potworka (pojedynczy wiersz kodu, ktry z powodu swojej dugoci musi
zosta podzielony na dwa wiersze) wida wyranie, e takie rozwizania nie powinny by stosowane w rzeczywistych programach. Nawet bardziej czytelna, rwnowana wersja stosujca
instrukcje def zamiast instrukcji lambda bdzie nie do koca zrozumiaa:
def q(x):
def o(s): return [i for i in x if cmp(i,x[0])==s]
return len(x)>1 and q(o(-1))+o(0)+q(o(1)) or x
Nieco czytelniejszy kod mona utworzy, rozbijajc bardzo zbit instrukcj len(x)>1 and ...
or x na instrukcje if/else i wprowadzajc odpowiednie nazwy zmiennych:
def q(x):
if len(x)>1:
lt = [i for i in x if cmp(i,x[0]) == -1 ]
eq = [i for i in x if cmp(i,x[0]) == 0 ]
gt = [i for i in x if cmp(i,x[0]) == 1 ]
return q(lt) + eq + q(gt)
else:
return x
Na szczcie prawdziwi Pythonianie s zbyt wraliwi, eby znosi i tworzy takie potworki
wypenione instrukcjami lambda, jak te prezentowane w tej recepturze. W rzeczywistoci wielu
z nas (cho,oczywicie nie wszyscy) czuje awersj do tej instrukcji (czciowo z powodu moliwoci jej naduywania) i zdecydowanie woli stosowa czytelniejsze konstrukcje z instrukcjami
def. W efekcie umiejtno odczytywania takich zamieconych wierszy nie jest wymagana
w wiecie Pythona, jak to bywa w innych jzykach programowania. W podobny sposb programista prbujcy zapisa sprytny kod moe naduy dowolnej funkcji jzyka, dlatego cz
Pythonian (zdecydowana mniejszo) podobn awersj darzy inne funkcje jzyka, takie jak listy
skadane (poniewa w wyraeniu listy skadanej mona umieci zbyt wiele niepotrzebnych
elementw, podczas gdy prosta ptla for byaby zdecydowanie czytelniejsza) lub wykorzystywanie warunkowoci operatorw logicznych and i or (poniewa mona w ten sposb tworzy
nieczytelne wyraenia, ktre daj si zastpi znacznie czytelniejsz instrukcj if).
Zobacz rwnie
Stron jzyka Haskell http://www.haskell.org.
248 |
Problem
Musimy wykonywa czste testy na obecno danego elementu w sekwencji. Wydajno O(n)
czsto wywoywanego operatora in moe bardzo negatywnie wpyn na prac programu,
ale nie moemy po prostu zastpi sekwencji sownikiem lub zbiorem, poniewa wana jest te
kolejno jej elementw.
Rozwizanie
Zamy, e musimy dodawa do sekwencji elementy, ale tylko wtedy, gdy nie zostay one
dodane do niej wczeniej. Ponisza funkcja jest doskonaym rozwizaniem tego zadania:
def addUnique(baseList, otherList):
auxDict = dict.fromkeys(baseList)
for item in otherList:
if item not in auxDict:
baseList.append(item)
auxDict[item] = None
Jeeli nasz kod ma dziaa wycznie w Pythonie 2.4, to dokadnie takie same efekty uzyskamy
za pomoc dodatkowego zbioru, a nie sownika.
Analiza
Najprostsze (naiwne?) rozwizanie problemu z tej receptury wyglda cakiem dobrze:
def addUnique_simple(baseList, otherList):
for item in otherList:
if item not in baseList:
baseList.append(item)
| 249
sownika. To wanie ten krok stanowi gwn rnic midzy obiema funkcjami, poniewa
operator in zastosowany wobec sownika dziaa w czasie staym, niezalenie od liczby elementw w sowniku. Oznacza to, e ptla for dziaa bdzie w czasie proporcjonalnym do dugoci
listy otherList, a caa funkcja dziaa w czasie proporcjonalnym do sumy dugoci obu list.
Tak analiz czasw dziaania funkcji powinnimy przeprowadzi nieco gbiej, poniewa
w funkcji addUnique_simple dugo listy baseList nie jest staa. Lista baseList ronie za
kadym razem, gdy badany element nie jest czci tej listy. Wynik takiej (zaskakujco zoonej) analizy nie rniby si jednak wcale od wynikw analizy uproszczonej. Jeeli kada z list
skadaaby si z 10 liczb cakowitych, z ktrych tylko 50% byoby ze sob zgodnych, to prostsza
wersja funkcji byaby o mniej wicej 30% wolniejsza od funkcji prezentowanej w rozwizaniu
taki spadek wydajnoci mona z czystym sumieniem zignorowa. Niestety, ju w przypadku list zawierajcych sto elementw pokrywajcych si w 50%, prostsza wersja jest a dwanacie
razy wolniejsza od wersji oficjalnej. Takich wynikw na pewno nie mona zignorowa, tym
bardziej, e rnica ta powiksza si wraz ze wzrostem wielkoci list.
Czasami mona uzyska jeszcze lepsz wydajno caego programu, stosujc pomocniczy sownik rwnolegle z sam sekwencj i zamykajc je w ramach jednego obiektu. W takim jednak
przypadku musimy dba o aktualizowanie zawartoci sownika w czasie modyfikowania sekwencji, zapewniajc tym samym jego synchronizacj z sekwencj. Zadanie takiego synchronizowania sownika z ca pewnoci nie jest trywialne i moe by zrealizowane na wiele rnych sposobw. Oto jeden ze sposobw synchronizowania sownika w razie potrzeby, czyli
tylko w momencie testowania obecnoci elementu w sekwencji, gdy istnieje prawdopodobiestwo, e sownik nie jest zsynchronizowany z sekwencj. Koszt takiej operacji jest niewielki, dlatego podana niej klasa optymalizuje metod index, jak rwnie testy na obecno elementu:
class list_with_aux_dict(list):
def __init__(self, iterable=()):
list.__init__(self, iterable)
self._dict_ok = False
def _rebuild_dict(self):
self._dict = {}
for i, item in enumerate(self):
if item not in self._dict:
self._dict[item] = i
self._dict_ok = True
def __contains__(self, item):
if not self._dict_ok:
self._rebuild_dict()
return item in self._dict
def index(self, item):
if not self._dict_ok:
self._rebuild_dict()
try: return self._dict[item]
except KeyError: raise ValueError
def _wrapMutatorMethod(methname):
_method = getattr(list, methname)
def wrapper(self, *args):
# Kasowanie znacznika 'sownik OK' i delegowanie prawdziwej metody modyfikujcej
self._dict_ok = False
return _method(self, *args)
# tylko w Pythonie 2.4: wrapper.__name__ = _method.__name__
setattr(list_with_aux_dict, methname, wrapper)
for meth in 'setitem delitem setslice delslice iadd'.split():
_wrapMutatorMethod('__%s__' % meth)
for meth in 'append insert pop remove extend'.split():
_wrapMutatorMethod(meth)
del _wrapMutatorMethod
# usuwamy funkcj pomocnicz, nie bdzie ju nam potrzebna
250
Klasa list_with_aux_dict rozbudowuje klas list i tworzy delegacj wszystkich jej metod
za wyjtkiem metod __contains__ i index. Kada z metod, ktra moe zmodyfikowa zawarto listy, opakowywana jest domkniciem kasujcym znacznik zgodnoci sownika z list.
W Pythonie operator in wywouje w obiekcie metod __contains__. Metoda ta w klasie list_
with_aux_dict powoduje przebudowanie pomocniczego sownika, chyba e znacznik zgodnoci jest ustawiony (bo wtedy przebudowa sownika nie jest ju konieczna). W podobny sposb
dziaa te metoda index.
Zamiast budowania i instalowania domkni opakowujcych wszystkie metody modyfikujce zawarto listy za pomoc funkcji pomocniczej klasy list_with_aux_dict, tak jak zrobiono
to w podanym kodzie, mona te napisa osobne opakowanie dla kadej z metod, korzystajc
przy tym z instrukcji def. Mimo to kod zaprezentowanej klasy ma nad takim rozwizaniem
niezaprzeczaln przewag, jako e minimalizuje potrzeb stosowania powtarzajcego si,
nudnego kodu o znacznej objtoci, w ktrym bardzo czsto ukrywaj si bdy. Moliwoci,
jakie Python daje nam w zakresie introspekcji i dynamicznej modyfikacji, pozwalaj nam zadecydowa: moemy budowa opakowania metod tak jak zostao to zrobione w prezentowanej
klasie, czyli spjnie i sprytnie, ale jeeli nie opanowalimy jeszcze czarnej magii introspekcji
i dynamicznej modyfikacji obiektw, to rwnie dobrze moe zdecydowa si na tworzenie powtarzajcego si kodu.
Architektura klasy list_with_aux_dict jest doskonaym przykadem bardzo powszechnego
wzorca uycia, stosowanego w sytuacjach, gdy operacje modyfikujce sekwencje zdarzaj si
w paczkach. Pomidzy takimi paczkami modyfikacji nastpuj okresy, w ktrych sekwencja nie jest poddawana adnym modyfikacjom, ale wykonywane s testy na obecno elementw. Niestety, prezentowana wczeniej funkcja addUnique_simple nie zyskaaby na prdkoci,
jeeli w parametrze baseList zamiast zwykego obiektu list otrzymaaby egzemplarz klasy
list_with_aux_dict, poniewa funkcja ta przeplata ze sob testy na obecno elementu i modyfikacje zawartoci listy. W takich warunkach zbyt wiele operacji przebudowywania pomocniczego sownika bardzo le wpywaoby na prdko dziaania funkcji (chyba e w znakomitej
wikszoci przypadkw elementy listy otherList s ju czci listy baseList, a zatem modyfikacji listy bdzie o wiele mniej ni operacji sprawdzania obecnoci).
Bardzo wan czci wszystkich takich optymalizacji testw obecnoci jest wymg, eby elementy sekwencji byy unikalne (jeeli tak nie bdzie, to nie mog one by oczywicie kluczami
sownika, ani elementami zbioru). Podane w tej recepturze funkcje mogyby by na przykad
wykorzystane do obsugi listy krotek, ale zupenie nie nadawayby si do obsugi listy list.
Zobacz rwnie
Podrczniki Library Reference i Python in a Nutshell w czciach opisujcych typy sekwencji
i typy odwzorowa.
Problem
Musimy znale wystpienia pewnej podsekwencji w ramach wikszej sekwencji.
5.13. Wyszukiwanie podsekwencji
251
Rozwizanie
Jeeli sekwencjami s cigi znakw (proste lub Unikodu), to zdecydowanie najlepszym wyjciem jest metoda find oraz standardowy modu re. W przypadku innych sekwencji naley
posuy si algorytmem Knutha-Morrisa-Pratta (KMP):
def KnuthMorrisPratt(text, pattern):
''' Zwraca wszystkie pozycje pocztkw kopii podsekwencji 'pattern'
w ramach sekwencji 'text' -- kady z parametrw moe by dowolnym
elementem iterowalnym. Przy kadym zwrceniu elementu parametr 'text'
zostaje odczytany do samego koca znalezionej podsekwencji 'pattern'. '''
# musimy zapewni sobie moliwo indeksowania w parametrze pattern,
# a jednoczenie utworzy jego kopi na wypadek wprowadzenia do niego zmian
# w czasie, gdy funkcja jest wstrzymana przez instrukcj `yield'
pattern = list(pattern)
length = len(pattern)
# budujemy "tablic wartoci przesuni" i nazywamy j 'shifts'
shifts = [1] * (length + 1)
shift = 1
for pos, pat in enumerate(pattern):
while shift <= pos and pat != pattern[pos-shift]:
shift += shifts[pos-shift]
shifts[pos+1] = shift
# wykonanie waciwego wyszukiwania
startPos = 0
matchLen = 0
for c in text:
while matchLen == length or matchLen >= 0 and pattern[matchLen] != c:
startPos += shifts[matchLen]
matchLen -= shifts[matchLen]
matchLen += 1
if matchLen == length: yield startPos
Analiza
W niniejszej recepturze implementujemy algorytm Knutha-Morrisa-Pratta przeznaczony do wyszukiwania kopii danego wzorca w ramach cigej podsekwencji wikszego tekstu. Algorytm
KMP korzysta z tekstu w sposb sekwencyjny, dlatego bardzo naturalnym rozwizaniem jest
zezwolenie na stosowanie tekstu w postaci dowolnego elementu iterowalnego. Po zakoczeniu fazy przygotowa wstpnych, w ktrej budowana jest tabela wartoci przesuni i ktra
zajmuje czas proporcjonalny do dugoci szukanego wzorca, kady z symboli przetwarzany
jest w czasie staym. Wyjanienia dotyczce pracy algorytmu KMP mona znale w dowolnej
dobrej ksice opisujcej algorytmy (rekomendacje podajemy w punkcie Zobacz rwnie).
Jeeli parametry text i pattern s cigami znakw, to mona zastosowa zdecydowanie szybsze rozwizanie, wykorzystujc przy tym metody wbudowane Pythona:
def finditer(text, pattern):
pos = -1
while True:
pos = text.find(pattern, pos+1)
if pos < 0: break
yield pos
zadanie realizuje w cigu 540 milisekund (te wyniki dotycz Pythona 2.3, w Pythonie 2.4 algorytm KMP dziaa nieco szybciej, a wspomniane zadanie realizowane jest w cigu 480 milisekund, ale mimo to jest to wynik ponad stukrotnie gorszy od wyniku funkcji finditer).
Naley zatem pamita: kod podany w tej recepturze nadaje si do przeszukiwania dowolnych
sekwencji, wcznie z tymi, ktrych nie da si w caoci przechowywa w pamici, ale przy przeszukiwaniu cigw znakw zdecydowanie lepiej sprawdzaj si metody wbudowane Pythona.
Zobacz rwnie
Podstawy algorytmw, ktre s opisywane w wielu doskonaych ksikach. Wrd nich jedn
z najbardziej polecanych jest pozycja Thomasa H. Cormena, Charlesa E. Leisersona, Ronalda
L Rivesta i Clifforda Steina Introduction to Algorithms, wydanie drugie (MIT Press).
Problem
Chcemy uy sownika do przechowywania odwzorowa kluczy i aktualnych wartoci ocen
tych kluczy. Niejednokrotnie musimy uzyska dostp do kluczy i ocen w kolejnoci naturalnej
(co oznacza malejce wartoci ocen) albo sprawdza aktualn pozycj danego klucza w takim
rankingu. To wszystko sugeruje, e samo zastosowanie sownika nie jest wystarczajce.
Rozwizanie
Moemy utworzy klas wywiedzion z klasy dict i doda do niej lub pokry potrzebne nam
metody. W ramach dziedziczenia wielobazowego moemy jako pierwsz klas bazow oznaczy klas UserDict.DictMixin i dopiero za ni dopisa klas dict, a w nowej klasie mona
ostronie przygotowa rne delegacje i pokrywania metod. W ten sposb uzyskamy rwnowag pomidzy niez wydajnoci klasy a koniecznoci tworzenia powtarzalnego kodu.
Wzbogacajc nasz klas o wiele przykadw zapisanych w jej dokumentacji, moemy skorzysta te z moduu biblioteki standardowej doctest, przez co klasa zostanie uzupeniona o funkcje testw moduowych, a my uzyskamy pewno, e przykady podane w dokumentacji bd
zgodne z prawd:
#!/usr/bin/env python
''' Wzbogacony sownik przechowujcy klucze powizane z ich ocenami '''
from bisect import bisect_left, insort_left
import UserDict
class Ratings(UserDict.DictMixin, dict):
""" klasa Ratings jest bardzo zbliona do sownika uzupenionego
o kilka dodatkowych funkcji: Warto powizana z kadym kluczem
jest traktowana jak jego 'ocena', a wszystkie klucze ukadane s wedug
tych ocen. Wartoci musz by porwnywalne, natomiast klucze, oprcz
tego, e musz by unikalne, musz te by porwnywalne na wypadek,
253
254 |
255
Analiza
Pod wieloma wzgldami sownik jest najbardziej naturaln struktur danych w zakresie przechowywania zwizkw midzy kluczami (na przykad nazwiskami uczestnikw pewnego konkursu) i ich aktualnymi ocenami (na przykad liczby punktw, jakie poszczeglni uczestnicy
zdobyli do tej pory albo najwysze stawki zaproponowane przez poszczeglnych uczestnikw
aukcji). Jeeli ze sownika skorzystamy w takim wanie celu, to najprawdopodobniej bdziemy
chcieli te odczytywa jego elementy w kolejnoci naturalnej, czyli w kolejnoci rosncych wartoci ocen, a poza tym bdziemy chcieli szybko uzyska aktualn pozycj klucza (w takim rankingu) wynikajc z jego aktualnej oceny (na przykad pobra dane uczestnika znajdujcego
si na trzecim miejscu lub ocen uczestnika z drugiego miejsca).
W tej recepturze osigamy takie moliwoci dziki klasie wywiedzionej z klasy dict i uzupenionej o odpowiednie funkcje, ktrych brakuje w klasie dict (metody rating, getValueByRating, getKeyByRating). Znacznie waniejsze s jednak subtelne modyfikacje wprowadzane
do metody keys i do innych podobnych metod, dziki ktrym metody te zwracaj listy lub
iteratory o wymaganym porzdku (na przykad kolejno rosncych ocen, a jeeli dwa elementy maj identyczne oceny, to kolejno okrelana jest przez bezporednie porwnanie dwch
kluczy). Wikszo najwaniejszych informacji o klasie zapisanych zostao w jej dokumentacji
umieszczonej w kodzie. Jest to niezwykle istotne, poniewa zapisanie dokumentacji klasy
i przykadw jej uycia wewntrz jej kodu pozwala nam wykorzysta modu doctest ze
standardowej biblioteki Pythona w celu wprowadzenia funkcji testw moduowych i jednoczesnego zapewnienia poprawnoci podanych przykadw.
Najbardziej interesujcym aspektem podanej implementacji jest to, e bardzo zmniejszono w niej
ilo powtarzalnego i nudnego, a co za tym idzie podatnego na bdy kodu, bez jednoczesnego
znaczcego zredukowania wydajnoci. Klasa Ratings dziedziczy wielobazowo po klasach dict
i DictMixin, przy czym ta druga umieszczana jest jako pierwsza na licie klas bazowych, przez
co wszystkie niepokryte jawnie metody klasy Ratings pochodz wanie z klasy DictMixin,
o ile ona je udostpnia.
Klasa DictMixin przygotowana przez Raymonda Hettingera zostaa pierwotnie opublikowana
jako receptura w sieciowej wersji ksiki Python Receptury, a pniej staa si czci biblioteki standardowej Pythona 2.3. Klasa DictMixin udostpnia wszystkie metody sownikowe
z wyjtkiem metod __init__, copy i czterech metod podstawowych: __getitem__, __setitem__,
__delitem__ i keys. W czasie tworzenia klasy sownikowej, ktra ma udostpnia wszystkie
metody w peni funkcjonalnego sownika, powinnimy przygotowa klas wywiedzion z klasy DictMixin i doda do niej przynajmniej wymienione wczeniej metody podstawowe (zalenie od semantyki klasy na przykad jeeli klasa ma mie niezmienne egzemplarze, to nie
ma potrzeby udostpniania metody modyfikujcych __setitem__ i __delitem__). Mona te
zaimplementowa inne metody, pokrywajc implementacje udostpniane przez klas DictMixin, poprawiajc w ten sposb ich wydajno. Architektur klasy DictMixin mona uzna
za doskonay przykad klasycznego wzorca projektowego Szablonu Metody (ang. Template
Method) zastosowanego pasywnie w bardzo przydatnym wariancie.
W klasie prezentowanej w tej recepturze po drugiej klasie bazowej (czyli po wbudowanym
typie dict) dziedziczymy metod __getitem__, a wszystkie inne metody delegujemy jawnie
do klasy dict (te, ktre mona wydelegowa). Samodzielnie musimy zapisa podstawowe metody modyfikujce (__setitem__ i __delitem__), poniewa oprcz wydelegowania ich do
bazowej klasy dict musimy dodatkowo zaktualizowa w nich pomocnicz struktur self.
256
_rating list par (ocena, klucz) utrzymywan w stanie posortowania za pomoc moduu
bisect z biblioteki standardowej. Metod keys implementujemy samodzielnie (a skoro ju
o tym wspominamy: implementujemy te metody __iter__ i iterkeys, poniewa najprostszym sposobem na implementowanie metody keys jest wykorzystanie metody __iter__), tak
aby wykorzysta w niej struktur self._rating i zwraca klucze sownika w potrzebnej nam
kolejnoci. W kocu, oprcz trzech standardowych metod obsugi ocen, dodajemy te oczywiste implementacje metod __init__ i copy.
Wynik okazuje si by ciekawym przykadem kodu o dobrze wywaonej spjnoci i czytelnoci, w ktrym bardzo szeroko wykorzystywane s funkcje udostpniane przez standardow
bibliotek Pythona. Jeeli z tego moduu skorzystamy w naszych aplikacjach, to dokadniejsze
badania mog wykaza, e niektre z metod prezentowanej klasy nie maj zadowalajcej wydajnoci. Wynika to z faktu, e natura klasy DictMixin wymusza stosowanie w niej bardzo
oglnych implementacji. W takiej sytuacji naley bezwzgldnie uzupeni klas wasnymi
implementacjami tych wszystkich metod, ktre s wymagane do osignicia lepszej wydajnoci. Na przykad, jeeli nasza aplikacja czsto w ptlach przeglda wyniki wywoania
r.iteritems(), gdzie r jest egzemplarzem klasy Ratings, to nieco lepsz wydajno osigniemy, dodajc do ciaa klasy bezporedni implementacj tej metody:
def iteritems(self):
for v, k in self._rating:
yield k, v
Zobacz rwnie
Podrczniki Library Reference i Python in a Nutshell w czciach opisujcych klas DictMixin
z moduu UserDict oraz modu bisect.
Problem
Chcemy przygotowa spis osb, w ktrym poszczeglne osoby zapisane byyby w porzdku
alfabetycznym i pogrupowane wedug ich inicjaw utworzonych na podstawie nazwisk.
Rozwizanie
W Pythonie 2.4 nowa funkcja itertools.groupby bardzo uatwia realizacj tego zadania:
import itertools
def groupnames(name_iterable):
sorted_names = sorted(name_iterable, key=_sortkeyfunc)
name_dict = {}
for key, group in itertools.groupby(sorted_names, _groupkeyfunc):
name_dict[key] = tuple(group)
return name_dict
pieces_order = { 2: (-1, 0), 3: (-1, 0, 1) }
def _sortkeyfunc(name):
257
''' name jest cigiem znakw zawierajcym imi i nazwisko oraz opcjonalne
drugie imi lub inicja rozdzielane spacjami. Zwraca cig znakw w kolejnoci
nazwisko-imi-drugie_imi, ktra wymagana jest w zwizku z sortowaniem. '''
name_parts = name.split()
return ' '.join([name_parts[n] for n in pieces_order[len(name_parts)]])
def _groupkeyfunc(name):
''' zwraca klucz grupowania, na przykad inicja nazwiska '''
return name.split()[-1][0]
Analiza
W niniejszej recepturze parametr name_iterable musi by elementem iterowalnym, ktrego
elementy s cigami znakw zawierajcymi dane osb zapisane w formie: pierwsze_imi
drugie_imi nazwisko. W wyniku wywoania funkcji groupnames na rzecz takiego
elementu iterowalnego otrzymamy sownik, ktrego klucze s inicjaami nazwisk, a powizane z nimi wartoci s krotkami zawierajcymi wszystkie nazwiska, na podstawie ktrych
mona utworzy dany inicja.
Pomocnicza funkcja _sortkeyfunc dzieli dane osoby zapisane w ramach jednego cigu znakw zawierajcego albo imi nazwisko albo pierwsze_imi drugie_imi nazwisko i tworzy
na ich podstawie list, zawierajc najpierw nazwisko, a za nim imi, ewentualne drugie imi
i na kocu inicja. Nastpnie funkcja czy te dane w cig znakw i zwraca go funkcji wywoujcej. Zgodnie z problemem opisywanym w tej recepturze, wynikowy cig znakw jest kluczem,
z jakiego chcemy skorzysta w ramach sortowania danych. Wbudowana w Pythona 2.4 funkcja sorted przyjmuje omawian funkcje w swoim opcjonalnym parametrze key (wywouje j
na rzecz kadego elementu w celu uzyskania klucza sortowania).
Pomocnicza funkcja _groupkeyfunc pobiera dane osoby w takim samym formacie, z jakim
pracuje funkcja _sortkeyfunc, i zwraca inicja z nazwiska bdcy kluczem grupowania, tak
jak zostao to zapisane w opisie problemu.
W ramach rozwizywania tego problemu gwna funkcja z tej receptury groupnames wykorzystuje dwie opisane wczeniej funkcje pomocnicze, funkcj sorted z Pythona 2.4 oraz funkcj itertools.groupby. Z ich pomoc tworzy ona i zwraca opisywany wczeniej sownik.
Jeeli prezentowany kod ma by stosowany rwnie w Pythonie 2.3, to trzeba nieco przebudowa sam funkcj groupnames, przy czym obie funkcje pomocnicze mog pozosta bez zmian.
Ze wzgldu na to, e w bibliotece standardowej Pythona 2.3 nie ma funkcji groupby, wygodnej
jest w nim najpierw wykona grupowanie elementw i dopiero potem posortowa dane w ramach poszczeglnych grup.
def groupnames(name_iterable):
name_dict = {}
for name in name_iterable:
key = _groupkeyfunc(name)
name_dict.setdefault(key, []).append(name)
for k, v in name_dict.iteritems():
aux = [(_sortkeyfunc(name), name) for name in v]
aux.sort()
name_dict[k] = tuple([ n for __, n in aux ])
return name_dict
Zobacz rwnie
Receptur 19.21. Podrcznik Library Reference z Pythona 2.4 w czci opisujcej modu itertools.
258 |