python

Unitat Didàctica 3: Tipus Avançats

Estructures avançades en Python

En Python, les dades es divideixen en mutables i immutables, depenent de si el seu contingut pot canviar després de la seua creació.

Els objectes immutables no poden canviar el seu contingut després de ser creats. Qualsevol “modificació” realment crea un nou objecte en memòria.

En Python, els tipus de dades immutables:


Els objectes mutables poden canviar el seu contingut sense canviar la seua identitat en memòria.

En Python, els tipus de dades mutables:


Objectes immutables (int, str, bool...)

a = 3    # Es crea el literal 3 en memòria i s'assigna el seu “identificador” a la variable a

b = 3    # Com que el literal 3 ja existeix, se li assigna el seu “identificador” a la variable b

a = 5    # Es crea el literal 5 en memòria i s'assigna el seu “identificador” a la variable a

inmutables

Per “identificador” es fa referència a un identificador d'objecte (no a una adreça de memòria). Aquest identificador es pot obtindre amb la funció id(var).

Prova:

  >>> a = 9
  >>> b = 10
  >>> id(a)
     11754152
  >>> id(b)
     11754184
  >>> b = a
  >>> id(b)
     11754152
      

En Python es produeix aliasing, és a dir, els objectes tenen individualitat, i múltiples noms poden referir-se al mateix objecte.

a=56
b=56
print(a is b)
True

