Python utilitza "duck typing", pero també pots utilitzar "nominative typing" per validar codi.
Introducció
Tal com vam veure a Objectes, Python utlitza el que es coneix com "duck typing", de la mateixa manera que ho fa Javascript, Go o altres llenguatges.
Pert tant, Python té tipus "flexibles" i una variable pot tenir un objecte de qualsevol tipus.
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 composar 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ò es 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 solventar aquesta situació, desde 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'apendre.
Duck typing
Python és un llenguatge que utiliza "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 una ànec i fa "cuac" com un ànec, llavors ha de ser un ànec.
Per tant, un objecte és d'un tipus concret si té tots les propietats que demana aquell tipus.
A continuació tens 3 objectes de classes diferents que són de tipud 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 pinguï?
En canvi, altres lleguatges fan servir "nominative typing", en que un objecte és d'un tipus concret perqué tu has dit expressament que era d'aquell tipus (o si es pot associar aquell tipus mijanç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 creas un objecte Duck
i un objecte Whale
, i només t'interessa tractar objectes que puguin nadar, en aquest cas concret el dos objecte 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:
> ptyhon3 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 🐳 🦆 !
> ptyhon test.py
Duck swimming
Whale swimming
Pero si vols que la ballena també voli ..
for animal in animals:
animal.swim()
animal.fly()
Python et dirà que al ballena no pot volar, no perqué Python sàpiga que le ballenes no poden volar, sinó perquè l'objecte 'Whale' no té l'atribut 'fly':
> python 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'
Mypy
Mypy is an optional static type checker for Python that aims to combine the benefits of dynamic (or "duck") typing and static typing. Mypy combines the expressive power and convenience of Python with a powerful type system and compile-time type checking. Mypy type checks standard Python programs; run them using any Python VM with basically no runtime overhead.
Instal.la l'extensió "MyPy Type Checker":
I elimina l'extensió "Pylance" si la tens instal.lada!
També pots instal.lar Mypy amb Poetry i verificar el codi desde la linia d'ordres:
TODO
Type hint
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 tipusnominatius ("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 inmutable:
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 ballena.
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 ballena a una llista en que només haurien d'haver ànecs:
I en aquest cas mypy i Pyhon estaran d'acord en que el codi esà mal escrit:
> python 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:
- mypy diu que és un error afegir una ballena a una llista en que només hi hauria d'haver ànecs
- 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:
- mypy segurirà dient que és un error afegir una ballena a una llista en que només hi hauria d'haver ànecs
- A Python, mentres que l'objecte tingui el mètode
swim()
, tant li fa que sigui una ballena, 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í 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àmter 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
:
Aquest codi, encara que funcioni, mai l'has d'escriure:
from math import pi
def area(radius: str) -> float:
return pi * float(radius) ** 2
area("3")
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.
Anota el paràmetre amb el tipus corresponent.
from math import pi
def printArea(radius: int):
area = pi * radius ** 2
print("area: " + area)
printArea(4)
mypy
informa d'aquest error: Unsupported operand types for + ("str" and "float")
Per tant, tens 2 opcions:
- Convertir l'
int
en unstr
:print("area: " + str(area))
- Utilitzar un f-string:
print(f"area: {area}")
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, estas 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 que 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 si que es produrirà 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 enlloc de tuples utilitzes una typing.NamedTuple
per donar més sentit als valors ...
from typing import NamedTuple
class Person(NamedTuple):
name: str
age: int
married: bool
person: Person
person = Person("David", 51, False)
person = Person("Gemma", "Vila", True) # Error de validació
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 que el valor potser no existex:
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 l'unió de tipus str | None | bool
que recull les tres opcions:
str
- El telèfon de la personaNone
- La persona no té telèfonbool
- Hem preguntat a la persona si té telefon: si no hem preguntat, llavaorphone == 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 reb 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)
def more_expensive(items: dict[str, float]) -> tuple[str, float] | None:
result: tuple[str, float] | None = None
for name, price in items.items():
if result is None:
result = (name, price)
else:
if price > result[1]:
result = (name, price)
return result
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 quatitat 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 els dos són clients, i de totes les seves caracteístiques només m'interessa el nom.
La sólució amb herència és crear una classe pare abstracta amb l'atribut name
i que les dos classes heretin de la principal:
classDiagram direction TD class Client { name: str } class Person { birthdate: date | None married: bool | None } class Organization { number_of_employees: int | None } Client <|-- Person Client <|-- Organization
Però això és una mala solució si aquestes dades s'han d'utitlizar d'altre 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 un aplicació d'un Institut o Universitat.
Cada situació concreta necessita una solució concreta, i només hi ha bones solucions, mai existeix la solució perfecta!
I el sistema de dades mai està en memòria, sinó en la base de dades!
Un sistema d'objectes amb estat serveix per dissenyar un intefície gràfica o simular objectes reals, mai per dissenyar un sistema de dades!
En el nostre sistema tenim registrat persones:
classDiagram direction LR class Person { name: str birthdate: date | None }
Escriu el codi corresponent:
from dataclasses import dataclass
import datetime
@dataclass
class Person:
name: str
birthdate: datetime.date | None = None
Una matèria l'ha de donar una persona de l'institut, però pot ser qualsevol persona?
@dataclass
class Subject:
name: str
classDiagram direction LR class Person { name: str birthdate: date | None } class Subject { name: str } class Deparment { name: str } class Teacher Teacher --> Person Teacher --> Deparment class Course { year: int } class Assignment Assignment --> Teacher Assignment --> Course Assignment --> Subject
Ets un professor de l'Institut perqué ets una persona assignada a un departament (tens aquest atribut)
@dataclass
class Teacher:
person: Person
# ...
I qualsevol persona de l'institut pot ser alumne d'una matèria?
És necessari crear la classe estudiant?
classDiagram direction LR class Person { name: str birthdate: date | None } class Subject { name: str } class Enrollment Enrollment --> Person Enrollment --> "1..*" Subject
@dataclass
class Enrollment:
person: Person
subjects: list[Subject]