Cześć!
Na jakiś czas odsunąłem na bok tę serię. Muszę przyznać, że trochę mnie zmęczyło skupianie się nad tym, czego nie powinno się robić. Efektem tej przerwy było powstanie innych, równie wartościowych wpisów. Niemniej jednak czułem, że nie zamknąłem tego tematu należycie, więc dziś zapraszam na ostatni wpis z serii „Czego nie powinniśmy robić w Pythonie?„.
Nim przejdziemy do sedna, podrzucam linki do reszty wpisów:
- Czego nie powinniśmy robić w Pythonie? #1
- Czego nie powinniśmy robić w Pythonie? #2
- Czego nie powinniśmy robić w Pythonie? #3
- Czego nie powinniśmy robić w Pythonie? #4
Unikanie typowania
Nowsze wersje Pythona pozwalają na definiowanie typów. Praktycznie już od 3.6 jest to w pełni działający mechanizm, szczegółowo opisany w dokumentacji.
Kod bez typowania oczywiście zadziała, więc pytanie po co definiować typy? Typowanie wprowadza porządek i pozwala pisać spójniejszy kod. Mimo że nie działa ono tak jak w Javie (bo zdefiniowanie typu np. w argumencie metody, nadal pozwala nam przekazać coś niezgodnego), to jest nieocenioną pomocą dla PyCharma. A dzięki temu PyCharm pomaga nam.
Zerknij na kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | from abc import ABC, abstractmethod class Animal(ABC): @abstractmethod def _voice(self) -> str: pass def speak(self) -> None: print(self._voice()) class Dog(Animal): def _voice(self) -> str: return "Hau hau" class Cat(Animal): def _voice(self) -> str: return "Miauł miauł" selected_animal: str = input("Type dog or cat to get voice!") globals_objs: dict = globals() animal = globals_objs[selected_animal.title()]() animal.speak() |
Zauważ, że w tym kodzie typ zmiennej animal
jest określany w runtime’ie. Patrząc na ten kod wiemy, że będzie tam obiekt klasy Animal. Jednak PyCharm tego nie może wiedzieć, więc nie dostaniemy na zmiennej animal
podpowiedzi do możliwych metod tego obiektu.
Dzięki typowaniu możemy określić mu, jaki to jest typ i tym samym uzyskać podpowiedź:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | from abc import ABC, abstractmethod class Animal(ABC): @abstractmethod def _voice(self) -> str: pass def speak(self) -> None: print(self._voice()) class Dog(Animal): def _voice(self) -> str: return "Hau hau" class Cat(Animal): def _voice(self) -> str: return "Miauł miauł" selected_animal: str = input("Type dog or cat to get voice!") globals_objs: dict = globals() animal: Animal = globals_objs[selected_animal.title()]() animal.speak() |
Sklejanie nazw metody
Nie jedna osoba pewnie widziała kod podobny do tego:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class A: def get_a(self) -> str: return "a!" def get_b(self) -> str: return "b!" def get_c(self) -> str: return "c!" value: str = "a" selected_method: str = f'get_{value}' a: A = A() print(getattr(a, selected_method)()) |
Błagam, nie róbcie tak! Taki kod tragicznie się czyta. Siedzisz i kopiesz debuggerem, żeby dowiedzieć się, co on tam tak pokrętnie wykonuje! Wiem, że takie rozwiązanie kusi, ale tylko do czasu, kiedy jest potrzeba analizy takiego kodu. Wtedy przestaje być to wygodne.
Oczywiście przykład nie pokazuje problemu dostatecznie, ale ubierzcie go w wyobraźni w dodatkowe abstrakcje, dodajcie kilkanaście tysięcy plików obok, skomplikowany przypadek, który badacie, a wyjdą Wam włosy z głowy. Same.
Podobny typ problemu pojawił się również w poprzednim akapicie. Z tą różnicą, że tam jest tworzenie obiektu na podstawie klasy, która jest tworzona ze stringa, którego przekazuje klient. Tak się zdecydowanie nie robi! Tam po pierwsze powinna być walidacja, czy umiemy obsłużyć to, co przyszło do nas od klienta, po drugie jakaś metoda wytwórcza, która tworzy obiekt. I wolę w niej po prostu IFy…
I też wiem, że czasem ciężko inaczej… Ale chociaż starajmy się unikać takich rozwiązań. A jak nie możemy, to dopisujmy logi, typy, wszystko, co pomaga ten kod zrozumieć.
Czekaj, stop!
Podoba Ci się to co tworzę? Jeśli tak to zapraszam Cię do zapisania się na newsletter:Jeśli to Cię interesuje to zapraszam również na swoje social media.
Jak i do ewentualnego postawienia mi kawy :)
Ciężkie obliczenia na wątkach
Python, a dokładnie CPython, ma pewien mankament, zwany GIL. GIL to Global Interpreter Lock, czyli po prostu mutex, który nie pozwala wielu wątkom używać interpretera w tym samym czasie. W efektcie obliczenia średnio dają się zrównoleglić. Zerknij na kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import time def count(_from, _to): while _from >= _to: _from -= 1 start = time.time() count(40000000, 0) end = time.time() print('Time taken in seconds -', end - start) |
Ten kod wykonuje się w okolicach 2 sekund. Odlicza on od 40000000 do zera. Czy jeśli zrobimy dwa wątki, jeden od 40000000 do 20000000, a drugi od 20000000 do 0, to kod będzie trwał w okolicach sekundy? Sprawdźmy!
Uruchamiam taki kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from threading import Thread import time def count(_from, _to): while _from >= _to: _from -= 1 start = time.time() t1 = Thread(target=count, args=(40000000, 20000000)) t2 = Thread(target=count, args=(20000000, 0)) t1.start() t2.start() t1.join() t2.join() end = time.time() print('Time taken in seconds -', end - start) |
A jego efektem jest wynik… Lekko większy od 2 sekund.
Co pokazuje że zrównoleglenia brak.
Alternatywnym rozwiązaniem jest użycie procesów zamiast wątków, czyli skorzystać z multiprocessing, np:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from multiprocessing import Process import time def count(_from, _to): while _from >= _to: _from -= 1 start = time.time() p1 = Process(target=count, args=(40000000, 20000000)) p2 = Process(target=count, args=(20000000, 0)) p1.start() p2.start() p1.join() p2.join() end = time.time() print('Time taken in seconds -', end - start) |
Co daje wynik w okolicach sekundy. Oczywiście procesy są cięższe od wątków, więc trzeba się liczyć z konsekwencjami tego rozwiązania.
Zmierzając do brzegu
W ramach tej serii artykułów powstało 5 wpisów, każdy średnio po trzy akapity, czyli łączenie około 15 rad na temat tego, czego warto unikać, pisząc programy w Pythonie. To sporo fajnej wiedzy. Mam nadzieje, że dowiedziałeś się czegoś przydatnego.
Mateusz Mazurek
Hej Mateusz,
Czy mógłbyś napisać czym różni się wątek od procesu? Trochę poczytałem tą serię postów na twoim blogu ale nie wiem czy mi się ta wiedza utrwali. Może będę miał jakiś lepszy pogląd na to czego nie robić w Pythonie.
Btw. Nie wiem co robi w komentarzach link do mojego postu o optymalizacjach w Pythonie. Pewnie pojawił się automatycznie.
Pozdrawiam :-).
Jasne że mogę! Proces to instancja Twojego programu. Ma on wirtualną przestrzeń adresową, kod wykonywalny, otwarte uchwyty do obiektów systemowych, kontekst zabezpieczeń, unikalny identyfikator procesu, itp. Każdy proces jest uruchamiany z jednym wątkiem, nazywany wątkiem głównym, ale może tworzyć dodatkowe wątki. Wątki pracują na zasobach dostarczanych w ramach procesu, zakończenie wątku nie powoduje zakończenia procesu, ale zakończenie procesu powoduje zakończenie wątku. Jeśli masz np. edytor tekstu to w jednym wątku edytor dba o to by zapisywać co jakiś czas dokument, a w innym np. sprawdza na bieżąco pisownię:) Zamknięcie edytora zakończy sprawdzanie pisowni jak i automatyczne zapisywanie dokumentu. A link… Czytaj więcej »
Dzięki Mateusz.
Polecam książkę „Czysty kod w Pythonie”.
Tam są opisane sztuczki ze strony i inne.
Najważniejsze wg mnie, to czego tu nie było, to sposób zapisywanie funkcji, klas, zmiennych, stałych w pythonowski sposób.
https://helion.pl/ksiazki/czysty-kod-w-pythonie-sunil-kapil,czykop.htm
książka w czasie urodzin Ebookpoint jest w promocji
Dzięki za info! Książki nie znam, może innym pomoże:)