Objectes mutables (llistes, diccionaris, conjunts, definits per l'usuari...)

En Python, les variables són referències a objectes en memòria, no contenidors de dades directament. Quan fas una assignació com:

a = [1, 2, 3]     # a és una llista, per tant mutable
b = a        
mutables

Tant a com b apunten al mateix objecte (una llista en aquest cas). No es crea una còpia de la llista; simplement, ambdues variables fan referència al mateix objecte. Si modifiques la llista a través de b:

b.append(4)    # això afegeix un element al final de la llista

La llista a la qual a apunta també es veu modificada, ja que a i b fan referència al mateix objecte en memòria.

Referències i aliasing

La diferència amb els objectes immutables és que quan modifiquem un objecte mutable, no es crea un de nou (si no existeix), com passa amb els immutables. Es modifica l’objecte i es mantenen les referències.

Una altra diferència amb els immutables és que quan assignem una llista a una variable, ES CREA una nova llista (encara que ja n’existisca una amb els mateixos elements).

mutables2

a = [1, 2, 3]

b = [1, 2, 3]

print(a is b)
False

print(a == b)
True

Això no pot passar amb els objectes immutables. En el cas de dos objectes immutables iguals, s’apunta al mateix.

Llistes []

llista

Les llistes són estructures de dades que poden emmagatzemar diversos elements.


Dir que les llistes de Python són ordenades significa que mantenen l'ordre d'inserció dels seus elements. (Una altra cosa és dir que els elements estan ordenats).

    Això implica
  1. Ordre d'Inserció Fixe: L'ordre en què afegiu els elements a una llista és l'ordre en què es guarden. Aquest ordre és estable i previsible i només canviarà si feu una operació específica per reordenar-la (com ara .sort() o .reverse()).
  2. Indexació: Podeu accedir a qualsevol element de la llista utilitzant el seu índex numèric (posició), començant per 0. Això és possible precisament perquè la posició de cada element no varia.
  3. Iteració Previsible: Quan recorreu la llista (amb un bucle for, per exemple), els elements sempre eixiran en la seqüència exacta en què varen ser introduïts

Vegem com manipular llistes amb exemples..

  llista1=[]                                     # declara llista buida
  fruites = ["poma", "plàtan", "cirera"]         # declara llista amb 3 elements
  llista1 = list("1234")                         # fa una llista a partir dels caracters d'un string
  llista2 = [1, "Hola", 3.67, [1, 2, 3], None]   # pot barrejar tipus diferents
  print(fruites[0])           # Impremeix el primer element
  fruites[0] = "pera"         # Canvia el valor del primer element de la llista
  del fruites[1]              # Esborra el segon element de la llista  (es reorganitza la llista)
  fruites.append("taronja")   # afegeix un element    
  llista1.insert(1, 15)       # Inserta en la posició 1, el element 15   (es reorganitza la llista)
  llista1.extend([7, 8, 9])   # Afegix els elements de la lista , u per u
  fruites.remove("cirera")    # Esborra l'element amb valor "cirera", si hi ha més d'u, sols esborra el primer.
  llista1.clear()             # equivalent a    llista1=[]
  print(fruites)    print(*fruites)    print(*fruites, sep=' -> ')   # Tres formes d'imprimir una llista
  len(fruites)                # Torna "quants" elements té la llista
  max(fruites)   min(fruites) # sols si els elements son comparables entre ells
  sum(llista1)                # Sols si els elements son numeros
  llista1[0] = 24    llista1[1] = 46   llista1[-1] = 33    llista1[-2] = 57    # Modificació de valors d'una llista 
  novafruita1 = fruites[2:4]     novafruita2 = fruites[:4]     novafruita3 = fruites[2:]    # Crea sub-llistes a partir d'una
  ultim = llista1.pop()       # Extrau l'últim element  
  altre = llista1.pop(2)      # posició 2  ( la primera pos es la 0, l'ultima la -1)
  print(10 in llista1)        # True o False
  vposic = fruites.index("plàtan")    # posició del primer element 'plàtan' ( si hi ha més d'u)
  
  llista1.sort()                      # ordena els elements de la llista , modificant-la !!
      # Si els elements d'una llista no son del mateix tipus, no es podran ordenar
      # I si s'intenta, donarà un Error =>  TypeError

  llista_ordenada = sorted(llista1)   # Torna una llista nova ordenada, pero no modifica la principal
  llista1.reverse()                   #  modifica...
  print(fruites.count("pera"))    
  copia_llista = llista1.copy()       # equivalent:   copia_llista = llista1[:]
  llista_niada = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
  llista3 = llista1 + llista2         # crea nova llista amb els elements de llista1 i de llista2
  llista_repetida = llista1 * 3       # crea nova llista amb els elements de llista1 tres vegades
  x, y, z = llista                    # list unpacking
  a, *resta = [1, 2, 3, 4]            # list unpacking, amb -resta-  serà una llista
  llista_sense_duplicats = list(set(mevallista))
  resultat = all( llista2)   # comprova si tots els elements son True  (no son False)
  resultat = any( llista2)   # comprova si algun element es True  (no  False)    

Com recorrer una llista

for e in llista: print(e)    # e serà un element de la llista en cada iteració
                             # e pot ser de qualsevol tipus, inclòs llista (llista niada)

for i in range(len(llista)):
        print(llista[i])     #  i serà un numero de 0 a len(llista)  , llista(i) serà l'element
        llista[i]=None       #  A més a més, d'esta forma es pot modificar el valor d'un element !!!

for index, l in enumerate(llista): print(index, l)   

#  dins dels bucles ens pot ser molt útil saber el tipus de cada element
type(llista1)
<class 'list'>
isinstance(llista1, list)
True        

Algunes conversions útils

# De string fer una llista
string1='Python'
l=list(string1)
print(l)     # l conté una llista  ['P','y','t','h','o','n']
 
# Per tornar a convertir una llista a string s'utilitzarà  join
# str no fa la conversió molt bé...
string2=''.join(a)
print(string2)          #  Python

# O es pot "unir" amb qualsevol lletra
string3=','.join(a)
print(string3)          #  P,y,t,h,o,n     

Aliasing versus Shallow Copy

  # Aliasing: a i b fan referència al mateix objecte
a = [1,2]
b = a 
b.append(3)
print(a,b)   #  [1, 2, 3] [1, 2, 3]

  # Shallow copy: a i c seran objectes diferents
a = [1,2] 
c=a.copy()   # també  c = list(a)     # també   c = a[:]
c.append(3)    
print(a,c)    # [1, 2] [1, 2, 3]    

Python List comprehension o Comprensió de llistes en Python

La comprensió de llistes en Python és una manera concisa i eficient de crear llistes a partir d'iterables com llistes, tuples, rangs, etc.

Sintaxi bàsica:

  nova_llista = [expressio for element in iterable if condició]
      • expressio: operació o transformació aplicada a cada element
      • element: variable que pren els valors de l'iterable
      • iterable: objecte iterable (llista, tupla, rang, etc.)
      • if condició (opcional): filtra els elements
      

Exemples de comprensió de llistes

  1. Crear una llista de quadrats:
      quadrats = [x**2 for x in range(10)]
      print(quadrats)  
      # Resultat: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  2. Filtrar nombres parells:
      parells = [x for x in range(10) if x % 2 == 0]
      print(parells)  
      # Resultat: [0, 2, 4, 6, 8]
  3. Convertir paraules a majúscules:
      paraules = ["hola", "món", "python"]
      majuscules = [paraula.upper() for paraula in paraules]
      print(majuscules)  
      # Resultat: ['HOLA', 'MÓN', 'PYTHON']

Tuples ()

tupla

Les tuples són com les llistes però, una vegada declarades, no es poden modificar (són immutables).

  mesosAny = ("Gen", "Feb", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Oct", "Nov", "Des")
  mesosAny[1]           # accés al segon element
  mesosAny[-1]          # accés a l'últim element
  mesosAny[:2]          # slicing
  mesosAny[0] = 8       # ERROR, no podem modificar
  tupla1 = ()           # tupla buida
  tupla2 = (6,)         # tupla d'un sol element, cal posar la coma!
  tupla1 + tupla2       # concatenar tuples
  tupla2 * 3            # repetir la tupla 3 vegades
  "Mai" in mesosAny     # retorna True
  len(tupla2)           # nombre d'elements
  max, min, sum         # si els valors són compatibles
  tupla1.count(23)      # comptar ocurrències
  tupla1.index(23)      # posició d'un valor
  tupla1.index(23, 3)   # a partir de la posició 3
  tupla = tuple(llista) # convertir una llista en tupla
  sorted(tupla)         # retorna una llista ordenada
  nova_llista = list(tupla)  # convertir una tupla en llista
  tupla = (1, 2, 3)
  a, b, c = tupla       # desempaquetat
  tupla = (1, 2, 3, 4, 5)
  TUPLA2 = 1, 2, 3, 4    # altra forma de definir
  a, *b, c = tupla       # a=1, b=[2,3,4], c=5
  v = 1                 # NO és una tupla
  x = (2)               # NO és una tupla
  y = (1,)              # SÍ és una tupla
  tupla = 1, 2, ('a', 'b'), 3   # tupla amb elements niats
  for t in tupla: print(t)     # recorregut de la tupla
  type(tupla2) is tuple        # comprova si és una tupla
      
# Una tupla no es pot re-ordenar, perquè és inmutable, 
# però es pot crear una altra tupla ordenada
t=(4,7,1,9,2,2)
t2=tuple(sorted(t))    # sorted(t) crea una llista ordenada a partir de t, 
                       # i tuple converteix una llista en una tupla   

Quan usar tuples en lloc de llistes?

Tuple Comprehension?

En Python no existeix la "tuple comprehension" com a tal.

Si usem parèntesis (()) per intentar fer una comprensió, Python ho interpreta com un generator expression:

  tupla = (x**2 for x in range(5))
  print(tupla)
  # Resultat: <generator object at 0x...>
      

Açò no crea una tupla, sinó un objecte generador. Per convertir-lo en tupla:

  tupla = tuple(x**2 for x in range(5))
  print(tupla)
      

Avantatges d’un generador:

  1. Eficiència de memòria: no carrega tots els elements a memòria.
  2. Rendiment: útil per a grans volums de dades.
  3. Simplicitat: codi més net que una funció generadora.

Conjunts -sets- {}

conjunt

Característiques principals

  1. Elements únics: Un conjunt no pot contindre duplicats.
  2. No ordenats: Els elements no tenen un ordre específic.
  3. Mutables: Es poden afegir o eliminar elements.
  4. Els elements han de ser immutables: podem afegir o llevar, però no modificar un element concret.

Ordenats o No ordenats: en Python els conjunts (sets) no són ordenats. Això vol dir que els elements d'un conjunt no tenen una posició específica o un ordre fixe. Quan s'imprimeix un conjunt o s'itera sobre ell, l'ordre dels elements pot ser diferent del que es va utilitzar per crear-lo i, de fet, pot canviar entre diferents execucions del programa o amb el temps.

🔢 Què passa amb els Nombres?

Quan Python mostra un conjunt de nombres, pot semblar que està ordenat de vegades, però això és una coincidència a causa de com es calcula el hash i com s'organitzen els nombres a la memòria, no és una propietat garantida del tipus de dades set

Exemples d’ús

  conjunt_buit={}        # No es construeix així un conjunt
  conjunt_buit = set()   #Així SI
  conjunt = {1, 2, 2, 3}
  print(conjunt)               # Eixida: {1, 2, 3}
  conjunt = set([1, 2, 3, 3])  # convertir una llista en conjunt
  conjunt = set( (1, 2, 3, 3) ) # convertir una tupla en conjunt
  conjunt.add(3)               # afegir un element ( el nombre 3)
  conjunt.remove(2)            # elimina el nombre 2, dona error si no existeix
  conjunt.discard(4)           # elimina el nombre 4, no dona error si no existeix
  conjunt.pop()                # elimina i retorna un element aleatori. Si c es buit dona Error
  conjunt.clear()              # buida el conjunt
  
  len(conjunt)                 # número d’elements
  print(5 in conjunt)          # pertinença: True si 5 està en el conjunt
  type(conjunt) is set         # comprova si és un conjunt
  
  a = frozenset([1, 2, 3])     # conjunt immutable
  conjunt = {1, 2.5, "hola", (1, 2), frozenset([3, 4])}
  
  for element in conjunt:
      print(element)
      

Operacions entre conjunts

  print(a.issubset(b))         # a està contingut en b
  print(a.issuperset(b))       # a conté tots els elements de b
  print(a.isdisjoint(b))       # a i b no tenen cap element en comú
 
  opseracions conjunts
  # Unió
  print(s1 | s2)       
  print(s1.union(s2))
  # No es modifica el conjunt original
  
  # Intersecció
  print(s1 & s2)
  print(s1.intersection(s2))
  
  # Diferència
  print(s1 - s2)
  print(s1.difference(s2))
  
  # Diferència simètrica
  print(s1 ^ s2)
  print(s1.symmetric_difference(s2))
      

Mètodes destructius (modifiquen el conjunt original):

  conjunt.update(iterable)                 # Afegix elements de l'iterable al conjunt
                            # Equival a  conjunt |= set(iterable)
  conjunt.intersection_update(iterable)
  conjunt.difference_update(iterable)
  conjunt.symmetric_difference_update(iterable)
      

Set Comprehension

És una forma compacta de crear conjunts.

  nou_conjunt = {expressio for element in iterable if condició}
  # Exemple:
  parells = {x for x in range(10) if x % 2 == 0}
      

Diccionaris { , }

diccionaris

Els diccionaris emmagatzemen parells clau-valor.

Característiques principals

  1. Mutables
  2. No permeten claus duplicades. Si s’afegeix una clau repetida, el seu valor serà sobreescrit.
  3. Les claus han de ser immutables. Poden ser str, int, tuple, però no list o dict.
  4. Mantenen l’ordre d’inserció (des de Python 3.7).

Exemples d’ús

  dic = {}  # Diccionari buit
  persona = {"nom": "Carlos", "edat": 30, "ciutat": "Madrid"}
  print(persona["nom"])  # Accedeix al valor d’una clau. Imprimeix 'Carlos'
  
  dic["nova_clau"] = "nou_valor"      # Afegir una nova clau
  dic["clau1"] = "valor_actualitzat"  # Modificar el valor d'una clau existent
  valor = dic["clau1"]  # Accedir a un valor (error si no existeix)
  valor = dic.get("clau1", "per defecte")  # Evita errors si no existeix
  
  del dic["clau1"]  # Eliminar una clau
  valor = dic.pop("clau2", None)  # Elimina i retorna el valor, o None
  
  dic.clear()  # Buida el diccionari
  
  if "clau1" in dic:
      print("Clau trobada")
  
  # Iteracions
  for clau in dic:
      print(clau)
  
  for valor in dic.values():
      print(valor)
  
  for clau, valor in dic.items():
      print(clau, valor)
  
  tamany = len(dic)  # Nombre d’elements
  nou_dic = dic.copy()  # Còpia superficial
  dic.update({"clau3": "valor3", "clau4": "valor4"})  # Afegir/modificar diverses claus
  
  claus = dic.keys()     # objecte dict_keys
  valors = dic.values()  # objecte dict_values
  items = dic.items()    # objecte dict_items
  
  type(dic) is dict  # Comprova si és un diccionari

  dict.update(dict2)   # Fundeix dos diccionaris. Modifica dict. Retorna None
  d.update(y=2, z=3)  # Passant més d'un parell, com arguments
  d.update([("y", 2), ("z", 3)])  # Passant tuples, com arguments
  
  dic1 | dic2   # (Python 3.9+) Fundeix dos diccionaris. Prioritza dic2. Retorna diccionari. 
                # No modifica ni dict1 ni dict2

  dic1 |= dic2  # Fusiona i actualitza dic1. Equival a dic1 = dic1 | dic2
      

Claus i valors mutables

Les claus han de ser immutables, però els valors poden ser de qualsevol tipus, fins i tot llistes o altres diccionaris.

  dic = {(1, 2): "punt", (3, 4): "altre punt"}
  print(dic[(1, 2)])  # "punt"
      

Comprensió de Diccionaris (Dictionary Comprehension)

Permet construir diccionaris de manera compacta a partir d’un iterable:

  numeros = [1, 2, 3, 4]
  quadrats = {n: n**2 for n in numeros}
  print(quadrats)  # {1: 1, 2: 4, 3: 9, 4: 16}
      

Funcions lambda (funcions anònimes)

Una expressió lambda en Python permet crear funcions petites i anònimes (sense nom). Són útils per definir comportaments ràpids o temporals, especialment dins d’estructures com diccionaris, funcions d’ordre superior (map, filter, sorted, etc.) o comprensions.

Sintaxi

      lambda arguments: expressió
      

Exemple bàsic:

      f = lambda x: x * 2
      print(f(4))   # Imprimeix 8
      

Exemple amb diccionari de funcions

      operacions = {
          "suma": lambda x, y: x + y,
          "resta": lambda x, y: x - y
      }
      print(operacions["suma"](10, 5))  # Imprimeix 15
      print(operacions["resta"](10, 5))  # Imprimeix 5
      

En este cas, operacions és un diccionari que guarda funcions lambda com a valors. Cada clau representa una operació aritmètica, i pots executar la funció cridant-la amb els arguments corresponents.

Treballar amb Arrays en Python

Què és un array: També se sol dir vector i és un tipus de dades estructurades, relacionat i homogèni, es a dir, totes les dades del vector seran del mateix tipus

vector matriu

Si els elemnts d'un vector, son vectors, estem parlant d'un vector bidimensional, o matriu

De igual manera es pot definir un cub, com a un vector de 3 dimensions, o un tesseracte o hipercub com un vector de 4 dimensions

En Python, es poden gestionar arrays mitjançant llistes o utilitzant el mòdul NumPy. Aquesta és una biblioteca externa que proporciona funcionalitats específiques per a computació numèrica.

Què és NumPy?

numpy

NumPy (Numerical Python) és una de les biblioteques més fonamentals i populars de Python per treballar amb computació científica i manipulació de dades numèriques.

És àmpliament utilitzada en àrees com:

NumPy proporciona:

  1. Arrays multidimensionals per treballar amb dades de manera més eficient que amb llistes de Python.
  2. Funcions matemàtiques optimitzades per fer operacions ràpides i vectoritzades.
  3. Eines per manipulació de dades, àlgebra lineal, transformades de Fourier, generació de nombres aleatoris, etc.

Instal·lació

Si no tens instal·lat NumPy, pots fer-ho amb la següent comanda:

$ pip3 install numpy

Característiques principals de NumPy

Arrays

El tipus de dades principal de NumPy és l'ndarray (array N-dimensional). Aquests són més ràpids i ocupen menys memòria que les llistes de Python.

Exemple bàsic:

import numpy as np

# Crear un array unidimensional
arreglo = np.array([1, 2, 3, 4])
print("Array:", arreglo)
print(arreglo[2])    # Un element
arreglo[2]=8         # Modificar un element

# Crear un array bidimensional (matriu)
matriu = np.array([[1, 2, 3], [4, 5, 6]])   # Dos files , tres columnes. files 0,1  columnes 0,1,2
print("Matriu:\n", matriu)
print("Element 1,2: ", matriu[1,2]) # ==> 6
matriu[1,2]=99                      # Canviar un element 
print("Matriu:\n", matriu)

Explicació dels exemples:


Vectorització amb NumPy

NumPy permet realitzar operacions matemàtiques sobre tots els elements d’un array d’una sola vegada, sense necessitat d’utilitzar bucles (loops).

Exemple:

# Operacions matemàtiques amb arrays
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print("Suma:", a + b)  # [5 7 9]
print("Producte escalar:", a * 2)  # [2 4 6]

Estalvi de memòria

NumPy emmagatzema les dades en un tipus homogeni (tots els elements són del mateix tipus), la qual cosa fa que siga més eficient en ús de memòria que les llistes tradicionals de Python.

Exemple:

import sys

llista = range(1000)
print("Mida de la llista:", sys.getsizeof(1) * len(llista))

arrel = np.arange(1000)
print("Mida de l'array NumPy:", arrel.nbytes)

Funcions útils

NumPy inclou moltes funcions predefinides per a càlculs matemàtics:

Exemple de càlculs estadístics:

# Estadístiques bàsiques
dades = np.array([10, 20, 30, 40])
print("Mitjana:", np.mean(dades))
print("Desviació estàndard:", np.std(dades))

Creació d'arrays especialitzats

NumPy facilita la creació d'arrays amb valors específics de forma ràpida i senzilla.

Exemples:

# Array de zeros
zeros = np.zeros((2, 3))  # 2 files, 3 columnes
print("Zeros:\n", zeros)

# Array d'unos
unos = np.ones((3, 2))
print("Unos:\n", unos)

# Array amb valors en un rang
rang = np.arange(0, 10, 2)  # Inici, fi, pas
print("Rang:", rang)

# Array amb valors espaiats uniformement
espaiats = np.linspace(0, 1, 5)  # Inici, fi, quantitat
print("Espaiats uniformement:", espaiats)

Nota: Aquestes funcions són molt útils per preparar dades per a experiments, simulacions o per crear estructures numèriques base de manera ràpida.


Indexació i slicing avançats

Amb NumPy pots accedir i modificar elements dels arrays utilitzant índexs o fins i tot condicions.

arrel = np.array([10, 20, 30, 40, 50])

# Accés per índex
print("Element en l'índex 2:", arrel[2])  # 30

# Slicing (subarray)
print("Subarray:", arrel[1:4])  # [20 30 40]

# Condicionals
print("Elements majors que 25:", arrel[arrel > 25])  # [30 40 50]

Operacions amb matrius

NumPy és molt eficient per a fer càlculs amb matrius, com el producte punt o la transposició.

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])

