Sobreescriptura, dunders i sobrecàrrega de funcions

La programació en Python no es limita a escriure funcions senzilles i procediments lineals. A mesura que els projectes es fan més complexos, és important comprendre i aplicar conceptes avançats de mòduls de funcions per millorar la modularitat, la reutilització del codi i la flexibilitat dels programes. En aquesta lliçó, explorareu diversos aspectes avançats de Python relacionats amb els mòduls de funcions, incloent-hi la sobreescriptura de funcions, els dunders, la sobrecàrrega de funcions, els comparadors i la seva aplicació en la programació imperativa, estructurada i modular.

Sobreescriptura de funcions i dunders

Es començarà amb la sobreescriptura de funcions, una pràctica que permet redefinir funcions existents amb noves implementacions per adaptar-les a les noves necessitats específiques. Després, aprendreu què són els dunders, els mètodes especials amb noms que comencen i acaben amb doble guió baix, que permeten personalitzar el comportament dels objectes d’una classe, i com adaptar-ho en el cas dels mòduls de funcions en la programació imperativa.

Sobreescriptura de funcions

La sobreescriptura de funcions, també coneguda com redefinició de funcions, és la pràctica de substituir o redefinir una funció existent amb una nova implementació. Aquest procés implica escriure una nova versió de la funció amb el mateix nom i la mateixa signatura (nombre d’arguments i tipus), però amb un comportament diferent. Quan es crida la funció, la nova implementació substitueix la versió original, canviant el seu comportament.

La sobreescriptura de funcions ofereix flexibilitat i personalització en el desenvolupament de software. El seu propòsit principal és permetre als desenvolupadors adaptar el comportament d’una funció preexistent per a satisfer necessitats específiques del programa. Aquesta pràctica és útil en situacions en què la funcionalitat d’una funció predeterminada necessita ser modificada o millorada, sense alterar el nom de la funció ni la seva signatura, evitant així la necessitat de canviar el codi existent que la crida.

Sobreescriptura de la funció len()

Si esteu codificant una aplicació en què cal comptar la longitud d’una cadena amb un caràcter de més, una opció és sobreescriure la funció len() (en comptes de sumar un al resultat cada vegada).

Es pot definir una nova funció len() en el mateix fitxer (que serà la més accessible i que, per tant, sobreescriurà a la del sistema), amb la mateixa signatura que la funció original.

Fixeu-vos en el programa següent:

def len(cadena):
	'''
	Funció que retorna la quantitat de caràcters d'una cadena més un.
	Entrada: str
	Sortida: int
	'''
	suma = 1
	for c in cadena:
		suma += 1
	return suma
	
if __name__ == "__main__":
	print(len('hola'))
	print(len(''))

I en el resultat de la seva execució:

$ python3 len.py 
5
1

Sobreescriptura en el mòdul calendari

Recordeu el mòdul calendari. En ell hi havia la funció crea_data(), definida com segueix:

def crea_data(d, m, a):
	'''
	Funció que crea una data en format vàlid
	Entrada: 3 int
	Sortida: str (data en format correcte)
	'''
	return f'{d:02}/{m:02}/{a:04}'

Si necessiteu fer un nou programa en el qual voleu mostrar la data de calendari junt amb el dia de la setmana, podeu definir la nova funció en el fitxer del programa, que sobreescriurà l’original.

Fixeu-vos en aquest programa:

import calendari

def crea_data(d, m, a):
    '''
    
    '''
    data = calendari.crea_data(d, m, a)
    data =f'{data}, {calendari.nom_dia_setmana(calendari.dia_setmana(data))}'
    return data
    
if __name__ == "__main__":
    print(crea_data(24, 4, 2030))

I en la seva execució:

$ python3 sobre_escriptura.py 24 4 2030
24/04/2030, dimecres
En aquest exemple heu vist com es pot aprofitar la funcionalitat d’un codi (el mòdul calendari), que es considera tancat i que no es vol modificar. Podria ser també que no tingueu accés al codi font (sinó que provingués d’una llibreria interna). Heu pogut utilitzar l’algorisme del càlcul del dia de la setmana, adaptant-lo a les vostres necessitats.

És important tenir present que la sobreescriptura de funcions només es pot fer sobre funcions “finals”. La nova funció crea_data() només és assequible des del nou fitxer. En el fitxer calendari.py, sempre que es faci referència a crea_data() serà l’antiga.

Dunders

Els mètodes, atributs o variables que comencen amb doble guió baix en Python es coneixen com a mètodes especials, “mètodes dunder” (de l’anglès double underscore), “atributs màgics” o “atributs dunder”. La paraula dunder és una contracció de double underscore.

Els mètodes dunder, anomenats així perquè comencen i acaben amb dos guions baixos (__), són mètodes especials en Python que tenen un significat especial per al llenguatge. Són utilitzats per diverses funcionalitats clau com ara la sobrecàrrega d’operadors i la representació de cadenes.

Per exemple, les classes poden redefinir el mètode __eq__() per modificar la manera de comparar els seus objectes (més enllà que siguin exactament el mateix) i les que defineixin el mètode __add__() permetran que les seves instàncies puguin ser “sumades” amb l’operador +’.

