Jak powszechnie wiadomo, Python należy do języków dynamicznie typowanych. W odróżnieniu od np. C, czy Javy, w momencie tworzenia zmiennej nie musimy deklarować jej typu. Ba, nie musimy go nawet znać. Dlatego, poniższy kod zadziała dla dowolnych typów, dla których zdefiniowano operator +:
def print_sum(a, b):
print(a + b)
a = 15
b = 25
print_sum(a, b) # 40
c = "fifteen"
d = "twenty-five"
print_sum(c, d) # "fifteentwenty-five"
e = "ten"
f = 2.45
print_sum(e, f) # TypeError: can only concatenate str (not "float") to str
Bywa to zbawieniem, zwłaszcza dla początkujących programistów, którym odpada przynajmniej jedno zmartwienie przy oblekaniu algorytmu w ciało funkcji.
Bywa to również powodem do dumy- w końcu zen Pythona zakłada prostotę i elastyczność. Tak samo jak nie ma w języku średników na końcu instrukcji, ani nawiasów klamrowych1
Ale ma też poważniejsze zalety. Choćby taką, że znacznie upraszcza stosowanie polimorfizmu- w języku statycznie typowanym musielibyśmy z góry zdefiniować jaki typ danych przyjmie nasza funkcja, i najpewniej napisać kilka jej wariantów dla różnych typów.
Jedną z naczelnych zasad Pythona jest również estetyka i czytelność kodu. A pozbycie się zarówno typów zmiennych, jak i słów kluczowych przy ich definicji niewątpliwie pozwala się pozbyć nadmiarowych znaków, przez co łatwiej wychwycić esencję myśli programisty.
Jednak, jeśli zaczniecie pracować nad bardziej złożonymi projektami, zwłaszcza w zespole, w którym będzie ktoś więcej poza wami, szybko okaże się, że te zalety mogły przesłonić kilka podstawowych wad.
Jedną z nich jest, o ironio, czytelność kodu. Jak wiemy, powinniśmy programować tak, jakby nasz kod miał być czytany przez psychopatyczne klauna ze strzelbą i notatnikiem pełnym adresów naszych bliskich. Chcemy, żeby ten klaun był spokojny i zadowolony. Dlatego zastanówmy się, która z poniższych funkcji jest czytelniejsza:
def do_something_weird(
person_age,
book_release_year,
lunar_phase):
return [do_some_magic(c, book_release_year)
for c in person_age
if some_boring_function(lunar_phase)]
Czy może:
def do_something_weird(
person_age: str,
book_release_year: Datetime,
lunar_phase: LUNAR_PHASE) -> List[str]:
return [do_some_magic(c, book_release_year)
for c in person_age
if some_boring_function(lunar_phase)]
Jeśli nie możecie się zdecydować, zadajcie sobie proste pytanie: ile czasu zajęło by wam zgadnięcie, że wiek osoby należy przekazać słownie (no dobra- int raczej nie jest iterable, więc wiadomo, że liczba tu nie zadziała, ale to nie do końca odpowiada na pytanie).
Stąd właśnie, wraz z wydaniem wersji 3.5, Python wzbogacił się o adnotacje i bibliotekę Typing. Nie są one w żaden sposób obowiązkowe, ani nie wpływają na wykonanie kodu, a jednak mają kolosalne znaczenie.
Po pierwsze- co już wskazaliśmy, znacznie ułatwiają czytanie kodu. Dla początkującego programisty, tworzącego swój pierwszy projekt, może się to czasem wydać nadmiarowe.
W końcu stara się nazywać zmienne i funkcje w sposób jasny i czytelny (choć i tak połowa kończy nazwana wyn_wysz_ksz
, albo roztenteguj(b)
).
Jako że żyje swoim projektem i napisał każdy znak w jego codebase, to nie ma problemu ze zrozumieniem, co która funkcja robi, i w jaki sposób działa przepływ danych.
Ba, typowanie jest czasem nawet kłopotliwe- skąd mam wiedzieć, co zwróci moja funkcja, zanim ją napiszę. Albo- skoro język jest dynamicznie typowany, to dlaczego funkcja nie może w jednym przypadku zwracać liczby, a w innym- listy liczb?
Jednak wierzcie mi, kiedy wpada się w cudzy legacy code, w dodatku od dawna nieruszany, jasne i przejrzyste typy są wybawieniem.
I nie musi to być nawet wynikiem tego, że poprzedni zespół składał się z pawianów szprycowanych kofeiną, których lead zapijał kolejny rozwód. Nawet świetnie wymyślony, dobrze napisany kod, pozbawiony typów, może sprawić na wejściu sporo kłopotów.
Ot po prostu, dzięki adnotacjom, ja wiem co zwraca dana funkcja i jakiego typu argumenty przyjmuje- to znacznie upraszcza korzystanie z nich i debugowanie. Wiemy co do funkcji podać, wiemy co powinna zwrócić, łatwiej więc wgryźć się w to, co się w niej dzieje.
Co więcej, wie to też edytor. Jeśli użyjemy prostej klasy:
class Duck:
def quack():
print(“Quack!”)
I przekażemy jej instancję do funkcji, to “quack” będziemy musieli napisać z palca. Bo w końcu skąd ten biedny edytor ma wiedzieć, że pływający ptak to właśnie kaczka?
W przypadku naszej kaczki, tracimy tylko kilka chwil, potrzebnych na napisanie prostej nazwy. Ale co jeśli operujemy na klasach, które mają 40 różnych metod?
Jak widać, proste dodanie typu może znacząco wpłynąć na wygodę pisania kodu, ale również i na bezpieczeństwo. I nie chodzi tylko i wyłącznie o możliwość walnięcia, trudnej do znalezienia, literówki.
Jeśli edytor wie, jaki jest zwracany typ funkcji, może wyłapać momenty, w których na przykład podajemy do funkcji string zamiast inta.
I wreszcie jeden z moich ulubionych argumentów. John DeGoes, w pierwszej wersji podręcznika “Zionomicon”2. Otóż deklaracja funkcji jest pewnego rodzaju obietnicą. Kontraktem zawieranym pomiędzy programistą a użytkownikiem kodu- czy będzie to jego kolega z zespołu, czy użytkownik biblioteki.
Brzmi to oczywiście górnolotnie, ale jak działa w praktyce? W tym samy “Zionomiconie”, deGoes nie tłumaczy implementacji kolejnych funkcji czy metod. Po prostu podaje definicje- typów, funkcji, traitów3. To wystarczy by zrozumieć, jak działają. To doskonała ilustracja znanej prawdy, że poprawnie napisany kod jest samodokumentujący.
Oczywiście, ktoś może powiedzieć- to po co pisać w Pythonie, skoro można zająć się językami, które typowania wymagają? Cytując klasyka - niech te plusy nie przesłonią nam minusów.
Python jest nadal językiem prostym, czytelnym i pięknym. Z reguły wystarczy mi około 5 minut obcowania z kodem w Javie, żeby czuć ulgę, że nie muszę niczego w niej pisać- ilość boilerplate`u jest tam zatrważająca a kod rzadko kiedy da się określić inaczej niż “potwornie brzydki”.
Nawet jeśli bardzo cenicie sobie dynamiczne typowanie, nadal nic nie stoi na przeszkodzie, abyście zmiennej najpierw nadawali wartość string, potem int, a potem worek_kartofli.
Natomiast w bardziej rozbudowanych projektach, lepiej takie rzeczy robić wewnątrz funkcji, natomiast typy wejścia i wyjścia niech będą stałe i jasno określone. I wasz zespół, i wy z przyszłości sobie za to podziękujecie.
Swoją drogą, to jeden z bardziej uroczych easter eggów Pythona. Jeśli chcielibyście używać nawiasów klamrowych, niczym w C czy Javie, wykonajcie instrukcję:
from __future__ import braces
Nie wiem, czy w kolejnych to zdanie również jest zawarte. Nawet jeśli nie piszecie w Scali, to zachęcam do zapoznania się. Książkę można otrzymać za darmo na stronie: https://www.zionomicon.com/ ↩
Wiem, po polsku powinno się mówić cech. Ale “trait” to słowo kluczowe Scali, i jakbym nie kochał spolszczania terminologii, uważam że lepiej używać tego pojęcia w takim brzmieniu, w jakim przyjmuje je kompilator. ↩