# Producte punt (matriu A · matriu B)
print("Producte punt:\n", np.dot(A, B))

# Transposició de matriu A
print("Transposada de A:\n", A.T)

Nota: El mètode np.dot calcula el producte matricial clàssic, mentre que A.T retorna la transposada (intercanviant files per columnes).

Generació de nombres aleatoris

NumPy ofereix diverses funcions per generar valors aleatoris de manera senzilla:

# Nombre aleatori entre 0 i 1
aleatori = np.random.rand()
print("Nombre aleatori:", aleatori)

# Matriu aleatòria d'enters
aleatoris = np.random.randint(1, 10, size=(3, 3))
print("Matriu aleatòria:\n", aleatoris)

Aquestes funcions són útils per simular dades, inicialitzar variables o fer proves estadístiques.


Per què utilitzar NumPy?

NumPy és una biblioteca fonamental en Python per al càlcul numèric i l'anàlisi de dades. Els principals motius per a utilitzar-la són:

  1. Velocitat: NumPy pot ser fins a 50 vegades més ràpid que les llistes normals de Python per a operacions numèriques gràcies al seu codi optimitzat en C.
  2. Optimització de memòria: Els arrays de NumPy utilitzen menys memòria que les estructures estàndard de Python.
  3. Facilitat d’ús: Inclou moltes funcions matemàtiques, estadístiques i d’àlgebra lineal que es poden utilitzar directament.
  4. Interoperabilitat: Funciona perfectament amb altres biblioteques com Pandas, Matplotlib, Scikit-learn, entre altres.