Les funcionalitats bàsiques dels mètodes dunder són les següents:

Sobrecarregar un operador és redefinir com aquest operador actua.

Sobrecàrrega d'Operadors

Els mètodes dunder permeten sobrecarregar operadors com +, -, *, ==, etc., per a objectes o estructures de dades personalitzats. Això permet als desenvolupadors definir el comportament dels operadors per als seus propis tipus de dades.

Si accediu a l’ajuda del tipus de dada str, observareu que al principi de la pàgina es defineixen un seguit de mètodes el nom dels quals comença pel doble guió baix. Són els mètodes màgics o mètodes dunder.

Help on class str in module builtins:

class str(object)
 |  str(object='') -> str
 |  str(bytes_or_buffer[, encoding[, errors]]) -> str
 |  
 |  Create a new string object from the given object. If encoding or
 |  errors is specified, then the object must expose a data buffer
 |  that will be decoded using the given encoding and error handler.
 |  Otherwise, returns the result of object.__str__() (if defined)
 |  or repr(object).
 |  encoding defaults to sys.getdefaultencoding().
 |  errors defaults to 'strict'.
 |  
 |  Methods defined here:
 |  
 | __add__(self, value, /)
 |  	Return self+value.
 |  
 |  __contains__(self, key, /)
 |  	Return key in self.
 |  
 |  __eq__(self, value, /)
 |  	Return self==value.

Com podeu observar, la classe str té definits els dunders __ad__() i __eq__(). És per això que les cadenes es poden sumar i es pot verificar si són o no la mateixa.

Representació de Cadena

El mètode __str__() permet definir una representació en forma de cadena d’un objecte. Això és útil per a la impressió i la presentació d’objectes en forma llegible per als humans.

Quan es crida la funció str() en un objecte, Python cerca un mètode __str__() definit per a aquella classe i utilitza el que troba per convertir l’objecte en una cadena.

La funció print(), per exemple, sempre aplica la funció str(), i és gràcies a la redefinició d’aquest dunder que, la representació d’una llista és tal com la coneixeu.

Aplicació a la programació imperativa

Els mètodes dunder només es poden utilitzar en la programació orientada a objectes, la qual es basa en objectes que contenen dades i procediments associats a aquests objectes.

Com aquest curs treballem en programació imperativa no utilitzarem els dunders directament, però sí que podem crear funcions anàlogues per millorar els nostres mòduls de funcions i ampliar el seu encapsulament. Veureu com utilitzar aquests conceptes amb exemples en els mòduls dels racionals i en el del calendari.

Mòdul nombres_racionals

Considereu el mòdul de funcions nombres_racionals. Segons la definició del mòdul, una dada de tipus nombre racional és un str amb el format a/b, on a i b són nombres enters i b és diferent de zero.

En el cas dels str o dels enters, dos elements són iguals només si són exactament el mateix (el nombre 3 només és equivalent a ell mateix). I l’operació:

a == 'hola'

només serà certa en cas que la variable a contingui exactament la mateixa cadena ‘hola’.

Però què passa amb el tipus de dada construït en el mòdul de nombres racionals? El que heu après de fraccions en la lliçó anterior us permet afirmar que les fraccions representades per les cadenes 2, 4/2, 6/3, 8/4… són totes la mateixa fracció. Totes es poden representar amb la mateixa fracció irreductible 2.

En canvi, si utilitzeu l’operador comparació entre aquestes fraccions obtindreu el següent:

>>> '2' == '4/2'
False
>>> '4/2' == '6/3'
False

Si l’estructura de dades que s’ha utilitzat per a definir els nombres racionals (que en realitat són cadenes amb unes restriccions), fos una classe, ara podríeu sobreescriure el mètode dunder __eq__() i el resultat d’aquestes operacions entre nombres decimals seria True.

Aquest mètode dunder __eq__() s’anomena mètode d’equivalència i és un dels que més se sobreescriu. Concretament, es sobreescriu per redefinir el resultat de la relació d’equivalència, el que determinen els operadors == i !=.

Utilitzarem aquests conceptes per a obtenir una millor encapsulació en el mòdul de funcions. L’objectiu és “oblidar” que el tipus de dada utilitzat és un str i operar amb els nombres racionals com si fossin un nou tipus de dada. Per això es definirà una funció anàloga al dunder d’equivalència, la funció booleana son_equivalents(). Aquesta operació no sobreescriurà realment els operadors == i =!, però la podreu utilitzar sempre que hàgiu de verificar que dues fraccions són la mateixa.

Per definir la funció s’utilitzarà la definició d’equivalència dels nombres racionals, reescrita segons la notació utilitzada en el mòdul nombres_racionals.py.

Dos nombres racionals a/b i c/d són equivalents si, i només si: a * d = b * c

Segons aquesta definició, podeu afegir la següent funció en el mòdul nombres_racionals.py:

def son_equivalents(rac1, rac2):
	'''
	Funció que implementa l'equivalència entre dos nombres racionals
	Entrada: str (nombre racional)
	Sortida: bool
	'''
	return (get_numerador(rac1) * get_denominador(rac2) == 
	 get_numerador(rac2) * get_denominador(rac1))

