
Język Python a typowanie
Typowanie to ważny element języków programowania. Odnosi się on do tego jakie dane mogą być przechowywane w zmiennych czy atrybutach oraz do tego w jaki sposób język może konwertować jeden typ na drugi. Rozważmy prosty przykład napisany w Pythonie.
a = 10
a = "jakiś napis"
Jeśli język pozwala na takie zmiany, to mówimy, że jest to język dynamicznie typowany. Jest to cecha charakterystyczna dla języków interpretowanych – takich jak Python. W takich językach zmiana typu wartości przechowywanej w zmiennej – jak w powyższym przykładzie nie spowoduje błędu. Inna grupą języków są języki kompilowane – przykładem może być Java. Tutaj typy określamy na etapie deklaracji zmiennej. Takie typowanie nazywamy statycznym.
int liczba = 10;
liczba = „jakiś napis”;
Powyższy kod zakończy się błędem na etapie kompilacji programu. Powodem jest próba przypisania napisu do zmiennej przygotowanej do przechowywania liczb całkowitych. Z jednej strony mamy więc elastyczność. Z drugiej strony zaś ścisły rygor i porządek. Oba te podejścia mają dobre i złe strony. Pierwsze sprawdzi w krótkich skryptach, które chcemy napisać szybko. Drugie podejście lepsze będzie w złożonych projektach, rozbitych na wiele plików, często pisanych przez różnych programistów w zespole. Daje ono większą pewność poprawności napisanego kodu. Z tego by wynikało, że w Pythonie lepiej nie pisać większych projektów. Nie jest jednak tak źle, Python oferuje narzędzia, które mogą upodobnić ten język do tych typowanych statycznie. Pierwszym z nich są tzw. adnotacje typów. Zmodyfikujmy nieco pierwszy przykład:
a: int = 10
a = "jakiś napis"
Z punktu widzenia wykonywania kodu nic się nie zmienia. Kod nadal działa tak jak działał, błędu nie ma. Programista jednak widzi, że a powinno być liczbą. Świadomy tego nie powinien napisać linijki drugiej. Gdyby o tym zapomniał to przypomni mu o tym środowisko programistyczne – o ile jest w miarę zaawansowane (PyCham, VSC).