Dates en Python

Python inclou de manera nativa eines per al tractament de dates i hores mitjançant el mòdul estàndard datetime. Aquest mòdul ve integrat en Python i no necessites instal·lar cap llibreria addicional per utilitzar-lo.

Importar el mòdul datetime

# el mòdul
import datetime  
# o funcions concretes
from datetime import date, datetime, timedelta, date  

Obtenir la data i hora actuals

Data i hora actuals:

ara = datetime.now()
print("Data i hora actual:", ara)
print(ara.hour)
print(ara.minute)
print(ara.second)  

Només la data actual:

hui = date.today()
print("Data actual:", hui)
print(hui.year)   # any
print(hui.month)  # mes
print(hui.day)    # dia  

Crear una data o hora específica

Crear una data específica:

mi_fecha = datetime(2025, 1, 28)  # Any, mes, dia
print("Data específica:", mi_fecha)

Crear una data i hora específica:

mi_fecha_hora = datetime(2025, 1, 28, 14, 30, 0)  # Any, mes, dia, hora, minut, segon
print("Data i hora específica:", mi_fecha_hora)

Format de dates (strftime)

Pots convertir una data o hora en un format específic com a text:

format = ara.strftime("%d/%m/%Y %H:%M:%S")
print("Format personalitzat:", format)

