Llistes i cadenes. Llistes multidimensionals
Fins ara s’han abordat les llistes des del punt de vista de les seves semblances amb les cadenes. En aquesta lliçó s’estudiaran les diferències entre els dos tipus de dades, quan s’ha d’escollir una o altra per emmagatzemar dades i com fer conversions de manera senzilla entre ambdues, aprofitant així al màxim les característiques de cada tipus.
Llistes i cadenes
Una llista (o una dada de tipus list), en Python és una seqüència ordenada i mutable d’elements, que pot contenir dades heterogènies i es defineix mitjançant els parèntesis [ ].
Les cadenes (str) són un tipus de dada seqüencial que consta d’una successió de caràcters. Son una seqüència ordenada i immutable de caràcters.
La principal diferència entre els dos tipus és la mutabilitat (a més a més de la llibertat per a escollir el tipus d’elements de les llistes). En aquesta lliçó aprendreu a fer amb les llistes accions que no es poden fer amb les cadenes, relacionades sobretot amb la característica de la seva mutabilitat.
Primer programa amb conversions entre llistes i cadenes
Farem un programa amb conversions entre llistes i cadenes amb el que aprendreu a treballar amb els aspectes diferencials de les llistes.
El programa pel que començarem té com objectiu preparar el taulell per a fer un programa interactiu que permeti a dos jugadors fer un 3 en ratlla.
El programa en aquesta etapa no tindrà entrades i només mostrarà el taulell. El primer jugador col·locarà una fitxa rodona al centre i el segon una creu a la cantonada superior dreta. El programa mostrarà:
jugada 0
...
...
...
jugada 1
...
.O.
...
jugada 2
..X
.O.
...
Es podria escriure una cadena per línia i modificar-la tota sencera cada vegada. Però s’utilitzaran llistes per emmagatzemar cada posició de manera que a cada jugada només calgui modificar el caràcter corresponent.
En aquesta ocasió, l’estructura del taulell serà una llista de llistes de caràcters. Però inicialment es definirà una llista de cadenes, que és més còmode d’escriure.
Tot seguit podeu veure codi del programa. Per veure el codi amb els números de línia, vegeu la figura.
import random
BLANCS = 5 # per no escriure el taulell a la 1a columna
inici = '... ... ...'
taulell = inici.split()
print('llista de cadenes')
print(taulell)
#transformació a llista de llistes de caràcters
for i in range(len(taulell)):
taulell[i] = list(taulell[i])
print('llista de llistes de caràcters')
print(taulell)
print()
print('jugada 0')
#mostrar el camp
for element in taulell:
print(BLANCS * ' ' + ''.join(element))
print()
print('jugada 1')
taulell[1][1] = '0'
#mostrar el camp
for element in taulell:
print(BLANCS * ' ' + ''.join(element))
print()
print('jugada 2')
taulell[0][2] = 'X'
#mostrar el camp
for element in taulell:
print(BLANCS * ' ' + ''.join(element))
Conceptes nous que s’utilitzen en el programa:
La cadena “hola” i la llista [‘h’, ‘o’, ‘l’, ‘a’] són dues successions que tenen exactament els mateixos elements en cada posició. Però la cadena és immutable i la llista no. En ser tipus de dades diferents no es permet la seva comparació amb els operadors >, <, >=, < =.
- Línia 5: conversió de la cadena inici a la llista taulell. El mètode de cadenes
split()trosseja la cadena que l’invoca pel caràcter (o cadena) que se li passa per argument. Si no rep cap paràmetre, trossejarà pels “espai o espais blancs” que trobi que hi hagi. - Línies 10 i 11: modificació de la variable
taulell, que passa de ser una llista de cadenes a ser una llista de llistes de caràcters. La funciólist(), quan actua sobre una cadena, la transforma en una llista de caràcters. - Línies 18 i 19 (i 25 i 26, i 32 i 33): mostrar el
taulell. Eltaulelles mostra per stdout com a 3 cadenes, una després de l’altra. El mètode de cadenesjoin(), que rep una llista com a paràmetre, la transforma en una cadena, utilitzant com a separador entre els elements lastrque el crida (en aquest cas, el separador és la cadena buida). - Línies 23 i 30: modificació d’un únic element de la llista de llistes. El primer camp indica la posició de la llista de llistes que es vol modificar. El segon l’element de la llista escollida (el caràcter en aquest exemple).
taulell[0][2]indica que, de la llista que ocupa la posició 0 de la llista taulell, s’ha d’accedir a l’element que ocupa la posició 2. En aquest cas es canvia el caràcter.pel caràcterX.
El codi del programa taulell.py seria:
import random
BLANCS = 5 # per no escriure el taulell a la 1a columna
inici = '... ... ...'
taulell = inici.split()
print('llista de cadenes')
print(taulell)
#transformació a llista de llistes de caràcters
for i in range(len(taulell)):
taulell[i] = list(taulell[i])
print('llista de llistes de caràcters')
print(taulell)
print()
print('jugada 0')
#mostrar el camp
for element in taulell:
print(BLANCS * ' ' + ''.join(element))
print()
print('jugada 1')
taulell[1][1] = '0'
#mostrar el camp
for element in taulell:
print(BLANCS * ' ' + ''.join(element))
print()
print('jugada 2')
taulell[0][2] = 'X'
#mostrar el camp
for element in taulell:
print(BLANCS * ' ' + ''.join(element))
El resultat de l’execució seria:
videoioc@debian-xtec:~/Documents/programacio$ python3 taulell.py
llista de cadenes
['...', '...', '...']
llista de llistes de caràcters
[['.', '.', '.'], ['.', '.', '.'], ['.', '.', '.']]
jugada 0
...
...
...
jugada 1
...
.0.
...
jugada 2
..X
.0.
...
A continuació s’expliquen aquests conceptes i altres que us ajudaran a fer transformacions entre llistes i cadenes, a conèixer nous mètodes de llistes i de cadenes i a ampliar els vostres coneixements sobre aquests tipus de dades.
Mutabilitat
El concepte de mutabilitat es refereix a la capacitat d’un objecte de ser modificat després de la seva creació. Un objecte mutable és aquell que pot ser canviat, actualitzat o modificat sense haver-ne de crear una nova instància. En contraposició, un objecte immutable és aquell que no pot ser modificat després de la seva creació.
Dues llistes, tot i tenir els mateixos elements (i ser iguals segons l’operador igualtat == ), poden no ser la mateixa. En canvi, totes les cadenes amb els mateixos caràcters són el mateix objecte.
Per a entendre aquests conceptes s’utilitzarà la funció id(), que pot admetre com a argument qualsevol objecte de Python.
La funció id() en Python retorna l’identificador únic (o identitat) d’un objecte. Aquest identificador és un nombre enter que identifica de manera única un objecte durant el temps d’execució del programa. La funció id() es pot aplicar a qualsevol objecte en Python, com ara variables, llistes, cadenes, etc. i és útil per comparar si dues referències apunten al mateix objecte.
És important destacar que l’identificador és únic per a cada objecte, però no es pot fer cap suposició específica sobre com es genera o quina informació pot contenir. A continuació, amb l’ajuda d’aquesta funció, es mostren exemples del diferent comportament de les llistes i les cadenes.
Mutabilitat d'un objecte de tipus list
Una llista es pot modificar sense que se n’hagi de crear una de nova:
>>> llista = [1, 2, 3, 4] >>> id(llista) 140246465077952 >>> llista[1] = 5 >>> id(llista) 140246465077952 >>> llista [1, 5, 3, 4]
L’operador d’indexació per a les llistes permet actualitzar qualsevol element. Amb mètodes i altres funcions també se’n poden afegir i eliminar, i la llista continua essent la mateixa.
Immutabilitat d'un objecte de tipus str
Si s’intenta modificar un caràcter d’una cadena el processador no ho permet:
>>> cadena = 'hola' >>> cadena[3] = 'i' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment
Una cadena no es pot modificar amb la indexació ni amb cap mètode ni funció, perquè la immutabilitat és una característica bàsica de les str. Si s’ha d’afegir o canviar algun caràcter cal crear un objecte nou.
A continuació podeu observar que dues llistes, tot i tenir els elements iguals, poden ser objectes diferents. En canvi, existeix una única cadena formada per la mateixa successió de caràcters, tot i que estigui desada en variables amb noms diferents.
Llistes amb els mateixos elements però que són objectes diferents
Fixeu-vos en el que indica l’intèrpret interactiu:
>>> llista1 = [1, 2, 3, 4] >>> llista2 = [1, 2, 3, 4] >>> llista1 == llista2 True >>> id(llista1) 140246465061568 >>> id(llista2) 140246465077056
Dues llistes poden ser iguals sense ser la mateixa. En canvi, dues cadenes iguals sempre són el mateix objecte.
La funció id() sobre 2 llistes i l’operador == no són equivalents.
Dues cadenes amb els mateixos caràcters sempre són el mateix objecte
Si s’efectuen les mateixes operacions sobre dades de tipus str:
>>> cadena1 = 'hola' >>> cadena2 = 'hola' >>> cadena1 == cadena2 True >>> id(cadena1) 140246466585392 >>> id(cadena2) 140246466585392
Dues cadenes amb els mateixos caràcters sempre són la mateixa. La funció id() sobre dues cadenes proporcionarà el mateix identificador només si el resultat de l’operador == és True.
Una referència és el nom d’una variable que apunta a un objecte. Poden haver-hi noms de variables diferents que apuntin al mateix, com en l’exemple de les cadenes que s’acaba de mostrar.
Amb les llistes i les seves referències cal anar amb compte. Us caldrà saber en cada moment si esteu treballant sobre dues llistes diferents, o sobre dues referències que apunten a la mateixa llista.
Afegir i eliminar elements
A més a més de modificar el valor d’un element, les llistes permeten afegir i esborrar elements sense haver de crear una llista nova.
Afegir elements a una llista
Ja sabeu que per afegir un element a una llista, podeu concatenar-la amb una altra llista d’un únic element fent servir l’operador concatenació
>>> llista = [1, 2, 3] >>> llista + [4] [1, 2, 3, 4]
A més a més, les llistes també poden invocar el mètode append() que els afegeix l’element passat per argument en l’última posició.
>>> llista = [1, 2, 3] >>> llista.append(4) >>> llista [1, 2, 3, 4]
Les diferències entre els dos mecanismes d’afegir elements són importants:
Les llistes poden ser molt llargues i en general, crear una còpia d’una llista requereix molts recursos. Quan escriviu un programa que no requereix mantenir la llista original, és més convenient utilitzar el mètode append() per afegir elements en comptes de l’operador de concatenació.
- L’operador concatenació crea i retorna una llista nova. La llista original continua existint i és accessible.
- El mètode
append()no retorna res. Actua sobre la llista que l’invoca modificant-la.
Diferències entre append i +
Fixeu-vos en les següents instruccions:
>>> llista1 = [1, 2, 3] >>> id(llista1) 140230081469312 >>> id(llista1 + [4]) 140230081452416 >>> llista1 [1, 2, 3] >>> llista1.append(4) >>> llista1 [1, 2, 3, 4] >>> id(llista1) 140230081469312
Comparant els identificadors podeu observar que l’operador + crea una llista nova i manté l’original intacta. En canvi, append() treballa sobre la llista, ja que no canvia el seu identificador.
Eliminar elements d'una llista
La instrucció delen Python s’utilitza per eliminar una variable o referència a un objecte, alliberant així els recursos associats a l’objecte i permetent que la memòria que ocupa sigui reciclada pel sistema.
La sintaxi bàsica és la següent:
del variableEn aquest cas la variable és el nom de la variable que es vol eliminar.
del no és una funció, un operador ni un mètode. És una paraula clau que pot actuar sobre determinats tipus de dades per a realitzar una instrucció. Una altra instrucció que funciona en els mateixos termes és la paraula break.
Per a eliminar elements d’una llista, del s’utilitza accedint a l’element a través del seu índex.
>>> llista1 = [1, 2, 3] >>> del llista1[1] >>> llista1 [1, 3]
Cal vigilar amb l’ús de del en llistes, ja que si s’utilitza amb una llista mentre s’està recorrent, es produeixen alteracions en la longitud i en els índexs que poden causar problemes.
A part de la instrucció del, existeixen diversos mètodes que actuen sobre una llista que permeten eliminar-ne elements.
Mètodes de llistes
Les llistes, com les cadenes, són objectes de Python i com a tals tenen els seus propis mètodes. Ja heu vist com funciona append().
Per tenir una visió de tots ells, podeu executar la següent instrucció a l’intèrpret interactiu.
help(list)
A continuació es mostren els més rellevants i com utilitzar-los. Tots ells actuen sobre la mateixa llista in situ, modificant-la si és necessari però mantenint la seva identitat.
append(x): afegeix l’elementxal final de la llista. Pot afegir un únic element.
>>> llista = [1, 2, 3] >>> llista.append(4) >>> llista [1, 2, 3, 4]
extend(iterable): afegeix tots els elements d’un iterable (com una altra llista) al final de la llista.
>>> llista1 = [1, 2, 3] >>> llista2 = [4, 5, 6] >>> llista1.extend(llista2) >>> llista1 [1, 2, 3, 4, 5, 6]
insert(i, x): insereix l’elementxa la posicióide la llista.
>>> llista = [1, 2, 3] >>> llista.insert(1, 4) >>> llista [1, 4, 2, 3]
remove(x): elimina la primera aparició de l’elementxa la llista.
>>> llista = [1, 2, 3, 2] >>> llista.remove(2) >>> llista [1, 3, 2]
pop([i]): Elimina i retorna l’element a la posiciói(o l’últim si no s’especifica cap posició).
>>> llista = [1, 2, 3] >>> element = llista.pop(1) >>> llista = [1, 2, 3] >>> print(llista.pop(1)) 2 >>> llista [1, 3]
index(x): retorna la primera posició on apareix l’elementxa la llista.
>>> llista = [1, 2, 3, 2] >>> posicio = llista.index(2) >>> llista = [1, 2, 3, 2] >>> print(llista.index(2)) 1
count(x): retorna el nombre d’aparicions de l’elementxa la llista.
>>> llista = [1, 2, 3, 2] >>> print(llista.count(2)) 2
Existeixen molts més mètodes. No cal aprendre’ls de memòria perquè sempre podeu accedir a la documentació amb la funció help(). Però és molt útil tenir-ne un coneixement general per si necessiteu utilitzar-los en un programa.
L'objecte None
Arribats a aquest punt cal explicar l’existència d’un objecte especial del Python: l’objecte None.
L’objecte None és un objecte que s’utilitza per representar la manca d’un valor o l’absència d’una referència a un objecte. És una constant que pertany al tipus de dada especial anomenat NoneType.
És utilitzat sovint com un valor de retorn per indicar que una funció no retorna cap valor. Per exemple, el mètode append(), que fa una operació sobre la llista que el crida, no retorna res. El canvi el fa sobre el mateix objecte.
És important no confondre None amb una cadena buida (“”), una llista buida ([]), o un valor numèric zero (0). None és un objecte específic que representa la manca de valor, mentre que altres valors buits o zero poden ser interpretats com a valors vàlids en certs contextos.
Si a una variable se li dona el valor de la cadena buida, la variable conté una dada de tipus cadena, tot i que no tingui cap caràcter. És una caixa que conté l’estructura per emmagatzemar una cadena, tot i que no la tingui.
En canvi, si a una variable se li assigna el valor None, l’analogia seria que aquella variable és una caixa buida que no conté res. No està ni especificat el tipus de dada que representa la variable.
L'objecte None
Fixeu-vos en el resultat de les instruccions següents:
>>> llista = [1, 2, 3] >>> print(llista.append(4)) None >>> llista [1, 2, 3, 4] >>> id(llista.append(5)) 9804992 >>> id(None) 9804992
Aquest exemple vol mostrar que quan es deia que el mètode append() no retorna res, era una manera inconcreta de parlar: el mètode append() (i molts més de llistes), en realitat retornen l’objecte None.
També cal remarcar que existeix una única instància d’aquest objecte: és tots els objectes None tenen el mateix identificador, que vol dir que només n’hi ha un.
Transformacions entre llistes i cadenes
Una instància d’un objecte és una representació en memòria d’aquell objecte. Podem tenir moltes instàncies d’un mateix objecte o bé només una, segons les necessitats del programa. Per exemple, en el cas de diferents cadenes iguals, només tindrem una instància. Tot i que tingui diferents noms de variables, totes apuntaran al mateix espai en memòria.
Llistes i cadenes, tot i ser ambdós tipus de dades que ens permeten emmagatzemar un seguit de valors, ens ofereixen funcionalitats diferents. És per això que a vegades ens interessarà tractar un conjunt de valors com si fos una llista o bé com si fos una cadena. Per poder fer la conversió des d’un tipus de dada a l’altre, tenim una sèrie de mètodes i funcions que ens ajudaran.
Funcions str() i list()
Els constructors de llistes i de cadenes són un tipus de funció que permeten la conversió d’un objecte a l’altre. Podeu veure com funcionen en els següents exemples, on es mostra com es fa ús de les funcions str() i list() per passar de llistes a cadenes.
>>> llista = [2, 5, 7, 19] >>> cadena = 'hola' >>> str(llista) '[2, 5, 7, 19]' >>> list(cadena) ['h', 'o', 'l', 'a'] >>> a = str(list(cadena)) >>> a "['h', 'o', 'l', 'a']" >>> b = list(str(llista)) >>> b ['[', '2', ',', ' ', '5', ',', ' ', '7', ',', ' ', '1', '9', ']']
La funció str(), si rep una llista com a argument, crea una cadena on el primer i l’últim caràcter són els símbols [ ]. En la cadena resultant hi ha la transformació a str dels elements de la llista (independentment del seu tipus original), cadascun d’ells seguit del caràcter , i d’un espai en blanc.
La funció str() es pot aplicar sobre qualsevol objecte de Python. Tot i que la cadena resultant de vegades és difícil d’interpretar.
Com podeu veure, si a una llista se li aplica la funció str() i després la funció list(), el resultat <newcontent>
dista
molt de ser la llista original. I a la inversa també.
Metòde split()
split() és un mètode de cadenes. Si consulteu l’ajuda en línia, help(str.split), obteniu la pàgina següent:
split(self, /, sep=None, maxsplit=-1)
Return a list of the substrings in the string, using sep as the separator string.
sep
The separator used to split the string.
When set to None (the default value), will split on any whitespace
character (including \n \r \t \f and spaces) and will discard
empty strings from the result.
maxsplit
Maximum number of splits (starting from the left).
-1 (the default value) means no limit.
Note, str.split() is mainly useful for data that has been intentionally
delimited. With natural text that includes punctuation, consider using
the regular expression module.
split() és un mètode que s’executa sobre una cadena, al qual se li pot passar un paràmetre com a separador. Talla la cadena per on troba el separador i retorna una llista amb tots els trossos obtinguts. Si no se li passa cap argument, per defecte els separadors són qualsevol whitespace.
El següent codi és un exemple de com fer servir split() i len per comptar les paraules d’una frase:
Què és un whitespace
Els “whitespace” (espais en blanc) són caràcters que no es mostren directament quan s’imprimeixen, però que es fan presents a la disposició i format de text.
Els més comuns són:
- Espai en blanc:
“ ”. - Tabulació:
\t. - Salt de línia:
\n. - Retorn de carro:
\r.
>>> frase = 'Aquesta és una frase qualsevol' >>> llista = frase.split() >>> len(llista) 5 >>> llista ['Aquesta', 'és', 'una', 'frase', 'qualsevol']
Tal com indica la documentació, s’ha de vigilar amb el text natural, ja que split() separa per whitespaces sense tenir en compte altres caràcters utilitzats per la puntuació.
Mètode join()
join() és un mètode de cadenes, i s’ha de cridar des de la dada de tipus str que es vol utilitzar per a unir els elements de l’iterable que rep per argument. Un iterable, bàsicament, és una dada que es pot recórrer amb un for; les str i les list són iterables.
La pàgina d’ajuda explica el seu ús:
join(self, iterable, /)
Concatenate any number of strings.
The string whose method is called is inserted in between each given string.
The result is returned as a new string.
Example: '.'.join(['ab', 'pq', 'rs']) -> 'ab.pq.rs'
Split() i join() són els inversos l’un de l’altre. Fixeu-vos en les instruccions següents i els resultats obtinguts:
>>> frase = 'Aquesta és una frase qualsevol'
>>> frase.split(' ')
['Aquesta', 'és', 'una', 'frase', 'qualsevol']
>>> ''.join(frase.split())
'Aquestaésunafrasequalsevol'
>>> ' '.join(frase.split())
'Aquesta és una frase qualsevol'
>>> 'chicken'.join(frase.split())
'Aquestachickenéschickenunachickenfrasechickenqualsevol'
Si s’invoca el mètode join() sobre el mateix separador que s’ha passat com a argument al mètode split(), s’obté la cadena original.
Es pot utilitzar la cadena buida (i qualsevol altre str) per a unir els elements d’una llista.
Tipus de dada tuple
Una tupla en Python és una successió ordenada i immutable d’elements. Les tuples són similars a les llistes en el sentit que permeten emmagatzemar diversos elements de diferents tipus, però la principal diferència radica en la seva mutabilitat: les tuples són immutables.
A continuació es mostren diverses operacions que es poden fer o no amb tuples.
>>> tup1 = (1, 2, 3) >>> tup2 = () >>> len(tup1) 3 >>> len(tup2) 0 >>> tup3 = tuple([5, 6, 7]) >>> llista = list(tup3) >>> tup3[0] 5 >>> llista[0] 5 >>> llista[0] = 1 >>> llista [1, 6, 7] >>> tup3[0] = 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>> tup1 + tup3 (1, 2, 3, 5, 6, 7) >>> tup1 * 3 (1, 2, 3, 1, 2, 3, 1, 2, 3) >>> tup1[1:] (2, 3)
Les cadenes, llistes i tuples son tipus de dades semblants entre si, però amb unes diferències notables.
Les seves principals semblances són:
- Successió d’elements: totes tres, tuples, llistes i cadenes, són successions ordenades d’elements.
- Indexació: es pot accedir als elements individualment mitjançant índexs.
- Iteració: es pot utilitzar bucles
forper recórrer els elements. - Longitud: es pot obtenir la longitud amb la funció
len().
En tant a les diferències, tingueu en compte que són en relació a la mutabilitat:
- Cadenes (
str): Són immutables. No pots canviar els caràcters existents d’una cadena. - Llistes (
list): Són mutables. Es poden afegir, eliminar o modificar elements. - Tuples (
tuple): Són immutables. Després de crear una tupla, no es poden modificar els seus elements.
Llistes i referències
Finalment, cal parlar d’alguns efectes secundaris que pot comportar l’ús de les llistes, quan no s’entén de manera precisa la relació entre una variable i la seva referència.
Una referència és una associació o vincle que connecta una variable amb un objecte o un valor específic en la memòria de l’ordinador.
En altres paraules, una referència proporciona una manera d’accedir a una ubicació de memòria específica on es guarda una dada. Aquesta associació permet que la variable faci referència o apunti al contingut de l’objecte.
Aquesta idea és important, especialment quan es treballa amb llistes o altres tipus de col·leccions mutables. Quan es realitza una assignació, s’està passant la referència a la llista, no una còpia real del contingut de la llista. Això pot conduir a alguns comportaments inesperats si no es té en compte.
Referències múltiples a objectes mutables
Observeu què passa amb les instruccions següents:
>>> llista1 = [1, 2, 3] >>> llista2 = llista1 >>> llista2[0] = 99 >>> llista1 [99, 2, 3] >>> id(llista1) 140230078771904 >>> id(llista2) 140230078771904Quan l’objecte al qual apunta una referència és capaç de mutar, si es modifica no es crea una instància nova. Quan es canvia algun element de
llista2, també es modifica llista1, ja que les dues referències apunten al mateix objecte.
Referències múltiples a objectes immutables
Fixeu-vos què passa en assignar una segona referència a un objecte immutable. Els enters, float i altres objectes primitius són immutables.
>>> num1 = 5 >>> id(num1) 10861320 >>> num2 = num1 >>> id(num2) 10861320 >>> num1 = 10 >>> id(num1) 10861480
Quan s’assigna una variable a una altra (num2 = num1), la segona variable (num2) fa referència al mateix objecte que la primera (num1). En modificar el valor de num1, que és un objecte immutable, la referència num1 apunta a un objecte diferent de l’inicial. I modificar la primera variable (num1 = 10) no afectarà la segona, ja que manté la seva referència a l’objecte original, 5.
Els objectes mutables com les llistes poden conduir a problemes quan es treballa amb múltiples referències. Si dues variables apunten a la mateixa instància d’una llista, un canvi afectarà totes les referències a aquesta llista.
Conceptes clau
Les llistes i les cadenes són successions d’elements. Les llistes poden contenir qualsevol mena d’elements, homogenis o heterogenis. Les cadenes, en canvi, són successions de caràcters.
La característica bàsica que determina el comportament diferent dels dos tipus de dades és la mutabilitat de les llistes enfront de la immutabilitat de les cadenes. La mutabilitat permet l’existència de diversos mètodes de llistes destinats a modificar, afegir o esborrar els seus elements.
Per esborrar elements d’una llista es pot utilitzar la instrucció del. Permet eliminar qualsevol objecte de Python, i en particular també es pot utilitzar per a eliminar un element concret d’una llista (utilitzant de nou la propietat de la mutabilitat).
Per a poder utilitzar de manera eficaç llistes i cadenes, és convenient poder fer conversions entre els dos tipus per maximitzar les seves propietats de manera senzilla. Les funcions str(), list() i els mètodes de str split() i join()simplifiquen molt la feina.
Existeix un altre tipus de dada que són les tuples. Una tupla té un comportament molt semblant a una llista, però és immutable. Les tuples es defineixen utilitzant els parèntesis ( ).
Finalment, cal entendre que les referències connecten variables amb objectes, i que les mutacions en objectes mutables poden afectar múltiples referències al mateix objecte. En altres paraules, si es modifica un objecte mutable apuntat per diverses referències, els canvis es produiran en totes elles.
Ús integrat de cadenes i llistes
En aquesta secció es desenvoluparà un programa que servirà d’exemple per integrar tot el que heu après en la unitat en un cas pràctic. El programa que s’utilitzarà com a base és el programa “calendari”, implementat amb coneixements sobre estructures seqüencials, alternatives i repetitives.
Cas pràctic: programa del calendari
El programa, que probablement ja heu resolt prèviament, és el que respon a l’enunciat següent:
Escriviu un programa que llegeixi una data correcta (dia, mes i year) i mostri els dies que falten fins a final de mes (inclòs el dia llegit).
Especificacions d’entrada: 3 enters dia, mes i year que formen una data correcta del calendari gregorià (dates més enllà de l’any 1582).
El Joc de proves el podeu veure a la taula.
| Entrada | Sortida |
|---|---|
| 27,12, 2030 | 27 12 2030 Divendres,…, 31 12 2030, Dimarts |
| 29,2, 2000 | 29 2 2000, Dimarts |
| 27,2, 2030 | 27 2 2030, Dimecres, 28 2 2030, Dijous |
El refinament seria:
# 1. llegir una data (i processar-la)
# 2. calcular el num del dia de la setmana
# 3. calcular l'últim dia del mes
# 4. mentre els dies son del propi mes
# 4.1. buscar el nom del dia
# 4.2. mostrar la data
# 4.3. buscar el dia següent
# 4.4.calcular el num del dia següent
I finament, el codi quedaria de la manera següent:
import sys
# 1. llegir una data
dia = int(input())
mes = int(input())
year = int(input())
# 2. calcular el num del dia de la setmana
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
# 3. calcular l'últim dia del mes
ultim_dia = 31
if mes == 2:
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
ultim_dia = 29
else:
ultim_dia = 28
elif mes == 4 or mes == 6 or mes == 9 or mes == 11:
ultim_dia = 30
# 4. mentre els dies son del propi mes
while dia <= ultim_dia:
#busco el nom del dia
dia_setmana = "Diumenge"
if i == 1 :
dia_setmana = "Dilluns"
elif i == 2 :
dia_setmana = "Dimarts"
elif i == 3 :
dia_setmana = "Dimecres"
elif i == 4 :
dia_setmana = "Dijous"
elif i == 5 :
dia_setmana = "Divendres"
elif i == 6 :
dia_setmana = "Dissabte"
# 4.2. mostrar la data
print( dia, mes, year, dia_setmana)
# 4.3. buscar el dia següent
dia = dia + 1
# 4.4. calcular el num del dia següent
i = (i + 1) % 7
L’objectiu de la secció és anar mostrant els canvis que simplifiquen i proporcionen millores al programa, relacionant-los amb la lliçó o objecte d’aprenentatge apresos.
Cadenes
L’ús de dades de tipus str pot proporcionar moltes millores al programa:
- La lectura d’una única dada en format “data” millorarà les entrades al programa. Si en comptes de llegir tres enters per separat es fa una única lectura (per exemple 27/12/2030), el programa serà més intuïtiu. A partir d’ara, una dada en format “data” serà una cadena del tipus dd/mm/aaaa, on dd, mm i aaaa corresponen a enters que configuren una data correcta (amb zeros si és necessari per obtenir la longitud desitjada).
- La sortida de les dates en format “data”. L’ús de les
f’stringspermetran de manera senzilla escriure les dades de sortida amb el format correcte. - Manipulació de cadenes. Els mètodes i funcions de cadenes us ajudaran a manipular l’entrada i trossejar-la en els bocins necessaris.
El primer pas serà fer les següents transformacions:
- Lectura d’una dada en forma “data” (pas 1 del refinament).
- Manipulació amb mètodes de cadenes (pas 1 del refinament).
- Sortida en el format correcte (pas 4.2 del refinament).
Com a mostra de què el programa es pot anar fent a bocins, es refarà cada punt per separat, de manera que la resta del programa no en resulti afectat.
Nova versió del punt 1 (també s’utilitzen llistes, però split() és un mètode de str):
Aquesta modificació afecta l’entrada i la sortida del programa. En aquest punt caldria modificar el joc de proves perquè concordés amb la nova versió, i utilitzes una mena d’objecte (un str però que representa una data) en comptes de 3 enters relacionats amb la data.
# 1. llegir una data (i processar-la)
data = input()
camps = data.split('/')
dia = int(camps[0])
mes = int(camps[1])
year = int(camps[2])
Nova versió del punt 4.2:
# 4.2. mostrar la data
print(f'{dia:02}/{mes:02}/{year:02}')
Està indentat perquè és part del codi d’un while.
Encara que l’ordre de la unitat suggereixi abordar el control d’errors en aquest punt, és crucial mantenir la coherència amb el que heu après fins ara i completar la codificació del programa principal. El control d’errors no hauria de ser considerat, i molt menys implementat, abans que el programa principal estigui totalment desenvolupat i hagi passat per verificacions adequades.
Llistes
La introducció del tipus de dada list permet simplificar aspectes molt enrevessats del programa inicial. Per exemple, la gestió de l’últim dia del mes o del nom del dia de la setmana són molt més simples i llegibles si es fa servir aquesta nova estructura.
Amb les llistes es poden millorar els punts 3 i 4.1 del refinament:
- # 3. calcular l’últim dia del mes.
- # 4.1. buscar el nom del dia.
D’entrada, cal definir les constants següents:
SETMANA = ['diumenge', 'dilluns', 'dimarts', 'dimecres', 'dijous', 'divendres', 'dissabte'] DIES_MES = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
Fent servir list el codi del
</newcontent càlcul de l’últim dia de mes <newcontent>
quedaria així:
:
# 3. calcular l'últim dia del mes
ultim_dia = DIES_MES[mes]
if mes == 2:
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
ultim_dia = 29
I el codi del nom del dia de la setmana podria simplificar-se de la següent manera:
# 4.1. buscar el nom del dia
dia_setmana = SETMANA[i]
Els canvis que s’han fet són a escala interna i no afecten entrades ni sortides. No cal modificar el joc de proves. Aplicant el que sabeu de llistes, el programa ha guanyat molt en llegibilitat.
Pas per argument
El millor mètode per entrar una única dada a un programa és utilitzant la línia d’ordres. A més a més, podeu oferir petites modificacions de l’ús del programa, afegint algun paràmetre per distingir opcions.
En aquest cas, el programa oferirà 2 opcions diferents, segons la quantitat de paràmetres llegits:
- Sense paràmetres mostrarà els dies des d’avui fins a final de mes.
- Amb una data com a paràmetre mostrarà els dies des de la data llegida fins al final del mes.
Resumint, l’ús del programa serà:
Usage: calendari.py [dd/mm/aaaa]
Per començar, cal redefinir el joc de proves i adaptar-lo a la nova situació. Vegeu la taula:
| Execució del programa | Sortida |
|---|---|
| calendari.py 25/02/2000 | 25/02/2000, 26/02/2000, 27/02/2000, 28/02/2000, 29/02/2000 |
| calendari.py | 29/01/2030, 30/01/2030, 31/01/2030 |
Avui és dia 29/01/2030. Si reflexioneu, us adonareu que els canvis que s’han afegit (tant en la manera de llegir les dades com en l’opció d’agafar el dia del sistema), només afecten el primer punt del refinament, de lectura i procés de dades.
Només caldrà modificar la lectura (amb sys.argv en comptes de stdin), i gestionar les opcions possibles segons la longitud de l’argument. També cal importar el mòdul sys al principi del programa.
# 1. Llegir la data (i processar-la)
# gestió dels arguments
if len(sys.argv) == 1:
# accedir a la data del sistema
avui = datetime.datetime.now()
data = f'{avui.day:02}/{avui.month:02}/{avui.year:04}'
else:
data = sys.argv[1]
camps = data.split('/')
dia = int(camps[0])
mes = int(camps[1])
year = int(camps[2])
Cal indicar que en el cas d’utilitzar la data d’avui, s’escriu en el format correcte i es torna a desmuntar. Això no faria falta si les línies a partir del split() s’afegissin a l’ else. S’ha optat per aquesta solució perquè el control d’errors que es farà a continuació quedi més clar.
Control d'errors
Si uniu els diversos bocins de programa, obtindreu el programa complet. Cal verificar-lo comprovant el joc de proves. Si tot és correcte, l’últim pas ha de ser fer que el programa sigui robust, evitar que, per molt que s’hi esforci l’usuari, sigui incapaç de produir cap error no controlat ni cap resultat absurd o inesperat.
Cal determinar les dades que no compleixen les especificacions d’entrada i desenvolupar un programa per descartar-les. El control d’errors es pot considerar com un programa independent que no segueix les indicacions de la programació estructurada, per facilitar la llegibilitat i la manipulació dels errors.
L’objectiu d’un programa de control d’errors és descartar totes les dades no vàlides abans de realitzar les accions considerades “perilloses”. D’entrada, cal començar definint el joc de proves del control d’errors:
| Entrada (argument amb què es crida el programa) | És acceptada? |
|---|---|
| 27/01/2030 | True |
| 29/02/2000 | True |
| 29/02/2030 | False |
| 27/1/2030 | False |
| pa/ta/ta | False |
| 2030-01-27 | False |
| 31/01/203/ | False |
| 31/04/2030 | False |
| 31/12/1582 | False |
| 23/00/2000 | False |
| (senseargument) | True |
Estudiant els possibles casos que poden provocar un error, es poden dividir en dos grups:
- Problemes amb el format:
- No tenir la longitud adequada
- No tenir les '/' on toca
- Que els trossos no siguin enters
- Problemes del contingut dels enters de la data:
- Que l’any no sigui correcte
- Que el mes no sigui correcte
- Que el dia del mes no sigui correcte
Els problemes de format poden ocasionar errors en temps d’execució. Tot i això, també s’ha de filtrar el segon tipus de problemes per evitar resultats inesperats.
Cal fer un refinament del control d’errors, ja que l’ordre en què es descartin les dades és important per evitar errors posteriors.
Després de realitzar el split(), és crucial descartar les llistes amb una longitud diferent de tres abans d’accedir a la posició dos. Si la longitud de la llista fos inferior, es produiria un error en el moment de la indexació.
També existeix la possibilitat que el control d’errors obligui a fer algun canvi en el refinament inicial: en l’exemple que ens ocupa serà necessari calcular l’últim dia del mes en el control d’errors, de manera que desapareixerà el punt 3 (# 3. calcular l’últim dia del mes) i aquest càlcul passarà a estar en el control d’errors.
Ja no sou programadors novells i els programes comencen a demanar a crits la introducció de les funcions. Utilitzant-les de manera efectiva serà molt més senzill moure blocs de codi i fer-ne ús en més d’una part del programa.
Quan el programa accepta arguments, la gestió dels arguments s’ha de fer dins el control dels errors (o abans si és possible), no en el programa principal. El motiu és que el programa principal ha de ser independent del mode de lectura.
Els arguments s’han de gestionar conjuntament amb els errors i desar les dades en variables que permetin l’accés al programa principal per una única via.
És essencial tenir un únic programa principal (una única branca) que rebi variables que s’hauran carregat amb els valors necessaris. No pot ser que hi hagi una branca del programa per un cas i una altra per a l’altre.
En el programa que s’està elaborant, la variable data pot contenir la data del sistema o la data llegida per argument. S’ha optat a transformar la del sistema en el format utilitzat per nosaltres. D’aquesta manera, la manipulació es pot fer de manera uniforme en ambdós casos, sense haver de recórrer a un if en el programa principal.
El refinament del control d’errors serà el següent:
# CONTROL D'ERRORS I ARGUMENTS # 1. control d'arguments # 2. control de format: # 3. control de contingut:
El programa complet el teniu a continuació. S’ha afegit el control d’errors i s’ha traspassat el càlcul de l’últim dia de mes a la zona on es controlen els errors. Per saber si el dia és correcte, es necessita conèixer l’últim dia del mes llegit, la mateixa dada que es necessita per sortir de la condició del bucle. Evidentment, no cal fer els càlculs dues vegades, però s’ha de codificar el bloc en el primer lloc on és necessari.
Codi del programa del calendari, amb control d’errors inclòs:
import sys
import datetime
MISSATGE = 'Usage: calendari.py [dd/mm/aaaa]'
SETMANA = ['diumenge', 'dilluns', 'dimarts', 'dimecres', 'dijous',
'divendres', 'dissabte']
DIES_MES = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
# CONTROL D'ERRORS I ARGUMENTS
# 1. control d'arguments
if len(sys.argv) == 1:
# accedir a la data del sistema
avui = datetime.datetime.now()
data = f'{avui.day:02}/{avui.month:02}/{avui.year:04}'
print(data)
else:
data = sys.argv[1]
# 2. control de format:
# 2.1 longitud i '/'
camps = data.split('/')
if not len(camps) == 3:
print(MISSATGE)
exit(1)
if not (len(camps[0]) == 2 and len(camps[1]) == 2 and len(camps[2]) == 4):
print(MISSATGE)
exit(1)
# 2.2 enters
for element in camps:
if not element.isdigit():
print(MISSATGE)
exit(1)
# conversió sense problemes
dia = int(camps[0])
mes = int(camps[1])
year = int(camps[2])
# 2.3 control de contingut:
if not year > 1582:
print(MISSATGE)
exit(1)
if not (mes >= 1 and mes <= 12):
print(MISSATGE)
exit(1)
# 2. 4 calcular l'últim dia del mes
ultim_dia = DIES_MES[mes]
if mes == 2:
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
ultim_dia = 29
if not (dia >= 1 and dia <= ultim_dia):
print(MISSATGE)
exit(1)
# PROGRAMA PRINCIPAL
# 2. calcular el num del dia de la setmana
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
# 3. mentre els dies son del propi mes
while dia <= ultim_dia:
# 3.1. buscar el nom del dia
dia_setmana = SETMANA[i]
# 3.2. mostrar la data
print(f'{dia:02}/{mes:02}/{year:02}')
# 3.3. buscar el dia següent
dia = dia + 1
# 3.4. calcular el num del dia següent
i = (i + 1) % 7
Conclusió final
Com heu comprovat, ara els programes que feu ja són tan llargs i fan tantes coses que és una mica complicat anar afegint blocs per a fer altres coses. En programes complexos, on hi ha diverses funcionalitats o blocs de codi recurrents (com càlculs de dates o validacions), o en situacions en què es preveu que cert codi serà reutilitzat, la creació de funcions proporciona una estructura clara i modular. Les funcions representen blocs ben definits de codi que poden ser fàcilment incorporats o reutilitzats sense problemes de noms de variables. Això millora la claredat i facilita la seva integració en altres programes.
Quan domineu més la matèria aprendreu a definir les funcions correctes a partir del refinament inicial. Això us permetrà programar cada pas (i subpassos) per separat, unint-los tots en el programa mitjançant la crida a les funcions. A més a més, amb una vegada que programeu una funció (la de l’últim dia de mes, per exemple), ja no ho haureu de fer més i podreu cridar-la des de qualsevol programa que la necessiti.