Aquí teniu el fitxer sencer, amb el test-driver corresponent:

'''
Mòdul de funcions per a operar amb nombres racionals.
Una dada de tipus racional és un str amb el format 'num/den' que 
acompleix les següents condicions:
- num és un nombre enter
- den és un nombre enter diferent de zero.
- si den val 1, el str es pot escriure com 'num'
'''

# Funcions incorporades en aquest mòdul per comoditat. Haurien d'estar 
# en un mòdul matematiques. Per això es declaren com a privades.
def _es_enter(s):
	'''
	Funció que ens diu si una cadena és o no un enter
	Entrada: str 
	Sortida: bool
	'''
	if len(s) == 0:
		return False
	#traiem el signe, si cal
	if s[0] in '+-':
		s = s[1:]
	return s.isdigit()
	
def _maxim_comu_divisor(a, b):
	'''Funció que calcula el màxim comú divisor de dos nombres
	Entrada: n, m enters positius
	Sortida: int'''
	# algorisme per a calcular el màxim comú divisor
	t = 0
	while b != 0:
		t = b
		b = a % b
		a = t
	return a

# Funcions bàsiques 
def es_racional_valid(s):
	'''
	Funció per validar si una cadena és un racional vàlid. Accepta com a 
	racionals 3/1 i també 3
	Entrada: str
	Sortida: bool
	'''
	#comprovem si es tracta d'un enter
	if _es_enter(s):
		return True
	#separem la cadena per /
	l = s.split('/')
	
	#si no té 2 camps, malament
	if len(l) != 2:
		return False
	#treiem el signe si hi és
	#si cada camp no és enter, malament
	if not (_es_enter(l[0]) and _es_enter(l[1])):
		return False
	if int(l[1]) == 0:
		return False
	
	return True

def crea_racional(num, den):
	'''
	Funció que construeix una dada de tipus nombre racional
	Atenció, s'ha de comprovar que el resultat sigui un racional vàlid
	Entrada: dos enters. El segon diferent de zero.
	Sortida: String en format de racional (n/d o n)
	'''
	if den == 1:
		return str(num)		#ATENCIÓ, ha de ser str!!!
	return '%d/%d' % (num, den) 
	
def get_numerador(racional):
	'''
	Funció que retorna el numerador d'una cadena de tipus nombre 
	racional.
	Entrada: str tipus racional
	Sortida: int
	'''
	llista_r = racional.split('/')	
	return int(llista_r[0])
	
def get_denominador(racional):	
	'''
	Funció que retorna el denominador d'una cadena de tipus nombre 
	racional.
	Entrada: str tipus racional
	Sortida: Int (serà 1 en el cas que el racional sigui un enter
	'''
	llista_r = racional.split('/')	
	denominador = 1
	#si es tracta d'un enter, no pot petar
	if len(llista_r) == 2:
		denominador = int(llista_r[1])
	return denominador
	
# Operacions amb racionals
def suma(rac1, rac2):
	'''Funció que suma dos racionals. Atenció, també pot acceptar 'num' com
	a racional
	Entrada: 2 str en format racional
	Sortida: str en format racional correcte
	'''
	#extraiem numeradors i denominadors
	num1 = get_numerador(rac1)
	num2 = get_numerador(rac2)
	den1 = get_denominador(rac1)
	den2 = get_denominador(rac2)

	#busquem el denominador final (la multipl dels dos denominadors)
	den = den1 * den2
	num = num1 * den2 + num2 * den1
	return fraccio_canonica(crea_racional(num, den))
	
def multiplicacio(rac1, rac2):
	'''
	Funció que multiplica dos racionals. Atenció, també pot acceptar 'num' com
	a racional
	Entrada: 2 str en format racional
	Sortida: str en format racional 
	'''
	#extraiem numeradors i denominadors
	num1 = get_numerador(rac1)
	num2 = get_numerador(rac2)
	den1 = get_denominador(rac1)
	den2 = get_denominador(rac2)

	#busquem el denominador final (la multipl dels dos denominadors)
	num = num1 * num2
	den = den1 * den2
	
	return fraccio_canonica(crea_racional(num, den))
	
def resta(rac1, rac2):
	'''
	Funció que resta el segon racional del primer.
	Entrada: str (nombre racional)
	sortida: str (nombre racional)
	'''
	return suma(rac1, oposat(rac2))
	
def divisio(rac1, rac2):
	'''
	Funció que resta el segon racional del primer.
	ATENCIÓ: Utilitza la funció invers, de manera que cal comprovar que
	el resultat sigui un racional vàlid.
	Entrada: str (nombre racional)
	sortida: str (nombre racional)
	'''
	return multiplicacio(rac1, invers(rac2))
	
def oposat(racional):
	'''
	Funció que retorna l'oposat de un racional. l'oposat de a/b és -a/b.
	Entrada: str (nombre racional)
	Sortida:str (nombre racional)
	'''
	a = get_numerador(racional)
	b = get_denominador(racional)
	return crea_racional(-a, b)
	