Exemples de codis de format:

Convertir i Operar amb Dates en Python

Convertir text en data (strptime)

Pots convertir una cadena de text en un objecte datetime:

texto = "28/01/2025 14:30:00"
fecha_convertida = datetime.strptime(texto, "%d/%m/%Y %H:%M:%S")
print("Text convertit a data:", fecha_convertida)

Operacions amb dates

Aquí tens algunes operacions que pots realitzar amb dates:

1. Afegir o restar temps amb timedelta:

Afegir 10 dies a la data actual:

nueva_fecha = ahora + timedelta(days=10)
print("Data amb 10 dies més:", nueva_fecha)

Restar 5 hores:

nueva_hora = ahora - timedelta(hours=5)
print("Data amb 5 hores menys:", nueva_hora)

** Pensa en timedelta com una quantitat de temps, no com una data.

Paràmetres en timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0 )

2. Calcular la diferència entre dues dates: ->timedelta

fecha1 = datetime(2025, 1, 28)
fecha2 = datetime(2025, 2, 5)
diferencia = fecha2 - fecha1
print("Dies de diferència:", diferencia.days)

Comparar dates

Pots utilitzar operadors de comparació com <, >, == per comparar objectes de data i hora:

if fecha1 < fecha2:
    print("Fecha1 és anterior a Fecha2")
