Typing

Python utilitza "duck typing", pero també pots utilitzar "nominative typing" per validar codi.

TODO: Pendent d’adaptar a ty enlloc de mypy

Introducció

Tal com vas veure a Objecte, Python utilitza el que es coneix com a “duck typing”, de la mateixa manera que ho fa Javascript, Go o altres llenguatges.

Per tant, Python té tipus “flexibles” i una variable pot tenir un objecte de qualsevol mena.

Aquesta flexibilitat fa que llenguatges com Python i Javascript siguin molt útils, sense les greus limitacions que tenen llenguatges com Java en la reutilització de llibreries, l’herència de classes, la dificultat de compondre objectes, etc.

Però aquesta flexibilitat a vegades pot causar problemes perquè l’objecte que té la variable no és del tipus que esperem perquè no té la propietat corresponent.

Això se soluciona amb tests, però normalment els programadors, i no diguem els alumnes, no veuen la necessitat de fer tests a menys que els obliguis.

Per solucionar aquesta situació, des de fa poc temps Python ha introduït els “type hints”.

Però el primer llenguatge a fer això va ser Typescript amb Javascript, quan aquest estava molt qüestionat, i ara està tan viu que és un dels llenguatges més importants que has d’aprendre.

Duck typing

Python és un llenguatge que utilitza “duck typing” (o tipus dinàmics).

Duck typing és una aplicació del duck test per determinar si un objecte es pot utilitzar per un propòsit particular.

El “duck test” diu que si un objecte camina com un ànec i fa “cuac” com un ànec, llavors ha de ser un ànec.

Per tant, un objecte és d’un tipus concret si té totes les propietats que demana aquell tipus.

A continuació tens 3 objectes de classes diferents que són de tipus poden nadar, però només 1 pot volar i només 1 pot parlar:

Però per tu qui s’assembla més a l’ànec de l’esquerra, l’ànec “Donald” o el pingüí?

En canvi, altres llenguatges fan servir “nominative typing”, en què un objecte és d’un tipus concret perquè tu has dit expressament que era d’aquell tipus (o si es pot associar aquell tipus mitjançant mecanismes com l’herència) sense importar els atributs que té.

A continuació tens dues classes diferents que tenen el mateix mètode swim:

class Duck:
    def swim(self):
        print("Duck swimming")

class Whale:
    def swim(self):
        print("Whale swimming")

Si crees un objecte Duck i un objecte Whale, i només t’interessa tractar objectes que puguin nadar, en aquest cas concret el dos objectes són del mateix tipus perquè tots dos tenen el mètode swim:

animals = [Duck(),Whale()]

for animal in animals:
    animal.swim()

I els dos poden nadar:

> uv run test.py
Duck swimming
Whale swimming

Però que passa si un ànec també pot volar?

class Duck:
    def swim(self):
        print("Duck swimming")

    def fly(self):
        print("Duck flying")

class Whale:
    def swim(self):
        print("Whale swimming")

animals = [Duck(),Whale()]

for animal in animals:
    animal.swim()

Doncs no passa res si només vols que l’objecte nadi 🐳 🦆!

> uv run test.py
Duck swimming
Whale swimming

Però si vols que la balena també voli …

for animal in animals:
    animal.swim()
    animal.fly()

Python et dirà que al balena no pot volar, no perquè Python sàpiga que le balenes no poden volar, sinó perquè l’objecte ‘Whale’ no té l’atribut ‘fly’:

> uv run test
Duck swimming
Duck flying
Whale swimming
Traceback (most recent call last):
  File "/home/box/test/app/test.py", line 15, in <module>
    animal.fly()
    ^^^^^^^^^^
AttributeError: 'Whale' object has no attribute 'fly'

Tools

TODO: REVISAR

Pycharm

Type hinting in PyCharm

PyCharm - Support ty as a type checker

Ty

ty is an extremely fast Python type checker.

Run ty with uvx to get started quickly:

uvx ty

Use the check command to run the type checker:

uvx ty check

