Els mòduls contenen un conjunt de funcions, variables o classes per a poder-los usar en qualsevol programa.
Introducció
La programació modular fa referència al procés de dividir una tasca de programació gran i feixuga en subtasques o mòduls separats, més petits i manejables. Després, els mòduls individuals es poden combinar com si fossin blocs de construcció per crear una aplicació més gran.
Un mòdul Python és un fitxer .py
que allotja un conjunt de funcions, variables o classes per a poder-los usar en qualsevol programa.
Crea un projecte module
amb UV
uv init module
Modul
Crear els nostres propis mòduls és molt fàcil 😉
Per exemple, podem definir un mòdul math.py
amb dues funcions add()
i subtract()
.
def add(a, b):
return a + b
def subtract(a, b):
return a - b
Un cop definit, aquest mòdul pot ser usat o importat en un altre fitxer.
Modifica el fitxer main.py
:
assert add(5,4) == 9
Si executes el fitxer main.py
python donarà un error:
Traceback (most recent call last):
File "C:\Users\david\Workspace\module\main.py", line 1, in <module>
assert add(5, 4) == 9
^^^
NameError: name 'add' is not defined
L’script main.py
ha d’importar de manera explícita les funcions del mòdul math.py
per poder-les utilitzar.
import math
assert math.add(5, 4) == 9
Si tornes a executar el fitxer main.py
l’error serà diferent.
Traceback (most recent call last):
File "C:\Users\david\Workspace\module\main.py", line 3, in <module>
assert math.add(5, 4) == 9
^^^^^^^^
AttributeError: module 'math' has no attribute 'add'
Diu que el mòdul math
no té l’atribut add
Search Path
Quan l’intèrpret executa la sentència d’importació anterior, busca mod.py
en una llista de directoris formada a partir de les fonts següents:
-
El directori des del qual s’ha executat l’script d’entrada o el directori actual si l’intèrpret s’està executant de manera interactiva.
-
La llista de directoris continguda a la variable d’entorn
PYTHONPATH
, si està definida. (El format dePYTHONPATH
depèn del sistema operatiu, però ha d’imitar la variable d’entornPATH
.) -
Una llista de directoris dependent de la instal·lació, configurada en el moment d’instal·lar Python
La ruta de cerca resultant és accessible a la variable de Python sys.path
, que s’obté d’un mòdul anomenat sys
.
Modifica el fitxer main.py
:
import sys
print(sys.path)
Pots veure el path de cerca:
['C:\\Users\\david\\Workspace\\module', 'C:\\Users\\david\\AppData\\Roaming\\uv\\python\\cpython-3.13.5-windows-x86_64-none\\python313.zip', 'C:\\Users\\david\\AppData\\Roaming\\uv\\python\\cpython-3.13.5-windows-x86_64-none\\DLLs', 'C:\\Users\\david\\AppData\\Roaming\\uv\\python\\cpython-3.13.5-windows-x86_64-none\\Lib', 'C:\\Users\\david\\AppData\\Roaming\\uv\\python\\cpython-3.13.5-windows-x86_64-none', 'C:\\Users\\david\\Workspace\\module\\.venv', 'C:\\Users\\david\\Workspace\\module\\.venv\\Lib\\site-packages']
Anem a veure d’on ve el mòdul math
:
import importlib.util
spec = importlib.util.find_spec("math")
print(spec)
Ara l’script mostra l’origen del mòdul (per exemple, “built-in” o un camí de fitxer si és disponible):
ModuleSpec(name='math', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in')
En molts entorns, el mòdul estàndard math
és un mòdul integrat o d’extensió.
Com que no estàs executant python de manera interactiva, el mòdul math
que està instal·lat per defecte té prioritat respecte al mòdul math
que has creat en el projecte module
.
Modifica el nom del fitxer math.py
per mymath.py
.
Si modifiques el fitxer main.py
:
import importlib.util
spec = importlib.util.find_spec("mymath")
print(spec)
Pots veure que ara el mòdul és el teu: 😁
ModuleSpec(name='mymath', loader=<_frozen_importlib_external.SourceFileLoader object at 0x000001E7ECD5BB60>, origin='C:\\Users\\david\\Workspace\\module\\mymath.py')
I ja pots importar i utilitzar el mòdul mymath
en el fitxer main.py
:
import mymath
assert mymath.add(9, 3) == 12
assert mymath.subtract(10, 2) == 8
La sentència import
El contingut d’un mòdul es posa a disposició del que importa amb la sentència import
.
La sentència import
té moltes formes diferents, que es mostren a continuació.
La forma més simple és la que ja hem vist més amunt:
import mymath
Tingues en compte que això no fa que el contingut del mòdul sigui accessible directament per al que importa. Cada mòdul té la seva pròpia taula de símbols privada, que fa de taula de símbols global per a tots els objectes definits dins del mòdul.
Així doncs, un mòdul crea un espai de noms separat, com ja s’ha indicat.
La sentència import mymath
només col·loca mymath
a la taula de símbols del mòdul que importa mypath
.
Els objectes definits dins del mòdul romanen a la taula de símbols privada del mòdul.
Des del que importa, els objectes del mòdul només són accessibles quan es prefixen amb mymath
mitjançant la notació de punt, tal com es mostra a continuació.
import mymath
assert mymath.add(9, 3) == 12
assert mymath.subtract(10, 2) == 8
Després de la sentència import
següent, mymath
es col·loca a la taula de símbols local.
Així, mymath
té sentit en el context local del que importa:
import math, mymath
print(math)
print(mymath)
<module 'math' (built-in)>
<module 'mymath' from 'C:\\Users\\david\\Workspace\\module\\mymath.py'>
Però add
i subtract
continuen a la taula de símbols privada del mòdul i no tenen sentit en el context local:
import mymath
assert add(9, 3) == 12
assert subtract(10, 2) == 8
Per poder-hi accedir en el context local, els noms dels objectes definits al mòdul s’han de prefixar amb mymath
.
To be accessed in the local context, names of objects defined in the module must be prefixed by mymath
.
from
Una forma alternativa de la sentència import
permet importar objectes individuals del mòdul directament a la taula de símbols del que importa:
from mymath import add, subtract
assert add(9, 3) == 12
assert subtract(10, 2) == 8
Com que aquesta forma d’import
col·loca els noms dels objectes directament a la taula de símbols del que importa, qualsevol objecte que ja existeixi amb el mateix nom serà sobreescrit:
from mymath import add
def add(a, b):
return a * b
assert add(2, 5) == 10
Si executes el codi, veuràs que add(2, 5)
retorna 10
en lloc de 7
, que seria el resultat si s’executés mymath.add(2, 5)
.
as
També pots importar un mòdul sencer amb un nom alternatiu:
import mymath as mm
assert mm.add(2, 5) == 7
També és possible importar objectes individuals però introduir-los a la taula de símbols local amb noms alternatius:
from mymath import add, subtract as sub
assert sub(4, 3) == 1
Això permet col·locar noms directament a la taula de símbols local, però evitar conflictes amb noms que ja existeixen.
Importar dins d’una funció
El contingut d’un mòdul es pot importar dins de la definició d’una funció.
En aquest cas, la importació no es produeix fins que no es crida la funció:
def foo():
import mymath as mm
assert mm.add(4, 3) == 7
foo()
Finalment, pots utilitzar una sentència try
amb una clàusula except ImportError
per protegir-se d’intents d’importació fallits:
try:
import magicmath
except ImportError:
print("Module not found")
print("All right!")
El codi s’executa fins al final:
Module not found
All right!
Executar un mòdul com a script
Qualsevol fitxer .py
que conté un mòdul és, essencialment, també un script de Python, i no hi ha cap motiu pel qual no es pugui executar com a tal.
mymath.py
es pot executar com a script:
python mymath.py
No hi ha errors, així que aparentment ha funcionat. Certament, no és gaire interessant. Tal com està escrit, només defineix objectes. No fa res amb ells i no genera cap sortida.
There are no errors, so it apparently worked. Granted, it’s not very interesting. As it is written, it only defines objects. It doesn’t do anything with them, and it doesn’t generate any output.
Modifica el mòdul de Python anterior perquè generi alguna sortida quan s’executi com a script:
def add(a, b):
return a + b
def subtract(a, b):
return a - b
print("Hello World!")
Ara hauria de ser una mica més interessant:
> python mymath.py
Hello World!
Malauradament, ara també genera sortida quan s’importa com a mòdul:
from mymath import subtract as sub
assert sub(4, 3) == 1
> python main.py
Hello World!
Això probablement no és el que vols. No és habitual que un mòdul generi sortida quan s’importa.
No estaria bé poder distingir entre quan el fitxer es carrega com a mòdul i quan s’executa com a script independent?
Quan un fitxer .py
s’importa com a mòdul, Python estableix la variable especial dunder __name__
amb el nom del mòdul.
Tanmateix, si un fitxer s’executa com a script independent, __name__
s’estableix a la cadena '__main__'
.
Utilitzant aquest fet, pots discernir quin cas es dona en temps d’execució i alterar el comportament en conseqüència:
def add(a, b):
return a + b
def subtract(a, b):
return a - b
if __name__ == '__main__':
print("Hello World!")
Ara, si s’executa com a script, obtens sortida:
> python mymath.py
Hello World!
Però si s’importa com a mòdul, no:
> python main.py
>
Sovint, els mòduls es dissenyen amb la capacitat d’executar-se com a script independent per provar la funcionalitat que contenen. Això es coneix com a proves unitàries (unit testing).
def subtract(a, b):
return a - b
if __name__ == '__main__':
assert subtract(10, 3) == 7
print("All right!")
El fitxer es pot tractar com a mòdul, i es pot importar la funció subtract()
:
> python main.py
>
Però també es pot executar com a script independent per fer proves:
> python mymath.py
All right!
Tornar a carregar un mòdul
Per motius d’eficiència, un mòdul només es carrega una vegada per sessió de l’intèrpret. Això està bé per a definicions de funcions i classes, que solen constituir la major part del contingut d’un mòdul. Però un mòdul també pot contenir sentències executables, normalment per a inicialització. Tingues present que aquestes sentències només s’executaran la primera vegada que s’importi un mòdul.
TODO: Exemple amb una connexió a una base de dades o similar…
Paquets
Suposa que has desenvolupat una aplicació molt gran que inclou molts mòduls. A mesura que creix el nombre de mòduls, es fa difícil mantenir-ne el control si es deixen tots en un únic lloc. Això és especialment cert si tenen noms o funcionalitats similars. Potser desitjaràs un mitjà per agrupar-los i organitzar-los.
Els paquets permeten estructurar jeràrquicament l’espai de noms dels mòduls utilitzant la notació de punts. De la mateixa manera que els mòduls ajuden a evitar col·lisions entre noms de variables globals, els paquets ajuden a evitar col·lisions entre noms de mòduls.
Crear un paquet és força senzill, ja que fa ús de l’estructura jeràrquica inherent del sistema de fitxers del sistema operatiu.
Considera la disposició següent:
TODO
REVISAR
Organització de mòduls.
És possible i molt habitual accedir a mòduls ubicats en una subcarpeta per a separar encara millor el codi. Imaginem la següent estructura:
.
├── exempleX.py
├── carpetaX
│ └── modulX.py
On modulo.py conté el següent:
# modulX.py
def hola(nom = "a tothom!"):
print(f"Hola {nom}")
Des del nostre exempleX.py, podem importar el mòdul modulX.py de la següent manera:
from carpetaX.modulo import *
print(hola())
# Hola a tothom!
Mòduls i Funció Main
Un problema molt recurrent és quan creem un mòdul amb una funció com al següent exemple, i afegim algunes sentències a executar.
modulB.py
def suma(a, b):
return a + b
c = suma(1, 2)
print("La suma es:", c)
Si en un altre mòdul importem el nostre modulB.py, tal com està el nostre codi el contingut s’executarà, i això pot no ser el que vulguem.
modulC.py
import modulB
# Sortida: La suma es: 3
Depenent de la situació, pot ser important especificar que només volem que s’executi el codi si el mòdul és el __main__
.
Anem a veure-ho amb aquest exemple; remarcant els punts importants:
modulMainB.py
import modulB
def main():
c = suma(5, 3)
if __name__ == "__main__":
main()
if name == “main”: Aquest bloc només s’executa si el programa s’executa com un script. Per exemple:
$ python modulMainB.py
Quan el fitxer s’importa com un mòdul, aquest bloc no s’executa.
Funció main(): Conté la lògica principal del programa, desglossada en subfuncions que es poden reutilitzar per separat.
Subfuncions: suma() és cridades des de main(), facilitant la lectura i manteniment del codi.
Activitats
Crea un mòdul anomenat epidemic_utils.py
que tingui funcions d’utilitat per a calcular taxes epidemiològiques de malalties.
Les 2 funcions que us demanem crear són:
Taxa d’incidència: mesura el nombre de casos nous d’una malaltia en una població específica durant un període de temps determinat.
Els paràmetres d’entrada que tenim són:
- casos_nous
- poblacio_total
Normalment, s’expressa per cada 1.000 o 100.000 persones. En aquest cas serà per cada 1000 persones.
Taxa de Mortalitat: Mesura el nombre de morts causades per una malaltia en una població durant un període determinat.
Els paràmetres d’entrada que tenim són:
- numero_morts
- casos_totals
S’expressa com un percentatge o per cada 1.000 persones. Per unificar les dues mesures serà per cada 1000 persones.
Fórmules
En segon lloc, has de crear un mòdul ( epidemic_tests.py
) amb mètodes de test per a les dues funcions, aquest ha d’importar el mòdul epidemic_utils.py
.
Si et fa falta, revisa com funcionen els tests en Python.
Per últim, crea un mòdul anomenat epidemic_main.py
que contingui la funció main i a sota de tot el bloc de codi que permeti executar el mòdul com un script:
if __name__ == "__main__":
main()
# epidemic_utils.py
def calcular_taxa_incidencia(casos_nous, poblacio_total):
"""
Calcula la taxa d'incidència per cada 1000 persones.
:param casos_nous: Nombre de casos nous.
:param poblacio_total: Població total.
:return: Taxa d'incidència.
"""
if poblacio_total <= 0:
raise ValueError("La població total ha de ser major que zero.")
return (casos_nous / poblacio_total) * 1000
def calcular_taxa_mortalitat(morts, casos_totals):
"""
Calcula la taxa de mortalitat per cada 1000 persones.
:param morts: Nombre de morts.
:param casos_totals: Nombre total de casos.
:return: Taxa de mortalitat.
"""
if casos_totals <= 0:
raise ValueError("El nombre total de casos ha de ser major que zero.")
return (morts / casos_totals) * 1000
# epidemic_tests.py
import pytest
from epidemic_utils import calcular_taxa_incidencia, calcular_taxa_mortalitat
def test_calcular_taxa_incidencia():
assert calcular_taxa_incidencia(30, 20000) == 1.5
assert calcular_taxa_incidencia(0, 10000) == 0.0
with pytest.raises(ValueError):
calcular_taxa_incidencia(10, 0)
def test_calcular_taxa_mortalitat():
assert calcular_taxa_mortalitat(5, 100) == 50.0
assert calcular_taxa_mortalitat(0, 1000) == 0.0
with pytest.raises(ValueError):
calcular_taxa_mortalitat(1, 0)
# Executa les proves si s'executa aquest fitxer directament
if __name__ == '__main__':
pytest.main()
# epidemic_main.py
from epidemic_utils import calcular_taxa_incidencia, calcular_taxa_mortalitat
def main():
# Exemple d'ús de les funcions
poblacio = 20000
casos_nous = 30
morts = 5
casos_totals = 100
taxa_incidencia = calcular_taxa_incidencia(casos_nous, poblacio)
taxa_mortalitat = calcular_taxa_mortalitat(morts, casos_totals)
print(f"Taxa d'incidència: {taxa_incidencia} casos per 1.000 persones")
print(f"Taxa de mortalitat: {taxa_mortalitat} morts per 1.000 persones")
if __name__ == "__main__":
main()