else:
    print("Fecha1 és posterior o igual a Fecha2")

Altres Mòduls Relacionats amb Dates en Python

Mòdul time

El mòdul time s'utilitza per treballar amb el temps del sistema i realitzar operacions com mesurar intervals o fer pauses.

Característiques principals:

Exemple bàsic:

import time

# Pausar l'execució per 2 segons
time.sleep(2)

# Obtenir el timestamp actual
timestamp = time.time()
print("Timestamp actual:", timestamp)

Un timestamp és un número que representa un moment exacte en el temps. Concretament, en Python (i en molts sistemes): és el nombre de segons que han passat des del 1 de gener de 1970 a les 00:00:00 (UTC) (això s’anomena Unix Epoch).

Convertir timestamp a data llegible

import time
from datetime import datetime

timestamp = time.time()
data = datetime.fromtimestamp(timestamp)
print(data)     

Convertir data a timestamp

dt = datetime(2026, 2, 10, 12, 0)
timestamp = dt.timestamp()
print(timestamp)   

Mòdul calendar

Aquest mòdul és útil per treballar amb calendaris i verificar propietats de dates (com dies de la setmana, anys bessis, etc.).

Característiques principals:

Exemple bàsic:

import calendar

# Verificar si un any és bisiest
print("¿2024 és bisiest?", calendar.isleap(2024))