Ostateczną bronią są narzędzia do statycznej analizy typów takie jak mypy, które wyłapią takie sytuacje podobnie jak w językach kompilowanych.
Te narzędzie po pierwsze trzeba zainstalować – np. przy pomocy polecenia pip.
pip install mypy
Ważne jest by wykonać je w terminalu a nie w pythonowym interpreterze
Następnie wywołujemy mypy.
mypy .
lub
mypy example.py
Jeśli nasz kod znajduje się w pliku example.py to dostaniemy taką informację o błędzie:
example.py:2: error: Incompatible types in assignment (expression has type „str”, variable has type „int”) [assignment]
Found 1 error in 1 file (checked 1 source file)
Jeśli temat Cię zainteresował, zachęcam do pogłębienia wiedzy w dalszej części artykułu. Znajdziesz tam dodatkowe informacje o naturze typowania, krótką historię adnotacji w Pythonie oraz kilka przykładów ich zastosowania. W Dodatku 1 znajdziesz zwięzłe opisy narzędzi alternatywnych lub uzupełniających dla mypy. W Dodatku 2 przybliżymy zagadnienia związane z siłą typowania, porównując różne języki programowania.
Zapraszamy do udziału w szkoleniu Python, które przybliży Tobie ten język programowania od podstaw.
Typowanie Silne i Słabe
Podział na statyczne i dynamiczne typowanie to nie jedyne rozróżnienie. Niezależnie od tego, czy język jest statycznie, czy dynamicznie typowany, można go również klasyfikować pod względem siły typowania. Rozróżniamy typowanie silne i słabe.
Typowanie słabe
Typowanie słabe mamy wtedy, gdy język dokonuje konwersji automatycznie. Jak w poniższym przykładzie
Przykład (JavaScript):
let liczba = 10;
let tekst = "20";
let wynik = liczba + tekst; // Automatyczna konwersja do string, wynik to "1020"
Język wybrał bardziej ogólny typ jakim jest napis i dokonał automatycznej konwersji liczby do tego typu. Liczba stała się napisem, który został złączony z innym napisem. Czasem takie rozwiązanie jest super. Ale w większości przypadków może okazać się to błędem, który da o sobie znać często w zupełnie innych miejscach kodu. Raczej nie chcielibyśmy mieć wyliczonego rachunki w ten sposób.
Typowanie silne
Typowanie silne oznacza, że konwersje między różnymi typami danych muszą być jawnie zdefiniowane przez programistę. Jeśli tego nie zrobimy, to dostaniemy błąd. Języki silnie typowane, takie jak Python czy Java, minimalizują ryzyko błędów wynikających z niejawnych konwersji typów.
Przykład (Python):
liczba = 10
tekst = „20”
wynik = liczba + int(tekst) # Wymagana jawna konwersja typu
Jeśli nie dokonamy tej konwersji jawnie, to dostaniemy błąd.
liczba = 10
tekst = „20”
wynik = liczba + tekst
Traceback (most recent call last):
File „<stdin>”, line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int’ and 'str’
Znajomość typowania w językach programowania jest kluczowa dla zrozumienia, jak dany język radzi sobie z typami danych oraz jakie są jego ograniczenia i zalety. Typowanie statyczne i silne zazwyczaj zapewniają większe bezpieczeństwo typów kosztem elastyczności, podczas gdy typowanie dynamiczne i słabe oferują większą elastyczność kosztem potencjalnie większej liczby błędów w czasie wykonania.
Python jest językiem typowanym dynamicznie i silnie. Typ wartości w zmiennej może się w Pythonie zmieniać, ale konwersje nie będą zazwyczaj wykonywane automatycznie.
Adnotacje w Pythonie
Historia wsparcia dla statycznej analizy typów w Pythonie zaczyna się w 2006 roku od wprowadzenia adnotacji funkcji opisanej w dokumencie PEP 3107. Funkcjonalność ta pozwalała na dodawanie metadanych do definicji funkcji. Mogły to być np. napisy, wartości a nawet proste wyrażenia, w zasadzie dowolny obiekt Pythona.
def function(a: „pierwszy czynnik”, b: 11, c: 6 + 5, flag: bool):
…
W zasadzie nie ma jakichś ograniczeń co do wykorzystania tych danych. Z punktu widzenia dostępu do nich, umieszczone są one w słowniku, który możemy znaleźć w atrybucie __annotations__ naszej funkcji.
print(function.__annotations__)
{’a’: 'pierwszy czynnik’, 'b’: 11, 'c’: 11, 'flag’: <class 'bool’>}
Korzystając z nazwy funkcji możemy sie dostać do nich także z jej wnętrza:
def function(a: „pierwszy czynnik”, b: 11, c: 6 + 5, flag: bool):
print(function.__annotations__)
function(1, 2, 3, True)
Efekt będzie ten sam.
Można w ten sposób np. utworzyć wewnątrz funkcji kolekcję, która będzie przechowywała różne wartości podawne jako argument przy jej wywołaniu:
def foo(a: set()):
foo.__annotations__[„a”].add(a)
foo(1)
foo(102)
print(foo.__annotations__)
{’a’: {1, 102}}
Z czasem pojawił się pomysł, by wykorzystywać ten nowy mechanizm do określania typowania. I tak 2014 roku wraz z Python 3.5 pojawił się nowy dokument PEP 484, opisujący szerzej jak adnotacje powinny być używane do określania typowania. Inne możliwości wykorzystania adnotacji nadal są oczywiście możliwe.
Adnotacje typów pomagają w wielu aspektach. Kod staje się bardziej czytelny i zrozumiały, zbliżając się do samodokumentującego się kodu. Narzędzia analizy statycznej, takie jak mypy, pozwalają sprawdzić zgodność typów w kodzie bez jego uruchamiania. Istnieją również inne narzędzia, które podchodzą do tego problemu w sposób bardziej dynamiczny, o których wspominamy pod koniec artykułu. Wszystko to pozwala na wcześniejsze wykrywanie potencjalnych błędów, zwiększając tym samym niezawodność kodu.
Możemy więc określić nie tylko typ argumentów, ale też zwracanej wartości:
def greet(name: str) -> str:
return f”Hello, {name}!”
W powyższym przykładzie name: str oznacza, że argument name powinien być typu str, a -> str wskazuje, że funkcja greet zwraca wartość typu str.
Wprowadzenie adnotacji funkcji przygotowało grunt pod bardziej zaawansowane systemy typowania, takie jak moduł typing, wprowadzony w Pythonie 3.5. Moduł ten dostarcza narzędzi do określania typów złożonych, takich jak listy i słowniki. Zawiera również typ Union, który pozwala na określenie, że w danym miejscu mogą pojawić się różne typy.
from typing import List, Dict, Union
def process_items(items: List[str]) -> None:
for item in items:
print(item)
def get_user_info(user_id: int) -> Dict[str, Union[str, int]]:
return {„name”: „Alice”, „age”: 30, „id”: user_id}
W powyższym przykładzie, funkcja process_items przyjmuje listę napisów (List[str]), natomiast get_user_info zwraca słownik (Dict[str, Union[str, int]]), gdzie klucze są typu str, a wartości mogą być typu str lub int.
Warto również wspomnieć o niektórych innych kluczowych elementach modułu typing: – Optional: Wskazuje, że zmienna może być typu określonego lub None. – Any: Używane wtedy, gdy typ może być dowolny. – Callable: Definiuje typy dla funkcji. –
Oto kilka przykładów użycia Optional i Callable:
from typing import Optional, Callable, List
def find_item(items: List[str], query: str) -> Optional[str]:
for item in items:
if item == query:
return item
return None
def apply_function(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
Funkcja find_item może zwrócić ciąg znaków lub None, natomiast apply_function przyjmuje funkcję jako argument (Callable[[int, int], int]), która z kolei przyjmuje dwa argumenty typu int i zwraca int. Widzimy więc, że typowanie pozwala na precyzyjne określenie oczekiwanych typów danych.
Wraz z kolejnymi wersjami Pythona wcielane są w życie nowe pomysły na usprawnienie typowania. Od wersji Python 3.9 wprowadzono możliwość używania typów wbudowanych bezpośrednio w adnotacjach, co upraszcza składnię i eliminuje potrzebę importowania typów z modułu typing. Oto kilka przykładów użycia typów wbudowanych:
def process_items(items: list[str]) -> None:
for item in items:
print(item)
def get_user_info(user_id: int) -> dict[str, str | int]:
return {„name”: „Alice”, „age”: 30, „id”: user_id}
Od wersji Python 3.10 wprowadzono operator |, który może być używany zamiast Union do wskazywania typów unijnych. Przykład:
def handle_input(data: int | str) -> None:
if isinstance(data, int):
print(f”Received an integer: {data}”)
else:
print(f”Received a string: {data}”)
Dzięki tym usprawnieniom kod staje się jeszcze bardziej czytelny i zwięzły.
Adnotacje typów są podstawą różnych, stosunkowo nowych narzędzi i obiektów, takich jak dataclasses czy modele Pydantic. Adnotacje typów (type hints) są coraz powszechniej stosowane w Pythonie i stają się standardem, którego każdy szanujący się programista Pythona powinien umieć używać w swojej codziennej pracy. Warto dodać, że typowanie jest wykorzystywane także przez środowiska programistyczne do podpowiadania kodu w trakcie jego pisania.
Artykuł jest zaledwie wprowadzeniem do szerokiego tematu adnotacji typów. Używanie ich wraz z narzędziami do statycznej analizy typów pozwala nie tylko na uniknięcie wielu błędów, ale także daje wgląd w to, na ile architektura naszego kodu jest przemyślana, często wymuszając jej zmianę. To jednak już tematy na kolejne artykuły.
Dodatek 1 - alternatywy i uzupełnienia mypy
Mypy jest potężnym narzędziem do statycznej analizy typów w Pythonie, które pomaga w wykrywaniu błędów typów na wczesnym etapie. Dzięki prostym komendom i konfiguracji można znacznie zwiększyć niezawodność i czytelność kodu. Zachęcamy do korzystania z mypy w codziennej pracy nad projektami w Pythonie, aby zminimalizować ryzyko związane z niepoprawnymi typami w naszym kodzie.
Alternatywne narzędzia
Oto niektóre dodatkowe narzędzia, które są albo alternatywą dla mypy, albo mogą uzupełnić jego działanie:
- Pyright:
- Pyright to narzędzie do statycznej analizy typów opracowane przez Microsoft. Jest znane z szybkiego działania i głębokiej integracji z Visual Studio Code, ale działa również jako samodzielne narzędzie.
- Link: Pyright
- Pyre:
- Pyre to narzędzie do statycznej analizy typów opracowane przez Facebook (Meta). Charakteryzuje się dużą szybkością i możliwością wykrywania potencjalnych błędów w kodzie.
- Link: Pyre
- Pylint:
- Pylint to narzędzie do analizy statycznej kodu, które sprawdza zgodność z PEP 8, standardami stylu kodowania w Pythonie, oraz oferuje podstawową analizę typów. Jest bardziej wszechstronne niż mypy, ale mniej skoncentrowane na typach.
- Link: Pylint
- Typeguard:
- Typeguard to narzędzie do dynamicznej analizy typów, które sprawdza typy w czasie wykonywania programu. Może być używane jako uzupełnienie statycznej analizy typów.
- Link: Typeguard
- pytype:
- Pytype to narzędzie do analizy typów opracowane przez Google. Działa zarówno jako narzędzie do statycznej analizy typów, jak i interpreter typu, który może generować pliki stub (.pyi).
- Link: pytype
Każde z tych narzędzi ma swoje unikalne zalety. Wybór odpowiedniego narzędzia zależy od specyficznych wymagań dotyczących analizy typów, wydajności oraz integracji z istniejącymi narzędziami i środowiskami pracy. Warto też śledzić rozwój narzędzi tego rodzaju – być może pojawią się nowe, jeszcze lepsze narzędzia wspomagające nas w wytwarzaniu kodu dobrej jakości.
Dodatek 2 - Siła typowania w różnych językach - Przykłady
Siła typowania bywa różna w różnych językach. Zobrazujemy to w poniższych przykładach.
Java
Java pozwala na automatyczną promocję typów w operacjach arytmetycznych, co oznacza, że zmienne różnych typów, takie jak int i float, mogą być dodawane bez jawnej konwersji.
Przykład:
public class Main {
public static void main(String[] args) {
int liczbaCalkowita = 5;
float liczbaZmiennoprzecinkowa = 3.2f;
float wynik = liczbaCalkowita + liczbaZmiennoprzecinkowa;
System.out.println("Wynik: " + wynik); // Wynik: 8.2
}
}
Rust
Rust wymaga jawnej konwersji typów przy operacjach arytmetycznych między różnymi typami.
Przykład:
fn main() {
let liczba_calkowita: i32 = 5;
let liczba_zmiennoprzecinkowa: f32 = 3.2;
let wynik = liczba_calkowita + liczba_zmiennoprzecinkowa;
println!("Wynik: {}", wynik);
}
Powyższy kod da nam błąd:
error[E0277]: cannot add `f32` to `i32`
Działająca wersja kodu to:
fn main() {
let liczba_calkowita: i32 = 5;
let liczba_zmiennoprzecinkowa: f32 = 3.2;
let wynik = liczba_calkowita as f32 + liczba_zmiennoprzecinkowa;
println!(„Wynik: {}”, wynik); // Wynik: 8.2
}
Haskell
Haskell jest językiem silnie i statycznie typowanym, który również wymaga jawnej konwersji typów przy operacjach arytmetycznych między różnymi typami.
Przykład:
Następujący kod da nam błąd:
main :: IO ()
main = do
let liczbaCalkowita = 5 :: Int
let liczbaZmiennoprzecinkowa = 3.2 :: Float
let wynik = liczbaCalkowita + liczbaZmiennoprzecinkowa
print wynik — Wynik: 8.2
Main.hs:5:35: error:
• Couldn’t match expected type ‘Int’ with actual type ‘Float’
• In the second argument of ‘(+)’, namely
‘liczbaZmiennoprzecinkowa’
In the expression: liczbaCalkowita + liczbaZmiennoprzecinkowa
In an equation for ‘wynik’:
wynik = liczbaCalkowita + liczbaZmiennoprzecinkowa
|
5 | let wynik = liczbaCalkowita + liczbaZmiennoprzecinkowa
| ^^^^^^^^^^^^^^^^^^^^^^^
Bez błędu wykona się wersja z jawną konwersją Int na Float za pomocą funkcji fromIntegral przed wykonaniem operacji arytmetycznych.
main :: IO ()
main = do
let liczbaCalkowita = 5 :: Int
let liczbaZmiennoprzecinkowa = 3.2 :: Float
let wynik = fromIntegral liczbaCalkowita + liczbaZmiennoprzecinkowa
print wynik — Wynik: 8.2
Python
Python zachowa się podobnie jak Java. Konwersja takich kompatybilnych typów zostanie dokonana automatycznie.
Przykład:
liczbaCalkowita = 5
liczbaZmiennoprzecinkowa = 3.2
wynik = liczbaCalkowita + liczbaZmiennoprzecinkowa
print(„Wynik:”, wynik) # Wynik: 8.2
Java i Python są bardziej elastyczne w konwersjach typów podczas operacji arytmetycznych, podczas gdy Rust i Haskell wymagają jawnych konwersji. To zapewnia większą kontrolę nad typami danych i może zapobiegać potencjalnym błędom, choć elastyczność przy takich „kompatybilnych” typach jak int i float wydaje się być uzasadnionym kompromisem między ścisłym trzymaniem się typu zmiennej a elastycznością.