Kacper Jankowski
mBank
Kacper Jankowski Python Developer @ mBank

Nie bój się Type Hints w Pythonie

Sprawdź, jak mądrze korzystać z mechanizmu Type Hints, aby uprzyjemnić sobie korzystanie z Pythona.
16.06.20206 min
Nie bój się Type Hints w Pythonie

Type Hints w Pythonie jest jednym z mechanizmów wprowadzających do języka kilka zalet języków statycznie typowanych, jednocześnie zachowując przy tym zalety języka dynamicznie typowanego. Przyjrzyjmy się dokładniej samym mechanizmom Type Hints oraz przeanalizujmy, skąd się biorą opory przed stosowaniem ich na co dzień. Chciałbym zaznaczyć, że odnosząc się w artykule do Type Hintingu w Pythonie, mam na myśli jego nowsze wydanie, od Pythona 3.5, nie wersję Hintingu w docstringach, stosowaną we wcześniejszych wersjach.  

Język statycznie typowany a dynamicznie typowany

Żeby w ogóle móc zacząć mówić o Type Hints, warto wyjaśnić różnice między typowaniem statycznym a dynamicznym. W językach statycznie typowanych zmienna musi mieć zadeklarowany konkretny typ i przez cały czas swojego życia ten typ pozostanie niezmienny. Z kolei przy typowaniu dynamicznym, typ nadawany jest w trakcie działania programu i może ulegać zmianie przy każdym przypisaniu do niego innej wartości.

O typowaniu dynamicznym i statycznym napisać można cały artykuł, ale nie o tym dziś mowa.

Czym jest Type Hinting

Type Hints zostały wprowadzone do Pythona w wersji 3.5 i opisane w PEP484, jednak w dalszym ciągu są rozwijane wraz z nowymi wersjami. Umożliwiają one określenie pożądanego typu zmiennych oraz typu zwracanego przez funkcję/metodę. Określenie typu nie ma wpływu na działanie aplikacji, jest jednak podpowiedzią dla wszelkiego rodzaju IDE, bądź innych narzędzi sprawdzających poprawność typów. Daje więc Pythonowi kilka zalet języka statycznie typowanego.

Co było przed wersją 3.5

To, w jaki sposób precyzowało się oczekiwane i zwracane zmienne w Pythonie przed dodaniem modułu *typing*, dobrze obrazuje poniższy screen:

Typowanie w docstringach pomogło ominąć braki w interpretowaniu Type Hitns przez język, jednocześnie informując o typach. Część narzędzi wspierało ten rodzaj typowania i również informowało na czas programistę o potencjalnych pomyłkach.


Przykłady

Składnia jest prosta i intuicyjna:

python
def multiply_string(factor: int, text: str) -> str:
   return text * factor


multiply_string('this is not an int', 'text')


Zadeklarowaliśmy funkcję multiply_string, przyjmującą argumenty: factor (oczekiwana liczba całkowita) oraz text (oczekiwany ciąg znaków) wraz z pożądanym zwracanym stringiem. Funkcja oczywiście przy próbie wykonania rzuci błąd: 

TypeError: can't multiply sequence by non-int of type 'str

 
Integracja różnych narzędzi (np: IDE) z mechanizmem Type Hints, pozwala wyłapywać takie błędy jeszcze przed uruchomieniem aplikacji. Przykładowo, w PyCharm  powyższy przykład będzie podświetlony, a po najechaniu zobaczymy na pierwszej zmiennej:

Expected type ‘int’, got ‘str’ instead


Przykład dosyć dobrze ilustruje potencjalne ratowanie programisty przed błędem w trakcie wykonywania kodu.

Podobnie użyjemy Type Hints do reszty wbudowanych typów prostych:

python
x: str = 'Lorem ipsum dolor sit amet'
x: int = 1_000_000  # https://www.python.org/dev/peps/pep-0515/ ;)
x: float = 0.5
x: bool = True


Z kolei do kolekcji, użyjemy aliasów z modułu typing:

python
from typing import List, Tuple, Set, Dict

x: List = [1, 2, 3]
x: Tuple = (1, 2, 3)
x: Set = {1, 2, 3}
x: Dict = {'one': 1, 'two': 2, 'three': 3}


Do bardziej precyzyjnego Type Hintingu służą parametry podanych wyżej aliasów. Przykładowo, jeśli chcemy sprecyzować możliwe typy, które lista powinna przyjmować, dodajemy argument do aliasu List:

python
from typing import List, Tuple, Union, Any

l1: List[int] = [1, 2, 3]  # tylko inty
l2: List[Union[int, str]] = ['text', 1, 2,]  # tylko elementy typu int lub string
l3: List[Tuple[Any, Any]] = [('1', 'two'), (3, 4.0)]  # lista tupli z dokładnie 2 elementami


A co z argumentami opcjonalnymi w funkcjach?

python
def multiply(a: int, b: int, c: Optional[int] = None) -> int:
   return a * b * c if c else a * b


