Conceptes avançats en l'ús de mòduls de funcions
En aquesta lliçó s’aprofundirà en l’ús dels mòduls de funcions i es treballarà el concepte d’encapsulament de dades i de l’existència de funcions internes o privades per afegir una característica important als mòduls de funcions: l’encapsulament del seu contingut.
Encapsulament
En aquest apartat es treballarà el concepte d’encapsulament i es proporcionarà un exemple per il·lustrar els avantatges que ofereix.
L’encapsulament és un concepte clau en la programació que consisteix a amagar la implementació interna d’una funció o classe i exposar només una interfície clara i definida per interactuar amb ella.
En el context dels mòduls de funcions, l’encapsulament implica organitzar les funcions dins del mòdul d’una manera que limita l’accés directe a les funcions internes i ofereix una interfície controlada per a interactuar amb elles des de l’exterior del mòdul.
En un mòdul de funcions en programació estructurada, l’encapsulament es basa en els següents principis bàsics:
- Ocultació de detalls d’implementació: l’encapsulament implica ocultar els detalls d’implementació de les funcions i les seves variables internes. Això significa que els detalls interns de com es realitza una operació o càlcul es mantenen ocults de l’usuari que fa servir les funcions, i només s’exposen les funcions necessàries per interactuar amb el mòdul. No cal saber l’aplicació interna d’un algorisme, sinó que amb la interfície necessària és suficient.
- Control d’accés a les funcions i variables: l’encapsulament permet controlar l’accés a les funcions i variables dins del mòdul. Això es pot aconseguir mitjançant l’ús de funcions “públiques” que s’exporten i funcions “privades” que es mantenen ocultes a l’exterior del mòdul. Python no permet la restricció a l’accés de les funcions i variables internes, però s’indica quines són i per convenció, se n’ha d’evitar l’ús des de l’exterior.
- Protecció de dades: l’encapsulament protegeix les dades internes del mòdul mitjançant l’ús de funcions anomenades getters i setters. Aquestes funcions permeten llegir, en el cas degetter, i modificar les dades de manera controlada, amb setter, evitant l’accés directe a les variables internes del mòdul.
- Promoció de la reutilització i la modularitat: l’encapsulament fomenta la reutilització del codi i la modularitat del sistema, ja que permet definir funcions cohesives i independents que poden ser utilitzades en diferents parts del programa sense necessitat de conèixer els detalls interns de com es realitza la funcionalitat.
En resum, l’encapsulament en un mòdul de funcions implica ocultar els detalls interns de les funcions i les seves dades, controlar l’accés a les funcions i variables, protegir les dades internes i promoure la reutilització i la modularitat del codi. Aquestes característiques ajuden a millorar la claredat, el manteniment i la robustesa del codi.
Encapsulament en el mòdul calendari
Tot i no saber-ho, heu estat aplicant alguns aspectes de la noció d’encapsulament quan heu treballat el mòdul calendari. El tipus de dada del mòdul és un str. Però no és una cadena qualsevol, ja que conté la informació que necessària per a utilitzar-la com a una data de calendari: el dia, el mes i l’any. En llenguatge de programació orientada a objectes, aquests enters s’anomenarien els atributs de l’objecte data.
La idea de l’encapsulament és mantenir els atributs inaccessibles per a les funcions exteriors del mòdul. Si es vol obtenir el valor d’una d’aquestes variables, cal cridar a la funció que ens permet obtenir-lo: un getter. Si es volgués modificar un d’aquestes atributs s’hauria de recórrer a una funció per assignar-li el nou valor, un setter, tot i que en el mòdul calendari no s’han definit aquestes funcions perquè es considera que no es poden modificar els atributs sense que es passi a considerar una nova data.
Per altra banda, també és important destacar el paper de la funció anomenada constructor (en el nostre exemple, seria la funció crea_data()). Si un mòdul està encapsulat no es poden construir “objectes” des de l’exterior sense cridar al constructor, que ho farà amb les regles exigides.
Com podeu observar en aquests exemples, el constructor, els getters i les altres funcions us proporcionen les eines per treballar amb els “objectes de tipus data”, sense que vosaltres hàgiu de notar que en realitat esteu treballant amb un str. I el mòdul us proporciona funcions per manipular aquest tipus d’objectes sense haver d’utilitzar els mètodes de cadenes.
Comproveu amb aquest exemple que tot i que en realitat la data sigui una cadena, la informació que té està encapsulada, i podeu utilitzar-la sense utilitzar cap mètode de str:
>>> import calendari >>> data1 = calendari.crea_data(23, 4, 2030) >>> data1 '23/04/2030' >>> data2 = calendari.dia_seguent(data1) >>> data2 '24/04/2030' >>> calendari. get_data(data2) (24, 4, 2030)Això permetrà, per exemple, modificar el tipus o el format de la data sense haver de redefinir totes les funcions del mòdul.
Al llarg d’aquesta lliçó s’afegiran eines a les que ja teniu (constructors, getters i setters) per garantir un bon encapsulament en els vostres mòduls de funcions.
Avantatges de l'encapsulament
L’encapsulament en un mòdul de funcions presenta diversos avantatges per al programador. Més endavant es proporciona un exemple sobre la facilitat amb la qual es pot reutilitzar el codi si s’ha estructurat seguint els principis de l’encapsulament.
- Claredat i simplicitat: l’encapsulament permet definir una interfície clara i concisa per al mòdul, ocultant els detalls interns de la implementació. Això facilita la comprensió del mòdul i el seu ús per part dels altres desenvolupadors.
- Manteniment: en limitar l’accés directe a les funcions internes del mòdul, l’encapsulament facilita el canvi i la millora de la implementació interna sense afectar els usuaris externs del mòdul. Això fa que sigui més senzill mantenir i evolucionar el codi amb el temps.
- Reutilització del codi: l’encapsulament permet empaquetar funcionalitat relacionada en mòduls reutilitzables, que es poden importar i utilitzar en diversos projectes sense haver de reescriure el codi.
Constructors, getters i setters
En un mòdul de funcions de programació modular estructurada, el constructor, els getters i els setters són eines clau per a l’encapsulament, ja que ajuden a controlar l’accés i la manipulació de les dades internes d’una estructura de dades, protegint-ne la coherència i ocultant-ne els detalls d’implementació.
Constructor
Els constructors es poden identificar de la següent manera:
- És una funció especial que s’utilitza per inicialitzar un objecte amb valors inicials.
- En un mòdul de funcions, el constructor pot ser implementat com una funció que crea una nova instància d’una estructura de dades i assigna valors inicials als seus atributs.
- Relació amb l’encapsulament, el constructor ajuda a protegir la coherència interna d’una estructura de dades, ja que permet garantir que l’objecte estigui en un estat vàlid des del moment de la seva creació.
Getters
Els getters es poden identificar de la següent manera:
- Són funcions que permeten obtenir o llegir el valor d’una variable interna d’una estructura de dades.
- En un mòdul de funcions, els getters poden ser implementats com funcions que retornen el valor d’una o més de les variables considerades com a internes…
- Relació amb l’encapsulament, els getters permeten accedir a les dades internes d’una estructura de dades de manera controlada, ja que només exposen els valors necessaris i protegeixen els detalls interns d’implementació.
Setters
Els setters es poden identificar de la següent manera:
- Són funcions que permeten modificar el valor d’una variable privada d’una estructura de dades.
- En un mòdul de funcions, els setters poden ser implementats com funcions que modifiquen el valor d’una variable o més de les variables internes de l’estructura de dades.
- Relació amb l’encapsulament, els setters permeten controlar l’accés i la modificació de les dades internes, ja que poden controlar que l’objecte mantingui la coherència interna. Una funcionalitat dels setters pot ser la validació de les noves dades per garantir que es mantingui aquesta coherència interna.
Funcions i variables privades o internes d'un mòdul
Les variables i les funcions privades són aquelles que es defineixen dins d’un mòdul de funcions i estan destinades a ser utilitzades només dins del mateix mòdul.
La seva finalitat principal és proporcionar funcionalitats o emmagatzemar dades que no s’han de compartir ni ser accessibles des de l’exterior del mòdul. Utilitzar funcions i variables internes ajuda a millorar l’encapsulament i a mantenir la cohesió del codi, ja que oculta els detalls d’implementació i evita que el codi extern depengui d’aspectes interns del mòdul.
Python, a diferència d’altres llenguatges de programació, no imposa l’encapsulament com ho fan els altres. Però es poden marcar les funcions d’un mòdul com a privades afegint un guió baix _ al principi del seu nom.
En molts llenguatges, com és el cas del C++ i del Java, no és possible l’accés a les funcions privades des de fora del mòdul. Python no imposa aquesta restricció, sinó que es limita a definir-les amb un guió al principi per indicar que no s’haurien d’utilitzar des de l’exterior.
Aquesta convenció ajuda a indicar que aquestes funcions no haurien de ser utilitzades directament per altres mòduls o programes. Comunica la intenció del desenvolupador i alerta els usuaris del mòdul que aquestes funcions o variables estan destinades a ús intern i poden canviar sense previ avís.
Exemple d'encapsulament en el mòdul calendari
Per a procedir a aplicar l’encapsulament en un mòdul de funcions, cal fer els passos següents:
- Identificar el tipus de dades que cal encapsular i crear constructors, setters i getters que s’ocupin de crear, modificar i accedir a la informació del tipus de dada que escollit.
- Identificar funcions internes i externes. Les funcions que es volen exposar a l’exterior del mòdul són les funcions externes, mentre que les funcions internes són les que es volen mantenir privades.
- Marcar funcions com a privades. Es poden marcar les funcions internes com a privades afegint un guió baix
_al principi del seu nom. Tot i que Python no imposa l’encapsulament com ho fan altres llenguatges, aquesta convenció ajuda a indicar que aquestes funcions no haurien de ser utilitzades directament per altres mòduls o programes.
A continuació s’aplicaran els passos per a millorar l’encapsulació del mòdul calendari. Les funcions de l’última versió del mòdul són les que s’indiquen en la pàgina obtinguda amb l’ordre help():
Help on module calendari:
NAME
calendari
DESCRIPTION
Mòdul per a treballar amb dates de calendari.
Una data de calendari és una cadena del tipus
dd/mm/aaaa
On dd, mm i aaaa son enters que corresponen a un dia, mes i any d'una
data real.
Només es tindran en compte anys positius
FUNCTIONS
crea_data(d, m, a)
Funció que crea una data en format vàlid
Entrada: 3 int
Sortida: str (data en format correcte)
dia_anterior(data)
Funció que rep una data i retorna el dia anterior
No funciona per la data 31/12/0001 perquè en retorna una incorrecta).
Entrada: str (Data correcta)
Sortida: str (data correcta)
dia_seguent(data)
Funció que rep una data i retorna el dia seguent
Entrada: str (Data correcta)
Sortida: str (data correcta)
dia_setmana(data)
Funció que ens dona el número del dia de la setmana en que cau una
data vàlida
Entrada: Data vàlida
Sortida: int (0 diumenge, 1 dilluns, 2 dimarts, etc)
dies_mes(m, a)
Funció que ens diu quants dies té un mes
Entrada: dos enters. m (mes) ha de ser entre 1 i 12 (inclosos) i a (any) positiu
Sortida: enter
es_any_traspas(a)
Funció que diu si un any és o no de traspàs
Entrada: enter positiu (any)
Sortida: boolean
es_data_format_correcte(cadena)
Funció que determina si una cadena qualsevol té format dd/mm/aaaa,
amb dd, mm i aaaa nombres enters
Entrada: Cadena
Sortida: Boolean
es_data_real(dia, mes, a)
Funció que determina di tres enters corresponen a un dia que existeix.
Considerem que els anys vàlids només son els positius.
Entrada: int, int, int
Sortida: bool
es_data_valida(cad)
Funció que determina si una data és vàlida (dd/mm/aaaa)
Entrada: cadena qualsevol
Output: Boolean
get_data(data)
Funció que donada una data en format correcte retorna el dia, mes i any
Entrada: str (Data en format correcte)
Sortida: tupla d’enters (dia, mes, any)
nom_dia_setmana(num)
Funció que ens dona el nom d'un dia de la setmana. 0 dilluns, 1
dimarts etc...
Entrada enter ente el 0 i el 6 (inclosos)
Sortida: string
FILE
De totes elles, les que són determinants des del punt de vista de l’encapsulament perquè accedeixen directament a la informació del tipus data de calendari són:
- crea_data(): és el “constructor” de l’objecte de tipus data. És la funció encarregada de muntar la cadena amb el format que s’hagi escollit, desant la informació necessària sobre el dia, mes i any. En el mòdul calendari no es necessita un setter, ja que es considera que dues dates que tenen qualsevol dels tres elements diferents, ja són una data diferent. És a dir, que no es pot modificar la informació del dia, mes o any d’una data sense construir-ne una de nova. Aquesta funció depèn directament del format de la data “dd/mm/aaaa”.
- get_data(): és la funció que proporciona la interfície per accedir a la informació d’una data. Aquesta funció necessita accedir al format específic de la data per retallar els camps i preparar-los (convertir a enter), perquè se’n pugui fer ús. Aquesta funció també depèn directament del format de la data.
- es_data_valida(): comprova si una data és vàlida. Ha de ser una funció assequible des de tot arreu i molt necessària per al mòdul. Com ja vau estudiar, necessita fer dues verificacions; la primera relativa al format de la cadena i la segona per comprovar que el dia, mes i any són correctes. Es va definir donant cada responsabilitat a una funció diferent. La pregunta és: té sentit que aquestes dues funcions siguin privades? Estudiem cadascuna d’aquestes dues funcions:
- es_data_format_correcte(): és necessari que un programa accedeixi a aquesta funció? Per a què es pot necessitar comprovar el format d’una cadena en termes de data, més enllà que per a comprovar si la data és vàlida? Aquest és un bon exemple de funció que no necessita ser accedida des de fora el mòdul. Les funcions del mòdul que la necessitin la poden utilitzar (com fa
data_valida()), però no des de fora del mòdul. Tingueu present que aquesta funció depèn directament del format concret de la data “dd/mm/aaaa”. - es_data_real(): no és necessari que els programes facin ús d’aquesta funció, ja que per validar la data només es necessita
valida_data(). Els algorismes que utilitza (any de traspàs, càlcul de l’últim dia del mes, etc.) són independents del format de la data.
En resum, per aplicar els principis de l’encapsulament només caldrà modificar els noms de les funcions _es_data_valida() i _es_data_real() per indicar als programadors que utilitzin el mòdul calendari que aquestes dues funcions són internes o privades i que no s’han de cridar des de l’exterior.
El principi de responsabilitat única ja va obligar a l’ús de getters i constructors per accedir a la informació, i el mòdul calendari ja va incorporar aquests conceptes necessaris en l’encapsulament. El mòdul calendari no necessita la definició de setters perquè no té sentit modificar algun dels atributs d’una data: si es modifica el dia, per exemple, passa a ser una data nova i s’ha de construir de nou (amb el constructor).
Fixeu-vos que tal com s’ha anat construint el test-driver del mòdul, és molt senzill modificar les funcions escollides i comprovar que tot funciona bé. Només cal afegir el guió baix a davant, posar el booleà del test-driver a True en els casos de les funcions modificades i en les que les utilitzen, com valida_data(), i comprovar que el joc de proves funciona.
El codi del mòdul amb les modificacions serà el següent:
'''
Mòdul per a treballar amb dates de calendari.
Una data de calendari és una cadena del tipus
dd/mm/aaaa
On dd, mm i aaaa son enters que corresponen a un dia, mes i any d'una
data real.
Només es tindran en compte anys positius
'''
def es_data_valida(cad):
'''
Funció que determina si una data és vàlida (dd/mm/aaaa)
Entrada: cadena qualsevol
Output: Boolean
'''
# 1. Comprovem que el format és correcte
if not _es_data_format_correcte(cad):
return False
# calculem dia, mes i any
(d, m, a) = get_data(cad)
# 2. Comprovem que els enters de la data son correctes.
return _es_data_real(d, m, a)
def get_data(data):
'''Funció que donada una data en format correcte retorna el dia, mes i any
Entrada: str (Data en format correcte)
Sortida: tupla d’enters (dia, mes, any)
'''
valors = data.split('/')
return (int(valors[0]), int(valors[1]), int(valors[2]))
# funció 'privada'
def _es_data_format_correcte(cadena):
'''
Funció que determina si una cadena qualsevol té format dd/mm/aaaa,
amb dd, mm i aaaa nombres enters
Entrada: Cadena
Sortida: Boolean
'''
# controlar longitud
if len(cadena) != 10:
return False
# que tingui 2 /
camps = cadena.split('/')
if len(camps) != 3:
return False
#cada camp amb la longitud que toca
if not (len(camps[0]) == 2 and len(camps[1]) == 2 and len(camps[2]) == 4):
return False
#que tots siguin enters positius
return camps[0].isdigit() and camps[1].isdigit() and camps[2].isdigit()
def es_any_traspas(a):
'''
Funció que diu si un any és o no de traspàs
Entrada: enter positiu (any)
Sortida: boolean
'''
return (a % 400 == 0) or ( a % 4 == 0 and a % 100 != 0)
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
# Funció privada
def _es_data_real(dia, mes, a):
'''
Funció que determina di tres enters corresponen a un dia que existeix.
Considerem que els anys vàlids només son els positius.
Entrada: int, int, int
Sortida: bool
'''
# comprovem que l'any és correcte
if a <= 0:
return False
# comprovem que el mes és correcte
if not (mes >= 1 and mes <= 12):
return False
# comprovem que el dia és correcte
return dia >= 1 and dia <= dies_mes(mes, a)
def dia_seguent(data):
'''
Funció que rep una data i retorna el dia seguent
Entrada: str (Data correcta)
Sortida: str (data correcta)
'''
(d, m, a) = get_data(data)
if d == dies_mes(m, a):
d = 1
m = m + 1
if m == 13:
m = 1
a = a + 1
else:
d = d + 1
return crea_data(d, m, a)
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}'
def dia_setmana(data):
'''
Funció que ens dona el número del dia de la setmana en que cau una
data vàlida
Entrada: Data vàlida
Sortida: int (0 diumenge, 1 dilluns, 2 dimarts, etc)
'''
dia, mes, year = get_data(data)
a = (14 - mes) // 12
b = year - a
c = mes - 2 + a * 12
d = b // 4
e = b // 100
f = b // 400
g = 31 * c // 12
h = dia + b + d -e + f + g
i = h % 7
return i
def nom_dia_setmana(num):
'''
Funció que ens dona el nom d'un dia de la setmana. 0 dilluns, 1
dimarts etc...
Entrada enter ente el 0 i el 6 (inclosos)
Sortida: string
'''
DIA_SETMANA = [ 'diumenge', 'dilluns', 'dimarts', 'dimecres', 'dijous',
'divendres', 'dissabte']
return DIA_SETMANA[num]
def dia_anterior(data):
'''
Funció que rep una data i retorna el dia anterior
No funciona per la data 31/12/0001 perquè en retorna una incorrecta).
Entrada: str (Data correcta)
Sortida: str (data correcta)
'''
(d, m, a) = get_data(data)
d = d - 1
if d == 0:
m = m - 1
if m == 0:
m = 12
a = a - 1
d = dies_mes(m, a)
return crea_data(d, m, a)
if __name__ == "__main__":
# Si el mòdul s'executa com a programa principal
# TEST-Driver es_data_valida
if True:
# 1. Segons si el format és correcte
print(es_data_valida('14/03/20'))
print(es_data_valida('14/3/2021'))
print(es_data_valida('14/03/2021'))
print(es_data_valida('4/03/2021'))
print(es_data_valida('14-03-2021'))
print(es_data_valida('14-03-02021'))
print(es_data_valida('1.4/03/2021'))
print(es_data_valida('data valida'))
print(es_data_valida('ab/12/abcd'))
print(es_data_valida('12/12/-212'))
print()
# 2. Segons si la data és correcta
# Si es té en compte que el mes sigui correcte
print(es_data_valida('14/13/2020'))
print(es_data_valida('14/00/2020'))
print(es_data_valida('14/03/-002'))
print()
# Si es té en compte que l’any sigui correcte
print(es_data_valida('14/03/0000'))
print(es_data_valida(''))
print()
# Si es té en compte l’any de traspàs
print(es_data_valida('29/02/2020'))
print(es_data_valida('29/02/2021'))
print(es_data_valida('29/02/2000'))
print(es_data_valida('29/02/2100'))
print()
# Si es té en compte que el dia sigui correcte
print(es_data_valida('31/01/2021'))
print(es_data_valida('31/04/2021'))
print(es_data_valida('32/07/2021'))
print(es_data_valida('31/05/2021'))
print(es_data_valida('00/03/2021'))
print(es_data_valida('31/12/2021'))
print()
# Test driver get_data
if False:
print(get_data('19/03/2021'))
print()
# Test_driver es_data_format_correcte
if True:
for cadena in ['14/03/20', '14/3/2021', '14/03/2021', '4/03/2021',
'14-03-2021', '14-03-02021', '1.4/03/2021', 'data valida', 'ab/12/abcd',
'12/12/-212'] :
print(_es_data_format_correcte(cadena))
print()
# Test driver es_any_traspàs
if False:
print(es_any_traspas(2000))
print(es_any_traspas(2100))
print(es_any_traspas(2020))
print(es_any_traspas(2021))
print()
# Test driver dies_mes
if False:
print(dies_mes(1, 2021))
print(dies_mes(2, 2021))
print(dies_mes(2, 2020))
print(dies_mes(4, 2021))
print()
# Test driver es_data_real
if True:
# 2. Segons si la data és correcta
# Si es té en compte que el mes sigui correcte
print(_es_data_real(14, 13, 2020))
print(_es_data_real(14, 0, 2020))
print(_es_data_real(14, 3, -123))
print()
# Si es té en compte que l’any sigui correcte
print(_es_data_real(14, 3, 0))
print(_es_data_real(14, 3, -10))
print()
# Si es té en compte l’any de traspàs
print(_es_data_real(29,2,2020))
print(_es_data_real(29,2,2021))
print(_es_data_real(29,2,2100))
print(_es_data_real(29,2,2000))
print()
# Si es té en compte que el dia sigui correcte
print(_es_data_real(31,1,2021))
print(_es_data_real(31,4,2021))
print(_es_data_real(32,7,2012))
print(_es_data_real(31,5,2021))
print(_es_data_real(0,3,2021))
print(_es_data_real(31,12,2021))
print()
# test-driver dia_seguent
if False:
print(dia_seguent('31/12/2000'))
print(dia_seguent('31/03/2000'))
print(dia_seguent('28/02/2000'))
print()
# test_driver crea data
if False:
print(crea_data(3, 5, 2000))
print(crea_data(33, 5, 2000))
# test_driver dia_setmana
if False:
print(dia_setmana('26/03/2021'))
print(dia_setmana('18/04/2005'))
print()
# test_driver nom_dia_setmana
if False:
print(nom_dia_setmana(0))
print(nom_dia_setmana(3))
print(nom_dia_setmana(6))
# test-driver dia_anterior
if False:
print(dia_anterior('01/03/2000'))
print(dia_anterior('31/03/2030'))
print(dia_anterior('01/01/2030'))
Les funcions internes no es mostren en l’ajuda del mòdul. Si torneu a importar-lo, veureu que el seu docstring ha desaparegut de la pàgina d’ajuda:
es_any_traspas(a)
Funció que diu si un any és o no de traspàs
Entrada: enter positiu (any)
Sortida: boolean
es_data_valida(cad)
Funció que determina si una data és vàlida (dd/mm/aaaa)
Entrada: cadena qualsevol
Output: Boolean
get_data(data)
Funció que donada una data en format correcte retorna el dia, mes i any
Entrada: str (Data en format correcte)
Sortida: tupla d’enters (dia, mes, any)
La funció _es_data_real(), existent en el mòdul i en el test_driver, no apareix en el llistat (alfabèticament li correspondria estar entre es_any_traspas() i es_data_valida(). No obstant això, el processador no en prohibeix l’ús, com passaria amb les funcions privades d’altres llenguatges, si l’usuari en coneix el nom.
>>> calendari._es_data_real(29, 2, 2030) False
Exemple d'ús de l'encapsulament
En aquest apartat es proporcionarà un exemple per entendre l’ús i els avantatges de l’encapsulament.
L’enunciat diu: escriure un programa que llegeixi una data i indiqui el dia de la setmana. En aquesta ocasió el programa ha de ser internacional, i per aquest motiu s’aplicarà la norma ISO 8601 que especifica el format estàndard de les dates. Apart del programa, es demana la construcció d’un mòdul de funcions que treballi amb dates que compleixen la norma ISO 8601.
La norma ISO 8601 especifica la notació estàndard utilitzada per a emmagatzemar les dates i el temps. Aquesta notació facilita la migració entre diverses plataformes. Se segueix el criteri d’ordenar d’esquerra a dreta del període més llarg de temps al més curt. Per exemple, per a indicar un dia concret, escriurem AAAA-MM-DD. On AAAA indica l’any, MM indica el mes i DD indica el dia.
Viquipèdia. ISO 8601. Wikipedia, The Free Encyclopedia.
Per al nou mòdul, el format d’una data vàlida és: 2030-04-28. Es modificarà el mòdul calendari perquè treballi amb l’especificació ISO 8601 en comptes del que fins ara era el “format correcte”. Com a bons informàtics, no voleu treballar més del necessari. Per això cal tenir clar quines funcions s’han de modificar perquè la seva responsabilitat afecta directament el format, i quines es poden mantenir intactes.
Si repasseu cadascuna de les funcions de l’antic mòdul calendari, observareu que les úniques funcions que depenen del format de la data són:
crea_data()get_data()(i tots els altres getters que hi hagi)_es_data_format_correcte()
El mòdul calendari ha estat ben construït, i cada vegada que s’ha accedit al format s’ha fet cridant a alguna d’aquestes funcions, separant la responsabilitat centrada en el format, de la responsabilitat d’aplicar l’algorisme específic de la funció.
Encapsulament en la funció dia_seguent()
Aquest és el codi de la funció dia_seguent()
def dia_seguent(data): ''' Funció que rep una data i retorna el dia següent Entrada: str (Data correcta) Sortida: str (data correcta) ''' (d, m, a) = get_data(data) if d == dies_mes(m, a): d = 1 m = m + 1 if m == 13: m = 1 a = a + 1 else: d = d + 1 return crea_data(d, m, a)
Fixeu-vos que en definir aquesta funció, vau separar les responsabilitats aplicant el SRP. L’única responsabilitat de la funció dia_seguent() és aplicar l’algorisme per al càlcul del dia següent. En el codi, vau utilitzar una funció que va assumir la responsabilitat d’extreure la informació de la data (la funció get_data()) i una altra amb la responsabilitat de construir la nova data seguint les especificacions necessàries en aquell moment (la funció crea_data()). La repartició de responsabilitats permet que la funció dia_seguent() no s’hagi de modificar quan canvieu el format de les dates amb les quals treballa.
Haver fet un bon ús del principi de responsabilitat única ha permès que s’hagi dut a terme l’encapsulament de forma automàtica i que per crear un mòdul de funcions que treballi amb dates ISO 8601, només sigui necessari modificar unes poques funcions, mantenint-ne la majoria intactes.
El codi del mòdul calendari_ISO_8601 serà el que es mostra a continuació. El test_driver final només ha necessitat comprovar que les funcions modificades funcionen, però no cal comprovar que les altres ho fan, ja que l’algorisme és el mateix. Fixeu-vos que només el codi de les 3 primeres funcions és diferent:
'''
Mòdul per a treballar amb dates de calendari, amb dates definides segons
la norma ISO 8601
Una data de calendari és una cadena del tipus
aaaa-mm-dd
On dd, mm i aaaa son enters que corresponen a un dia, mes i any d'una
data real.
Només es tindran en compte anys positius
'''
def get_data(data):
'''Funció que donada una data en format correcte retorna el dia, mes i any
Entrada: str (Data en format correcte)
Sortida: tupla d’enters (dia, mes, any)
'''
valors = data.split('-')
return (int(valors[2]), int(valors[1]), int(valors[0]))
def _es_data_format_correcte(cadena):
'''
Funció que determina si una cadena qualsevol té format dd/mm/aaaa,
amb dd, mm i aaaa nombres enters
Entrada: Cadena
Sortida: Boolean
'''
# controlar longitud
if len(cadena) != 10:
return False
# que tingui 2 -
camps = cadena.split('-')
if len(camps) != 3:
return False
#cada camp amb la longitud que toca
if not (len(camps[0]) == 4 and len(camps[1]) == 2 and len(camps[2]) == 2):
return False
#que tots siguin enters positius
return camps[0].isdigit() and camps[1].isdigit() and camps[2].isdigit()
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'{a:04}-{m:02}/{d:02}'
def es_data_valida(cad):
'''
Funció que determina si una data és vàlida
Entrada: cadena qualsevol
Output: Boolean
'''
# 1. Comprovem que el format és correcte
if not _es_data_format_correcte(cad):
return False
# calculem dia, mes i any
(d, m, a) = get_data(cad)
# 2. Comprovem que els enters de la data son correctes.
return _es_data_real(d, m, a)
def es_any_traspas(a):
'''
Funció que diu si un any és o no de traspàs
Entrada: enter positiu (any)
Sortida: boolean
'''
return (a % 400 == 0) or ( a % 4 == 0 and a % 100 != 0)
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
def _es_data_real(dia, mes, a):
'''
Funció que determina di tres enters corresponen a un dia que existeix.
Considerem que els anys vàlids només son els positius.
Entrada: int, int, int
Sortida: bool
'''
# comprovem que l'any és correcte
if a <= 0:
return False
# comprovem que el mes és correcte
if not (mes >= 1 and mes <= 12):
return False
# comprovem que el dia és correcte
return dia >= 1 and dia <= dies_mes(mes, a)
def dia_seguent(data):
'''
Funció que rep una data i retorna el dia seguent
Entrada: str (Data correcta)
Sortida: str (data correcta)
'''
(d, m, a) = get_data(data)
if d == dies_mes(m, a):
d = 1
m = m + 1
if m == 13:
m = 1
a = a + 1
else:
d = d + 1
return crea_data(d, m, a)
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}'
def dia_setmana(data):
'''
Funció que ens dona el número del dia de la setmana en que cau una
data vàlida
Entrada: Data vàlida
Sortida: int (0 diumenge, 1 dilluns, 2 dimarts, etc)
'''
dia, mes, year = get_data(data)
a = (14 - mes) // 12
b = year - a
c = mes - 2 + a * 12
d = b // 4
e = b // 100
f = b // 400
g = 31 * c // 12
h = dia + b + d -e + f + g
i = h % 7
return i
def nom_dia_setmana(num):
'''
Funció que ens dona el nom d'un dia de la setmana. 0 dilluns, 1
dimarts etc...
Entrada enter ente el 0 i el 6 (inclosos)
Sortida: string
'''
DIA_SETMANA = [ 'diumenge', 'dilluns', 'dimarts', 'dimecres', 'dijous',
'divendres', 'dissabte']
return DIA_SETMANA[num]
def dia_anterior(data):
'''
Funció que rep una data i retorna el dia anterior
No funciona per la data 31/12/0001 perquè en retorna una incorrecta).
Entrada: str (Data correcta)
Sortida: str (data correcta)
'''
(d, m, a) = get_data(data)
d = d - 1
if d == 0:
m = m - 1
if m == 0:
m = 12
a = a - 1
d = dies_mes(m, a)
return crea_data(d, m, a)
if __name__ == "__main__":
# Si el mòdul s'executa com a programa principal
if True:
print(es_data_valida('28/02/2030'))
print(es_data_valida('2030-02-28'))
print()
if True:
print(get_data('2030-02-28'))
if True:
print(crea_data(28, 2, 2030))
El nou codi del programa dia_naixement.py, que treballa aamb dates ISO 8601, serà:
import sys
import calendari_ISO_8601
MISSATGE = f'Usage: {sys.argv[0]} data_correcta'
# Control d'errors
if len(sys.argv) < 2:
print(MISSATGE)
exit(1)
# existeix argument
data = sys.argv[1]
# Comprovar que la data és correcta:
if not calendari_ISO_8601.es_data_valida(data):
print(MISSATGE)
exit(1)
# Programa_principal
print(calendari_ISO_8601.nom_dia_setmana(calendari_ISO_8601.dia_setmana(data)))
I per acabar, comproveu el resultat de la següent execució:
~$ python3 dia_naixement_ISO.py 2000-02-29 dimarts
Amb molt poca feina s’ha pogut adaptar el mòdul al canvi de format, ja que el mòdul calendari estava ben dissenyat, seguint el principi SRP. L’encapsulament ha permès que només modificant les poques funcions que accedeixen a les dades s’aconsegueixi l’adaptació al nou format.



