python

Unitat Didàctica 2: Estructures de control

Estructures de Control en Python

Execució Seqüencial

L'execució seqüencial és el comportament per defecte en un programa Python, on les instruccions es processen en l'ordre en què apareixen al codi. En altres paraules, les línies de codi s'executen de dalt a baix, una per una, sense saltar-se cap instrucció.

Exemple d'Execució Seqüencial

Quan s'executa un programa, el flux de control segueix una seqüència lineal. Aquí tens un exemple senzill:

# Programa d'exemple per a execució seqüencial
nom = input("Escriu el teu nom: ")
edat = int(input("Escriu la teva edat: "))
anys_per_100 = 100 - edat
edat_mes_10 = edat + 10
edat_doble = edat * 2
nom_majuscules = nom.upper()
nom_minuscules = nom.lower()
llargada_nom = len(nom)
inicial = nom[0]
print("Hola", nom_majuscules)
print("El teu nom té", llargada_nom, "lletres")
print("La inicial del teu nom és:", inicial)
print("D'aquí", anys_per_100, "anys tindràs 100 anys")
print("D'aquí 10 anys tindràs", edat_mes_10)
print("El doble de la teva edat és", edat_doble)               
sequencial

En este exemple, el programa executa primer les peticions de dades amb input, després realitza uns càlculs i per fi imprimeix els resultats. No hi ha condicions ni bucles ni salts, per tant, cada instrucció es fa de manera seqüencial.

Fluxe de Control

En l'execució seqüencial, el flux de control sempre segueix el codi de dalt a baix. No hi ha salts de línia ni condicions que puguin alterar aquest ordre.

Això és el que fa que el programa siga molt senzill de comprendre i mantenir en comparació amb programes més complexos que utilitzen altres estructures de control com condicions, bucles o funcions.

L'execució seqüencial és la forma més bàsica d'execució en un programa. Cada línia s'executa una darrere de l'altra. Encara que aquest tipus d'execució és útil per a programes senzills, la majoria de programes utilitzen estructures de control com if, for, while, etc., per gestionar fluxos de control més complexos.


condicional

Execució Condicional (if, elif, else)

Les estructures condicionals permeten executar blocs de codi segons les condicions que s'hi especifiquen. El operador : marca l'inici d'un bloc de codi estructurat i indentat, i és essencial perquè Python interprete correctament la jerarquia del programa.

La indentació s'indica amb 4 espais (mínim) o un tabulador. Aquest principi és fonamental en Python, ja que defineix l'estructura del programa, a diferència d'altres llenguatges que utilitzen claus {}.

A Python la indentació no és només estètica, és part de la sintaxi, i sense ella el codi simplement no funciona.

La indentació no només s’usa en un if, sinó en tots els blocs de codi on cal indicar jerarquia o dependència. És una part fonamental de la sintaxi. S'usa també en: Bucles for i while, Funcions (def), Excepcions (try, except, finally), match-case, Classes (class), Blocs with

Exemple d'ús del condicional if - else

edat = int(input('Quina edat tens ? '))
if edat >= 18:
    print("Eres major d'edat")     # aquesta ordre està indentada
    print("==================")    # aquestes ordres formen un bloc
else:
    print("Eres menor d'edat")  # aquesta ordre també està indentada        

Exemple d'ús del condicional if amb elif i else

n = float(input("Quina nota has tret en l'examen?"))
if n<=4:
    print ("Suspes")
elif n<=6:
    print("Aprovat")
elif n<=8:
    print("Notable")
else:
    print("Excelent")          

Condicional en una sola línia

Si la estructura només té una línia, no és necessari indentar, tot i que és habitual fer-ho així:

if edat >= 18: print("Eres major d'edat")        

Condicional Ternari en Python

Aquesta estructura permet assignar un valor a una variable segons una condició en una sola línia de codi, evitant així la necessitat de tenir un bloc de codi amb múltiples línies.

Sintaxi:

valor_si_veritable if condició else valor_si_fals

Exemple:

edad = 20  # Exemple d'edat, o edad=input('Dis-me edad:')
missatge = "Major d'edat" if edad >= 18 else "Menor d'edat"
print(missatge)  # Eixida: Major d'edat         

També es pot posar dins del print

print ( "Major" if edat>18 else "Menor")

match-case

