Python ne zahteva (in niti ne omogoča) deklaracije spremenljivk, temveč le definicije. Z "deklaracijo" moramo v nekaterih jezikih povedati, da bomo imeli spremenljivko s takšnim in takšnim imenom in tipom. Z "definicijo" teh spremenljivki damo vrednosti. Python je drugačen, saj nima spremenljivk temveč imena; ime nima tipa in pravzaprav "nima vrednosti", pač pa je ime povezano z vrednostjo.

Kakorkoli. Za lastne potrebe in za potrebe okolij lahko "deklariramo", da bo neko ime povezano z vrednostmi določenega tipa (ali, kot bi si predstavljali prišleki iz drugačnih jezikov, da bo imela spremenljivka vrednosti določenega tipa). Python se ne to ne ozira, te deklaracije lahko, kar se tiče njega, tudi kršimo.

Deklaracija tipa je preprosta. Opravimo jo lahko vnaprej ali sproti, ob določanju vrednosti. Sintaksa je takšna.

x: int

S tem povemo, da se bo ime x nanašalo na reči tipa int.

x: int = 42

S tem povemo, da se bo ime x nanašalo na reči tipa int, hkrati pa mu že priredimo vrednost.

Malo drugače je - kot bomo videli spodaj - le pri rezultatih funkcij.

Kratek izlet v zgodovino

Za deklaracije tipov obstajata dva sloga. Nemara je kdo vajen C-jevskega

int x;

C se je tega navzel iz Fortrana (1957), ki je uvedel idejo določanja tipov. Za ta slog pravijo, da je osredotočen na računalnik, ki mu je najbolj pomemben tip ("imel bom spremenljivko vrste int ..."), ne pa toliko ime ("... in ime ji bo x").

Pythonov slog,

x: int

izhaja iz Pascala (1970), ki je poudarjal berljivost. Za človeka je pomembnejše ime ("imel bom spremenljivko z imenom x") in šele potem tip ("ki bo vrste int").

Prva oblika je še vedno običajna v jezikih, ki so tudi po siceršnji sintaksi podobni C-ju ali pa celo izhajajo iz njega (Java, C#), v zadnjih desetletjih pa bolj prevladuje druga. V svojih programerskih karierah jo boste gotovo srečali vsaj v TypeScriptu, kar verjetno tudi v Rustu, ki ima popolnoma enako sintakso, pa v Kotlinu, Go-ju...

Kaj in kje

Tipe lahko določamo spremenljivkam, argumentom in rezultatom funkcij, ujetim izjemam...

def razmerje(a: int, b: int) -> float:
    return a / b

Argumentom funkcij smo določili tipe tako kot spremenljivkam. Rezultat pa smo opisali z -> float. Sintaksa : float tu ne bi delovala, saj ima dvopičje v glavi funkcije že drugo vlogo.

Sestavljeni tipi

Seznam int-ov opišemo z

razdalje: list[int]

Ne spreglejte, da uporabljamo oglate oklepaje. Okrogli bi pomenili klic.

Slovar, katerega ključi so nizi, vrednosti pa int-i, je

imenik: dict[str, int]

Za terke v osnovi predpostavimo, da so fiksne dolžine in naštejemo tipe njenih elementov. Terka, ki vsebuje niz in dva int-a, je

kraj: tuple[str, int, int]

Terka, ki vsebuje poljubno število elementov določenega tipa, na primer float, pa je

cene: tuple[float, ...]

Te stvari je seveda mogoče gnezditi:

zbirka: dict[str, tuple[str, int, list[float]]]

Unije tipov

Zgodi se (dobro pa je, da se zgodi čim manjkrat), da je za določeno ime možno, da se bo nanašala na različne tipe. Takšna imena označimo s tipom, ki ga uvozimo iz modula typing: Union. Naštejemo mu možne tipe:

from typing import Union

tri: Union[int, float, bool]

Slovar, katerega ključi so nizi, vrednosti pa int ali bool, je

imenik: dict[str, Union[int, bool]]

Pogosteje se bo zgodilo, da bo vrednost znana ali ne; če bo neznana, bo None. Vrednost None smemo uporabiti tudi kot tip.

imenik: dict[str, Union[int, None]]

Tega sicer ne počnemo, saj imao bližnjico: kadar se neko ime nanaša bodisi na reč določenega tipa bodisi na None, uporabimo tip Optional, ki ga prav tako uvozimo z typing.

from typing import Optional

imenik: dict[str, Optional[int]]

Abstraktni tipi

Kako določiti tip argumentu te funkcije?

def zlepi(s):
    v = ""
    for x in s:
        v += x
    return v

Lahko bi rekli, da je s: list[str], vendar bi funkciji s tem naredili krivico: s je lahko tudi terka, ali množica (ali celo slovar).

Pisati s: Union[list[int], tuple[int, ...], set[int] bi bilo nepraktično, pisati s: Union[list, set, tuple][int] pa narobe, ker se Union ne da uporabljati na ta način.

Ne samo to: funkcija kot argument prebavi tudi datoteko:

zlepi(open("stevilke.txt"))
'3\n5\n8\n4\n2\n8\n3\n10\n11\n5\n13\n4\n'

Funkcija sprejme karkoli, čez kar je možno iti z zanko for. Pravilna anotacija funkcije je

from collections.abc import Iterable

def zlepi(s: Iterable[str]):
    v = ""
    for x in s:
        v += x
    return v

Iterable je abstraktni osnovni tip (abstract base class), ki ga dobimo v modulu collections.abc. Pomeni točno to, kar potrebujemo "nekaj, čez kar je možno iti z zanko". (To pravzaprav ni čisto točno. Pomeni "nekaj, kar zna vrniti iterator".) Ker smo poleg tega dodali [str], smo zahtevali še, naj zanka prek te reči vrača nize.

Takšnih tipov je še zelo veliko. Dokumentacija jih opisuje precej tehnično - z metodami, ki jih podpirajo. Tako je najosnovnejši tip, Container opisan s tem, da vsebuje metodo __contains__. Če imamo

from collections.abc import Container

s: Container[int]

to pomeni, da bomo smeli v zvezi s s-jem uporabljati pogoj x in s, pri čemer bo moral biti x vrste int. Hashable so stvari, ki jih lahko uporabimo kot ključ v slovarju ali damo v množico, Sized so stvari, za katere lahko pokličemo len...

Nekatere od teh stvari so res zgolj za type annotation nazije.

Generiki

In če smo že pri njih, pokažimo še generike.

def vsota[T](s: Iterable[T]) -> T:
    v: Optional[T] = None
    for x in s:
        if v is None:
            v = x
        else:
            v += x
    return v

vsota je funkcija, ki prejme s, čez katerega je možno iterirati z zanko for. s bo pri iteriranju vračal elemente tipa T; T bo tudi rezultat funkcije. v bo bodisi T bodisi None. Ah, da, odkod pa se je vzel T? No, T je pa pač nek tip - mogoče float, mogoče int, mogoče str. Uvedli smo ga s [T] v vsota[T].

Tole sicer še ni čisto popolno: povedati bi morali še, da je T neka stvar, ki se jo da seštevati. Če je T slučajno slovar (in naše oznake tega ne prepovedujejo!), bo funkcija javila napako v v += x.

To se menda da, vendar tega nisem še nikoli počel. In tudi ne mislim. :)