ty will run on all Python files in the working directory and or subdirectories. If used from a project, ty will run on all Python files in the project (starting in the directory with the pyproject.toml)

You can also provide specific paths to check:

uvx ty check example.py

Type hint

TODO. Canviar imatges a Pycharm

Els “type hints”, o anotacions, són una anotació especial que permet anotar una variable amb el tipus que tu vulguis.

Per tant, són tipus nominatius (“nominative typing”): un objecte és d’aquell tipus perquè tu has dit que és d’aquell tipus.

Per exemple, pots anotar la variable x amb el tipus bool:

x: bool
x = True
x = 1 ## Type error
print(x)

I mypy t’indicarà un error si a la variable x li assignes un int:

Però aquest error l’indica mypy, no és un error de Python.

Pots executar el codi sense problemes:

> python test.py
1

A Python li importa un rabe, són anotacions.

Però les anotacions no només són per indicar que una variable és d’una classe.

Per exemple, pots dir que una variable és immutable:

from typing import Final

num: Final = 3
num = 4

I mypy et dirà que és un error modificar el valor de la variable x, però per Python no existeixen variables inmutables.

I precaució, ves amb compte amb el tipus bool, perquè en realitat està implementat com un int: False és 0, i True és 1 quan es converteixen en números:

x: int = 3
x = "hola" # Error de validació
x = True

print(x + 1)

I ja podem tornar al nostre exemple del principi, el de l’ànec i la balena.

Com que tens activat mypy, l’editor et dirà que: "object" has no attribute "swim".

Si anotes la llista amb tipus list[Duck], mypy et dirà que l’error és afegir una balena a una llista en què només haurien d’haver ànecs:

I en aquest cas mypy i Python estaran d’acord en el fet que el codi esà mal escrit:

> uv run test.py
Duck swimming
Duck flying
Whale swimming
Traceback (most recent call last):
  File "/home/box/test/app/test.py", line 15, in <module>
    animal.fly()
    ^^^^^^^^^^
AttributeError: 'Whale' object has no attribute 'fly'

Però tal com indica l’error estan d’acord per motius diferents:

  1. mypy diu que és un error afegir una ballena a una llista en que només hi hauria d’haver ànecs
  2. Python diu que és un error cridar la funció fly en un objecte que no té aquest atribut.

Però que passa si no utilizes el mètode fly?

for animal in animals:
    animal.swim()

Doncs que:

  1. mypy segurirà dient que és un error afegir una balena a una llista en què només hi hauria d’haver ànecs
  2. A Python, mentre que l’objecte tingui el mètode swim(), tant li fa que sigui una balena, com un ànec, com una sardina.
> ptyhon test.py
Duck swimming
Whale swimming

I llavors, quina utilitat tenen els type “hints” … 😕?

Moltíssima!! Els editors et poden dir quins mètodes pots utilitzar i les eines t’avisen de que t’has equivocat.

Ajuda

A continuació tens un exemple:

def get_full_name(first_name, last_name):
    full_name = f"{first_name.title()} {last_name.title()}"
    return full_name

print(get_full_name("gemma", "vila"))

Si executes aquest programa, tens aquest resultat:

Gemma Vila

L’única dificultat que té aquest codi és saber que la classe str té el mètode title() per convertir la primera lletra a majúscula.

I segur que ningú ho sabies, i d’aquí a pocs dies segurament no recordes com es deia.

Normalment la situació és aquesta:

def get_full_name(first_name, last_name):
    full_name = f"{first_name.

Com es diu el mètode que transforma la primera lletra en majúscula: upper, uppercase, first_uppercase, capitalize, … ?

Si demanes a la IDE que t’ajudi amb Ctrl+Space, no et pot ajudar:

En canvi, si utilitzes “type hints” en els paràmetres de la funció, llavors si que et pot ajudar sense inventar-se res:

def get_full_name(first_name: str, last_name: str):
    full_name = f"{first_name.

La IDE mira quins són els mètodes de la classe str i te’ls mostra:

L’únic que has de fer es escollir el mètode que necessites.

Errors

Els “type hints” també permeten detectar errors en el codi que has escrit.

Per exemple, aquest codi té un error i mypy no el pot detectar:

from math import pi

def area(radius):
    return pi * radius ** 2

area("3")

Si vull ajuda de mypy necessito declarar tipus nominatius:

from math import pi

def area(radius: int) -> float:
    return pi * radius ** 2

area("3")

Com que el paràmetre radius està anotat amb el “tipus” int, l’editor t’avisa de que no pots cridar la funció area amb un str:

Si vols pots fer que aquest codi funcioni amb un str:

L’error és bastant evident, però la majoria no ho són.

A continuació tens un exemple:

from math import pi

def printArea(radius):
    area = pi * radius ** 2
    print("area: " + area)

printArea(4)

Digues si el codi és correcte o no sense executar el codi.

Tipus genèrics

Tots els tipus que tenen tipus interns s’anomenen tipus genèrics.

Normalment, els tipus gènerics són contenidors: un objecte que té com a propòsit contenir altres objectes.

List

Per exemple, si tens una llista i només vols que tingui objectes str, la pots anotar amb list[str].

El tipus és list, i el tipus intern (o paràmetre de tipus) és str

Diem paràmetre perqué el tipus list, en cap cas la classe list, necessita un paràmetre de tipus, igual que una funció necessita almenys un paràmetre.

A continuació tens un exemple:

def process_items(items: list[int]):
    for item in items:
        print(item)

En aquest cas, estàs dient que la variable items ha de ser una list i que tots els items de la llista han de ser int.

Si executes la funció amb una llista en què no tots són int

def process_items(items: list[int]):
    for item in items:
        print(item)

process_items([1,"a"])

“mypy” indica l’error: List item 1 has incompatible type "str"; expected "int"

Però el codi s’executarà sense error perquè la funció print pot imprimir per pantalla qualsevol objecte.

> python3 test.py
1
a

Però si modifiques la funció process_items tal com es mostra a continuació:

def process_items(items: list[int]):
    print(sum(items))


process_items([1, "a"])

Llavors sí que es produirà un error:

> python3 test.py
    print(sum(items))
          ^^^^^^^^^^
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Tuple

Una tupla també es pot anotar amb un tipus.

A continuació tens un exemple:

person: tuple[str,int,bool]
person = ("David", 51, False)
person = ("Gemma","Vila",True) # Error de validació

I si en lloc de tuples utilitzes una typing.NamedTuple per donar més sentit als valors …

Dict

Per anotar un dict has de passar 2 paràmetres de tipus: el primer per les claus, el segon pels valors.

items: dict[str, float] = {}
items["apple"] = 0.45
items["orange"] = "Very expensive" # Error de validació

Union

La unió de tipus permet que una variable pugui gestionar més d’un tipus a la vegada sense que les classes tinguin una classe en comú.

Per exemple, una variable pot referenciar el tipus str o el tipus int amb aquesta anotació:

x: bool | int = 3
x = True

x = "hola!" # Error de validació

Null

Els valors nuls són un dels grans problemes de la programació,

La majoria de llenguatges tenen problemes per gestionar els valors nuls perquè són rígids.

Però amb un llenguatge que permet un tipus “union” es resol el problema.

Per exemple tinc una variable name de tipus str:

name: str = None # Error de validació
print(f"Hello, {name}")

I aquesta variable sempre ha de tenir un nom!

En canvi puc declarar una variable name en què el valor potser no existeix:

name: str | None = None
print(f"Hello, {name}")

Un exemple absurd? No, quan no saps si el valor existirà o no existirà.

Per example, pots dissenyar una classe Person amb name i phone, on només name és obligatori:

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    phone: str | None = None


def call(person:Person):
    if person.phone is None:
        print(f"{person.name} has not phone")
    else:
        print(f"Calling {person.name} ...")

david = Person("David")
call(david)