multiply(5, 6)
multiply(1, 2, 3)
Optional[T]` jest skróconą wersją `Union[T, None]


Również w przypadku chęci przypisania obiektu typu callable sprawa jest prosta:

python
from typing import Callable


def do_something(): pass


fun: Callable = do_something


Type Hintsy działają również ze stworzonymi przez nas strukturami:

python
from dataclasses import dataclass


@dataclass
class Point:
   x: int = 0
   y: int = 0


p: Point = Point()


To chyba najpopularniejsze przykłady użycia, jednak samych mechanizmów jest dużo więcej, chociażby dekorator @overload czy @final, ale  do szczegółów odsyłam do dokumentacji.

Python 3.9

Gdy piszę ten artykuł, aktywnie trwają prace nad Pythonem 3.9, w którym zmienione zostało podejście do Type Hints standardowych kolekcji. Od tej wersji nie będziemy zmuszeni do korzystania z biblioteki typing, aby oznaczyć typ kolekcji, ale będziemy mogli spokojnie korzystać z wbudowanych typów:

python
x: list = [1, 2, 3]
x: tuple = (1, 2, 3)
x: set = {1, 2, 3}
x: dict = {'one': 1, 'two': 2, 'three': 3}

Zalety stosowania w projekcie

Zamiast jednak opisywać dokładnie dokumentację, chciałbym się skupić na opisaniu zalet stosowania Type Hintingu w projektach. 

Pierwsze, co jest intuicyjne i o czym wspominałem już wcześniej, to wyłapywanie większej ilości błędów typowych dla języka dynamicznie typowanego, już na etapie pisania kodu, przed jego właściwym wykonywaniem.

Jedną z cech Type Hints jest również zwiększenie czytelności kodu. Dzięki temu mechanizmowi jesteśmy w stanie na pierwszy rzut oka zobaczyć typ zmiennej, bez potrzeby dogłębnej analizy kodu. Sprawia to, że nasz kod jest samo-dokumentujący się.

Z kolei integracja z IDE sprawia, że dzięki lepszym podpowiedziom możemy też łatwiej i szybciej pisać kod, przykładowo widząc od razu typ parametru, jaki powinniśmy podać:

Skoro jest tak kolorowo, to skąd opory przed korzystaniem z tej opcji?

Mimo zalet, które zostały opisane wyżej, wielu programistów w wielu projektach nie stosuje Type Hintsów w codziennej pracy. Po rozmowach dotyczących Type Hintingu oraz bazując na projektach, z którymi miałem styczność, przewija się zazwyczaj kilka powodów. 

Po pierwsze, jest niemało projektów działających jeszcze na starszych wersjach Pythona i to nie tylko wersjach Python3.x starszych niż Python3.5, lecz także Python2.x. Takie projekty oczywiście polecam przepisać do najnowszych wersji, ale wiemy, jaka jest czasami rzeczywistość. Jeśli nie mamy możliwości podniesienia wersji, a kod nie ma Hintingu w postaci docstringów znanego ze starszych wersji, polecam wprowadzać go iteracyjnie przy okazji prac rozwojowych. 

Jeśli jednak mamy możliwość podniesienia Pythona do nowszej wersji, to również nie mamy łatwo. Otóż czeka nas teraz przedzieranie się przez wszystkie zmienne, funkcje, metody, wszystkie klasy, we wszystkich modułach i dopisywanie do każdej zmiennej jej typu, do każdej funkcji czy metody - jej zwrotki. A to najprostszy wariant. Przy wszystkich innych pracach związanych z podnoszeniem wersji języka, taka praca może wydawać się zdecydowanie mniej ważna, i do tego zajmująca dużo czasu.

Innym istotnym powodem, o którym słyszałem już od kilku osób, jest lenistwo. To te mroczki przed oczami na myśl, że trzeba po nazwie zmiennej dopisać dwukropek i jej typ, a w najgorszym wypadku trzeba dodać importy i zwrotkę dla funkcji. I tak przy każdej zmiennej, każdej funkcji i metodzie. Zrozumiałe, prawda? ;) Coś, co mogłoby takie osoby przekonać do korzystania z Type Hintsów, to jakiś automat uzupełniający to za nich. Całe szczęście jest kilka projektów rozwijających takie rozwiązania, jak np. MonkeyType, czy Pyannotate.

Podsumowanie

Type Hints, jako mechanizm wprowadzający wiele zalet języków statycznie typowanych, jest w mojej opinii ciągle mało popularny wśród programistów Pythona. Wynika to często z braku znajomości mechanizmu oraz braku czasu na zgłębienie go na tyle, aby swobodnie i ze świadomością używać go w projektach. Jednak Type Hinsty są proste i intuicyjne, polecam więc poświęcić na nie kilku minut, gdyż mogą znacząco uprzyjemnić korzystanie z Pythona.

<p>Loading...</p>