Tytuł oryginału: Effective Python: 59 Specific Ways to Write Better Python (Effective Software
Development Series)
Tłumaczenie: Robert Górczyński
ISBN: 978-83-283-1540-2
Authorized translation from the English language edition, entitled: EFFECTIVE PYTHON: 59 SPECIFIC
WAYS TO WRITE BETTER PYTHON; ISBN 0134034287; by Brett Slatkin; published by Pearson
Education, Inc, publishing as Addison Wesley Professional.
Copyright © 2015 by Pearson Education, Inc.
All rights reserved. No part of this book may by reproduced or transmitted in any form or by any means,
electronic or mechanical, including photocopying, recording or by any information storage retrieval system,
without permission from Pearson Education, Inc.
Polish language edition published by HELION S.A. Copyright © 2015.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej
publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną,
fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje
naruszenie praw autorskich niniejszej publikacji.
Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich
właścicieli.
Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były
kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane
z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie
ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji
zawartych w książce.
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail:
helion@helion.pl
WWW:
http://helion.pl (księgarnia internetowa, katalog książek)
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/efepyt
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Printed in Poland.
Spis treĤci
Wprowadzenie ................................................................................ 11
Podzičkowania ............................................................................... 15
O autorze ....................................................................................... 17
Rozdziaã 1. Programowanie zgodne z duchem Pythona ................... 19
Sposób 1. Ustalenie uİywanej wersji Pythona ........................................... 19
Sposób 2. Stosuj styl PEP 8 ...................................................................... 21
Sposób 3. Róİnice mičdzy typami bytes, str i unicode ............................... 23
Sposób 4. Decyduj sič na funkcje pomocnicze
zamiast na skomplikowane wyraİenia ....................................... 26
Sposób 5. Umiejčtnie podziel sekwencje .................................................... 29
Sposób 6. Unikaj uİycia indeksów poczĈtek, koniec
i wartoĤci kroku w pojedynczej operacji podziaãu ...................... 31
Sposób 7. Uİywaj list skãadanych zamiast funkcji map() i filter() ................... 33
Sposób 8. Unikaj wičcej niİ dwóch wyraİeę na liĤcie skãadanej .................... 35
Sposób 9. Rozwaİ uİycie generatora wyraİeę dla duİych list skãadanych ....36
Sposób 10. Preferuj uİycie funkcji enumerate() zamiast range() ...................... 38
Sposób 11. Uİycie funkcji zip() do równoczesnego przetwarzania iteratorów ... 39
Sposób 12. Unikaj bloków else po pčtlach for i while ................................... 41
Sposób 13. Wykorzystanie zalet wszystkich bloków
w konstrukcji try-except-else-finally ......................................... 44
Rozdziaã 2. Funkcje ....................................................................... 47
Sposób 14. Preferuj wyjĈtki zamiast zwrotu wartoĤci None .......................... 47
Sposób 15. Zobacz, jak domkničcia wspóãdziaãajĈ z zakresem zmiennej ...... 49
Sposób 16. Rozwaİ uİycie generatorów, zamiast zwracaþ listy ...................... 54
8
Spis treĤci
Sposób 17. Podczas iteracji przez argumenty
zachowuj postawč defensywnĈ .................................................. 56
Sposób 18. Zmniejszenie wizualnego zagmatwania
za pomocĈ zmiennej liczby argumentów pozycyjnych ................ 61
Sposób 19. Zdefiniowanie zachowania opcjonalnego
za pomocĈ argumentów w postaci sãów kluczowych .................. 63
Sposób 20. Uİycie None i docstring w celu
dynamicznego okreĤlenia argumentów domyĤlnych ................... 66
Sposób 21. Wymuszaj czytelnoĤþ kodu,
stosujĈc jedynie argumenty w postaci sãów kluczowych ............ 69
Rozdziaã 3. Klasy i dziedziczenie .................................................... 73
Sposób 22. Preferuj klasy pomocnicze zamiast sãowników i krotek .............. 73
Sposób 23. Dla prostych interfejsów akceptuj funkcje zamiast klas ............ 78
Sposób 24. Uİycie polimorfizmu @classmethod
w celu ogólnego tworzenia obiektów .......................................... 82
Sposób 25. Inicjalizacja klasy nadrzčdnej za pomocĈ wywoãania super() ...... 87
Sposób 26. Wielokrotnego dziedziczenia
uİywaj jedynie w klasach narzčdziowych .................................. 91
Sposób 27. Preferuj atrybuty publiczne zamiast prywatnych ....................... 95
Sposób 28. Dziedziczenie po collections.abc
w kontenerach typów niestandardowych ................................... 99
Rozdziaã 4. Metaklasy i atrybuty ................................................... 105
Sposób 29. Uİywaj zwykãych atrybutów zamiast metod typu getter i setter ...105
Sposób 30. Rozwaİ uİycie @property zamiast refaktoryzacji atrybutów ..... 109
Sposób 31. Stosuj deskryptory, aby wielokrotnie wykorzystywaþ
metody udekorowane przez @property .................................... 113
Sposób 32. Uİywaj metod __getattr__(), __getattribute__() i __setattr__()
dla opóĮnionych atrybutów ..................................................... 117
Sposób 33. Sprawdzaj podklasy za pomocĈ metaklas ................................ 122
Sposób 34. Rejestruj istniejĈce klasy wraz z metaklasami ......................... 124
Sposób 35. Adnotacje atrybutów klas dodawaj za pomocĈ metaklas .......... 128
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ ..................................... 131
Sposób 36. Uİywaj moduãu subprocess
do zarzĈdzania procesami potomnymi ..................................... 132
Sposób 37. Uİycie wĈtków dla operacji blokujĈcych wejĤcie-wyjĤcie,
unikanie równolegãoĤci ........................................................... 136
Sposób 38. Uİywaj klasy Lock, aby unikaþ stanu wyĤcigu w wĈtkach ....... 140
Sposób 39. Uİywaj klasy Queue do koordynacji pracy mičdzy wĈtkami ..... 143
Spis treĤci
9
Sposób 40. Rozwaİ uİycie wspóãprogramów
w celu jednoczesnego wykonywania wielu funkcji ................... 150
Sposób 41. Rozwaİ uİycie concurrent.futures(),
aby otrzymaþ prawdziwĈ równolegãoĤþ .................................... 158
Rozdziaã 6. Wbudowane moduãy ....................................................163
Sposób 42. Dekoratory funkcji definiuj za pomocĈ functools.wraps ........... 163
Sposób 43. Rozwaİ uİycie poleceę contextlib i with
w celu uzyskania wielokrotnego uİycia konstrukcji try-finally .... 166
Sposób 44. Niezawodne uİycie pickle wraz z copyreg ................................ 169
Sposób 45. Podczas obsãugi czasu lokalnego uİywaj moduãu datetime
zamiast time ........................................................................... 174
Sposób 46. Uİywaj wbudowanych algorytmów i struktur danych .............. 178
Sposób 47. Gdy waİna jest precyzja, uİywaj moduãu decimal ................... 183
Sposób 48. Kiedy szukaþ moduãów opracowanych przez spoãecznoĤþ? ....... 185
Rozdziaã 7. Wspóãpraca .................................................................187
Sposób 49. Dla kaİdej funkcji, klasy i moduãu utwórz docstring ............... 187
Sposób 50. Uİywaj pakietów do organizacji moduãów
i dostarczania stabilnych API .................................................. 191
Sposób 51. Zdefiniuj gãówny wyjĈtek Exception
w celu odizolowania komponentu wywoãujĈcego od API ........... 196
Sposób 52. Zobacz, jak przerwaþ krĈg zaleİnoĤci ...................................... 199
Sposób 53. Uİywaj Ĥrodowisk wirtualnych
dla odizolowanych i powtarzalnych zaleİnoĤci ......................... 204
Rozdziaã 8. Produkcja ...................................................................211
Sposób 54. Rozwaİ uİycie kodu o zasičgu moduãu
w celu konfiguracji Ĥrodowiska wdroİenia ............................... 211
Sposób 55. Uİywaj ciĈgów tekstowych repr
do debugowania danych wyjĤciowych ..................................... 214
Sposób 56. Testuj wszystko za pomocĈ unittest ........................................ 217
Sposób 57. Rozwaİ interaktywne usuwanie bãčdów za pomocĈ pdb ........... 220
Sposób 58. Przed optymalizacjĈ przeprowadzaj profilowanie ...................... 222
Sposób 59. Stosuj moduã tracemalloc, aby poznaþ sposób uİycia pamičci
i wykryþ jej wycieki ................................................................. 226
Skorowidz ........................................................................................ 229
WspóãbieİnoĤþ
i równolegãoĤþ
WspóãbieİnoĤþ wystčpuje wtedy, gdy komputer pozornie wykonuje jednocze-
Ĥnie wiele róİnych zadaę. Na przykãad w komputerze wyposaİonym w pro-
cesor o tylko jednym rdzeniu system operacyjny bčdzie bardzo szybko
zmieniaã aktualnie wykonywany program na inny. Tym samym programy
sĈ wykonywane na przemian, co tworzy iluzjč ich jednoczesnego dziaãania.
Z kolei równolegãoĤþ to faktyczne wykonywanie jednoczeĤnie wielu róİnych
zadaę. Jeİeli komputer jest wyposaİony w wielordzeniowy procesor, to po-
szczególne rdzenie mogĈ jednoczeĤnie wykonywaþ róİne zadania. Poniewaİ
poszczególne rdzenie procesora wykonujĈ polecenia innego programu, wičc
poszczególne aplikacje dziaãajĈ jednoczeĤnie i w tym samym czasie kaİda
z nich odnotowuje postčp w dziaãaniu.
W ramach jednego programu wspóãbieİnoĤþ to narzčdzie uãatwiajĈce pro-
gramistom rozwiĈzywanie pewnego rodzaju problemów. Programy wspóãbieİ-
ne pozwalajĈ na zastosowanie wielu róİnych Ĥcieİek dziaãania, aby uİyt-
kownik miaã wraİenie, İe poszczególne operacje w programie odbywajĈ sič
jednoczeĤnie i niezaleİnie.
Kluczowa róİnica mičdzy wspóãbieİnoĤciĈ i równolegãoĤciĈ to szybkoĤþ. Kiedy
w programie sĈ stosowane dwie oddzielne Ĥcieİki jego wykonywania, to czas
potrzebny na wykonanie caãego zadania programu zmniejsza sič o poãowč.
Wspóãczynnik szybkoĤci wykonywania wynosi wičc dwa. Z kolei wspóãbieİnie
dziaãajĈce programy mogĈ wykonywaþ tysiĈce oddzielnych Ĥcieİek dziaãania,
ale to nie przeãoİy sič w ogóle na zmniejszenie iloĤci czasu, jaki jest potrzebny
na wykonanie caãej pracy.
Python uãatwia tworzenie programów wspóãbieİnych. Ponadto jest uİywany
do równolegãego wykonywania zadaę za pomocĈ wywoãaę systemowych,
podprocesów oraz rozszerzeę utworzonych w jčzyku C. Jednak osiĈgničcie
132
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
stanu, w którym wspóãbieİny kod Pythona bčdzie faktycznie wykonywany
równolegle, moİe byþ bardzo trudne. Dlatego teİ niezwykle waİne jest po-
znanie najlepszych sposobów wykorzystania Pythona w tych nieco odmien-
nych sytuacjach.
Sposób 36. Uİywaj moduãu subprocess
do zarzĈdzania procesami potomnymi
Python oferuje zaprawione w bojach biblioteki przeznaczone do wykonywa-
nia procesów potomnych i zarzĈdzania nimi. Tym samym Python staje sič
doskonaãym jčzykiem do ãĈczenia ze sobĈ innych narzčdzi, na przykãad
dziaãajĈcych w powãoce. Kiedy istniejĈce skrypty powãoki z czasem stajĈ sič
skomplikowane, jak to czčsto sič zdarza, wówczas przepisanie ich w Pytho-
nie jest naturalnym wyborem w celu zachowania czytelnoĤci kodu i moİli-
woĤci jego dalszej obsãugi.
Procesy potomne uruchamiane przez Pythona mogĈ dziaãaþ równolegle,
a tym samym Python moİe wykorzystaþ wszystkie rdzenie komputera i zmak-
symalizowaþ przepustowoĤþ aplikacji. Wprawdzie sam Python moİe byþ ogra-
niczany przez procesor (patrz sposób 37.), ale bardzo ãatwo wykorzystaþ
ten jčzyk do koordynowania zadaę obciĈİajĈcych procesor.
Na przestrzeni lat Python oferowaã wiele sposobów uruchamiania podpro-
cesów, mičdzy innymi za pomocĈ wywoãaę
popen
,
popen2
i
os.exec*
. Obecnie
najlepszym i najprostszym rozwiĈzaniem w zakresie zarzĈdzania procesami
potomnymi jest uİycie wbudowanego moduãu
subprocess
.
Uruchomienie podprocesu za pomocĈ moduãu
subprocess
jest proste. W po-
niİszym fragmencie kodu konstruktor klasy
Popen
uruchamia proces. Z kolei
metoda
communicate()
odczytuje dane wyjĤciowe procesu potomnego i czeka
na jego zakoęczenie.
proc = subprocess.Popen(
['echo', 'Witaj z procesu potomnego!'],
stdout=subprocess.PIPE)
out, err = proc.communicate()
print(out.decode('utf-8'))
>>>
Witaj z procesu potomnego!
Procesy potomne bčdĈ dziaãaãy niezaleİnie od ich procesu nadrzčdnego,
czyli interpretera Pythona. Ich stan moİna okresowo sprawdzaþ, gdy Python
wykonuje inne zadania.
proc = subprocess.Popen(['sleep', '0.3'])
while proc.poll() is None:
print('Pracujú...')
# Miejsce na zadania, których wykonanie wymaga dużo czasu.
Sposób 36. Uİywaj moduãu subprocess do zarzĈdzania procesami potomnymi
133
# ...
print('Kod wyjħcia', proc.poll())
>>>
Pracujú...
Pracujú...
Kod wyjħcia 0
Oddzielenie procesów potomnego i nadrzčdnego oznacza, İe proces nadrzčdny
moİe równoczeĤnie uruchomiþ dowolnĈ liczbč procesów potomnych. Moİna
to zrobiþ, uruchamiajĈc jednoczeĤnie wszystkie procesy potomne.
def run_sleep(period):
proc = subprocess.Popen(['sleep', str(period)])
return proc
start = time()
procs = []
for _ in range(10):
proc = run_sleep(0.1)
procs.append(proc)
Nastčpnie moİna czekaþ na zakoęczenie przez nie operacji wejĤcia-wyjĤcia
i zakoęczyþ ich dziaãanie za pomocĈ metody
communicate()
.
for proc in procs:
proc.communicate()
end = time()
print('Zakoēczono w ciægu %.3f sekund' % (end - start))
>>>
Zakoēczono w ciægu 0.117 sekund
Wskazówka
Jeżeli wymienione procesy działają w sekwencji, to całkowite opóźnienie wynosi sekundę, a nie
tylko mniej więcej 0,1 sekundy, jak to zostało zmierzone w omawianym programie.
Istnieje równieİ moİliwoĤþ potokowania danych z programu Pythona do
podprocesów oraz pobierania ich danych wyjĤciowych. Tym samym moİna
wykorzystaþ inne programy do równoczesnego dziaãania. Na przykãad przyj-
mujemy zaãoİenie, İe narzčdzie powãoki
openssl
jest uİywane do szyfrowa-
nia pewnych danych. Uruchomienie procesu potomnego wraz z argumen-
tami pochodzĈcymi z powãoki oraz potokowanie wejĤcia-wyjĤcia jest ãatwe.
def run_openssl(data):
env = os.environ.copy()
env['password'] = b'\xe24U\n\xd0Ql3S\x11'
proc = subprocess.Popen(
['openssl', 'enc', '-des3', '-pass', 'env:password'],
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
proc.stdin.write(data)
proc.stdin.flush() # Gwarantujemy, że proces potomny otrzyma dane wejściowe.
return proc
134
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
W przedstawionym fragmencie kodu potokujemy losowo wygenerowane bajty
do funkcji szyfrujĈcej. W praktyce bčdĈ to dane wejĤciowe podane przez uİyt-
kownika, uchwyt do pliku, gniazdo sieciowe itd.
procs = []
for _ in range(3):
data = os.urandom(10)
proc = run_openssl(data)
procs.append(proc)
Procesy potomne bčdĈ dziaãaãy równolegle z nadrzčdnym, a takİe bčdĈ ko-
rzystaãy z danych wejĤciowych procesów nadrzčdnych. W poniİszym kodzie
czekamy na zakoęczenie dziaãania procesów potomnych, a nastčpnie po-
bieramy wygenerowane przez nie ostateczne dane wyjĤciowe.
for proc in procs:
out, err = proc.communicate()
print(out[-10:])
>>>
b'o4,G\x91\x95\xfe\xa0\xaa\xb7'
b'\x0b\x01\\\xb1\xb7\xfb\xb2C\xe1b'
b'ds\xc5\xf4;j\x1f\xd0c-'
Moİna teİ tworzyþ ãaęcuchy równoczeĤnie dziaãajĈcych procesów, podobnie
jak potoków w systemie UNIX, uİywajĈc danych wyjĤciowych jednego pro-
cesu potomnego jako danych wejĤciowych innego procesu potomnego itd.
Poniİej przedstawiãem funkcjč uruchamiajĈcĈ proces potomny, który z kolei
spowoduje, İe polecenie powãoki
md5
pobierze strumieę danych wejĤciowych:
def run_md5(input_stdin):
proc = subprocess.Popen(
['md5'],
stdin=input_stdin,
stdout=subprocess.PIPE)
return proc
Wskazówka
Wbudowany moduł Pythona o nazwie
hashlib oferuje funkcję md5(), a więc uruchomienie te-
go rodzaju procesu potomnego nie zawsze jest konieczne. Moim celem jest tutaj pokazanie, jak
podprocesy mogą potokować dane wejściowe i wyjściowe.
Teraz wykorzystujemy zbiór procesów
openssl
do szyfrowania pewnych da-
nych, a kolejny zbiór procesów do utworzenia wartoĤci hash na podstawie
zaszyfrowanych danych.
input_procs = []
hash_procs = []
for _ in range(3):
data = os.urandom(10)
proc = run_openssl(data)
input_procs.append(proc)
hash_proc = run_md5(proc.stdout)
hash_procs.append(hash_proc)
Sposób 36. Uİywaj moduãu subprocess do zarzĈdzania procesami potomnymi
135
Operacje wejĤcia-wyjĤcia mičdzy procesami potomnymi bčdĈ zachodziãy au-
tomatycznie po uruchomieniu procesów. Twoim zadaniem jest jedynie za-
czekaþ na zakoęczenie dziaãania procesów potomnych i wyĤwietliþ ostateczne
wyniki ich dziaãania.
for proc in input_procs:
proc.communicate()
for proc in hash_procs:
out, err = proc.communicate()
print(out.strip())
>>>
b'7a1822875dcf9650a5a71e5e41e77bf3'
b'd41d8cd98f00b204e9800998ecf8427e'
b'1720f581cfdc448b6273048d42621100'
Jeİeli masz obawy, İe procesy potomne nigdy sič nie zakoęczĈ lub coĤ bč-
dzie blokowaão potoki danych wejĤciowych bĈdĮ wyjĤciowych, to upewnij sič,
czy metodzie
communicate()
zostaã przekazany parametr
timeout
. Przekazanie
tego parametru sprawi, İe nastĈpi zgãoszenie wyjĈtku, jeĤli proces potomny
nie udzieli odpowiedzi w podanym czasie. Tym samym zyskasz moİliwoĤþ za-
koęczenia dziaãania nieprawidãowo zachowujĈcego sič procesu potomnego.
proc = run_sleep(10)
try:
proc.communicate(timeout=0.1)
except subprocess.TimeoutExpired:
proc.terminate()
proc.wait()
print('Kod wyjħcia', proc.poll())
>>>
Kod wyjħcia -15
Niestety, parametr
timeout
jest dostčpny jedynie w Pythonie 3.3 oraz no-
wych wydaniach. We wczeĤniejszych wersjach Pythona konieczne jest uİy-
cie wbudowanego moduãu
select
w
proc.stdin
,
proc.stdout
i
proc.stderr
w celu
wymuszenia stosowania limitu czasu w trakcie operacji wejĤcia-wyjĤcia.
Do zapamičtania
Uİywaj moduãu
subprocess
do uruchamiania procesów potomnych oraz
zarzĈdzania ich strumieniami danych wejĤciowych i wyjĤciowych.
Procesy potomne dziaãajĈ równolegle wraz z interpreterem Pythona, co
pozwala na maksymalne wykorzystanie dostčpnego procesora.
Uİywaj parametru
timeout
w metodzie
communicate()
, aby unikaþ zakleszczeę
i zawieszenia procesów potomnych.
136
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
Sposób 37. Uİycie wĈtków dla operacji blokujĈcych
wejĤcie-wyjĤcie, unikanie równolegãoĤci
Sposób 37. Uİycie wĈtków dla operacji blokujĈcych wejĤcie-wyjĤcie
Standardowa implementacja Pythona nosi nazwč CPython. Implementacja
ta uruchamia program Pythona w dwóch krokach. Pierwszy to przetworze-
nie i kompilacja kodu Įródãowego na kod bajtowy. Drugi to uruchomienie
kodu bajtowego za pomocĈ interpretera opartego na stosie. Wspomniany
interpreter kodu bajtowego ma stan, który musi byþ obsãugiwany i spójny
podczas wykonywania programu Pythona. Jčzyk Python wymusza spójnoĤþ
za pomocĈ mechanizmu o nazwie GIL (ang. global interpreter lock).
W gruncie rzeczy mechanizm GIL to rodzaj wzajemnego wykluczania (mutex)
chroniĈcy CPython przed wpãywem wywãaszczenia wielowĈtkowego, gdy je-
den wĈtek przejmuje kontrolč nad programem przez przerwanie dziaãania
innego wĈtku. Tego rodzaju przerwanie moİe doprowadziþ do uszkodzenia
interpretera, jeĤli wystĈpi w nieoczekiwanym czasie. Mechanizm GIL chroni
przed wspomnianymi przerwaniami i gwarantuje, İe kaİda instrukcja kodu
bajtowego dziaãa poprawnie z implementacjĈ CPython oraz jej moduãami
rozszerzeę utworzonych w jčzyku C.
Mechanizm GIL powoduje pewien waİny negatywny efekt uboczny. W przy-
padku programów utworzonych w jčzykach takich jak C++ lub Java wiele
wĈtków wykonywania oznacza, İe program moİe jednoczeĤnie wykorzystaþ
wiele rdzeni procesora. Wprawdzie Python obsãuguje wiele wĈtków wykony-
wania, ale mechanizm GIL powoduje, İe w danej chwili tylko jeden z nich
robi postčp. Dlatego teİ jeĤli sičgasz po wĈtki w celu przeprowadzania rów-
nolegãych obliczeę i przyĤpieszenia programów Pythona, to bčdziesz srodze
zawiedziony.
Przyjmujemy zaãoİenie, İe chcesz w Pythonie wykonaþ zadanie wymagajĈce
duİej iloĤci obliczeę. Uİyjemy algorytmu rozkãadu liczby na czynniki.
def factorize(number):
for i in range(1, number + 1):
if number % i == 0:
yield i
Rozkãad zbioru liczb moİe wymagaþ caãkiem duİej iloĤci czasu.
numbers = [2139079, 1214759, 1516637, 1852285]
start = time()
for number in numbers:
list(factorize(number))
end = time()
print('Operacja zabrađa %.3f sekund' % (end - start))
>>>
Operacja zabrađa 1.040 sekund
W innych jčzykach programowania uİycie wielu wĈtków bčdzie miaão sens,
poniewaİ wówczas wykorzystasz wszystkie rdzenie dostčpne w procesorze.
Sposób 37. Uİycie wĈtków dla operacji blokujĈcych wejĤcie-wyjĤcie
137
Spróbujmy to zrobiþ w Pythonie. Poniİej zdefiniowaãem wĈtek Pythona prze-
znaczony do przeprowadzenia tych samych obliczeę co wczeĤniej:
from threading import Thread
class FactorizeThread(Thread):
def __init__(self, number):
super().__init__()
self.number = number
def run(self):
self.factors = list(factorize(self.number))
Teraz uruchamiam wĈtki w celu równolegãego rozkãadu poszczególnych liczb.
start = time()
threads = []
for number in numbers:
thread = FactorizeThread(number)
thread.start()
threads.append(thread)
Pozostaão juİ tylko zaczekaþ na zakoęczenie dziaãania wszystkich wĈtków.
for thread in threads:
thread.join()
end = time()
print('Operacja zabrađa %.3f sekund' % (end - start))
>>>
Operacja zabrađa 1.061 sekund
ZaskakujĈce moİe byþ, İe równolegãe wykonywanie metody
factorize()
trwaão
dãuİej niİ w przypadku jej szeregowego wywoãywania. PrzeznaczajĈc po
jednym wĈtku dla kaİdej liczby, w innych jčzykach programowania moİna
oczekiwaþ przyĤpieszenia dziaãania programu nieco mniejszego niİ cztero-
krotne, co wynika z obciĈİenia zwiĈzanego z tworzeniem wĈtków i ich ko-
ordynacjĈ. W przypadku komputera wyposaİonego w procesor dwurdzeniowy
moİna oczekiwaþ jedynie okoão dwukrotnego przyĤpieszenia wykonywania
programu. Jednak nigdy nie bčdziesz sič spodziewaã, İe wydajnoĤþ bčdzie
gorsza, gdy do obliczeę moİna wykorzystaþ wiele rdzeni procesora. To demon-
struje wpãyw mechanizmu GIL na programy wykonywane przez standar-
dowy interpreter CPython.
IstniejĈ róİne sposoby pozwalajĈce CPython na wykorzystanie wielu wĈtków,
ale nie dziaãajĈ one ze standardowĈ klasĈ
Thread
(patrz sposób 41.) i imple-
mentacja tych rozwiĈzaę moİe wymagaþ doĤþ duİego wysiãku. MajĈc Ĥwia-
domoĤþ istnienia wspomnianych ograniczeę, moİesz sič zastanawiaþ, dla-
czego Python w ogóle obsãuguje wĈtki. Mamy ku temu dwa dobre powody.
Pierwszy — wiele wĈtków daje zãudzenie, İe program wykonuje jednocze-
Ĥnie wiele zadaę. Samodzielna implementacja mechanizmu jednoczesnego
wykonywania zadaę jest trudna (przykãad znajdziesz w sposobie 40.). Dzički
wĈtkom pozostawiasz Pythonowi obsãugč równolegãego uruchamiania funkcji.
138
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
To dziaãa, poniewaİ CPython gwarantuje zachowanie równoĤci mičdzy uru-
chomionymi wĈtkami Pythona, nawet jeĤli ze wzglčdu na ograniczenie na-
kãadane przez mechanizm GIL w danej chwili tylko jeden z nich robi postčp.
Drugi powód obsãugi wĈtków w Pythonie to blokujĈce operacje wejĤcia-
-wyjĤcia, które zachodzĈ, gdy Python wykonuje okreĤlonego typu wywoãania
systemowe. Za pomocĈ wspomnianych wywoãaę systemowych programy
Pythona proszĈ system operacyjny komputera o interakcjč ze Ĥrodowiskiem
zewnčtrznym. Przykãady blokujĈcych operacji wejĤcia-wyjĤcia to odczyt i zapis
plików, praca z sieciami, komunikacja z urzĈdzeniami takimi jak monitor
itd. WĈtki pomagajĈ w obsãudze blokujĈcych operacji wejĤcia-wyjĤcia przez
odizolowanie Twojego programu od czasu, jakiego system operacyjny potrze-
buje na udzielenie odpowiedzi na İĈdania.
Zaãóİmy, İe za pomocĈ portu szeregowego chcesz wysãaþ sygnaã do zdalnie
sterowanego Ĥmigãowca. Jako proxy dla tej czynnoĤci wykorzystamy wolne
wywoãanie systemowe (
select
). Funkcja prosi system operacyjny o blokadč
trwajĈcĈ 0,1 sekundy, a nastčpnie zwraca kontrolč z powrotem do programu.
Otrzymujemy wičc sytuacjč podobnĈ, jaka zachodzi podczas uİycia synchro-
nicznego portu szeregowego.
import select
def slow_systemcall():
select.select([], [], [], 0.1)
Szeregowe wykonywanie wywoãaę systemowych powoduje liniowe zwičk-
szanie sič iloĤci czasu niezbčdnego do ich wykonania.
start = time()
for _ in range(5):
slow_systemcall()
end = time()
print('Operacja zabrađa %.3f sekund' % (end - start))
>>>
Operacja zabrađa 0.503 sekund
Problem polega na tym, İe w trakcie wykonywania funkcji
slow_systemcall()
program nie moİe zrobiþ İadnego innego postčpu. Gãówny wĈtek programu
zostaã zablokowany przez wywoãanie systemowe
select
. Tego rodzaju sytu-
acja w praktyce jest straszna. Potrzebujesz sposobu pozwalajĈcego na obli-
czanie kolejnego ruchu Ĥmigãowca podczas wysyãania sygnaãu, w przeciwnym
razie Ĥmigãowiec moİe sič rozbiþ. Kiedy wystčpuje potrzeba jednoczesnego
wykonania blokujĈcych operacji wejĤcia-wyjĤcia i pewnych obliczeę, najwyİ-
sza pora rozwaİyþ przeniesienie wywoãaę systemowych do wĈtków.
W poniİszym fragmencie kodu mamy kilka wywoãaę funkcji
slow_systemcall()
w oddzielnych wĈtkach. To pozwoli na jednoczesnĈ komunikacjč z wieloma
portami szeregowymi (i Ĥmigãowcami), natomiast wĈtek gãówny bčdzie po-
zostawiony do wykonywania niezbčdnych obliczeę.
Sposób 37. Uİycie wĈtków dla operacji blokujĈcych wejĤcie-wyjĤcie
139
start = time()
threads = []
for _ in range(5):
thread = Thread(target=slow_systemcall)
thread.start()
threads.append(thread)
Po uruchomieniu wĈtków mamy do wykonania pewnĈ pracč, czyli oblicze-
nie kolejnego ruchu Ĥmigãowca przed oczekiwaniem na zakoęczenie dziaãa-
nia wĈtków obsãugujĈcych wywoãania systemowe.
def compute_helicopter_location(index):
# ...
for i in range(5):
compute_helicopter_location(i)
for thread in threads:
thread.join()
end = time()
print('Operacja zabrađa %.3f sekund' % (end - start))
>>>
Operacja zabrađa 0.102 sekund
Caãkowita iloĤþ czasu potrzebnego na równolegãe wykonanie operacji jest
pičciokrotnie mniejsza niİ w przypadku szeregowego wykonywania zadaę.
To pokazuje, İe wywoãania systemowe sĈ wykonywane równoczeĤnie w wielu
wĈtkach Pythona, nawet pomimo ograniczeę nakãadanych przez mechanizm
GIL. Wprawdzie mechanizm GIL uniemoİliwia równolegãe wykonywanie kodu
utworzonego przez programistč, ale nie ma wpãywu ubocznego na wywoãania
systemowe. Przedstawione rozwiĈzanie sič sprawdza, poniewaİ wĈtki Pythona
zwalniajĈ mechanizm GIL przed wykonaniem wywoãaę systemowych i ponow-
nie do niego powracajĈ po zakoęczeniu wywoãania systemowego.
Poza wĈtkami istnieje jeszcze wiele innych sposobów pracy z blokujĈcymi
operacjami wejĤcia-wyjĤcia, na przykãad uİycie moduãu
asyncio
. Wspomniane
rozwiĈzania alternatywne przynoszĈ waİne korzyĤci. Jednak wymagajĈ takİe
dodatkowej pracy w postaci koniecznoĤci refaktoryzacji kodu Įródãowego,
aby go dopasowaþ do innego modelu wykonywania (patrz sposób 40.). Uİycie
wĈtków to najprostszy sposób na równolegãe wykonywanie blokujĈcych ope-
racji wejĤcia-wyjĤcia i jednoczeĤnie wymaga wprowadzania jedynie mini-
malnych zmian w programie.
Do zapamičtania
Z powodu dziaãania globalnej blokady interpretera (mechanizm GIL)
wĈtki Pythona nie pozwalajĈ na równolegãe uruchamianie kodu bajtowe-
go w wielu rdzeniach procesora.
Pomimo istnienia mechanizmu GIL wĈtki Pythona nadal pozostajĈ uİyteczne,
poniewaİ oferujĈ ãatwy sposób jednoczesnego wykonywania wielu zadaę.
140
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
Uİywaj wĈtków Pythona do równoczesnego wykonywania wielu wywoãaę
systemowych. Tym samym bčdzie moİna jednoczeĤnie wykonywaþ blo-
kujĈce operacje wejĤcia-wyjĤcia oraz pewne obliczenia.
Sposób 38. Uİywaj klasy Lock, aby unikaþ stanu wyĤcigu
w wĈtkach
Po dowiedzeniu sič o istnieniu mechanizmu GIL (patrz sposób 37.) wielu
nowych programistów Pythona przyjmuje zaãoİenie, İe moİna zrezygnowaþ
z uİycia muteksu w kodzie. Skoro mechanizm GIL uniemoİliwia wĈtkom
Pythona ich równoczesne dziaãanie w wielu rdzeniach procesora, wičc moİna
wysnuþ wniosek, İe ta sama blokada musi dotyczyþ takİe struktur danych
programu, prawda? Pewne testy przeprowadzone na typach takich jak listy
i sãowniki mogĈ nawet pokazaþ, İe przyjčte zaãoİenie jest sãuszne.
Musisz mieþ jednak ĤwiadomoĤþ, İe niekoniecznie tak jest. Mechanizm GIL
nie zapewnia ochrony programowi. Wprawdzie w danej chwili jest wykony-
wany tylko jeden wĈtek Pythona, ale operacje wĈtku na strukturach danych
mogĈ byþ zakãócone mičdzy dwoma instrukcjami kodu bajtowego w interpre-
terze Pythona. To jest niebezpieczne, jeĤli jednoczeĤnie z wielu wĈtków pró-
bujesz uzyskaþ dostčp do tych samych obiektów. Struktury danych mogĈ byþ
praktycznie w kaİdej chwili uszkodzone na skutek wspomnianych zakãó-
ceę, co doprowadzi do uszkodzenia programu.
Zaãóİmy, İe tworzysz program przeprowadzajĈcy równoczeĤnie wiele opera-
cji, takich jak sprawdzanie poziomu Ĥwiatãa w pewnej liczbie czujników
sieciowych. Jeİeli chcesz okreĤliþ caãkowitĈ liczbč próbek, jakie miaãy miej-
sce w danym czasie, moİesz je agregowaþ za pomocĈ nowej klasy.
class Counter(object):
def __init__(self):
self.count = 0
def increment(self, offset):
self.count += offset
WyobraĮ sobie, İe kaİdy czujnik ma wãasny wĈtek roboczy, poniewaİ odczyt
czujnika wymaga blokujĈcej operacji wejĤcia-wyjĤcia. Po przeprowadzeniu
pomiaru wĈtek roboczy inkrementuje wartoĤþ licznika, cykl jest powtarzany
aİ do osiĈgničcia maksymalnej liczby oczekiwanych operacji odczytu.
def worker(sensor_index, how_many, counter):
for _ in range(how_many):
# Odczyt danych z czujnika.
# ...
counter.increment(1)
Sposób 38. Uİywaj klasy Lock, aby unikaþ stanu wyĤcigu w wĈtkach
141
Poniİej przedstawiãem definicjč funkcji uruchamiajĈcej wĈtek roboczy dla
poszczególnych czujników oraz oczekujĈcej na zakoęczenie odczytu przez
kaİdy z nich:
def run_threads(func, how_many, counter):
threads = []
for i in range(5):
args = (i, how_many, counter)
thread = Thread(target=func, args=args)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
Jednoczesne uruchomienie pičciu wĈtków wydaje sič proste, a dane wyj-
Ĥciowe powinny byþ oczywiste.
how_many = 10**5
counter = Counter()
run_threads(worker, how_many, counter)
print('Oczekiwana liczba próbek %d, znaleziona %d' %
(5 * how_many, counter.count))
>>>
Oczekiwana liczba próbek 500000, znaleziona 278328
Jednak wynik znacznie odbiega od oczekiwanego! Co sič staão? Jak coĤ tak
prostego mogão sič nie udaþ, zwãaszcza İe w danej chwili moİe dziaãaþ tylko
jeden wĈtek interpretera Pythona?
Interpreter Pythona wymusza zachowanie sprawiedliwoĤci mičdzy wyko-
nywanymi wĈtkami, aby wszystkie otrzymaãy praktycznie takĈ samĈ iloĤþ
czasu procesora. Dlatego teİ Python bčdzie wstrzymywaþ dziaãanie bieİĈ-
cego wĈtku i wznawiaþ dziaãanie kolejnego. Problem polega na tym, İe do-
kãadnie nie wiesz, kiedy Python wstrzyma dziaãanie Twoich wĈtków. WĈtek
moİe byþ wičc wstrzymany nawet w poãowie operacji, która powinna pozostaþ
niepodzielna. Tak sič wãaĤnie staão w omawianym przykãadzie.
Metoda
increment()
obiektu
Counter
wyglĈda na prostĈ.
counter.count += offset
Jednak operator
+=
uİyty w atrybucie obiektu tak naprawdč nakazuje Pytho-
nowi wykonanie w tle trzech oddzielnych operacji. Powyİsze polecenie jest
odpowiednikiem trzech poniİszych:
value = getattr(counter, 'count')
result = value + offset
setattr(counter, 'count', result)
WĈtki Pythona przeprowadzajĈce inkrementacjč mogĈ zostaþ wstrzymane
mičdzy dwoma dowolnymi operacjami przedstawionymi powyİej. To bčdzie
problematyczne, jeĤli stara wersja
value
zostanie przypisana licznikowi. Oto
przykãad nieprawidãowej interakcji mičdzy dwoma wĈtkami A i B:
142
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
# Wykonywanie wątku A.
value_a = getattr(counter, 'count')
# Przełączenie kontekstu do wątku B.
value_b = getattr(counter, 'count')
result_b = value_b + 1
setattr(counter, 'count', result_b)
# Przełączenie kontekstu z powrotem do wątku A.
result_a = value_a + 1
setattr(counter, 'count', result_a)
Po przeãĈczeniu kontekstu z wĈtku A do B nastĈpião usuničcie caãego po-
stčpu w trakcie operacji inkrementacji licznika. Dokãadnie to zdarzyão sič
w przedstawionym powyİej przykãadzie obsãugi czujników Ĥwiatãa.
Aby zapobiec tego rodzaju sytuacji wyĤcigu do danych oraz innym formom
uszkodzenia struktur danych, Python zawiera solidny zestaw narzčdzi do-
stčpnych we wbudowanym module
threading
. Najprostsze i najuİyteczniej-
sze z nich to klasa
Lock
zapewniajĈca obsãugč muteksu.
Dzički zastosowaniu blokady klasa
Counter
moİe chroniþ jej wartoĤþ bieİĈcĈ
przed jednoczesnym dostčpem z wielu wĈtków. W danej chwili tylko jeden
wĈtek bčdzie miaã moİliwoĤþ naãoİenia blokady. W poniİszym fragmencie
kodu uİyãem polecenia
with
do naãoİenia i zwolnienia blokady. To znacznie
uãatwia ustalenie, który kod jest wykonywany w trakcie trwania blokady
(wičcej informacji szczegóãowych na ten temat znajdziesz w sposobie 43.).
class LockingCounter(object):
def __init__(self):
self.lock = Lock()
self.count = 0
def increment(self, offset):
with self.lock:
self.count += offset
Teraz podobnie jak wczeĤniej uruchamiam wĈtki robocze, ale w tym celu
uİywam wywoãania
LockingCounter()
.
counter = LockingCounter()
run_threads(worker, how_many, counter)
print('Oczekiwana liczba próbek %d, znaleziona %d' %
(5 * how_many, counter.count))
>>>
Oczekiwana liczba próbek 500000, znaleziona 500000
Otrzymany wynik dokãadnie pokrywa sič z oczekiwanym. Klasa
Lock
pozwo-
liãa na rozwiĈzanie problemu.
Do zapamičtania
Choþ Python ma mechanizm GIL, nadal pozostajesz odpowiedzialny za uni-
kanie powstawania sytuacji wyĤcigu do danych mičdzy wĈtkami uİywany-
mi przez Twój program.
Sposób 39. Uİywaj klasy Queue do koordynacji pracy mičdzy wĈtkami
143
Twoje programy mogĈ uszkodziþ stosowane w nich struktury danych, jeĤli
pozwolisz, aby wiele wĈtków jednoczeĤnie modyfikowaão te same obiekty
bez nakãadania na nie blokad.
Klasa
Lock
oferowana przez wbudowany moduã
threading
to standardowa
implementacja mutekstu w Pythonie.
Sposób 39. Uİywaj klasy Queue do koordynacji pracy
mičdzy wĈtkami
Programy Pythona równoczeĤnie wykonujĈce wiele zadaę czčsto muszĈ ko-
ordynowaþ tč pracč. Jednym z najuİyteczniejszych narzčdzi przeznaczo-
nych do koordynacji jednoczeĤnie wykonywanych zadaę jest potokowanie
funkcji.
Potokowanie dziaãa na zasadzie podobnej do linii montaİowej w przedsič-
biorstwie. Potoki majĈ wiele faz w serii wraz z okreĤlonymi funkcjami dla
poszczególnych faz. Nowe zadania do wykonania sĈ nieustannie umieszczane
na poczĈtku potoku. Wszystkie funkcje mogĈ równolegle pracowaþ nad zada-
niami w obsãugiwanych przez nie fazach. Caãa praca przesuwa sič do przodu,
gdy wszystkie funkcje zakoęczĈ swoje zadanie. Cykl trwa aİ do wykonania
wszystkich faz. Tego rodzaju podejĤcie jest szczególnie dobre w przypadku
pracy wymagajĈcej uİycia blokujĈcych operacji wejĤcia-wyjĤcia lub podproce-
sów — czyli w przypadku zadaę, które mogĈ byþ ãatwo wykonywane rów-
nolegle za pomocĈ Pythona (patrz sposób 37.).
Na przykãad chcesz zbudowaþ system, który bčdzie pobieraã staãy strumieę
zdjčþ z aparatu cyfrowego, zmieniaã ich wielkoĤþ, a nastčpnie przekazywaã
zdjčcia do galerii w internecie. Tego rodzaju program moİna podzieliþ na
trzy fazy potoku. W pierwszej fazie bčdĈ pobierane nowe zdjčcia z aparatu.
W drugiej fazie pobrane zdjčcia zostanĈ przetworzone przez funkcjč odpo-
wiedzialnĈ za zmianč ich wielkoĤci. Nastčpnie w trzeciej i ostatniej fazie zmo-
dyfikowane zdjčcia bčdĈ za pomocĈ odpowiedniej funkcji przekazane do
galerii internetowej.
WyobraĮ sobie, İe juİ utworzyãeĤ funkcje Pythona przeznaczone do wyko-
nywania poszczególnych faz:
download()
,
resize()
i
upload()
. W jaki sposób moİ-
na przygotowaþ potok, aby praca mogãa byþ prowadzona równoczeĤnie?
Przede wszystkim potrzebny jest sposób umoİliwiajĈcy przekazywanie pra-
cy mičdzy poszczególnymi fazami potoku. Do tego celu moİna wykorzystaþ
zapewniajĈcĈ bezpieczeęstwo wĈtków kolejkč producent-konsument. (Za-
poznaj sič ze sposobem 38., aby zrozumieþ wagč bezpieczeęstwa wĈtków
w Pythonie. Z kolei w sposobie 46. znajdziesz wičcej informacji o klasie
deque
).
144
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
class MyQueue(object):
def __init__(self):
self.items = deque()
self.lock = Lock()
Producent, czyli w omawianym przykãadzie aparat cyfrowy, umieszcza no-
we zdjčcia na koęcu listy oczekujĈcych elementów.
def put(self, item):
with self.lock:
self.items.append(item)
Konsument, czyli w omawianym przykãadzie pierwsza faza potoku przetwa-
rzania, usuwa zdjčcia z poczĈtku listy oczekujĈcych elementów.
def get(self):
with self.lock:
return self.items.popleft()
Poniİej poszczególne fazy potoku przedstawiãem jako wĈtek Pythona, który
pobiera pracč z kolejki, takiej jak wczeĤniej wspomniana, wykonuje odpo-
wiedniĈ funkcjč, a nastčpnie uzyskany wynik umieszcza w innej kolejce.
Ponadto monitoruje liczbč razy, jakie wĈtek roboczy zostaã sprawdzony pod
kĈtem nowych danych wejĤciowych oraz iloĤþ wykonanej pracy.
class Worker(Thread):
def __init__(self, func, in_queue, out_queue):
super().__init__()
self.func = func
self.in_queue = in_queue
self.out_queue = out_queue
self.polled_count = 0
self.work_done = 0
Najtrudniejsza czčĤþ wiĈİe sič z tym, İe wĈtek roboczy musi prawidãowo ob-
sãuİyþ sytuacjč, w której kolejka danych wejĤciowych bčdzie pusta, ponie-
waİ poprzednia faza jeszcze nie zakoęczyãa swojego zadania. Tym zajmujemy
sič tam, gdzie nastčpuje zgãoszenie wyjĈtku
IndexError
. Moİna to potraktowaþ
jako przestój na linii montaİowej.
def run(self):
while True:
self.polled_count += 1
try:
item = self.in_queue.get()
except IndexError:
sleep(0.01) # Brak zadania do wykonania.
else:
result = self.func(item)
self.out_queue.put(result)
self.work_done += 1
Teraz pozostaão juİ poãĈczenie trzech wymienionych faz ze sobĈ przez
utworzenie kolejek przeznaczonych do koordynacji oraz odpowiednich wĈt-
ków roboczych.
Sposób 39. Uİywaj klasy Queue do koordynacji pracy mičdzy wĈtkami
145
download_queue = MyQueue()
resize_queue = MyQueue()
upload_queue = MyQueue()
done_queue = MyQueue()
threads = [
Worker(download, download_queue, resize_queue),
Worker(resize, resize_queue, upload_queue),
Worker(upload, upload_queue, done_queue),
]
Moİna uruchomiþ wĈtki, a nastčpnie wstrzyknĈþ pewnĈ iloĤþ pracy do pierw-
szej fazy potoku. W poniİszym fragmencie kodu jako proxy dla rzeczywi-
stych danych wymaganych przez funkcjč
download()
wykorzystaãem zwykãy
egzemplarz
object
.
for thread in threads:
thread.start()
for _ in range(1000):
download_queue.put(object())
Pozostaão juİ zaczekaþ do chwili, gdy wszystkie elementy zostanĈ przetwo-
rzone przez potok i znajdĈ sič w kolejce
done_queue
.
while len(done_queue.items) < 1000:
# Zrób coś użytecznego podczas oczekiwania.
# ...
RozwiĈzanie dziaãa prawidãowo, ale wystčpuje interesujĈcy efekt uboczny
spowodowany przez wĈtki sprawdzajĈce ich kolejki danych wejĤciowych
pod kĈtem nowych zadaę do wykonania. Najtrudniejsza czčĤþ podczas prze-
chwytywania wyjĈtków
IndexError
w metodzie
run()
jest wykonywana bardzo
duİĈ liczbč razy.
processed = len(done_queue.items)
polled = sum(t.polled_count for t in threads)
print('Prztworzono', processed, 'elementów po wykonaniu',
polled, 'sprawdzeē')
>>>
Przetworzono 1000 elementów po wykonaniu 3030 sprawdzeē
SzybkoĤþ dziaãania poszczególnych funkcji roboczych moİe byþ róİna, a wičc
wczeĤniejsza faza moİe uniemoİliwiþ dokonanie postčpu w póĮniejszych fa-
zach, tym samym korkujĈc potok. To powoduje, İe póĮniejsze fazy sĈ wstrzy-
mane i nieustannie sprawdzajĈ ich kolejki danych wejĤciowych pod kĈtem
nowych zadaę do wykonania. Skutkiem bčdzie marnowanie przez wĈtki
robocze czasu procesora na wykonywanie nieuİytecznych zadaę (bčdĈ ciĈ-
gle zgãaszaþ i przechwytywaþ wyjĈtki
IndexError
).
To jednak dopiero poczĈtek nieodpowiednich dziaãaę podejmowanych przez tč
implementacjč. WystčpujĈ w niej jeszcze trzy kolejne bãčdy, których równieİ
naleİy unikaþ. Po pierwsze, operacja okreĤlenia, czy wszystkie dane wejĤciowe
zostaãy przetworzone, wymaga oczekiwania w kolejce
done_queue
. Po drugie,
146
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
w klasie
Worker
metoda
run()
bčdzie wykonywana w nieskoęczonoĤþ w pčtli. Nie
ma moİliwoĤci wskazania wĈtkowi roboczemu, İe czas zakoęczyþ dziaãanie.
Po trzecie (to najpowaİniejszy w skutkach z bãčdów), zatkanie potoku moİe
doprowadziþ do awarii programu. Jeİeli w fazie pierwszej nastĈpi duİy po-
stčp, natomiast w fazie drugiej duİe spowolnienie, to kolejka ãĈczĈca obie
fazy bčdzie sič nieustannie zwičkszaþ. Druga faza po prostu nie bčdzie w sta-
nie nadĈİyþ za pierwszĈ z wykonywaniem swojej pracy. Przy wystarczajĈco
duİej iloĤci czasu i danych wejĤciowych skutkiem bčdzie zuİycie przez pro-
gram caãej wolnej pamičci, a nastčpnie awaria aplikacji.
Moİna wičc wyciĈgnĈþ wniosek, İe potoki sĈ zãym rozwiĈzaniem. Trudno
samodzielnie zbudowaþ dobrĈ kolejkč producent-konsument.
Ratunek w postaci klasy Queue
Klasa
Queue
z wbudowanego moduãu
queue
dostarcza caãĈ funkcjonalnoĤþ,
której potrzebujemy do rozwiĈzania przedstawionych wczeĤniej problemów.
Klasa
Queue
eliminuje oczekiwanie w wĈtku roboczym, poniewaİ metoda
get()
jest zablokowana aİ do chwili udostčpnienia nowych danych. Na przykãad po-
niİej przedstawiãem kod uruchamiajĈcy wĈtek, który oczekuje na pojawie-
nie sič w kolejce pewnych danych wejĤciowych.
from queue import Queue
queue = Queue()
def consumer():
print('Konsument oczekuje')
queue.get() # Uruchomienie po metodzie put() przedstawionej poniżej.
print('Konsument zakoēczyđ pracú')
thread = Thread(target=consumer)
thread.start()
Wprawdzie wĈtek jest uruchomiony jako pierwszy, ale nie zakoęczy dziaãa-
nia aİ do chwili umieszczenia elementu w egzemplarzu
Queue
, gdy metoda
get()
bčdzie miaãa jakiekolwiek dane do przekazania.
print('Producent umieszcza dane')
queue.put(object()) # Uruchomienie przed metodą get() przedstawioną powyżej.
thread.join()
print('Producent zakoēczyđ pracú')
>>>
Konsument oczekuje
Producent umieszcza dane
Konsument zakoēczyđ pracú
Producent zakoēczyđ pracú
W celu rozwiĈzania problemu z zatykaniem potoku, klasa
Queue
pozwala na
podanie maksymalnej liczby zadaę, jakie mogĈ mičdzy dwoma fazami oczeki-
waþ na wykonanie. Bufor ten powoduje wywoãanie metody
put()
w celu naão-
Sposób 39. Uİywaj klasy Queue do koordynacji pracy mičdzy wĈtkami
147
İenia blokady, gdy kolejka jest juİ zapeãniona. W poniİszym fragmencie kodu
przedstawiãem definicjč wĈtku oczekujĈcego chwilč przed uİyciem kolejki:
queue = Queue(1) # Bufor o wielkości 1.
def consumer():
time.sleep(0.1) # Oczekiwanie.
queue.get() # Drugie wywołanie.
print('Konsument pobiera dane 1')
queue.get() # Czwarte wywołanie.
print('Konsument pobiera dane 2')
thread = Thread(target=consumer)
thread.start()
Oczekiwanie powinno pozwoliþ wĈtkowi producenta na umieszczenie obu
obiektów w kolejce, zanim wĈtek konsumenta w ogóle wywoãa metodč
get()
.
Jednak wielkoĤþ
Queue
wynosi
1
. To oznacza, İe producent dodajĈcy elementy
do kolejki bčdzie musiaã zaczekaþ, aİ wĈtek konsumenta przynajmniej raz
wywoãa metodč
get()
. Dopiero wtedy drugie wywoãanie
put()
zwolni blokadč
i pozwoli na dodanie drugiego elementu do kolejki.
queue.put(object()) # Pierwsze wywołanie.
print('Producent umieszcza dane 1')
queue.put(object()) # Trzecie wywołanie.
print('Producent umieszcza dane 2')
thread.join()
print('Producent zakoēczyđ pracú')
>>>
Producent umieszcza dane 1
Konsument pobiera dane 1
Producent umieszcza dane 2
Konsument pobiera dane 2
Producent zakoēczyđ pracú
Klasa
Queue
moİe równieİ monitorowaþ postčp pracy, uİywajĈc do tego metody
task_done()
. W ten sposób moİna zaczekaþ, aİ kolejka danych wejĤciowych fazy
zostanie opróİniona, co eliminuje koniecznoĤþ sprawdzania kolejki
done_queue
na koęcu potoku. Na przykãad poniİej zdefiniowaãem wĈtek konsumenta
wywoãujĈcy metodč
task_done()
po zakoęczeniu pracy nad elementem.
in_queue = Queue()
def consumer():
print('Konsument oczekuje')
work = in_queue.get() # Zakończone jako drugie.
print('Konsument pracuje')
# Wykonywanie pracy.
# ...
print('Konsument zakoēczyđ pracú')
in_queue.task_done() # Zakończone jako trzecie.
Thread(target=consumer).start()
148
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
Teraz kod producenta nie musi ãĈczyþ sič z wĈtkiem konsumenta lub spraw-
dzaþ go. Producent moİe po prostu poczekaþ na zakoęczenie pracy przez
kolejkč
in_queue
, wywoãujĈc metodč
join()
w egzemplarzu
Queue
. Nawet jeĤli
kolejka
in_queue
jest pusta, to nie bčdzie moİna sič do niej przyãĈczyþ, do-
póki nie zostanie wywoãana metoda
task_done()
dla kaİdego elementu, który
kiedykolwiek byã kolejkowany.
in_queue.put(object()) # Zakończone jako pierwsze.
print('Producent oczekuje')
in_queue.join() # Zakończone jako czwarte.
print('Producent zakoēczyđ pracú')
>>>
Konsument oczekuje
Producent oczekuje
Konsument pracuje
Konsument zakoēczyđ pracú
Producent zakoēczyđ pracú
Wszystkie wymienione funkcje moİna umieĤciþ razem w podklasie klasy
Queue
, która równieİ poinformuje wĈtek roboczy o koniecznoĤci zakoęczenia
przetwarzania. W poniİszym fragmencie kodu znajduje sič zdefiniowana
metoda
close()
dodajĈca do kolejki element specjalny, który wskazuje, İe po
nim nie powinny znajdowaþ sič juİ İadne elementy danych wejĤciowych:
class ClosableQueue(Queue):
SENTINEL = object()
def close(self):
self.put(self.SENTINEL)
Nastčpnie definiujemy iterator dla kolejki, który wyszukuje wspomniany
element specjalny i zatrzymuje iteracjč po znalezieniu tego elementu. Metoda
iteratora
__iter__()
powoduje równieİ wywoãanie metody
task_done()
w odpo-
wiednim momencie, co pozwala na monitorowanie postčpu pracy w kolejce.
def __iter__(self):
while True:
item = self.get()
try:
if item is self.SENTINEL:
return # Powoduje zakończenie działania wątku.
yield item
finally:
self.task_done()
Teraz moİna przedefiniowaþ wĈtek roboczy, aby opieraã sič na funkcjonal-
noĤci dostarczanej przez klasč
ClosableQueue
. WĈtek zakoęczy dziaãanie po
zakoęczeniu pčtli.
class StoppableWorker(Thread):
def __init__(self, func, in_queue, out_queue):
# ...
Sposób 39. Uİywaj klasy Queue do koordynacji pracy mičdzy wĈtkami
149
def run(self):
for item in self.in_queue:
result = self.func(item)
self.out_queue.put(result)
Poniİej przedstawiãem kod odpowiedzialny za utworzenie zbioru wĈtków
roboczych na podstawie nowej klasy:
download_queue = ClosableQueue()
# ...
threads = [
StoppableWorker(download, download_queue, resize_queue),
# ...
]
Po uruchomieniu wĈtków roboczych sygnaã zatrzymania podobnie jak wcze-
Ĥniej jest wysyãany przez zamkničcie kolejki danych wejĤciowych dla pierw-
szej fazy po umieszczeniu w niej wszystkich elementów.
for thread in threads:
thread.start()
for _ in range(1000):
download_queue.put(object())
download_queue.close()
Pozostaão juİ tylko oczekiwanie na zakoęczenie pracy przez poãĈczenie po-
szczególnych kolejek znajdujĈcych sič mičdzy fazami. Gdy dana faza zo-
stanie zakoęczona, to jest to sygnalizowane kolejnej fazie przez zamkničcie
jej kolejki danych wejĤciowych. Na koęcu kolejka
done_queue
zawiera zgodnie
z oczekiwaniami wszystkie obiekty danych wyjĤciowych.
download_queue.join()
resize_queue.close()
resize_queue.join()
upload_queue.close()
upload_queue.join()
print(done_queue.qsize(), 'elementów zostađo przetworzonych')
>>>
1000 elementów zostađo przetworzonych
Do zapamičtania
Potoki to doskonaãy sposób organizowania sekwencji zadaę jednoczeĤnie
wykonywanych przez wiele wĈtków Pythona.
Musisz byþ Ĥwiadom, İe podczas tworzenia potoków, które jednoczeĤnie
wykonujĈ wiele zadaę, pojawiajĈ sič problemy: oczekiwanie blokujĈce do-
stčp, zatrzymywanie wĈtków roboczych i niebezpieczeęstwo zuİycia caãej
dostčpnej pamičci.
Klasa
Queue
oferuje caãĈ funkcjonalnoĤþ, jakiej potrzebujesz do przygoto-
wania niezawodnych potoków: obsãugč blokad, bufory o wskazanej wiel-
koĤci i doãĈczanie do kolejek.
150
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
Sposób 40. Rozwaİ uİycie wspóãprogramów
w celu jednoczesnego wykonywania wielu funkcji
Sposób 40. Uİycie wspóãprogramów w celu jednoczesnego wykonywania wielu funkcji
WĈtki umoİliwiajĈ programistom Pythona pozornie jednoczesne wykony-
wanie wielu funkcji (patrz sposób 37.). Jednak z wĈtkami wiĈİĈ sič trzy
powaİne problemy.
Q
WymagajĈ zastosowania specjalnych narzčdzi do koordynacji bezpie-
czeęstwa (patrz sposoby 38. i 39). Dlatego teİ kod oparty na wĈtkach jest
trudniejszy do zrozumienia niİ kod proceduralny wykonywany w jednym
wĈtku. Wspomniana trudnoĤþ powoduje, İe kod wykorzystujĈcy wĈtki
staje sič trudniejszy do rozbudowy i obsãugi.
Q
WĈtki wymagajĈ duİej iloĤci pamičci — mniej wičcej 8 MB dla kaİdego
wykonywanego wĈtku. W wielu komputerach iloĤþ dostčpnej pamičci po-
zwala na obsãugč sporej liczby wĈtków. Co sič jednak stanie, gdy program
bčdzie próbowaã wykonywaþ „jednoczeĤnie” dziesiĈtki tysičcy funkcji?
Wspomniane funkcje mogĈ odpowiadaþ İĈdaniom uİytkowników kiero-
wanym do serwera, pikselom na ekranie, czĈsteczkom w symulacji itd.
Próba uruchomienia oddzielnego wĈtku dla kaİdej unikalnej czynnoĤci
sič nie sprawdza.
Q
Uruchamianie wĈtków jest kosztowne. Jeİeli program ma nieustannie two-
rzyþ nowe jednoczeĤnie dziaãajĈce funkcje i koęczyþ ich dziaãanie, to obciĈ-
İenie zwiĈzane z uİyciem wĈtków stanie sič ogromne i spowolni program.
Python pozwala na zniwelowanie wszystkich wymienionych powyİej pro-
blemów za pomocĈ wspóãprogramów. Wspóãprogramy pozwalajĈ na uİycie
w programie Pythona wielu pozornie jednoczeĤnie wykonywanych funkcji.
Wspóãprogramy sĈ implementowane jako rozszerzenie generatorów (patrz
sposób 16.). Kosztem uruchomienia wspóãprogramu generatora jest wywoãa-
nie funkcji. Po uruchomieniu kaİdy z nich uİywa poniİej 1 KB pamičci.
Dziaãanie wspóãprogramu polega na umoİliwieniu kodowi uİywajĈcemu gene-
ratora na wykonanie funkcji
send()
w celu wysãania wartoĤci z powrotem do
funkcji generatora po kaİdym wyraİeniu
yield
. Funkcja generatora otrzymuje
wartoĤþ przekazanĈ funkcji
send()
jako wynik wykonania odpowiedniego wyra-
İenia
yield
.
def my_coroutine():
while True:
received = yield
print('Otrzymano:', received)
it = my_coroutine()
next(it) # Wywołanie generatora.
it.send('Pierwszy')
it.send('Drugi')
Sposób 40. Uİycie wspóãprogramów w celu jednoczesnego wykonywania wielu funkcji
151
>>>
Otrzymano: Pierwszy
Otrzymano: Drugi
PoczĈtkowe wywoãanie
next()
jest wymagane do przygotowania generatora
na otrzymanie pierwszego wywoãania
send()
przez przejĤcie do pierwszego
wyraİenia
yield
. Razem polecenie
yield
i wywoãanie
send()
zapewniajĈ gene-
ratorowi standardowy sposób na zróİnicowanie kolejnej wartoĤci w odpo-
wiedzi na zewnčtrzne dane wejĤciowe.
Na przykãad chcesz zaimplementowaþ wspóãprogram generatora dostar-
czajĈcy wartoĤþ minimalnĈ, która byãa dotĈd uİyta. W poniİszym fragmencie
kodu
yield
przygotowuje wspóãprogram wraz z poczĈtkowĈ wartoĤciĈ mini-
malnĈ pochodzĈcĈ z zewnĈtrz. Nastčpnie generator ciĈgle otrzymuje nowe
minimum w zamian za nowĈ wartoĤþ do rozwaİenia.
def minimize():
current = yield
while True:
value = yield current
current = min(value, current)
Kod wykorzystujĈcy generator moİe wykonywaþ po jednym kroku w danej
chwili i bčdzie wyĤwietlaã wartoĤþ minimalnĈ po otrzymaniu kolejnych da-
nych wejĤciowych.
it = minimize()
next(it) # Wywołanie generatora.
print(it.send(10))
print(it.send(4))
print(it.send(22))
print(it.send(-1))
>>>
10
4
4
-1
Funkcja generatora bčdzie pozornie dziaãaãa w nieskoęczonoĤþ i robiãa postčp
wraz z kaİdym nowym wywoãaniem
send()
. Podobnie jak wĈtki, wspóãpro-
gramy to niezaleİne funkcje pobierajĈce dane wejĤciowe z ich Ĥrodowiska
i generujĈce dane wyjĤciowe. Róİnica polega na pauzie po kaİdym wyraİeniu
yield
w funkcji generatora i wznowieniu dziaãania po kaİdym wywoãaniu
send()
pochodzĈcym z zewnĈtrz. Tak wyglĈda magiczny mechanizm wspóãprogramów.
Przedstawione powyİej zachowanie pozwala, aby kod wykorzystujĈcy gene-
rator podejmowaã dziaãanie po kaİdym wyraİeniu
yield
we wspóãprogramie.
Kod moİe uİyþ wartoĤci danych wyjĤciowych generatora w celu wywoãania
innych funkcji i uaktualnienia struktur danych. Co waİniejsze, moİe po-
sunĈþ do przodu inne funkcje generatora, aİ do ich nastčpnego wyraİenia
yield
. Dzički przesuničciu do przodu wielu oddzielnych generatorów wydaje
152
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
sič, İe wszystkie one dziaãajĈ jednoczeĤnie. To pozwala w Pythonie na na-
Ĥladowanie zachowania wĈtków.
Gra w İycie
MoİliwoĤþ jednoczesnego dziaãania wspóãprogramów zademonstrujč teraz
na przykãadzie. Zaãóİmy, İe chcemy je wykorzystaþ do implementacji gry
w İycie. Reguãy gry sĈ proste: mamy dwuwymiarowĈ planszč o dowolnej wiel-
koĤci. Kaİde pole na planszy moİe byþ İywe lub puste.
ALIVE = '*'
EMPTY = '-'
Postčp w grze jest oparty na jednym tykničciu zegara. W trakcie tykničcia
nastčpuje sprawdzenie kaİdego pola i ustalenie, ile z jego oĤmiu sĈsiednich
pól nadal pozostaje İywych. Na podstawie liczby İywych sĈsiadów podejmo-
wana jest decyzja o stanie sprawdzanego pola: pozostaje İywe, umiera lub
sič regeneruje. Poniİej przedstawiãem przykãad planszy o wymiarach 5×5
po czterech kolejkach. Kaİdy kolejny stan gry jest przedstawiony po prawej
stronie poprzedniego. ObjaĤnienie konkretnych reguã znajdziesz poniİej.
0 | 1 | 2 | 3 | 4
----- | ----- | ----- | ----- | -----
-*--- | --*-- | --**- | --*-- | -----
--**- | --**- | -*--- | -*--- | -**--
---*- | --**- | --**- | --*-- | -----
----- | ----- | ----- | ----- | -----
Grč moİna modelowaþ, przedstawiajĈc poszczególne pola jako wspóãpro-
gram generatora dziaãajĈcy ramič w ramič z innymi.
Aby zaimplementowaþ grč, przede wszystkim potrzebny jest sposób na po-
branie stanu sĈsiednich pól. Do tego celu moİemy wykorzystaþ wspóãprogram
o nazwie
count_neighbors()
, którego dziaãanie polega na dostarczaniu obiektów
Query
. WspomnianĈ klasč
Query
zdefiniujemy samodzielnie. Jej przeznaczeniem
jest dostarczenie wspóãprogramu generatora sprawdzajĈcego stan otaczajĈce-
go go Ĥrodowiska.
Query = namedtuple('Query', ('y', 'x'))
Wspóãprogram dostarcza obiekt
Query
dla kaİdego sĈsiedniego pola. Wyni-
kiem poszczególnych wyraİeę
yield
bčdzie wartoĤþ
ALIVE
lub
EMPTY
. Mičdzy
wspóãprogramem i korzystajĈcym z niego kodem zostaã zdefiniowany interfejs.
Generator
count_neighbors()
sprawdza stan sĈsiednich pól i zwraca liczbč pól
uznawanych za İywe.
def count_neighbors(y, x):
n_ = yield Query(y + 1, x + 0) # Północ.
ne = yield Query(y + 1, x + 1) # Północny wschód.
# Zdefiniowanie kolejnych kierunków e_, se, s_, sw, w_, nw ...
Sposób 40. Uİycie wspóãprogramów w celu jednoczesnego wykonywania wielu funkcji
153
# ...
neighbor_states = [n_, ne, e_, se, s_, sw, w_, nw]
count = 0
for state in neighbor_states:
if state == ALIVE:
count += 1
return count
Wspóãprogramowi
count_neighbors()
moİemy teraz dostarczyþ przykãadowe
dane, aby przetestowaþ jego dziaãanie. Poniİej pokazaãem, jak obiekty
Query
bčdĈ dostarczane dla kaİdego sĈsiedniego pola. Wspóãprogram oczekuje na
informacje o stanie kaİdego obiektu
Query
przekazywane metodĈ
send()
wspóã-
programu. Ostateczna wartoĤþ licznika jest zwracana w wyjĈtku
StopIteration
,
który jest zgãaszany, gdy generator jest wyczerpany przez polecenie
return
.
it = count_neighbors(10, 5)
q1 = next(it) # Pobranie pierwszego obiektu.
print('Pierwsze wyraľenie yield: ', q1)
q2 = it.send(ALIVE) # Wysłanie informacji o stanie q1, pobranie q2.
print('Drugie wyraľenie yield:', q2)
q3 = it.send(ALIVE) # Wysłanie informacji o stanie q2, pobranie q3
# ...
try:
count = it.send(EMPTY) # Wysłanie informacji o stanie q8, pobranie ostatecznej wartości licznika.
except StopIteration as e:
print('Liczba: ', e.value) # Wartość pochodząca z polecenia return.
>>>
Pierwsze wyraľenie yield: Query(y=11, x=5)
Drugie wyraľenie yield: Query(y=11, x=6)
...
Liczba: 2
Teraz potrzebujemy moİliwoĤci wskazania, İe pole przejdzie do nowego stanu
w odpowiedzi na liczbč İywych sĈsiadów zwróconĈ przez
count_neighbors()
.
W tym celu definiujemy kolejny wspóãprogram o nazwie
step_cell()
. Ten
generator bčdzie wskazywaã zmianč stanu pola przez dostarczanie obiektów
Transition
. To jest kolejna klasa, która podobnie jak
Query
bčdzie zdefiniowana.
Transition = namedtuple('Transition', ('y', 'x', 'state'))
Wspóãprogram
step_cell()
otrzymuje argumenty w postaci danych wspóãrzčd-
nych pola na planszy. Pobiera obiekt
Query
w celu uzyskania poczĈtkowego
stanu wspomnianych wspóãrzčdnych. Uruchomi wspóãprogram
count_neighbors()
do sprawdzenia sĈsiednich pól. Wykonuje takİe logikč gry w celu ustalenia,
jaki stan dane pole powinno mieþ dla kolejnego tykničcia zegara. Na koniec
pobierany jest obiekt
Transition
, aby wskazaþ Ĥrodowisku nastčpny stan pola.
def game_logic(state, neighbors):
# ...
def step_cell(y, x):
state = yield Query(y, x)
154
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
neighbors = yield from count_neighbors(y, x)
next_state = game_logic(state, neighbors)
yield Transition(y, x, next_state)
Co waİniejsze, wywoãanie
count_neighbors()
uİywa wyraİenia
yield from
. Wy-
raİenie to pozwala Pythonowi na ãĈczenie wspóãprogramów generatora, co
uãatwia wielokrotne uİycie niewielkich fragmentów funkcjonalnoĤci i two-
rzenie skomplikowanych wspóãprogramów na podstawie prostych. Po wy-
czerpaniu
count_neighbors()
ostateczna wartoĤþ zwracana przez wspóãprogram
(za pomocĈ polecenia
return
) bčdzie przekazana do
step_cell()
jak wynik wyra-
İenia
yield from
.
Teraz moİemy wreszcie zdefiniowaþ prostĈ logikč gry w İycie. Tak napraw-
dč mamy jedynie trzy reguãy.
def game_logic(state, neighbors):
if state == ALIVE:
if neighbors < 2:
return EMPTY # Śmierć: zbyt mało.
elif neighbors > 3:
return EMPTY # Śmierć: zbyt wiele.
else:
if neighbors == 3:
return ALIVE # Regeneracja.
return state
Wspóãprogramowi
step_cell()
dostarczamy przykãadowe dane, aby go prze-
testowaþ.
it = step_cell(10, 5)
q0 = next(it) # Obiekt Query położenia początkowego.
print('Ja: ', q0)
q1 = it.send(ALIVE) # Wysłanie mojego stanu, ustawienie pola sąsiada.
print('Q1: ', q1)
# ...
t1 = it.send(EMPTY) # Wysłanie stanu q8, podjęcie decyzji w grze.
print('Wynik: ', t1)
>>>
Ja: Query(y=10, x=5)
Q1: Query(y=11, x=5)
...
Wynik: Transition(y=10, x=5, state='-')
Celem gry jest wykonanie tej logiki dla wszystkich pól znajdujĈcych sič na
planszy. W tym celu moİemy umieĤciþ wspóãprogram
step_cell()
we wspóã-
programie
simulate()
. Wspóãprogram bčdzie analizowaã kolejne pola planszy
przez wielokrotne pobieranie
step_cell()
. Po sprawdzeniu wszystkich wspóã-
rzčdnych nastčpuje dostarczenie obiektu
TICK
, wskazujĈcego, İe bieİĈca gene-
racja pól zostaãa zakoęczona.
TICK = object()
def simulate(height, width):
Sposób 40. Uİycie wspóãprogramów w celu jednoczesnego wykonywania wielu funkcji
155
while True:
for y in range(height):
for x in range(width):
yield from step_cell(y, x)
yield TICK
W przypadku wspóãprogramu
simulate()
imponujĈce jest to, İe pozostaje on
caãkowicie niezwiĈzany z otaczajĈcym go Ĥrodowiskiem. Nadal nie zdefiniowa-
liĤmy sposobu przedstawienia planszy w obiektach Pythona, obsãugi war-
toĤci
Query
,
Transition
i
TICK
na zewnĈtrz, a takİe tego, jak gra pobiera stan
poczĈtkowy. Jednak logika pozostaje czytelna. Kaİde pole przeprowadzi
zmianč stanu za pomocĈ
step_cell()
. Nastčpnie mamy tykničcie zegara gry.
Proces bčdzie kontynuowany w nieskoęczonoĤþ, dopóki trwa postčp we
wspóãprogramie
simulate()
.
Na tym polega pičkno wspóãprogramów. PomagajĈ skoncentrowaþ sič na lo-
gice tego, co próbujesz osiĈgnĈþ. PozwalajĈ na oddzielenie poleceę kodu dla
Ĥrodowiska od jego implementacji, a tym samym wspóãprogramy mogĈ dziaãaþ
równoczeĤnie. Na przestrzeni czasu zyskujesz moİliwoĤþ poprawienia im-
plementacji wspomnianych poleceę kodu bez koniecznoĤci zmiany wspóã-
programów.
Teraz chcemy uruchomiþ
simulate()
w prawdziwym Ĥrodowisku. W tym celu
potrzebujemy sposobu na przestawienie stanu poszczególnych pól planszy.
Poniİej przedstawiãem klasč odpowiedzialnĈ za obsãugč planszy:
class Grid(object):
def __init__(self, height, width):
self.height = height
self.width = width
self.rows = []
for _ in range(self.height):
self.rows.append([EMPTY] * self.width)
def __str__(self):
# ...
Plansza pozwala na pobieranie i ustawianie wartoĤci dowolnej wspóãrzčd-
nej. Wspóãrzčdne wykraczajĈce poza granice bčdĈ zawijane, co powoduje,
İe plansza dziaãa na zasadzie nieskoęczonego miejsca.
def query(self, y, x):
return self.rows[y % self.height][x % self.width]
def assign(self, y, x, state):
self.rows[y % self.height][x % self.width] = state
Musimy jeszcze zdefiniowaþ funkcjč interpretujĈcĈ wartoĤci otrzymane ze
wspóãprogramu
simulate()
oraz jego wszystkich wewnčtrznych wspóãpro-
gramów. Funkcja ta zamienia instrukcje ze wspóãprogramów na interakcje
z otaczajĈcym Ĥrodowiskiem. Dla caãej planszy wykonuje jeden krok do przo-
du, a nastčpnie zwraca nowĈ planszč zawierajĈcĈ kolejny stan.
156
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
def live_a_generation(grid, sim):
progeny = Grid(grid.height, grid.width)
item = next(sim)
while item is not TICK:
if isinstance(item, Query):
state = grid.query(item.y, item.x)
item = sim.send(state)
else: # Konieczne jest przekształcenie.
progeny.assign(item.y, item.x, item.state)
item = next(sim)
return progeny
Aby zobaczyþ tč funkcjč w dziaãaniu, konieczne jest utworzenie planszy
i ustawienie jej stanu poczĈtkowego. Poniİej przedstawiãem przykãad utwo-
rzenia klasycznego ksztaãtu.
grid = Grid(5, 9)
grid.assign(0, 3, ALIVE)
# ...
print(grid)
>>>
---*-----
----*----
--***----
---------
---------
Teraz moİemy wykonaþ jeden krok naprzód. Moİesz zobaczyþ, İe w oparciu
o proste reguãy zdefiniowane w funkcji
game_logic()
ksztaãt ten zostaje prze-
suničty na dóã i w prawĈ stronč.
class ColumnPrinter(object):
# ...
columns = ColumnPrinter()
sim = simulate(grid.height, grid.width)
for i in range(5):
columns.append(str(grid))
grid = live_a_generation(grid, sim)
print(columns)
>>>
0 | 1 | 2 | 3 | 4
---*----- | --------- | --------- | --------- | ---------
----*---- | --*-*---- | ----*---- | ---*----- | ----*----
--***---- | ---**---- | --*-*---- | ----**--- | -----*---
--------- | ---*----- | ---**---- | ---**---- | ---***---
--------- | --------- | --------- | --------- | ---------
Najlepsze w omawianym podejĤciu jest to, İe moİna zmieniþ funkcjč
game_
´
logic()
bez koniecznoĤci wprowadzania jakichkolwiek modyfikacji w ota-
czajĈcym jĈ kodzie. Istnieje wičc moİliwoĤþ zmiany reguã lub dodania wičk-
szych sfer wpãywu za pomocĈ istniejĈcej mechaniki obiektów
Query
,
Transition
i
TICK
. To pokazuje, jak wspóãprogramy pozwalajĈ na zachowanie podziaãu
zadaę, co jest niezwykle waİnĈ zasadĈ projektowĈ.
Sposób 40. Uİycie wspóãprogramów w celu jednoczesnego wykonywania wielu funkcji
157
Wspóãprogramy w Pythonie 2
Niestety, Python 2 nie oferuje pewnych syntaktycznych cech, dzički którym
wspóãprogramy sĈ tak eleganckim rozwiĈzaniem w Pythonie 3. W Pythonie 2
istniejĈ dwa powaİne ograniczenia.
Pierwsze to brak wyraİenia
yield from
. Jeİeli wičc chcesz ãĈczyþ wspóãprogra-
my generatora w Pythonie 2, musisz zastosowaþ dodatkowĈ pčtlč w punk-
cie delegacji.
# Python 2
def delegated():
yield 1
yield 2
def composed():
yield 'A'
for value in delegated(): # Odpowiednik wyrażenia yield from w Pythonie 3.
yield value
yield 'B'
print list(composed())
>>>
['A', 1, 2, 'B']
Drugie ograniczenie polega na braku obsãugi polecenia
return
w generato-
rach Pythona 2. W celu uzyskania tego samego zachowania, zapewniajĈcego
prawidãowe dziaãanie z blokami
try-except-finally
, konieczne jest zdefiniowanie
wãasnego typu wyjĈtku i jego zgãaszanie, gdy ma byþ zwrócona wartoĤþ.
# Python 2
class MyReturn(Exception):
def __init__(self, value):
self.value = value
def delegated():
yield 1
raise MyReturn(2) # Odpowiednik polecenia return 2 w Pythonie 3.
yield 'Nie osiægniúto'
def composed():
try:
for value in delegated():
yield value
except MyReturn as e:
output = e.value
yield output * 4
print list(composed())
>>>
[1, 8]
158
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
Do zapamičtania
Wspóãprogramy oferujĈ efektywny sposób wykonywania dziesiĈtek tysič-
cy funkcji pozornie w tym samym czasie.
W przypadku generatora wartoĤciĈ wyraİenia
yield
bčdzie wartoĤþ prze-
kazana metodzie
send()
generatora z poziomu zewnčtrznego kodu.
Wspóãprogramy sĈ waİnym narzčdziem pozwalajĈcym na oddzielenie pod-
stawowej logiki programu od jego interakcji z otaczajĈcym go Ĥrodowiskiem.
Python 2 nie obsãuguje wyraİenia
yield from
, a takİe zwrotu wartoĤci
z generatorów.
Sposób 41. Rozwaİ uİycie concurrent.futures(),
aby otrzymaþ prawdziwĈ równolegãoĤþ
Na pewnym etapie tworzenia programów w Pythonie moİesz dotrzeþ do
Ĥciany, jeĤli chodzi o kwestie wydajnoĤci. Nawet po przeprowadzeniu opty-
malizacji kodu (patrz sposób 58.) wykonywanie programu wciĈİ moİe okazaþ
sič za wolne w stosunku do potrzeb. W nowoczesnych komputerach, w któ-
rych nieustannie zwičksza sič liczba dostčpnych rdzeni procesora, moİna
przyjĈþ zaãoİenie, İe jedynym rozsĈdnym rozwiĈzaniem jest równolegãoĤþ.
Co sič stanie, jeİeli kod odpowiedzialny za obliczenia podzielisz na nieza-
leİne fragmenty jednoczeĤnie dziaãajĈce w wielu rdzeniach procesora?
Niestety, mechanizm GIL w Pythonie uniemoİliwia osiĈgničcie prawdziwej
równolegãoĤci w wĈtkach (patrz sposób 37.), a wičc tč opcjč moİna wyklu-
czyþ. InnĈ czčsto pojawiajĈcĈ sič propozycjĈ jest ponowne utworzenie kodu
o znaczeniu krytycznym dla wydajnoĤci. Nowy kod powinien mieþ postaþ
moduãu rozszerzenia i byþ utworzony w jčzyku C. Dzički jčzykowi C zbli-
İasz sič bardziej do samego sprzčtu, a utworzony w nim kod dziaãa szybciej
niİ w Pythonie, co eliminuje koniecznoĤþ zastosowania równolegãoĤci. Rozsze-
rzenia utworzone w jčzyku C mogĈ równieİ uruchamiaþ rodzime wĈtki dzia-
ãajĈce równoczeĤnie i wykorzystujĈce wiele rdzeni procesora. API Pythona
przeznaczone dla rozszerzeę tworzonych w jčzyku C jest doskonale udo-
kumentowane i stanowi doskonaãe wyjĤcie awaryjne.
Jednak ponowne utworzenie kodu w jčzyku C wiĈİe sič z wysokim kosztem.
Kod, który w Pythonie jest krótki i zrozumiaãy, w jčzyku C moİe staþ sič
rozwlekãy i skomplikowany. Tego rodzaju kod wymaga starannego przete-
stowania i upewnienia sič, İe funkcjonalnoĤþ odpowiada pierwotnej, utwo-
rzonej w Pythonie. Ponadto trzeba sprawdziþ, czy nie zostaãy wprowadzone
nowe bãčdy. Czasami wãoİony wysiãek sič opãaca, co wyjaĤnia istnienie w spo-
ãecznoĤci Pythona ogromnego ekosystemu moduãów rozszerzeę utworzonych
w jčzyku C. Dzički wspomnianym rozszerzeniom moİna przyĤpieszyþ operacje
Sposób 41. Rozwaİ uİycie concurrent.futures(), aby otrzymaþ prawdziwĈ równolegãoĤþ
159
takie jak przetwarzanie tekstu, tworzenie obrazów i operacje na macierzach.
IstniejĈ nawet narzčdzia typu open source, na przykãad Cython (http://cython.
org/) i Numba (http://numba.pydata.org/) uãatwiajĈce przejĤcie do jčzyka C.
Problem polega na tym, İe utworzenie jednego fragmentu programu w jč-
zyku C w wičkszoĤci przypadków okaİe sič niewystarczajĈce. Zoptymalizo-
wane programy Pythona zwykle nie majĈ tylko jednego Įródãa powolnego
dziaãania, ale raczej wiele powaİnych Įródeã. Aby wičc wykorzystaþ szybkoĤþ
oferowanĈ przez jčzyk C i wĈtki, konieczne bčdzie przepisanie duİych frag-
mentów programu, co drastycznie wydãuİa czas potrzebny na jego przetesto-
wanie i zwičksza ryzyko. Musi istnieþ lepszy sposób pozwalajĈcy na rozwiĈ-
zywanie trudnych problemów obliczeniowych w Pythonie.
Wbudowany moduã
multiprocessing
, ãatwo dostčpny za pomocĈ innego wbu-
dowanego moduãu,
concurrent.futures
, moİe byþ dokãadnie tym, czego po-
trzebujesz. Pozwala Pythonowi na jednoczesne wykorzystanie wielu rdzeni
procesora dzički uruchomieniu dodatkowych interpreterów jako procesów
potomnych. Wspomniane procesy potomne sĈ niezaleİne od gãównego in-
terpretera, a wičc ich blokady globalne równieİ pozostajĈ oddzielne. Kaİdy
proces potomny moİe w peãni wykorzystaþ jeden rdzeę procesora. Ponadto
kaİdy z nich ma odwoãanie do procesu gãównego, z którego otrzymuje pole-
cenia przeprowadzenia obliczeę i do którego zwraca wynik.
Na przykãad przyjmujemy zaãoİenie, İe w Pythonie ma zostaþ przeprowa-
dzona operacja wykonujĈca intensywne obliczenia i wykorzystujĈca wiele
rdzeni procesora. W poniİszym przykãadzie uİyãem implementacji algoryt-
mu wyszukujĈcego najwičkszy wspólny mianownik dwóch liczb jako proxy
dla dwóch znacznie bardziej wymagajĈcych obliczeę algorytmów, takich jak
symulacja dynamiki cieczy i równania Naviera-Stokesa.
def gcd(pair):
a, b = pair
low = min(a, b)
for i in range(low, 0, -1):
if a % i == 0 and b % i == 0:
return i
Szeregowe wykonywanie tej funkcji oznacza liniowy wzrost czasu potrzebnego
na przeprowadzenie obliczeę, poniewaİ nie zostaãa uİyta równolegãoĤþ.
numbers = [(1963309, 2265973), (2030677, 3814172),
(1551645, 2229620), (2039045, 2020802)]
start = time()
results = list(map(gcd, numbers))
end = time()
print('Operacja zabrađa %.3f sekund' % (end - start))
>>>
Operacja zabrađa 1.170 sekund
160
Rozdziaã 5. WspóãbieİnoĤþ i równolegãoĤþ
Jeİeli ten kod zostanie wykonany w wielu wĈtkach Pythona, nie spowoduje to
İadnej poprawy wydajnoĤci, poniewaİ mechanizm GIL uniemoİliwia Pythono-
wi jednoczesne uİycie wielu rdzeni procesora. Poniİej prezentujč, jak wyglĈda
przeprowadzenie tych samych obliczeę za pomocĈ moduãu
concurrent.futures
,
jego klasč
ThreadPoolExecutor
i dwa wĈtki robocze (w celu dopasowania ich do
liczby rdzeni w moim komputerze).
start = time()
pool = ThreadPoolExecutor(max_workers=2)
results = list(pool.map(gcd, numbers))
end = time()
print('Operacja zabrađa %.3f sekund' % (end - start))
>>>
Operacja zabrađa 1.199 sekund
Jak widzisz, czas wykonania zadania jeszcze sič wydãuİyã, co ma zwiĈzek
z obciĈİeniem dotyczĈcym uruchomienia puli wĈtków i komunikacji z niĈ.
Pora na coĤ zaskakujĈcego: zmiana tylko jednego wiersza kodu wystarczy,
aby staão sič coĤ magicznego. Jeİeli klasč
ThreadPoolExecutor
zastĈpimy klasĈ
ProcessPoolExecutor
z moduãu
concurrent.futures
, to wszystko ulegnie przy-
Ĥpieszeniu.
start = time()
pool = ProcessPoolExecutor(max_workers=2) # Jedyna zmiana w kodzie.
results = list(pool.map(gcd, numbers))
end = time()
print('Operacja zabrađa %.3f sekund' % (end - start))
>>>
Operacja zabrađa 0.663 sekund
Po uruchomieniu kodu na moim dwurdzeniowym komputerze widaþ zna-
czĈcĈ poprawč wydajnoĤci. Jak to moİliwe? Poniİej przedstawiam faktycz-
ny sposób dziaãania klasy
ProcessPoolExecutor
z uİyciem niskiego poziomu
konstrukcji dostarczanych przez moduã
multiprocessing
:
1.
Kaİdy element danych wejĤciowych
numbers
zostaje przekazany do
map
.
2.
Dane sĈ serializowane na postaþ danych binarnych za pomocĈ moduãu
pickle
(patrz sposób 44.).
3.
Serializowane dane sĈ z procesu interpretera gãównego kopiowane
do procesu interpretera potomnego za pomocĈ gniazda lokalnego.
4.
Kolejnym krokiem jest deserializacja danych na postaþ obiektów
Pythona z wykorzystaniem
pickle
. Odbywa sič to w procesie potomnym.
5.
Import moduãu Pythona zawierajĈcego funkcjč
gcd
.
6.
Uruchomienie funkcji wraz z otrzymanymi danymi wejĤciowymi.
Inne procesy potomne wykonujĈ tč samĈ funkcjč, ale z innymi danymi.
7.
Serializacja wyniku na postaþ bajtów.
Sposób 41. Rozwaİ uİycie concurrent.futures(), aby otrzymaþ prawdziwĈ równolegãoĤþ
161
8.
Skopiowanie bajtów przez gniazdo lokalne do procesu nadrzčdnego.
9.
Deserializacja bajtów z powrotem na postaþ obiektów Pythona w procesie
nadrzčdnym.
10.
PoãĈczenie wyników z wielu procesów potomnych w pojedynczĈ listč
bčdĈcĈ ostatecznym wynikiem.
Wprawdzie przedstawiony powyİej proces wydaje sič prosty dla programisty,
ale moduã
multiprocessing
i klasa
ProcessPoolExecutor
muszĈ wykonaþ ogromnĈ
pracč, aby równolegãe wykonywanie zadaę byão moİliwe. W wičkszoĤci innych
jčzyków programowania jedynym miejscem wymagajĈcym koordynacji dwóch
wĈtków jest pojedyncza blokada lub niepodzielna operacja. ObciĈİenie zwiĈ-
zane z uİyciem moduãu
multiprocessing
jest duİe z powodu koniecznoĤci
przeprowadzania serializacji i deserializacji mičdzy procesami nadrzčdnym
i potomnymi.
Schemat ten wydaje sič doskonale dopasowany do pewnego typu odizolo-
wanych zadaę, w duİej mierze opartych na dĮwigni. Tutaj „odizolowanych”
oznacza, İe funkcja nie musi z innymi czčĤciami programu wspóãdzieliþ
informacji o stanie. Z kolei wyraİenie „w duİej mierze opartych na dĮwigni”
oznacza tutaj sytuacjč, gdy mičdzy procesami nadrzčdnym i potomnym musi
byþ przekazywana jedynie niewielka iloĤþ danych niezbčdnych do przeprowa-
dzenia duİych obliczeę. Algorytm najwičkszego wspólnego mianownika jest
przykãadem takiej sytuacji, choþ wiele innych algorytmów matematycznych
dziaãa podobnie.
Jeİeli charakterystyka obliczeę, które chcesz przeprowadziþ, jest inna od
przedstawionej powyİej, to obciĈİenie zwiĈzane z uİyciem moduãu
multipro-
cessing
moİe uniemoİliwiþ zwičkszenie wydajnoĤci dziaãania programu po
zastosowaniu równolegãoĤci. W takich przypadkach moduã
multiprocessing
oferuje funkcje zaawansowane zwiĈzane z pamičciĈ wspóãdzielonĈ, bloka-
dami mičdzy procesami, kolejkami i proxy. Jednak wszystkie wymienione
funkcje sĈ niezwykle skomplikowane. Naprawdč trudno znaleĮþ uzasad-
nienie dla umieszczania tego rodzaju narzčdzi w pamičci jednego procesu
wspóãdzielonego mičdzy wĈtkami Pythona. Przeniesienie tego poziomu skom-
plikowania do innych procesów i angaİowanie gniazd jeszcze bardziej utrud-
nia zrozumienie kodu.
Sugerujč unikanie moduãu
multiprocessing
i uİycie wymienionych funkcji za
pomocĈ prostszego moduãu
concurrent.futures
. Moİesz rozpoczĈþ od zasto-
sowania klasy
ThreadPoolExecutor
w celu wykonywania odizolowanych i sta-
nowiĈcych duİe obciĈİenie funkcji w wĈtkach. Nastčpnie moİesz przejĤþ
do klasy
ProcessPoolExecutor
, aby zwičkszyþ szybkoĤþ dziaãania aplikacji. Po
wyczerpaniu wszystkich opcji moİesz rozwaİyþ bezpoĤrednie uİycie moduãu
multiprocessing
.
Skorowidz
A
adnotacje atrybutów klas, 128
algorytmy wbudowane, 178
API, 78, 196, 205
argumenty
funkcji, 66
pozycyjne, 61
z gwiazdkĈ, 61
ASCII, 32
atrybut foo, 118
atrybuty, 105
prywatne, 95
publiczne, 95
B
blok
else, 41, 42, 45
except, 197
finally, 46
try, 44
bãĈd
w implementacji, 198
zakresu, 52
bufory, 149
C
ciĈg tekstowy, 126, 214
collections.abc, 99
czas koordynowany UTC, 174
D
dane JSON, 45
debuger, 220
debugowanie danych
wyjĤciowych, 214
dekorator @property, 112–115
dekoratory
klasy, 127
funkcji, 163
deserializacja, 171, 173
ciĈgu tekstowego, 125
danych, 160
danych JSON, 169
deskryptor, 113, 114
Field, 129
Grade, 115, 117
diamentowa hierarchia klas, 89
docstring, 66, 187, 191
dokumentacja, 187, 188
dokumentowanie
funkcji, 190
klas, 189
moduãów, 188
doãĈczanie do kolejek, 149
domieszka, 91
domkničcia, 49
dostčp do
atrybutów, 115
docstring, 188
elementu sekwencji, 100
nazwy klasy, 123
wãaĤciwoĤci prywatnych, 97
dwukierunkowa kolejka, 178
dynamiczne okreĤlenie
argumentów, 66
dynamiczny import, 203
dziedziczenie, 73, 99
dziedziczenie wielokrotne, 91
E
EDT, Eastern Daylight Time,
176
F
FIFO, first-in, first-out, 178
filtrowanie elementów, 182
format JSON, 94, 124
functools.wraps, 163
funkcja, 47
__init__(), 89
configure(), 202
create_workers(), 86
datetime.now(), 67
download(), 145
enumerate(), 39
eval(), 215
fibonacci(), 164
filter(), 33
generate_inputs(), 84, 85
help(), 164, 165
helper(), 52
index_words(), 54, 55
inspect(), 192
int(), 28
iter(), 59
localtime(), 175
log(), 61
log_missing(), 79
map(), 33
MapReduce, 83
mapreduce(), 84, 86
my_utility(), 225
next(), 37, 55
normalize(), 57, 59
print(), 214
range(), 38, 39
register_class(), 127
repr(), 214, 216
safe_division(), 70
safe_division_b(), 70
send(), 150
setattr(), 120
slow_systemcall(), 138
strptime(), 176
super(), 89
test(), 223
wrapper(), 164
wraps(), 165
zip(), 39, 40
zip_longest(), 41
funkcje
domkničcia, 54
generujĈce, 54
metaklasy, 128
moduãu itertools, 182
pierwszorzčdne, 79
230
Skorowidz
G
generator, 54
generator wyraİeę, 36
GIL, global interpreter lock, 136
gra w İycie, 152
gwiazdka, 61
H
hierarchia klas, 89
I
ignorowanie przepeãnienia, 69
implementacja moduãu API,
198
import, 202
import dynamiczny, 203
inicjalizacja klasy nadrzčdnej,
87
interaktywny debuger, 220
interfejs, 78
CountMissing, 80
publiczny mypackage, 194
iteracja, 56
iterator, 57
J
jčzyk C, 162
K
klasa, 73
BetterSerializable, 127
ClosableQueue, 148
Counter, 142
Customer, 129
Decimal, 184, 185
defaultdict, 79
deque, 143, 178
Exception, 198, 199
GameState, 170
GenericWorker, 85
Grade, 116
InputData, 82
JsonMixin, 94
Lock, 140
OrderedDict, 179
ProcessPoolExecutor, 160,
161
Queue, 143, 146
RegisteredSerializable, 127
TestCase, 219
Thread, 137
ThreadPoolExecutor, 160
ToDictMixin, 93
ValidatingDB, 119
klasy
nadrzčdne, 87
pomocnicze, 73
potomne, 97
kodowanie
ASCII, 32
UTF-8, 23
kolejka
FIFO, 178
sterty, 180
kolejnoĤþ poleceę import, 201
komunikaty o bãčdach, 57
konfiguracja, 202
konfiguracja Ĥrodowiska
programistycznego, 211
konstrukcja
if-else, 28
try-except-else, 42
try-except-else-finally, 44,
45
try-finally, 42, 166
konstruktor, 88
kontekst, 167
konwencje nazw, 21
koordynacja pracy
mičdzy wĈtkami, 143
krĈg zaleİnoĤci, 200
krotka, 48, 75
L
listy skãadane, 33
â
ãĈczenie elementów, 182
M
mapowanie obiektowo-
relacyjne, 128
mechanizm GIL, 136, 139
menedİer kontekstu, 167
metaklasy, 105, 122, 128, 130
metoda
__call__(), 81, 82
__getattr__(), 117–119
__getattribute__(), 117,
119, 121
__getitem__(), 29, 101
__init__(), 87, 88
__setattr__(), 120, 121
__setitem__(), 29
_traverse(), 92
average_grade(), 74, 75
communicate(), 132, 133
deduct(), 111
factorize(), 137
fill(), 111
foo.__iter__(), 59
get(), 26
increment(), 141
index(), 181
put(), 146
report_grade(), 74
run(), 145
runcall(), 223, 226
sort(), 181
super(), 90
task_done(), 147
metody
@property, 108
typu getter, 105
typu setter, 105, 107
moduã
app, 200
collections, 76, 179
configparser, 213
copyreg, 171–174
cProfile, 223
datetime, 174, 176
decimal, 183
dialog, 201
functools, 165
gc, 228
hashlib, 134
itertools, 41, 182
main, 202
models, 194
multiprocessing, 159–162
pickle, 169, 170, 174
pytz, 177, 185
queue, 146
subprocess, 132, 135
sys, 213
threading, 142
time, 175, 176
tracemalloc, 226–228
unittest, 217, 219
unittest.mock, 218
weakref, 116
moduãy wbudowane, 163
MRO, method resolution
order, 88
N
nadpisanie klasy, 97
narzčdzia
iteratora, 182
profilowania, 222, 223
narzčdzie
Cython, 159
openssl, 133
pip, 186, 204
Skorowidz
231
Pylint, 23
pyvenv, 208, 211
virtualenv, 209
O
obiekty pierwszorzčdne, 50
obsãuga
blokad, 149
czasu lokalnego, 174
zdarzeę, 199
odtworzenie zaleİnoĤci, 208
okno dialogowe, 199
operator
*, 61, 71
**, 71
organizacja moduãów, 191
ORM, object- -relationalship
mappings, 128
P
pakiet mypackage, 194
pakiety, 191
parametr timeout, 135
PDT, Pacific Daylight Time,
176
pčtla
for, 41
while, 41
plik
__init__.py, 192, 194
models.py, 194
requirements.txt, 208,
209
pliki __main__, 212
pobieranie danych, 52
podziaã, 181
polecenia, 22
debugera, 221
import, 202
powãoki, 221
polecenie
class, 89, 124
contextlib, 166
def, 191
if, 27
import, 193, 204
import *, 196
nonlocal, 52, 54
python, 20
pyvenv, 206
try-except, 198
with, 142, 166–168
yield, 151
polimorfizm @classmethod,
82, 85
potokowanie, 143
procesy potomne, 132, 135
produkcja, 211
protokóã iteratora, 58
przekazywanie argumentów
poprzez ich poãoİenie, 63
za pomocĈ sãowa
kluczowego, 63
przeãĈczanie kontekstu, 142
przestrzeę nazw, 192
R
refaktoryzacja, 47
atrybutów, 109
klasy, 113
kodu, 76, 139
repozytorium PyPI, 186
rozszerzenie klasy, 97
równoczesne przetwarzanie
iteratorów, 39
równolegãe wykonywanie
metody, 137
równolegãoĤþ, 131, 158
S
sekwencja, 100
serializacja obiektów, 164
serializowane dane, 160, 169
skanowanie liniowane
danych, 222
sãownik, 74
__dict__, 118
_values, 116
dict, 179
domyĤlny, 180
JSON, 124
OrderedDict, 179
uporzĈdkowany, 179
specyfikacja PEP 8, 21
sprawdzanie typu, 217
stabilne
API, 191, 193, 195
Ĥcieİki importu, 174
staãa TESTING, 212
stan wyĤcigu, 140
sterta, 180
strefa czasowa
EDT, 176
PDT, 176
struktury danych, 178
styl
PEP 8, 21
Pythonic, 19
szyfrowanie, 134
ģ
Ĥcieİki importu, 173
Ĥrodowisko
produkcyjne, 211
programistyczne, 211
uruchomieniowe, 226
wirtualne, 204, 206, 209
T
tabela hash, 179
test, 217
integracji, 219
jednostkowy, 219
tworzenie
docstrine, 187
testów, 217
testów jednostkowych,
220
wĈtków roboczych, 149
typ
bytes, 23
namedtuple, 76–78
str, 23
unicode, 23, 26
typy niestandardowe, 99
U
unikanie równolegãoĤci, 136
UTC, Universal Coordinated
Time, 174, 177
UTF-8, 23, 32
uİycie
@property, 109
atrybutów prywatnych,
98, 99
concurrent.futures(), 158
konstrukcji try-finally,
166
metaklas, 124, 130
pamičci, 226
polecenia with, 167
uİycie wĈtków, 136
W
wartoĤþ
domyĤlna atrybutu, 171
False, 48
None, 47, 48, 67
wĈtek, 136
gãówny, 138
roboczy, 140
wersja Pythona, 20
wersjonowanie klas, 122, 172
232
Skorowidz
wãaĤciwoĤci chronione, 97, 99
wspóãbieİnoĤþ, 131
wspóãprogram, 150, 153–158
wstrzykiwanie zaleİnoĤci, 203
wyciek pamičci, 226
wyjĈtek, 49
AttributeError, 119, 201
Exception, 196
IndexError, 144
OverflowError, 69
StopIteration, 57
SyntaxError, 217
TypeError, 60, 72
ValueError, 45, 196
ZeroDivisionError, 69
wykonanie
kodu, 202
wielu funkcji, 150
wyraİenia, 22
wyraİenia generatorowe, 37
wyraİenie yield, 56, 150, 167
Z
zaczepy, 78
zakres, 53
globalny, 51
zmiennej, 49
zarzĈdzanie procesami
potomnymi, 132
znak
@, 164
zachčty, 220
znaki odstčpu, 21