# Obtenir el calendari de gener 2025
print(calendar.month(2025, 1))

Mòdul zoneinfo (Python 3.9+)

Aquest mòdul permet gestionar zones horàries de manera més senzilla i eficient.

Característiques principals:

Exemple bàsic:

from datetime import datetime
from zoneinfo import ZoneInfo

# Definir zones horàries
utc = datetime.now(ZoneInfo("UTC"))
madrid = datetime.now(ZoneInfo("Europe/Madrid"))

print("Hora UTC:", utc)
print("Hora Madrid:", madrid)

Llibreries externes (opcionals)

Tot i que Python inclou eines robustes per gestionar dates, de vegades pots necessitar llibretes externes per a casos més complexos. Les més populars són:

Funcions Avançades en Python


Ús de *args i **kwargs

*args permet passar un nombre variable d’arguments posicionals a una funció. S’emmagatzemen com una tupla.

def sumar(*args):
    return sum(args)

print(sumar(1, 2, 3, 4))  # Output: 10    

**kwargs permet passar arguments amb clau i valor (com un diccionari) sense definir-los prèviament.

def mostrar_info(**kwargs):
    for clau, valor in kwargs.items():
        print(f"{clau}: {valor}")

mostrar_info(nom="Anna", edat=30, ciutat="València")     

Diferència:

Exemple combinat:

def exemple(*args, **kwargs):
      print("Args:", args)
      print("Kwargs:", kwargs)
exemple(1, 2, 3, nom="Joan", edat=25)    

Funcions Lambda

Les funcions lambda són funcions anònimes d’una sola línia, útils per a operacions curtes.