def invers(racional):
	'''
	Funció que retorna l'invers de un racional. l'invers de a/b és b/a.
	ATENCIÓ: s'ha de comprovar que el numerador resultat sigui un racional vàlid
	Entrada: str (nombre racional)
	Sortida:str (nombre racional)
	'''
	a = get_numerador(racional)
	b = get_denominador(racional)
	return crea_racional(b, a)	
	
	
def fraccio_canonica(racional):
	'''
	Funció que simplifica, i retorna la fracció canònica d'un racional
	Entrada: str (nombre racional)
	Sortida: str (nombre racional)
	'''
	
	a = get_numerador(racional)
	b = get_denominador(racional)
	
	# guardar el signe
	positiu = True
	if a * b < 0:
		positiu = False
		
	# càlcul del mcm
	a = abs(a)
	b = abs(b)
	mcd = _maxim_comu_divisor(a, b)
	
	# valors del numerador i divisor (han de ser enters)
	a = a // mcd
	b = b // mcd
	
	#correcció del signe
	if not positiu:
		a = -a
		
	return crea_racional(a, b)
	
def representacio_decimal(racional):
	'''
	Funció que proporciona la representació decimal d'un nombre racional.
	Entrada: str (nombre racional)
	Sortida: float
	'''
	a = get_numerador(racional)
	b = get_denominador(racional)
	return a / b
	
# Mètodes semblants als dunders
def son_equivalents(rac1, rac2):
	'''
	Funció que implementa l'equivalència entre dos nombres racionals
	Entrada: str (nombre racional)
	Sortida: bool
	'''
	return (get_numerador(rac1) * get_denominador(rac2) == 
	 get_numerador(rac2) * get_denominador(rac1))

#Test driver
if __name__ == '__main__':
	
	if False:
		print(_es_enter('-22'))
		print(_es_enter('-22.2'))
		print(_es_enter('+2'))
		print(_es_enter('2'))
		print(_es_enter(''))
		print()
		
	if False:
		print(('es un racional vàlid?'))
		print('45/4', es_racional_valid('45/4'))
		print('2.3/7', es_racional_valid('2.3/7'))
		print('-45/9', es_racional_valid('-45/9'))
		print('234/0', es_racional_valid('234/0'))
		print('23', es_racional_valid('23'))
		print('3/-33', es_racional_valid('3/-33'))
		print('2/1/-2', es_racional_valid('2/1/-2'))
		print('34m/23', es_racional_valid('34/23m'))
		print('34m/23.', es_racional_valid('34m/23.'))
		print('/', es_racional_valid('/'))
		print('6/', es_racional_valid('6/'))
		print('0/2', es_racional_valid('0/2'))		
		print('3/1', es_racional_valid('3/1'))
		print()


	if False:
		print('constructor de racionals')
		print(crea_racional(-4, -234))
		print(crea_racional(-2, 3))
		print(crea_racional(-23, 1))
		print(crea_racional(23, -1))
		print()

	if False:
		print('getters')
		print('23/-1')
		print('numerador: ', get_numerador('23/-1'))
		print('denominador: ', get_denominador('23/-1'))
		print('23')
		print('numerador: ', get_numerador('23'))
		print('denominador: ', get_denominador('23'))
		print()
		
	if False:
		print('suma')
		print('1/2  +  1/2 =', suma('1/2', '1/2'))
		print('2/4  +  1/6 =', suma('2/4', '1/6'))
		print('1/2  +  1/-2 =', suma('1/2', '1/-2'))
		print('3 + 5/2 = ', suma('3', '5/2'))
		print('3 + 5/1 = ', suma('3', '5/1'))
		print('3 + 5 = ', suma('3', '5'))
		print()
		
	if False:
		print('multiplicació')
		print('1/2  *  1/2	=', multiplicacio('1/2', '1/2'))
		print('-6/5  *  5/-2 =', multiplicacio('-6/5', '5/-2'))
		print('-6/-1  *  1/-6  =', multiplicacio('-6/-1', '1/-6'))
		print()
	
	if False:
		print('màxim comú divisor')
		print(_maxim_comu_divisor(4, 10))
		print(_maxim_comu_divisor(7, 60))
		print(_maxim_comu_divisor(7, 7))
		print()
		
	if False:
		print('la fracció canònica')
		print('2/4',fraccio_canonica('2/4'))
		print('14/24', fraccio_canonica('14/24'))
		print('1/4', fraccio_canonica('1/4'))
		print('-30/-10', fraccio_canonica('-30/-10'))
		print('6/-6', fraccio_canonica('6/-6'))
		print()
		
	if False:
		print('representacio_decimal')
		print('1/2', representacio_decimal('1/2'))
		print('4/2', representacio_decimal('4/2'))
		print('-6/-3', representacio_decimal('-6/-3'))
		print()
		
	if False:
		print(f'oposat i invers')
		print(f"1/2  oposat: {oposat('1/2')} invers: {invers('1/2')}")
		print(f"0/2  oposat: {oposat('0/2')} invers: {invers('0/2')}")
		print('ATENCIÓ: després de invers cal utilitzar es_racional_valid')
		print(f"4/-10  oposat: {oposat('4/-10 ')} invers: {invers('4/-10 ')}")
		print()

	if False:
		print(f'resta i divisió')
		print(f"1/2, 2/3 resta: {resta('1/2', '2/3')} divisió: {divisio('1/2', '2/3')}")
		print(f"0/2 , -2/-3 resta: {resta('0/2', '-2/-3')} divisio: {divisio('0/2', '-2/-3')}")
		print(f"-2/-3, 0 resta: {resta('-2/-3', '0')} divisio: {divisio('-2/-3', '0')}")
		print('ATENCIÓ: després de invers cal utilitzar es_racional_valid')
		print()
		
	if True:
		print('equivalència')
		print(f"2 i 4/2, {son_equivalents('2', '4/2')}")
		print(f"4/2 i 6/3, {son_equivalents('4/2', '6/3')}")
		print(f"2 i 6/3, {son_equivalents('2', '6/3')}")
		print(f"1/1000 i 1/1001, {son_equivalents('1/1000', '1/1001')}")