Des de Python 3.10 està disponible la instrucció match-case, una estructura pareguda al switch (d'altres llenguatges)

value = int(input("Dona'm un valor sencer: "))
match value:
    case 1:
        print ( "Uno" )
    case 2:
        print ( "Dos" )
    case 3:
        print ( "Tres" )
    case _:
        print ( "Otro valor" )        

case _: funciona com default


value = int(input("Dona'm un valor sencer: "))
match value:
    case 1 | 2:
        print ( "Uno o Dos" )
    case 3:
        print ( "Tres" )
    case _:
        print ( "Otro valor mayor que 3" )       

En un case es poden posar més d'un valor, separant amb |


Guia de Estils de Python (PEP 8)

El moment de definir la indentació és una bona ocasió per parlar de les guies d'estil oficials per al codi Python. El document PEP 8 (Python Enhancement Proposals) estableix les millors pràctiques per tal de fer el codi més llegible, consistent i mantenible.

Principals Regles de PEP 8





bucles

Bucles (for, while) en Python

Els bucles són una estructura essencial en la programació que permet repetir un conjunt d'operacions múltiples vegades.

1. Bucles for

El bucle for itera sobre una secuència (com una llista o un rang). Aquí tens alguns exemples:

Exemple 1: Iteració amb range

La funció range és una eina molt útil a Python per generar una seqüència d'elements. Aquesta seqüència es pot utilitzar en estructures de control de flux com el bucle for per realitzar iteracions de manera eficient.

for i in range(5):  # Des de 0 fins a 4
    print(i)

Exemple 2: Iteració amb range amb límits específics

for i in range(2, 5):  # Des de 2 fins a 4   🤔Recorda al rebanat !!
    print(i)

Exemple 3: Iteració amb un pas definit

for i in range(2, 10, 2):  # Des de 2 fins a 8 amb pas 2
    print(i)

Exemple 4: Iteració cap enrere

for i in range(12, 7, -1):  # Des de 12 fins a 8 amb compte enrere
    print(i)

Exemple 5: Iteració sobre una cadena (string)

for lletra in "Python":
    print(lletra)

Exemple 6: Iteració sobre una llista

colla = ["Pikachu", "Charizard", "Bulbasaur"]    # Açò és una llista, que es vorà més endavant
for i in colla:
    print(i)

Paraules clau dins d'un bucle for:

2. Bucles while

El bucle while executa una acció (bloc de codi) mentre una condició siga certa (True).

while condicio:
    # codi que es repeteix
    # poden ser una o més línies
    # No pot quedarse buit (sense codi)
    

Exemple: Bucles while

contador = 0
while contador < 5:
    print(f"Contador: {contador}")
    contador += 1  # Augmenta el contador
print(f"El contador després d'acabar el while val: {contador}")  
    Resum:
  1. while repeteix codi metre la condicio siga True
  2. S'ha de vigilar de no crear bucles infinits (incrementar comptador, introduir valors nous cada iteració, etc...)
  3. És útil quan no se sap quantes repeticions faran falta (a diferència de for )

Els bucles for i while són molt útils per repetir accions múltiples vegades i controlar el flux de l'execució dels programes en funció de condicions específiques.


Clàusula else en bucles en Python

Aquesta tècnica de fer servir else després d’un bucle while o for en Python s’anomena:

▶️ Clàusula else en bucles (Loop else clause)

Descripció:

És una característica pròpia de Python (i poc comuna en altres llenguatges) que permet executar un bloc de codi quan el bucle s'acaba sense un break.

Es pot aplicar a:

Per què és útil?

Permet distingir entre:

Exemple típic:

for numero in [1, 3, 5, 7]:
    if numero % 2 == 0:
        print("Té un nombre parell")
        break
else:
    print("Cap nombre parell trobat")

🔹 Resultat: Si no es troba cap parell, s’executa l’else.


Estructura de Control de Flux: pass

El pass és una instrucció que es pot utilitzar com a marcador de lloc. Serveix quan cal una sintaxi vàlida però no es vol que el codi faci res en una part específica.

Exemple d'ús de pass

if condición:
    pass  # No hace nada, pero se puede dejar como espacio para agregar código más tarde

El pass és útil quan necessites una sintaxi vàlida sense que el codi realitzi cap acció.

Salts en Python

En Python es poden fer salts en el flux d'execució, però d'una manera limitada i controlada.
Python no té una instrucció 🚫goto🚫 , com altres llenguatges antics (C, BASIC...), però pots modificar el flux amb altres estructures.

🔀 Maneres de fer "salts" en Python

1. break

Surt d’un bucle (for o while) immediatament.

for i in range(10):
    if i == 5:
        break
    print(i)
# Resultat: 0, 1, 2, 3, 4

2. continue

Salta a la següent iteració del bucle, sense executar la resta del codi de dins.

for i in range(5):
    if i == 2:
        continue
    print(i)
# Resultat: 0, 1, 3, 4

3. return

Surt immediatament d’una funció i pot retornar un valor.

def verifica(x):
    if x < 0:
        return "Negatiu"
    return "Positiu o zero"

4. raise

Fa un "salt" provocant una excepció: trenca l'execució normal.

def dividir(x, y):
    if y == 0:
        raise ValueError("No es pot dividir per zero")
    return x / y

🚫 No hi ha goto en Python

Python no permet goto perquè va en contra dels principis de llegibilitat i claredat que promou el llenguatge. En lloc d’això, es recomana:

✅ Alternatives recomanades

Manejador de Errors / Excepcions en Python

En el següent programa, poden donar-se dos errors

a=int(input('Dame un numero:  '))
print(f'La inversa de {a} es { 1/(a) }')
print(' ===  Fi del programa  ===')

Si posem una lletra

Dame un numero:  t
Traceback (most recent call last):
  File "c:\Users\enrique\OneDrive - Conselleria d'Educació\_PYTHON\proves\_prova.py", line 2, in 
    a=int(input('Dame un numero:  '))
ValueError: invalid literal for int() with base 10: 't'
==> I el programa es para....

L'última línea no s'executa si es produeix un error. Cap línia després de l'error s'executarà


Si posem un 0

Traceback (most recent call last):
  File "c:\Users\enrique\OneDrive - Conselleria d'Educació\_PYTHON\proves\_prova.py", line 5, in 
    print(f'La operacion resulta  {1/(a)}')
                                   ~^^~~
ZeroDivisionError: division by zero
==> I el programa es para....

En Python, utilitzem les estructures try i except per capturar i gestionar els errors que poden sorgir durant l'execució d'un programa.

Exemple de Manejador d'Errors

try:
    resultado = 10 / 0
except ZeroDivisionError:
    print("Error: No se puede dividir entre cero.")
else:
    print("La operación ha sido correcta")
finally:
    print("Esta parte se ejecuta siempre")
print("Y por supuesto el programa continua después del try")

Millora al codi incial

try:
    a=int(input('Dame un numero: '))
    print(f'La inversa de {a} es {1/(a) }')
except ValueError:
    print('Introduce un numero, no una letra')
except ZeroDivisionError:
    print('division por 0....')
except Exception
    print('Otro error no previsto.')

print('=====  Fin del programa  ======')

El manejador d'errors amb try i except és essencial per controlar excepcions i evitar que el programa es pare en cas d'errors inesperats.

Funcions en Python

Les funcions en Python són blocs de codi que es poden reutilitzar per realitzar operacions específiques. Aquestes poden ser :

  • Funcions incorporades (Built-In Functions) que Python proporciona per realitzar operacions comunes com càlculs matemàtics, conversions de tipus de dades, obtenció d'informació del sistema, entre altres.
  • Funcions personalitzades Funcions definides per l'usuari
  • Funcions incorporades

    Conversió de tipus

    Python inclou diverses funcions per convertir entre diferents tipus de dades:

    ASCII i Unicode Al final de la unitat hi ha una ampliació d'este punt d'ASCII i Unicode

    Exemples:  int("123") # 123 ,  int("-45") # -45 ,  int("1010", 2)  # 10 (binari)   , int("FF", 16) # 255 (hexadecimal) 
    int(3.9)  # 3 (Trunca la part decimal (no arrodonix))    int(-3.9)  # -3  ,   int(True)  # 1   int(False)  # 0 
    bool() → False  per als valors: False, 0, 0.0, "" (cadena buida), [], (), {}, set() (estructures buides),  None 
    A estos valors se'ls anomena (valors falsy), a la resta, (valors truthy)
    La resta (valors truthy) → True. 📌 Detall important: bool() no mira el contingut, només si el valor és “buit” o “zero”. 
    bool("False")  # True 😅 

    Funcions útils amb variables

    Algunes funcions que es poden utilitzar per obtenir informació sobre variables són:

    Funcions matemàtiques

    Python també inclou diverses funcions per realitzar càlculs matemàtics:

    Enllaços d'interès

    Per a més funcions útils de Python, consulta la documentació oficial: Funcions Built-In en Python

    Funcions de Cadenes en Python

    Python proporciona diverses funcions incorporades (Built-In Functions) per treballar amb cadenes de text. Aquestes funcions permeten realitzar operacions comunes com modificar el format de la cadena, cercar subcadenes, o fins i tot combinar cadenes.

    Funcions comunes per treballar amb cadenes:

    Us de funcions: 'pedro'.upper() v.upper() upper(v) pero… len('sdfs') v.len() str(335)

    Nombres aleatoris

    Els nombres aleatoris són nombres que es generen a l’atzar, és a dir, sense seguir cap patró previsible. Cada nombre té la mateixa probabilitat de sortir, i no es pot saber quin serà el següent.

    Els ordinadors, en realitat, solen generar nombres pseudoaleatoris: semblen aleatoris, però provenen de càlculs matemàtics.

    # En python : exemple
    from random import randrange
    x=randrange(1,21)   
       # genera un nombre aleatori entre 1 i 20 (inclosos)        

    Dates

    Per poder treballar amb dates en python es necessita importar funcions específiques. La primera que usarem per obtenir la data actual serà date i es pot vore un exemple de com utilitzar-la

    from datetime import date
    
    avui = date.today()
    print(avui)        # 2026-02-10 (per exemple)
    print(avui.year)   # any
    print(avui.month)  # mes
    print(avui.day)    # dia    

    I per saber l'hora (minut, segon ) s'usarà datetime

    from datetime import datetime
            
    ara= datetime.now()
    print(ara)
    print(ara.hour)
    print(ara.minute)
    print(ara.second)  

    Funcions Definides per l'Usuari en Python

    A més de les funcions incorporades en el llenguatge, Python permet la definició de funcions noves, també conegudes com a funcions definides per l'usuari.

    Les funcions definides per l'usuari són blocs de codi reutilitzables que realitzen una tasca específica. Aquestes funcions es creen utilitzant la paraula clau def, seguida del nom de la funció i parèntesis que poden incloure paràmetres. El cos de la funció està indentat i pot incloure declaracions, càlculs i una declaració return per retornar un valor.

    Exemple de Funció Definida per l'Usuari

    # Definir funció
    def saluda():
        print("Hola mon")
        print("Soc Python !!")
        
    # Cridar la funció
    saluda()      # Esta funció no retorna cap valor, sols imprimeix
                  # En realitat torna el valor   None    
    def sumaEsp(a, b):
        if a > b :        
           resultat = a + b + b + 1
        else:
           resultat = a + a + b + 2
        return resultat
    
    # Cridar la funció
    resultat_suma = sumaEsp(3, 5)    # Esta funció no imprimeix res, pero torna un valor
    print(resultat_suma)      # Eixida: 13
    print ( sumaEsp ( 243, 521) )  # Eixida 1009
            

    En aquest exemple, la funció sumaEsp(a, b) rep dos paràmetres, a i b, i retorna la seua suma "especial".

    Components d'una Funció Definida per l'Usuari:

    La indentació fa referència a la tècnica d'afegir espais al principi de les línies de codi, el que ajuda a delimitar visualment els blocs i les estructures de control.

    En Python, la indentació és molt important ja que és la manera que utilitza el llenguatge per definir l'estructura del codi, a diferència d'altres llenguatges que utilitzen claus ({}) per identificar els blocs. Un codi sense una correcta indentació pot generar errors d'execució o de lògica.

    On / Quan definir les funcions

    S'ha de tindre en compte que, fins que no s'ha definit una funció, no es pot fer servir. Python llegirà el codi de dalt a baix, i si encara no coneix la funció… => ❌ Error: NameError

    resultat = suma(3, 5)   # ❌ ací encara no existeix
        
    def suma(a, b):
        return a + b    
    NameError: name 'suma' is not defined 

    Així que, quan abans, millor. Si pot ser, al principi del codi.

    Funcions amb Valor de Retorn

    def multiplicar(a, b):
        return a * b
    
    resultat_multiplicacio = multiplicar(4, 3)
    print(resultat_multiplicacio)  # Eixida: 12           

    Les funcions també poden retornar un valor mitjançant la declaració return, com es veu en l'exemple de la funció multiplicar.

    Una funcio pot tindre més d'una intrucció return , però sols s'executarà una, que eixirà de la funció

    Funcions amb Paràmetres Opcionals

    def saludar(nom="Usuari"):
        print(f"Hola, {nom}!")
    
    saludar("Joan")  # Eixida: Hola, Joan!
    saludar()        # Eixida: Hola, Usuari!       

    Aquest exemple mostra com es poden definir paràmetres opcionals en una funció. Si no es passa cap valor a la funció, es fa servir el valor per defecte (en aquest cas, "Usuari").

    def multiplicar(a=0, b=1):
        return a * b
    
    resultat_multiplicacio = multiplicar(4, 3)
    print(resultat_multiplicacio)  # Eixida: 12
    print( multiplicar(4))  # Eixida: 4 
    print( multiplicar())  # Eixida:  0           

    Aquest exemple mostra com es poden definir paràmetres opcionals en una funció. Si no es passa cap valor a la funció, tornarà un 0, i si es passa sols un, tornarà el mateix valor.

    si hi han alguns opcionals i altres obligatòris , els paràmetres opcionals no poden estar els primers, sempre els ultims !!

    Regla important: Els paràmetres opcionals SEMPRE van després dels obligatoris

    def exemple(a=1, b):   # ❌ Incorrecte:
        pass       
    def rectangle(ample, alt=10):    #  ✅ Correcte:
        return ample * alt
    
    print(rectangle(5))
    print(rectangle(5, alt=3))   # en la cridada a la funció es pot especificar el nom del paràmetre !!

    Assignar funcions a variables

    Això permet passar-les com a paràmetres, guardar-les en estructures, etc.

    def saludar():
        print("Hola!")
    
    f = saludar      # En esta instrucció NO es crida la funció (no porta parèntesis)
    f()              # Executa saludar()       

    Funcions amb Tipus de Dades (Type Hinting)

    En Python, es poden afegir indicadors de tipus (type hints) per especificar el tipus de les variables o els arguments d'una funció. Això millora la llegibilitat del codi i ajuda als editors a proporcionar suggeriments i detecció d'errors.

    def dividir(a: float, b: float) -> float:   # Indicació de tipus per a arguments i valor de retorn
        return a / b              

    Funcions amb múltiples arguments

    En Python, es poden definir funcions que accepten un nombre variable d'arguments usant els símbols * i **:

    Arguments arbitraris (Amb Comes)

    A priori, no se sap quants arguments es passaran. Seran diversos arguments passats amb comes.

    def sumar_todos(*numeros):  # Diversos arguments passats amb comes
        return sum(numeros)              

    En aquest exemple, la funció sumar_todos pot acceptar qualsevol nombre d'arguments, i els processa com una tupla.

    # Exemple de com usar la funció
    resultat = sumar_todos(1, 2, 3, 4)
    print(resultat)   # Output: 10 
    
    llista = [5, 6, 7]
    resultat = sumar_todos(*llista)
    print(resultat)   # Output: 18  

    Arguments amb clau i valor (Clave-Valor)

    def mostrar_info(**info):  # Diversos arguments passats com a clau-valor
        for clau, valor in info.items():
            print(f"{clau}: {valor}")                

    Aquesta funció accepta un nombre arbitrari de parells de clau-valor, com un diccionari.

    # Exemple d'us
    mostrar_info(nom="Anna", edat=25, ciutat="Barcelona")
    
    dades = {"nom": "Marc", "edat": 30}
    mostrar_info(**dades)      

    Funcions que retornen més d'un valor

    def operacions(a, b):
        suma = a + b
        resta = a - b
        producte = a * b
        return suma, resta, producte
    
    x,y,z = operacions(10,5)
    resultat = operacions(10, 5)
    print(resultat)        # (15, 5, 50)
    

    Funcions Lambda

    Les funcions lambda són funcions anònimes que es poden definir en una sola línia. Són útils per a operacions simples i per passar com arguments a altres funcions.

    multiplicar = lambda x, y: x * y  # Funció lambda per multiplicar dos valors
    print(multiplicar(2, 3))  # Eixida: 6                

    Decoradors (Següent Tema)

    Els decoradors en Python són una forma de modificar o millorar el comportament d'una funció sense canviar-ne el codi. Són un tema avançat que tractarem més endavant.


    Ámbit o Scope en Python

    En Python, l'àmbit d'una variable es refereix a la regió del codi on una variable és accessible. Python té regles específiques per determinar on una variable es pot utilitzar o modificar. Els àmbits més comuns són:

    Ámbit Local

    Una variable definida dins d'una funció té un àmbit local. Només pot ser accedida dins d'aquesta funció. Si intentes accedir a una variable local fora de la seua funció, obtindràs un error.

    Exemple d'àmbit local

    def funcion():
        y = 5  # Variable local
        print(y)  # Eixida: 5
    funcion()
    print(y)  # Error: NameError, 'y' no està definida en l'àmbit global      

    Ámbit Global

    Una variable definida fora de totes les funcions o blocs de codi té un àmbit global. Pot ser accedida des de qualsevol part del codi, incloent dins de les funcions.

    Però, accedir no és el mateix que modificar.....

    Si es necessita modificar una variable global dins d'una funció, s'ha d'utilitzar la paraula clau global.

    Exemple de variable global

    x = 10  # Variable global
    def mostrar_x():
        print(x)  # Accés a la variable global
    mostrar_x()  # Eixida: 10                    
    def mostrar_x():
        print(x)  # Accés a la variable global
    
    x = 10  # Variable global. No hi ha problema perquè es defineix abans de cridar a la funció    
    mostrar_x()  # Eixida: 10                    

    Modificació d'una variable global dins d'una funció sense global (no es pot)

    x = 10
    def modificar_x():
        x = 20  # Crea una nova variable local, no modifica la global
        print(x)  # Eixida: 20
    modificar_x()
    print(x)  # Eixida: 10 (la variable global no ha canviat)           

    Modificació d'una variable global dins d'una funció amb global

    x = 10
    def modificar_x():
        global x
        x = 20  # Modifica la variable global
        print(x)  # Eixida: 20
    modificar_x()
    print(x)  # Eixida: 20 (la variable global ha canviat)       

    Ámbit de Bucle o Bloc

    En Python, els blocs (if, for, while) NO creen un nou àmbit (scope). Només les funcions, classes i mòduls creen àmbits nous. A diferència d'altres llenguatges, en Python les variables definides en un bucle o bloc no estan limitades a aquell bloc i poden ser accedides fora d'ell.

    Exemple d'àmbit de bucle

    for i in range(3):
        print(i)  # Eixida: 0, 1, 2
        x=10
    print(i)  # Eixida: 2 (la variable 'i' segueix accessible fora del bucle)
    print(x)  # Eixida: 10        

    Això pot resultar confús si vens d'altres llenguatges on les variables de bucle tenen un àmbit limitat al bloc.

    Ámbit No Local (Nonlocal)

    En python es poden definir funcions , dins d'altres funcions.

    L'àmbit no local aplica a variables definides en una funció externa (però no global) i que es poden accedir des d'una funció interna (anidada). Per modificar una variable d'una funció externa dins d'una funció interna, es fa servir la paraula clau nonlocal.

    Exemple de variable no local

    def funcion_externa():
        z = 15  # Variable no local
        def funcion_interna():
            nonlocal z
            z = 25  # Modifica la variable de la funció externa
            print(z)  # Eixida: 25
        funcion_interna()
        print(z)  # Eixida: 25 (la variable no local ha canviat)
    funcion_externa()
            

    Bones Pràctiques al Definir Funcions en Python

    Quan es defineixen funcions en Python, hi ha algunes bones pràctiques que ajuden a millorar la llegibilitat, mantenibilitat i eficiència del codi:

    1. Noms descriptius

    El nom de la funció ha de reflectir clarament el seu propòsit. Això facilita la comprensió del codi i la seua reutilització.

    2. Comentaris i documentació (Docstrings)

    És important documentar les funcions per explicar què fan, quins són els paràmetres d'entrada i el valor de retorn. El millor mètode per documentar una funció és utilitzar docstrings, que es defineixen entre triple cometes (""" ... """).

    Exemple de funció amb docstring

    def sumar(a, b):
        """
        Suma dos números y devuelve el resultado.
        :param a: Primer número
        :param b: Segundo número
        :return: La suma de a y b
        """
        return a + b       

    A més a més, esta forma de documentar permet activar l'ajuda de la funció definida

    help(sumar)
    
    sumar(a, b)
        Suma dos números y devuelve el resultado.
        :param a: Primer número
        :param b: Segundo número
        :return: La suma de a y b             

    3. Evitar efectes secundaris

    Una funció hauria de fer només una cosa. No hauria de modificar variables globals o tindre efectes secundaris inesperats. Això ajuda a que el comportament de la funció siga més previsible.

    4. Mantindre les funcions curtes

    Les funcions han de ser curtes i fàcils de llegir. Si una funció és massa llarga, es recomana dividir-la en funcions més petites per a fer-les més comprensibles i reutilitzables.


    Ús del Guió Baix (_) en Python

    El guió baix (_) en Python té diversos usos depenent del context en què es trobe. A continuació es mostren alguns dels usos més comuns:

    1. Ignorar valors en assignacions

    Es fa servir com una variable "desechable" quan no necessites utilitzar un valor en una assignació. El valor es descarta utilitzant el guion baix.

    _, y, z = (1, 2, 3)
    print(y, z)  # Eixida: 2 3

    2. Iteracions quan no es necessita el valor

    En els bucles, es fa servir for _ in range(n) quan no necessites utilitzar la variable del bucle.

    for _ in range(3):
        print("Hola")  # Es imprimeix "Hola" tres vegades

    3. Últim resultat en la consola interactiva

    En la terminal de Python, _ emmagatzema l'últim resultat calculat.

    >> 5 + 3
    8
    >>> _ * 2
    16

    4. Prefixe per a variables privades

    Es fa servir _variable per indicar que una variable és "privada" (això és una convenció, no una restricció real).

    class MiClasse:
        def __init__(self):
                self._secreta = 42  # Indica que és privada (però encara accessible)

    5. Doble guió baix per evitar col·lisions de noms (name mangling)

    Quan una variable comença amb __, Python canvia el seu nom internament per evitar col·lisions.

    class Exemple:
    def __init__(self):
        self.__privada = 99
            
    e = Exemple()
    print(dir(e))  # Es converteix en _Exemple__privada

    6. Utilitzar _ en noms de funcions i variables per evitar conflictes

    Es fa servir _ en noms per evitar conflictes amb paraules clau de Python.

    def class_(nom):
        return f"Classe: {nom}"

    En Python no hi ha pas per referència, sinó pas de referències a objectes. Només els objectes mutables poden ser modificats dins d’una funció


    En Python una funció es pot cridar des de dins d'una altra funció


    En Python una funció es pot cridar a si mateix ( recursivitat ) i possible DoS si no es fa bé

    Exemple recursivitat:
    def factorial(n):
        # Si el nombre és negatiu, llançem una excepció
        # Programació defensiva !!
            if n < 0:
                raise ValueError("El factorial no està definit per a nombres negatius.")
        # 1. Cas base: Atura la recursivitat
        if n == 1 or n == 0:
            return 1
        # 2. Cas recursiu: La funció es crida a si mateixa
        else:
            return n * factorial(n - 1)
    
    # Prova de la funció
    print(factorial(5))  # Resultat: 120
        

    Elements imprescindibles d'una funció recursiva:
    Cas base: És la condició d'eixida. Sense ell, la funció es cridaria infinitament i donaria un error de tipus RecursionError.
    Cas recursiu: On la funció s'apropa progressivament al cas base.

    Mòduls i Paquets en Python

    Un mòdul és simplement un fitxer de Python amb extensió .py que conté definicions de funcions, classes i variables. Pots reutilitzar el codi d'un mòdul important-lo en altres arxius o programes.

    Crear un fitxer Python (mi_modulo.py)

    # mi_modulo.py
    def saludar(nombre):
        return f"Hola, {nombre}!"
    PI = 3.14159

    Importar i utilitzar el mòdul en un altre arxiu

    # programa.py
    import mi_modulo
    print(mi_modulo.saludar("Juan"))  # Eixida: Hola, Juan!
    print(mi_modulo.PI)               # Eixida: 3.14159

    Quan hem importat un mòdul i volem utilitzar algun element del mateix, hem de fer servir el seu espai de noms seguit d'un punt i el nom de l'element que volem utilitzar. L'espai de noms no és més que el nom que hem indicat després del import.

    Maneres d'importar mòduls:

    Importar tot el mòdul:

    import mi_modulo                # L'espai de noms és => mi_modulo
    print(mi_modulo.saludar("Ana"))  # D'aquesta manera s'ha de fer servir l'espai de noms

    Importar elements específics del mòdul: (sense espai de noms)

    from mi_modulo import saludar
    print(saludar("Carlos"))       # D'aquesta manera NO s'ha de fer servir l'espai de noms

    Importar TOTS els elements del mòdul: (sense espai de noms)

    from mi_modulo import *
    print(saludar("Carlos"))       # D'aquesta manera NO s'ha de fer servir l'espai de noms

    Renombrar un mòdul:

    import mi_modulo as mm
    print(mm.PI)                # D'aquesta manera s'ha de fer servir l'espai de noms

    Paquets en Python

    Un paquet és una col·lecció de mòduls organitzats en un directori. Serveix per estructurar projectes grans en jerarquies de submòduls.

    Com crear un paquet:

    1. Crea una carpeta per al paquet (per exemple, mi_paquete).
    2. Dins de la carpeta, afegeix un arxiu especial anomenat __init__.py. Aquest arxiu pot estar buit o contenir codi d'inicialització per al paquet.

    Estructura bàsica d'un paquet:

    mi_paquete/
            __init__.py
            modulo1.py
            modulo2.py

    Exemple de mòduls dins del paquet:

    modulo1.py:
    def sumar(a, b):
            return a + b
    modulo2.py:
    def restar(a, b):
            return a - b

    Ús del paquet en un programa:

    # programa.py
    from mi_paquete.modulo1 import sumar
    from mi_paquete.modulo2 import restar
    
    print(sumar(5, 3))  # Eixida: 8
    print(restar(5, 3)) # Eixida: 2

    Paquets niats:

    Els paquets poden contenir subpaquets. Per exemple:

    mi_paquete/
            __init__.py
            utilidades/
                __init__.py
                herramientas.py

    Pots importar submòduls així:

    from mi_paquete.utilidades.herramientas import alguna_funcion

    Mòduls i Paquets Estàndard de Python

    Python inclou molts mòduls estàndard llestos per utilitzar. Alguns exemples comuns són:

    Exemples de mòduls estàndard:

    math: Funcions matemàtiques.
    import math
        print(math.sqrt(16))  # arrel quadrada → 4.0
        print(math.pi)        # π
        # altres funcions com:   sin, cos, tan, e, log, pow, factorial...  
    os: Operacions del sistema operatiu.
    import os
        print(os.getcwd())  # Obtén el directori actual.
        # altres funcions: chdir('..')  listdir() mkdir() ....   
    random: Generació de números aleatoris.
    import random
    print(random.randint(1, 10))  # Número aleatori entre 1 i 10.
    random.uniform(1, 10)  # float entre 1 i 10
    random.random()        # float entre 0.0 i 1.0
    
    llista = [1, 2, 3, 4, 5]
    random.choice(llista)        # element aleatori
    random.choices(llista, k=3)  # diversos elements (pot repetir)
    random.sample(llista, 3)     # elements únics
    random.shuffle(llista)       # Barrejar llista
    
    # Exemples per a Jocs i simulacions
    dau = random.randint(1, 6)
    moneda = random.choice(["cara", "creu"])  
    
    datetime: Tractament de dates (i hores) en python.
    from datetime import datetime, date
    
    avui = date.today()
    ara = datetime.now()
    
    print(avui)  # data actual
    print(avui.year)
    print(avui.month)
    
    print(ara)   # data i hora actual           

    Instal·lació de Paquets Externs

    Per utilitzar paquets desenvolupats per tercers, els pots instal·lar des del repositori PyPI utilitzant pip:

    pip3 install nom_del_paquet
    Exemple: Instal·lar requests per realitzar sol·licituds HTTP:
    import requests
        response = requests.get("https://example.com")
        print(response.status_code)

    Altres biblioteques en Python

    Incloses en la biblioteca estàndard

    Pots veure tots els mòduls disponibles amb help() dins de l’intèrpret de Python, i després escriure modules.

    Biblioteques externes populars

    Nota: Les biblioteques externes s’instal·len normalment amb pip, per exemple: pip install numpy.

    Ús de __main__ en Python

    En Python, la construcció if __name__ == "__main__": és una forma estàndard d'assegurar-se que un bloc de codi només s'executi quan el fitxer s'executa directament, i no quan s'importa com un mòdul en un altre fitxer.

    Desglossament de la seua funció:

    Per què és útil?

    Això és especialment útil per:

    Exemple pràctic:

    # Arxiu: mi_script.py
    def funcion_util():
        print("Aquesta funció pot ser utilitzada en altres mòduls.")
    
    if __name__ == "__main__":
        print("Aquest script s'està executant directament.")
        funcion_util()

    Si executem mi_script.py directament:

    $ python mi_script.py
    Aquest script s'està executant directament.
    Aquesta funció pot ser utilitzada en altres mòduls.

    Si importem mi_script des d'un altre fitxer:

    import mi_script
    mi_script.funcion_util()
    Aquesta funció pot ser utilitzada en altres mòduls.

    El bloc dins de if __name__ == "__main__": no s'executarà perquè el fitxer no s'està executant directament.

    ASCII / UNICODE

    ascii

    ASCII (American Standard Code for Information Interchange) és un codi que assigna números als caràcters perquè els ordinadors els puguen entendre.

    ASCII Estés (extended ASCII)

    L’ASCII estès és una ampliació no oficial de l’ASCII original.
    L’ASCII original només usa 7 bits → valors del 0 al 127
    L’ASCII estès usa 8 bits → valors del 0 al 255
    Això permet afegir 128 caràcters més (del 128 al 255), com: lletres amb accents (á, é, ñ), símbols (ç, £, ©) ,caràcters gràfics

    Exemple i breu història de Taula ASCII

    UNICODE

    Unicode és un estàndard que assigna un número únic a cada caràcter de tots els idiomes i símbols del món, perquè els ordinadors puguen representar qualsevol text de manera consistent.

    En resum: Unicode és l’evolució de l’ASCII, permetent que qualsevol text del món siga representat sense problemes.

    Caracters compostos

    En python.. 
    a = "é"
    b = "é"  # e + ◌́ (e + ACUTE COMBINING)
    print(a == b)  # ❌ False
    

    Tot i que visualment semblen iguals (“é”), internament són diferents:

    Python compara els codis Unicode, no com es veuen en pantalla. Per això a != b.

    Com comprovar-ho 
    for c in a:
        print(hex(ord(c)))
    
    for c in b:
        print(hex(ord(c)))      
     Eixida 
    # a
    0xe9
    
    # b
    0x65
    0x301      

    Això mostra clarament que un és un únic caràcter i l’altre són dos.

     Solució: Normalització Unicode 
    import unicodedata
    
    a = "é"
    b = "é"
    
    # Normalitzar a la forma composta NFC
    a_norm = unicodedata.normalize("NFC", a)
    b_norm = unicodedata.normalize("NFC", b)
    
    print(a_norm == b_norm)  # ✅ True       

    En el exemple ◌́ (e + ACUTE COMBINING) és un caràcter "marca combinant".
    Les marques combinants són caràcters Unicode dissenyats per modificar visualment un caràcter base anterior, com accents o punts, sense ocupar espai propi i tenen una marca amb categoria “combining mark” (Mn, Mc) .

    Exercicis

    Els exercicis i tests corresponents a esta unitat es troben en Aules


    Encara que no s'ha vist la utilització de llistes, es pot avançar alguna funció concreta per enriquir els exercicis proposats.

    # convertir un String a llista (símbol a símbol / lletra a lletra)
    s2 = 'Hola mon 1234'
    l2 = list(s1)
    print(l2)
    ['H', 'o', 'l', 'a', ' ', 'm', 'o', 'n', ' ', '1', '2', '3', '4']
    
    #Recorrer una llista
    for ind in range(len(l2)):
        if l2[ind]=' ':
            l2[ind] = '_'
    
    #convertir una llista a String
    s2=''.join(l2)
    print(s2)
    Hola_mon_1234   
    
    # Compte amb els números
    s3='1234'   # Un string que conté un número
    l3=list(s3)
    print(s3)
    ['1', '2', '3', '4' ]   # son caràcters
    
    # Convertir un string de paraules separades per espais 
        #en una llista de paraules
    s4 = 'Hola ke ase, como estas? 123'
    l4 = s4.split()
    print(l4)
    ['Hola', 'ke', 'ase,', 'como', 'estas?', '123']
       # Compte!, és una llista de strings, el '123' es un string