Programació Funcional en Python

El concepte / paradigma de la programació funcional fa referència a un estil de programació que utilitza les funcions com a unitat bàsica de codi.

Hi ha des de llenguatges purament funcionals com ara Haskell o Lisp, fins a llenguatges multiparadigma com Python, així que no és tan fàcil separar els llenguatges que suporten la programació funcional.

Perquè un llenguatge permeti la programació funcional, ha de tractar les funcions com a ciutadans de primera classe. És el que passa amb Python; les funcions són objectes, igual que els strings, els números i les llistes.

Molts llenguatges donen suport a la programació funcional; com Javascript, C#, PHP i Java.

Anem a veure els conceptes més importants per entendre aquest paradigma.

Funcions pures.

Aquest paradigma tracta d’emprar el màxim possible de funcions pures (almenys una segur que hi ha)

Funcions pures:

  • Sol llegeix els seus paràmetres d'entrada
  • Sol escriu els seus paràmetres de sortida
  • Pels mateixos paràmetres d'entrada sempre retorna els mateixos paràmetres de sortida.
  • No tenen efectes colaterals fora de la funció.

Exemple funció pura:

def mult2(i: float) -> float: 
    return i*2

# let's test
assert mult2(4) == 8

Només podrem cridar funcions dins d’altres funcions sempre i quan siguin pures.

Exemple funció impura.

name = 'John'
def greetings_from_outside():
  name = 'aaaa',name
  return(f"Greetings from {name}")

És impura perquè està escrivint la sortida amb variables de fora de la funció, utilitza una `variable global name`. 

La podem arreglar:

```py
def greetings_from_outside(name: str):
  return(f"Greetings from {name}")

Pros:

  • Són les funcions més reutilitzables.
  • Més testejables.

És important que les funcions no tinguin gaires línies en general.

Encara que no usem Programació Funcional, val la pena usar funcions pures per minimitzar errors.

Funcions d'ordre superior.

A partir d’una funció pura i una col·lecció (llista, diccionari …) podem aplicar funcions d'ordre dintre dels seus elements.

Ens estalvien usar bucles, i a més a més són més elegants.

Les més habituals són:

  • map, per editar tots els vaqlors
  • filter, per filtrar un subconjunt.
  • reduce, va bé per agrupar valors, però s'utilitza menys.
  • zip, s'utilitza si volem treballar amb tuples.

Més endavant veurem que altres llibreries de dades, com Pandas, accepten funcions pures per recalcular les seves variables.

Ara toca veure amb exemples les possibilitats que ens ofereixen :)

Map.

Crea una llista de números i una funció per multiplicar per 3 un número; per tal que es mostrin per pantalla tots els números de la llista multiplicats per 3.

def mult3 (num: float) -> float:
    return num * 3

# range genera sequències de números
# range (num_ini, num_fin, step)
llistaNums: float = list(range(10,40,2))
print("Llista números original.")
print(llistaNums)
print("Llista números multiplicats per 3.")
llistaNumsPer3 = list(map(mult3,llistaNums))
print(llistaNumsPer3)

Filter.

Crea una llista de números (pex de notes d'alumnat) i una funció per a comprovar si el número és major o igual a 5, i fes que es mostrin per pantalla únicament els números de la llista majors o iguals que 5. Finalment, calcula el percentatge de números filtrats (els >=5) arrodonit a 2 decimals.

def greaterOrEqual5 (num: float) -> bool:
    return num >= 5

llistaNotes: float = [8,5,6.2,4.2,10,6.8,3.4,7.9,9.3,8,2.4,9.7,7.6]

print("Llista notes original.")
print(llistaNotes)

print("Llista notes majors o iguals a 5.")
llistaNotesMajorsIguals5 = list(filter(greaterOrEqual5,llistaNotes))
print(llistaNotesMajorsIguals5)

print("Percentatge aprovats.")
# Dividim la longitud de les notes >5 respecte el total de notes 
# La funció len ens permet veure el número d'elements de les llistes. 
percAprov: int = len(llistaNotesMajorsIguals5) / len(llistaNotes)

## Per arrodonir a 2 decimals usem funció round.
print( str( round(percAprov,4)*100) + ' %')

Fixa't on està la màgia:

list(filter(greaterOrEqual5,llistaNotes))
  • filter la funció d'ordre superior

  • greaterOrEqual5 nom de la funció

  • llistaNotes nom de la llista on volem aplicar la funció.

  • list per comoditat el resultat el retornem com a llista.

Podem realitzar una implementació més genèrica i flexible:

def greaterOrEqual5 (num: float) -> bool:
    return num >= 5

def positiveNums (num: float) -> bool:
    return num >= 0

llistaNums: float = [-4.5,8,5,6.2,4.2,10,6.8,3.4,7.9,9.3,8,2.4,9.7,-12.3]

def filterNums(lista, filter):
    result = []
    for i in lista:
        if filter(i):
            result.append(i)

    return result

print("---")
print(filterNums(llistaNums, greaterOrEqual5))
print(filterNums(llistaNums, positiveNums))

Exercici Filter. Crea una llista de números (pex de notes d'alumnat) i una funció per a comprovar si el número és major o igual a 5, i fes que es mostrin per pantalla únicament els números de la llista majors o iguals que 5. Finalment, calcula el percentatge de números filtrats (els >=5) arrodonit a 2 decimals.

Exemple de llista de notes: llistaNotes: list[float] = [8,5,6.2,4.2,10,6.8,3.4,7.9,9.3,8,2.4,9.7,7.6]

def greaterOrEqual5 (num: float) -> bool:
    return num >= 5

llistaNotes: list[float] = [8,5,6.2,4.2,10,6.8,3.4,7.9,9.3,8,2.4,9.7,7.6]

print("Llista notes original.")
print(llistaNotes)

print("Llista notes majors o iguals a 5.")
llistaNotesMajorsIguals5 = list(filter(greaterOrEqual5,llistaNotes))

print(llistaNotesMajorsIguals5)

print("Percentatge aprovats.")
# Dividim la longitud de les notes >5 respecte el total de notes 
# La funció len ens permet veure el número d'elements de les llistes. 
percAprov: int = len(llistaNotesMajorsIguals5) / len(llistaNotes)

## Per arrodonir a 2 decimals usem funció round.
print( str( round(percAprov,4)*100) + ' %')

Resultat: Llista notes original. [8, 5, 6.2, 4.2, 10, 6.8, 3.4, 7.9, 9.3, 8, 2.4, 9.7, 7.6] Llista notes majors o iguals a 5. [8, 5, 6.2, 10, 6.8, 7.9, 9.3, 8, 9.7, 7.6] Percentatge aprovats. 76.92 %

zip.

Tenim una llista de noms de països i una altra amb la seva població. Volem que es crei una llista hi hagi una tupla amb el nom i població de cada país. Així podrem iterar tota la informació d’un sol cop.

paises = ["China", "India", "Estados Unidos", "Indonesia", "Vietnam"]
poblaciones = [1391, 1364, 327, 264]
list(zip(paises, poblaciones))
# [('China', 1391), ('India', 1364), ('Estados Unidos', 327), ('Indonesia', 264)]
for pais, poblacion in zip(paises, poblaciones):
   print("{}: {} millones de habitantes.".format(pais, poblacion))

# També serveix per crear diccionaris.
dict1 = dict(zip(paises, poblaciones))
print(dict1)

Repte: Com s’implementaria el zip de Python a mà ?

l1 = [1,2,3,4]
l2 = ["1","2","3"]

def zip(lis1, lis2):
    result = []
   
    ## Si el tamany de les llistes és diferent
    ## fem que tinguin el mateix tamany
    l = len(lis1)
    if len(lis2) < l:
        l = len(lis2)

    for num in range(l):
        # Fusionem les 2 llistes en una tupla ( )
        result.append((lis1[num],lis2[num]))
        # Si volguessim un diccionari en comptes d'una tupla
        # result.append({lis1[num],lis2[num]})

    return result

print(zip(l1,l2))

Reduce.

Tenim una llista de números, i volem calcular el producte de tots els números, en un únic valor

from functools import reduce

producto = reduce((lambda x, y: x * y), [1, 2, 3, 4])
# Salida: 24

En aquest exemple veiem el concepte de funció anònima o Lambda.

lambda x, y: x * y

Funcions lambda (anònimes)

En ocasions no ens interessa tenir una funció creada per un sol cop que volem usar el codi. Per exemple, si únicament la volem per a fer map, filter... ens podem estalviar crear una funció amb nom i declarar-la com a anònima amb la paraula lambda.

Així, també millorem el rendiment.

Veiem un exemple senzill de funció lambda aplicat a la funció map:

# Lambda. Llista números multiplicats per 5.")
print(list(map(lambda x: x * 5,[3, 4, 8, 10])))
# Retorna: 
# [15, 20, 40, 50]

Referències.

Referències: