Mòdul

  • 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 de PYTHONPATH depèn del sistema operatiu, però ha d’imitar la variable d’entorn PATH.)

    • 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()