Si l’executeu obtindreu el següent resultat:

$ python3 nombres_racionals.py 
equivalència
2 i 4/2, True
4/2 i 6/3, True
2 i 6/3, True
1/1000 i 1/1001, False

La funció suma() que ja vau definir correspondria al concepte de sobreescriptura del dunder __add__().

Adaptació dels conceptes dunder al mòdul de nombres_racionals

En aquest exemple es pot comprovar com tot i definir les funcions son_equivalents() i suma() no s’aconsegueix la sobreescriptura dels operadors.

import nombres_racionals

print(f'operadors == i !=')
print(f"2 i 4/2, {'2' == '4/2'}")
print(f"4/2 i 6/3, {'4/2' == '6/3'}")
print(f"2 i 6/3, {'2' == '6/3'}")
print(f"1/1000 i 1/1001, {'1/1000' == '1/1001'}")
print()

print(f'funció son_equivalents()')
print(f"2 i 4/2, {nombres_racionals.son_equivalents('2', '4/2')}")
print(f"4/2 i 6/3, {nombres_racionals.son_equivalents('4/2', '6/3')}")
print(f"2 i 6/3, {nombres_racionals.son_equivalents('2', '6/3')}")
print(f"1/1000 i 1/1001, {nombres_racionals.son_equivalents('1/1000', '1/1001')}")
print()

print(f'operador +')
print(f"2 i 4/2, {'2' + '4/2'}")
print(f"4/2 i 6/3, {'4/2' + '6/3'}")
print(f"2 i 6/3, {'2' + '6/3'}")
print(f"1/1000 i 1/1001, {'1/1000' + '1/1001'}")
print()

print(f'funció suma()')
print(f"2 i 4/2, {nombres_racionals.suma('2', '4/2')}")
print(f"4/2 i 6/3, {nombres_racionals.suma('4/2', '6/3')}")
print(f"2 i 6/3, {nombres_racionals.suma('2', '6/3')}")
print(f"1/1000 i 1/1001, {nombres_racionals.suma('1/1000', '1/1001')}")
print()

Aquest és el resultat de la seva execució:

$ python3 no_dunders.py 
operadors == i !=
2 i 4/2, False
4/2 i 6/3, False
2 i 6/3, False
1/1000 i 1/1001, False

funció son_equivalents()
2 i 4/2, True
4/2 i 6/3, True
2 i 6/3, True
1/1000 i 1/1001, False

operador +
2 i 4/2, 24/2
4/2 i 6/3, 4/26/3
2 i 6/3, 26/3
1/1000 i 1/1001, 1/10001/1001

funció suma()
2 i 4/2, 4
4/2 i 6/3, 4
2 i 6/3, 4
1/1000 i 1/1001, 2001/1001000

Tot i que seria més senzill i llegible poder fer les operacions amb els operadors pertinents, la creació de les funcions anàlogues als dunders proporciona facilitat i llegibilitat per operar amb dades de tipus racional. L’ús d’aquestes funcions us ajudarà tant a vosaltres com als programadors que utilitzin el mòdul a mantenir un concepte mental més proper a la concepció matemàtica de nombre racional, i separar-lo cada cop més del que és un objecte de tipus str de Python.

Mòdul calendari

Considereu ara el mòdul de funcions calendari. Segons el mòdul, una data de calendari és un str, amb el format dd/mm/aaaa, en el qual dd, mm, aaaa corresponen a cadenes que representen nombres enters amb les xifres indicades (afegint zeros davant si cal). A més a més, han de respondre a una data correcta amb un any major o igual a zero.

Funció son_equivalents() anàloga a __eq__()

Caldria definir una funció son_equivalents() anàloga al mètode dunder __eq__()? Per respondre’ns a aquesta pregunta caldria plantejar-nos què cal que acompleixin dues dates de calendari per a ser exactament la mateixa. La resposta és que cadascuna d’elles tingui els nombres relatius al dia, al mes i a l’any exactament igual que l’altra.

En aquest cas, dues dates a i b eran equivalents si, i només si la seva representació com a cadena és exactament igual. És a dir, si el resultat de la següent operació és True:

a == '02/05/2030'

Això voldrà dir que la variable a conté exactament la cadena '02/05/2030'. En aquest “tipus” de dada definit per vosaltres, l’equivalència entre dues dades ve determinada per la mateixa equivalència de les dades de tipus str (que és la classe utilitzada com a base). En aquest cas, no caldria sobreescriure el mètode dunder eq() i tampoc serà necessària la definició de cap funció son_equivalents() en el mòdul calendari.

L’equivalència és entre dues dades de tipus calendari és la mateixa que entre dues dades de tipus str.

Funció suma() anàloga a __add__()

S’hauria de sobreescriure l’operador + (en el cas que ens referíssim a una classe), o definir una funció suma() en aquest exemple del calendari? Per respondre’ns hem de plantejar-nos si té sentit l’operació de sumar dues dates de calendari. Perquè una operació entre dos elements es pugui considerar com una addició és imprescindible que tant els dos sumands com el resultat siguin una dada del mateix tipus.

Si es defineix l’operació de sumar dues dates de calendari com a la data resultant d’afegir la quantitat de dies, mesos i anys d’una sobre l’altra, el resultat és una nova dada de tipus data.

Tal com està definit el mòdul calendari, no es pot definir la suma de dates. A una data se li poden afegir una quantitat de dies, però el sentit comú no proporciona un algorisme per a definir la suma entre dates. Sembla lògic que es podria definir una operació entre dates de la següent manera:

Si es “sumen” les dues dates de calendari '02/05/2030' i '02/01/0000', es pot considerar que la data resultant és la data de calendari '04/06/2030'.

Però aquesta definició presenta un problema. Si es volen sumar 40 dies aa una data de calendari d, se li hauria d’afegir la 'data' amb valor '40/00/0000'. I està clar que això no és una data vàlida segons les especificacions amb les quals heu definit el mòdul calendari. Tot això fa que no sigui raonable definir una operació suma() entre dues dates de calendari.

Dates de calendari i l'operador +.

Si dues dates s’operen amb l’operador +, s’utilitza el dunder __add__() tal i com està definit per als str que fa que els resultats no siguin dates vàlides. el resultat no és una data vàlida.

>>> import calendari
>>> data1 = '02/05/2030'
>>> data2 = '02/01/0000'
>>> calendari.es_data_valida(data1)
True
>>> calendari.es_data_valida(data2)
True
>>> data3 = data1 + data2
>>> data3
'02/05/203002/01/0000'
>>> calendari.es_data_valida(data3)
False

Funció to_str()’ anàloga a __str__()

En definir els tipus de dades dels vostres mòduls ja s’han proporcionat funcions per representar d’una manera coherent en format str les dades del tipus. En el mòdul del calendari, la funció crea_data() proporciona una representació en format cadena del concepte de data de calendari.

En el cas d’aquests mòduls, les funcions de crear tenen una doble utilitat: “construeixen” una nova dada del tipus adequat (seria l’equivalent al que farien en programació orientada a objecte els constructors, i la sobreescriptura del mètode __init__()), i proporcionen una versió per convertir-les a cadena (seria el corresponent a la sobreescriptura del mètode dunder __str__()).

Sobrecàrrega de funcions i funcions per comparar

En aquest apartat aprendreu els conceptes de sobrecàrrega de funcions i com s’aplica en Python. Aquesta és una tècnica que permet definir múltiples versions d’una funció amb el mateix nom per a diferents tipus d’arguments, millorant la flexibilitat i la versatilitat del codi. Continuareu amb l’exploració dels comparadors, que són funcions especials que permeten comparar objectes d’una classe segons criteris específics. Són el primer pas per començar a parlar del concepte de l’ordre entre dades del mateix tipus.

Sobrecàrrega de funcions

La sobrecàrrega de funcions és una característica de molts llenguatges de programació que permet definir diverses versions d’una funció amb el mateix nom, però amb diferents nombres d’arguments o tipus d’arguments.

A diferència d’altres llenguatges com Java o C++, Python no té sobrecàrrega de funcions en el sentit tradicional. En lloc d’això, s’utilitzen tècniques com la dels múltiples arguments i els valors per defecte dels arguments, i segons siguin aquests arguments, la funció proporcionarà unes funcionalitats o altres.

En Python es poden definir funcions que acceptin un nombre variable d’arguments mitjançant *args. La variable *args permet recollir múltiples arguments posicionals en una tupla.

Funció que accepta múltiples arguments

Fixeu-vos en la definició de la funció suma:

def suma(*args):
    '''
    Suma d'enters
    Entrada: tupla d'enters
    Sortida: enter
    '''
    total = 0
    for num in args:
        total += num
    return total

# Utilitzant la funció suma amb múltiples arguments
resultat = suma(1, 2, 3, 4, 5)
print(resultat)

resultat = suma(1)
print(resultat)
I fixeu-vos en l’execució, no té problemes a acceptar 5 arguments o un de sol:
$ python3 multiple_suma.py 
15
1