Però None que vol dir: que la persona no té teléfon o que no sabem quin número de teléfon té?

Pots anotar la variable phone amb la unió de tipus str | None | bool que recull les tres opcions:

  • str - El telèfon de la persona
  • None - La persona no té telèfon
  • bool - Hem preguntat a la persona si té telèfon: si no hem preguntat, llavors phone == False
from dataclasses import dataclass

@dataclass
class Person:
    name: str
    phone: str | None | bool = False


def call(person:Person):

    if person.phone == False:
        print(f"We don't know if {person.name} has a phone")
    elif person.phone is None:
        print(f"{person.name} has not phone")
    else:
        print(f"Calling {person.name} ...")

call(Person("David"))
call(Person("Eva", None))

Activitat

Implementa aquesta funció, que rep com a paràmetre un dict i tornar com a resultat la tupla amb el valor més alt o None si el dict està buit:

def more_expensive(items: dict[str, float]) -> tuple[str, float] | None :

A continuació tens un codi d’ajuda:


def more_expensive(items: dict[str, float]) -> tuple[str, float] | None:
    pass


assert more_expensive({}) == None
assert more_expensive({"apple": 0.45, "orange": 0.34}) == ("apple", 0.45)

Herència

L’herència de classes és útil fins a cert límit i en uns contextos concrets.

I la prova més clara són l’enorme quantitat d’exemples absurds per explicar l’herència.

Per exemple, pots tenir dos “data class” que no tenen res a veure un amb l’altre:

from dataclasses import dataclass
import datetime

@dataclass
class Person:
    name: str
    birthdate: datetime.date | None = None
    married: bool | None = None


@dataclass
class Organization:
    name: str
    number_of_employees: int | None

Però que passa si resultat que en un ús concret per mi són el mateix?

Per exemple, per al meu programa tots dos són clients, i de totes les seves característiques només m’interessa el nom.

La solució amb herència és crear una classe pare abstracta amb l’atribut name i que les dos classes heretin de la principal:

Client

name: str

Person

birthdate: date | None

married: bool | None

Organization

number_of_employees: int | None

Però això és una mala solució si aquestes dades s’han d’utilitzar d’altra manera o s’han de relacionar amb altres dades per usos diferents.

Amb una unió de tipus pots crear un tipus Client amb TypeAlias per tractar les dos dades a la vegada:

from dataclasses import dataclass
import datetime
from typing import TypeAlias

@dataclass
class Person:
    name: str
    birthdate: datetime.date | None = None
    married: bool | None = None


@dataclass
class Organization:
    name: str
    number_of_employees: int | None

Client: TypeAlias = Person | Organization

I pots utilitzar el tipus Client sense problemes i accedir a l’atribut name directament, perque Python treballa amb “duck typing”:

client: Client = Person("David")
print(client.name)

L’únic que importa és que tingui un atribut name!

Composició

Un dels principis bàsics de programació és composar objectes, enlloc de crear relacions d’herència.

I si un mòdul, de la mateixa aplicació o d’una altra aplicació, vol afegir nous atributs al tipus Client?

Llavors has de crear la classe Client:

@dataclass
class Client:
    id: int
    party: Person | Organization
    payment: str
    credit: float

client: Client = Client(1, Person("David"))

Tipus dinàmics

El tipus “union” són molt utils quan has de crear tipus per APIs amb tipus dinàmics, i és el motiu que són un part integral de Typescript

Python no utilitza tipus dinàmics.

Activitat

Imagina’t una aplicació d’un Institut o Universitat.

En el nostre sistema tenim registrat persones:

Escriu el codi corresponent:

Una matèria l’ha de donar una persona de l’institut, però pot ser qualsevol persona?

@dataclass
class Subject:
    name: str

Ets un professor de l’Institut perqué ets una persona assignada a un departament (tens aquest atribut)

I qualsevol persona de l’institut pot ser alumne d’una matèria?

És necessari crear la classe estudiant?

1..*

Person

name: str

birthdate: date | None

Subject

name: str

Enrollment