Mateusz Mazurek – programista z pasją

Czyli o użyciu Pythona i kilku innych technologii do tworzenia świetnej jakości aplikacji w oparciu o stabilny proces dostarczania oprogramowania.

Programowanie Programowanie webowe

Czego nie powinniśmy robić w Pythonie? #5

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:

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:
a w ramach prezentu otrzymasz całkowicie za darmo, dwa dokumenty PDF „6 (nie zawsze oczywistych) błędów popełnianych podczas nauki programowania” który jest jednym z efektów ponad siedmioletniej pracy i obserwacji rozwoju niejednego programisty oraz „Wstęp do testowania w Pythonie”, będący wprowadzeniem do biblioteki PyTest.
Jeśli to Cię interesuje to zapraszam również na swoje social media.

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.

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

1 Komentarz
Inline Feedbacks
View all comments

[…] It is a blog by Mateusz Mazurek. I recommend this series of posts about “What to not do in Python?”. […]