Sembla que la funció suma() permeti l’entrada de múltiples arguments. En realitat, però, el que entra és una dada de tipus tupla. Les tuples, en principi, han d’estar definides amb els elements entre parèntesis i separats per comes, com per exemple (1, 2, 3, 4, 5). Però Python quan troba elements separats per comes els converteix automàticament en una tupla.

Python transforma en tupla automàticament qualsevol successió d’elements separats amb comes, com es pot veure en aquest exemple:

»> a = 2, 3

»> a

(2, 3)

Les tuples poden contenir un nombre indeterminat d’elements i de diferents tipus. Una vegada els arguments són dins de la funció, aquesta es pot encarregar de separar les diferents implementacions distingint segons la quantitat d’arguments o el seu tipus.

Sobrecàrrega de la funció ''dies_mes()'' de calendari

A continuació es mostra com la sobrecàrrega d’una funció perquè permet d’acceptar dades de diferents tipus.

Recordeu com està codificada la funció dies_mes() en el mòdul calendari:

def dies_mes(m, a):
    '''
    Funció que ens diu quants dies té un mes
    Entrada: dos enters. m ha de ser entre 1 i 12 (inclosos) i a positiu
    Sortida: enter
    '''
    DIES_MES = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    
    dies = DIES_MES[m]
    
    # si l'any és de traspàs el febrer té 29 dies
    if m == 2 and es_any_traspas(a):
        dies = 29
    
    return dies
Llegeix dos enters, corresponents al mes i a l’any, i retorna la quantitat de dies del mes. És una funció correcta, i que compleix amb l’objectiu de quan vau definir-la. El problema que té és que si la voleu utilitzar per saber quin serà l’últim dia del mes corresponent a la data 02/05/2030, caldrà que desfeu la data per buscar el mes i l’any. Serà més net i treballareu de manera més encapsulada si permeteu a la funció rebre una data de calendari i que sigui ella mateixa (i no el programa que la crida), l’encarregada d’accedir a les dades internes d’una data.

Per mantenir la coherència amb el codi que heu fet fins ara, però, també us interessa mantenir la signatura de la funció tal com està actualment. La solució és “sobrecarregar” (en el sentit en què Python permet la sobrecàrrega de funcions), i definir-la amb dues signatures diferents: pot rebre dos enters, i actuarà com està ara, o pot rebre un únic argument, que en aquest cas serà una cadena data de calendari.

La nova funció (que substitueix la dies_mes() original) es defineix utilitzant *args, i serà ella mateixa la que escollirà una opció o l’altra.

El codi és el següent:

def dies_mes(*args):
    '''
    Funció sobrecarregada que mostra els dies que té un mes.
    Entrada: dos enters (corresponents a dia i any correctes)
             ó
             un str (data correcta)
    Sortida: enter
    '''
    DIES_MES = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    
    if len(args) == 1:
        (d, m, a) = get_data(args[0])
    else:
        m = args[0]
        a = args[1]
    
    dies = DIES_MES[m]
    
    # si l'any és de traspàs el febrer té 29 dies
    if m == 2 and es_any_traspas(a):
        dies = 29
    
    return dies
Podeu veure com la funció actua de manera diferent segons la signatura. Si hi ha un sol argument, la funció accedeix a les dades que necessita. De manera que l’usuari d’aquesta funció no cal que es preocupi per l’accés al dia i any de la data.

Si afegiu el següent test-driver al mòdul calendari, observareu que la funció dies_mes() accepta les dues signatures sense cap problema.

# test-driver de la sobrecàrrega de dies_mes()
    if True:
        print(dies_mes(1, 2021))
        print(dies_mes(2, 2021))
        print(dies_mes('31/03/2030'))
        print(dies_mes('01/02/2030'))
        print()

Auest és el resultat de l’execució del mòdul (amb tots els if a False excepte l’últim):

videoioc@debian-xtec:~/Documents/programacio/treball$ python3 calendari.py 
31
28
31
28

Comparadors

En la programació és habitual necessitar comparar dades de diferents tipus, com ara nombres, cadenes de text, llistes, etc. Això implica la utilització de comparadors per determinar l’ordre relatiu entre aquestes dades. En aquest apartat, explorareu com es realitzen les comparacions en la programació imperativa, com es generalitza l’ordre dels enters a altres tipus de dades i com aquest concepte es compara amb els mètodes dunder del Python.

Operadors de comparació en dades numèriques

Es podria considerar que els operadors de comparació entre les dades de tipus numèric (és a dir, entre int i float) actuen com a funcions que prenen dos valors i retornen un valor booleà que indica la seva relació (major que, menor que, etc.). Aquests comparadors ja els coneixeu i són: >, >=, < i ⇐.

Els comparadors de comparació sempre s’han d’utilitzar entre dues dades del mateix tipus. L’única excepció són les dades numèriques, ja que el Python (igual que les matemàtiques) no té cap problema a comparar dades de tipus enter i float entre elles. Això es pot veure en el següent exemple de codi:

>>> 3 > 2.3
True
>>> 2.2 >= 2
True
>>> 3 < 2
False
>>> -2.3 < -7.23
False

Generalització de l’ordre dels enters