suma = lambda x, y: x + y
print(suma(3, 5))  # 8

cuadrat = lambda x: x ** 2
print(cuadrat(4))  # 16

es_par = lambda x: x % 2 == 0
print(es_par(10))  # True      

Funcions d'ordre superior - map, filter

Una funció d’ordre superior és una funció que: rep una funció com a argument, i/o retorna una funció. reduce també és d'ordre superior, pero no és built-in

map

Què és map: És una funció que servix per aplicar una (mateixa) operació a cada element d’un iterable (com una llista, tupla, etc.) i retornar els resultats.

map(funció, iterable)
valors = ["1", "2", "3"]
numeros = map(int, valors)   # La funció pot set també "user-defined"
print(numeros)
<map object at 0x00000198FA7D5F00> 
print(list(numeros))
[1, 2, 3]  

filter

filter és una funció que servix per seleccionar elements d’un iterable segons una condició. A diferència de map, que transforma valors, filter decideix quins es queden.

nums = [1, 2, 3, 4, 5, 6]
parells = filter(lambda x: x % 2 == 0, nums)
list(parells)  

Com map, retorna un iterador, no una llista

Exemples de lambda amb funcions d’ordre superior

  # map
  numeros = [1, 2, 3, 4]
  dobles = list(map(lambda x: x * 2, numeros))
  
  # filter
  pares = list(filter(lambda x: x % 2 == 0, numeros))
  
  # sorted amb clau
  paraules = ["poma", "plàtan", "kiwi"]
  ordenades = sorted(paraules, key=lambda x: len(x))
  

Decoradors en Python

Un decorador és una funció que rep una altra funció, l’amplia amb funcionalitat addicional, i retorna una nova funció.

  def mi_decorador(func):
      def nova_funcio():
          print("Abans de cridar la funció")
          func()
          print("Després de cridar la funció")
      return nova_funcio
  
  @mi_decorador
  def salutacio():
      print("Hola, món!")
  
  salutacio()
  

Decoradors amb paràmetres

  def decorador_amb_args(func):
      def embolcall(*args, **kwargs):
          print(f"Cridant {func.__name__} amb {args} {kwargs}")
          return func(*args, **kwargs)
      return embolcall
  
  @decorador_amb_args
  def suma(a, b):
      return a + b
  

Decorador amb paràmetres personalitzats

  def repetidor(n):
      def decorador(func):
          def embolcall(*args, **kwargs):
              for _ in range(n):
                  func(*args, **kwargs)
          return embolcall
      return decorador
  
  @repetidor(3)
  def hola():
      print("Hola!")
  
  hola()
  

Decoradors predefinits

  class Persona:
      def __init__(self, nom):
          self._nom = nom
  
      @property
      def nom(self):
          return self._nom
  
  p = Persona("Carles")
  print(p.nom)  # Sense parèntesis
  

Generadors

Els generadors són una manera més senzilla i eficient de crear iterables en Python. S'utilitzen funcions amb la paraula clau yield en lloc de return.

Quan una funció conté yield, es converteix en un generador i pot conservar l'estat entre iteracions.

Exemple de generador:

def comptador(inici, fi):
  while inici <= fi:
      yield inici
      inici += 1

for numero in comptador(1, 5):
  print(numero)
  

Este codi té el mateix comportament que la classe Comptador de l'exemple anterior, però és molt més curt i fàcil de llegir.

Avantatges dels generadors:

El Garbage Collector (GC) de Python

El Garbage Collector és el mecanisme encarregat d'administrar la memòria automàticament en Python. Elimina els objectes que ja no s’utilitzen, alliberant espai i evitant fugues de memòria.

Exemple conceptual

    gc
  >>> b = 3
  >>> a = 5
  >>> a = b
  

Quan a = 5, l’objecte 5 té una referència. Però després a = b, a passa a referenciar 3. Com ningú apunta ja a 5, queda sense ús i el GC el pot eliminar.


Funcionament intern

Python fa neteges més freqüents en la generació 0 que en les superiors, perquè és més probable que els objectes nous siguen de curta duració.

Control manual amb el mòdul gc

Encara que el GC funciona automàticament, es pot controlar manualment amb el mòdul gc:

  import gc
  
  gc.collect()      # Força una execució del garbage collector
  gc.get_stats()    # Mostra estadístiques sobre el GC
  

TADs Tipus Abstractes de Dades

Accedir a TADs

Exercicis

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