En molts llenguatges de programació, l’ordre dels enters es generalitza a altres tipus de dades utilitzant els mateixos comparadors. Per exemple, en Python, els operadors de comparació es poden utilitzar per comparar no només enters, sinó també cadenes de text, llistes, tuples, etc. L’ordre es determina mitjançant les regles de comparació específiques de cada tipus de dada. El següent exemple de codi podeu veure com es fan servir comparadors entre tipus de dades predefinits

>>> [1, 2, 3] > [1, 2]
True
>>> 'abc' >= 'abcdari'
False
>>> (1, 2) < (1, -2)
False

Comparació amb funcions anàlogues als mètodes dunder

En la programació orientada a objectes, es poden utilitzar mètodes dunder com lt, gt etc., per personalitzar el comportament de comparació entre objectes. Aquests mètodes permeten definir el comportament de comparació específic per a les classes definides per l’usuari, permetent comparar objectes basant-se en els criteris definits pel desenvolupador.

De la mateixa manera que vau estudiar com definir la funció son_equivalents() entre dues dades del mateix tipus definit en un mòdul de funcions que segueix el paradigma de la programació imperativa, es pot definir la funció es_major() per simulat el concepte del dunder gt() (greater than).

A continuació veurem com definir la funció major_que(), anàloga al dunder gt()), en el mòdul de funcions de nombres racionals. Segurament teniu la idea intuïtiva que les fraccions tenen un ordre definit: el nombre racional 1 és major que el nombre 1/2, i el resultat de la comparació 3/5 > 4/5 ha de donar False.

Un mètode per determinar quin és el major de dos nombres racionals és calcular la seva representació decimal i comparar quina és més gran. La representació decimal és el nombre float resultant de la divisió entre el numerador i el denominador. Per exemple, la representació decimal d’1/2 és 0.5, la d’1 és el mateix nombre 1, la de 3/5 val 0.6 i la de 4/5 val 0.8.

Amb aquestes premisses és fàcil definir la següent funció en el mòdul nombres_racionals:

def es_major(rac1, rac2):
	'''
	Funció que diu si el primer nombre és major que el segon
	Entrada: str (nombre racional)
	Sortida: bool
	'''
	return representacio_decimal(rac1) > representacio_decimal(rac2)

i el test-driver corresponent:

	if True:
	  print("Comparació amb l'operador >: ")
		print(f"1 > 1/2 {'1' > '1/2'}")
		print(f"3/5 > 4/5 {'3/5' > '4/5'}")
		print('es_major():')
		print(f"1 > 1/2 {es_major('1', '1/2')}")
		print(f"3/5 > 4/5 {es_major('3/5', '4/5')}")

El resultat de l’execució del mòdul amb tots els if’s a False excepte l’últim serà:

$ python3 nombres_racionals.py 
Comparació amb l'operador > :
1 > 1/2 False
3/5 > 4/5 False
es_major():
1 > 1/2 True
3/5 > 4/5 False

Cal que us fixeu que en les primeres operacions, l’ordre que utilitza l’operador > és el de cadenes. Per això la resposta a ‘1’ > ‘1/2’ és False, ja que el segon caràcter de la segona cadena és major al segon de la primera (que com que és la cadena buida té codi ASCII 0.)

Definir i utilitzar una funció per a cada comparador pot ser una mica pesat i poc pràctic, i més si tampoc es pot aprofitar l’agilitat dels operadors. Per a comparar tipus de dades definits per vosaltres en els mòduls de funcions, serà molt més útil definir comparators en comptes d’utilitzar funcions semblants als dunders.

Funcions comparator

Es considera una funció comparator tota aquella que és capaç de comparar dos elements segons una relació d’ordre establerta. Una funció comparator(dada1, dada2) retorna un enter (1, 0 o -1) seguint la norma següent:

  • 1 si dada1 és major que dada2 segons la relació d’ordre.
  • 0 si dada1 i dada2 són iguals segons la relació d’ordre
  • -1 si dada1 és menor que dada2 segons la relació d’ordre.

En el mòdul de nombres racionals podeu definir la funció següent:

def compara_racionals(rac1, rac2):
	'''
	Funció comparator de nombres racionals
	Entrada: 2 str (nombres racionals)
	Sortida: int 
				1 si rac1 > rac2
				0 si rac1 == rac2
				-1 si rac1 < rac2
	'''
	resultat = -1
	if es_major(rac1, rac2):
		resultat = 1
	elif son_equivalents(rac1, rac2):
		resultat = 0
	return resultat

Amb el test-driver:

	if True:
		print('compara_racionals')
		print(f"1 i 1/2 {compara_racionals('1', '1/2')}")
		print(f"3/5 i 4/5 {compara_racionals('3/5', '4/5')}")
		print(f"1/2 i 1/2 {compara_racionals('1/2', '1/2')}")

El resultat de l’execució del mòdul amb tots els if’s a False excepte l’últim serà:

$ python3 nombres_racionals.py 
compara_racionals
1 i 1/2 1
3/5 i 4/5 -1
1/2 i 1/2 0
Anar a la pàgina anterior:
Contingut
Anar a la pàgina següent:
Índex general