Python 3 per a no programadors/Més sobre llistes

Ja hem vist com es fan servir les llistes. Ara que ja teniu més coneixements sobre el tema podem veure més detalladament les llistes. Primer veurem més maneres d'obtenir elements d'una llista i com copiar-los.

Aquí hi ha alguns exemples sobre la utilització dels índexs per accedir a un únic element d'una llista:

>>> alguns_numeros = ['zero', 'un', 'dos', 'tres', 'quatre', 'cinc']
>>> alguns_numeros[0]
'zero'
>>> alguns_numeros[4]
'quatre'
>>> alguns_numeros[5]
'cinc'

Tots aquests exemples us haurien de ser familiars. Si voleu accedir al primer element de la llista únicament heu de fixar-vos en l'índex 0. El segon element correspon a l'índex 1, i així anar fent. Què hem de fer si volem obtenir el darrer element de la llista ? Una manera podria ser fent servir la funció len() d'aquesta manera alguns_numeros[len(alguns_numeros) - 1]. Això funciona en base a que la funció len() sempre retorna el darrer índex més 1 (El que fa és retornar el nombre d'elements que té la llista, i aleshores s'ha de tenir en compte que el primer índex és el 0, i per tant hem de restar 1). El penúltim element el podem obtenir amb el codi alguns_numeros[len(alguns_numeros) - 2]. Hi ha una manera més fàcil de fer això. En Python el darrer element sempre és index -1. El penúltim és index -2, i així anar fent.

Aquí hi ha alguns exemples:

>>> alguns_numeros[len(alguns_numeros) - 1]
'cinc'
>>> alguns_numeros[len(alguns_numeros) - 2]
'quatre'
>>> alguns_numeros[-1]
'cinc'
>>> alguns_numeros[-2]
'quatre'
>>> alguns_numeros[-6]
'zero'

D'aquesta manera qualsevol element de la llista es pot indexar de dues maneres: des de l'inici i des del final.

Una altra manera útil d'accedir a parts de llistes és fent servir particions. Aquí hi ha un altre exemple perquè tingueu una idea de com es poden fer servir:

>>> coses = [0, 'Fred', 2, 'S.P.A.M.', 'Stocking', 42, "Jack", "Jill"]
>>> coses[0]
0
>>> things[7]
'Jill'
>>> coses[0:8]
[0, 'Fred', 2, 'S.P.A.M.', 'Stocking', 42, 'Jack', 'Jill']
>>> coses[2:4]
[2, 'S.P.A.M.']
>>> coses[4:7]
['Stocking', 42, 'Jack']
>>> coses[1:5]
['Fred', 2, 'S.P.A.M.', 'Stocking']

La partició es fa servir per retornar una part d'una llista. L'operador de parts es fa servir així: coses[primer_index:darrer_index]. La partició talla la llista abans del primer_index i darrere del darrer_index i retorna els elements de la part que hi ha al mig. Es poden fer servir ambdós tipus d'indexació:

>>> coses[-4:-2]
['Stocking', 42]
>>> coses[-4]
'Stocking'
>>> coses[-4:6]
['Stocking', 42]

Un altre truc amb les particions és l'índex no especificat. Si no s'especifica el primer índex, s'assumeix que correspon a l'inici de la llista. Si no s'especifica el darrer índex, aleshores s'inclou fins al final de la llista. Aquí hi ha alguns exemples:

>>> coses[:2]
[0, 'Fred']
>>> coses[-2:]
['Jack', 'Jill']
>>> coses[:3]
[0, 'Fred', 2]
>>> coses[:-5]
[0, 'Fred', 2]

Aquí hi ha un programa d'exemple, inspirat en HTML (copieu i enganxeu a la definició "poem" si voleu):

 poem = ["<B>", "Jack", "and", "Jill", "</B>", "went", "up", "the",
        "hill", "to", "<B>", "fetch", "a", "pail", "of", "</B>",
        "water.", "Jack", "fell", "<B>", "down", "and", "broke",
        "</B>", "his", "crown", "and", "<B>", "Jill", "came",
        "</B>", "tumbling", "after"]

 def get_bolds(text):
    true = 1
    false = 0
    ## is_bold retorna si actualment s'està actuant sobre una part del text en negrea
    is_bold = false
    ## start_block és l'índex d'inici, tant dels segments en negreta com dels que no. 
    start_block = 0
    for index in range(len(text)):
        ## Inici del text en negreta
        if text[index] == "<B>":
            if is_bold:
                print("Error: Negreta de més")
            ## retorna "Not Bold:", text[start_block:index]
            is_bold = true
            start_block = index + 1
        ## Final del text en negreta
        ## Recordeu que el darrer nombre d'una partició és l'índex posterior al   
        ## darrer que s'ha fet servir.
        if text[index] == "</B>":
            if not is_bold:
                print("Error: Tanca negreta de més")
            print("Bold [", start_block, ":", index, "]", text[start_block:index])
            is_bold = false
            start_block = index + 1

 get_bolds(poem)

amb la sortida següent:

Bold [ 1 : 4 ] ['Jack', 'and', 'Jill']
Bold [ 11 : 15 ] ['fetch', 'a', 'pail', 'of']
Bold [ 20 : 23 ] ['down', 'and', 'broke']
Bold [ 28 : 30 ] ['Jill', 'came']

La funció get_bold() s'aplica a una llista formada per paraules i etiquetes. Les etiquetes que cerca són <B> -que inicia el text en negreta- i </B> -que el tanca-. La funció get_bold() cerca les etiquetes d'inici i final a tot el text.

La següent característica de les llistes que veurem és la possibilitat de fer-ne còpies. Es pot provar alguna cosa senzilla com:

>>> a = [1, 2, 3]
>>> b = a
>>> print(b)
[1, 2, 3]
>>> b[1] = 10
>>> print(b)
[1, 10, 3]
>>> print(a)
[1, 10, 3]

Probablement això pot semblar força sorprenent ja que fent una modificació a b resulta que també queda modificat a a. El que ha passat és que la declaració b = a fa de b una referència d'a. Això vol dir que b es converteix en una altra manera d'anomenar a. A partir d'aquest moment, qualsevol modificació de b canvia també a. (Una "referència" vincula els dos elements, de manera que el que passi a un, passa a l'altre. Una "còpia" genera un altre element igual, però independent).

De totes maneres algunes assignacions no creen dos noms per una llista:

>>> a = [1, 2, 3]
>>> b = a * 2
>>> print(a)
[1, 2, 3]
>>> print(b)
[1, 2, 3, 1, 2, 3]
>>> a[1] = 10
>>> print(a)
[1, 10, 3]
>>> print(b)
[1, 2, 3, 1, 2, 3]

En aquest cas b no és una referència d'a ja que l'expressió a * 2 crea una llista nova. Aleshores la declaració b = a * 2 dóna a b una referència respecte a * 2 i no pas una referència a a. Totes les operacions d'assignació creen una referència. En passar una llista d'argument a funció es crea també una referència. La majoria de les vegades no cal preocupar-se de crear referències en comptes de còpies. De totes maneres quan sigui necessari fer modificacions a una llista sense canviar un altre nom de la llista s'ha de fer assegurant-se d'haver creat una còpia.

Hi ha diverses maneres de fer una còpia d'una llista. La més simple, que funciona en la majoria de casos és l'operador de particions ja que sempre fa una llista nova fins i tot si és una partició de tota una llista:

>>> a = [1, 2, 3]
>>> b = a[:]
>>> b[1] = 10
>>> print(a)
[1, 2, 3]
>>> print(b)
[1, 10, 3]

Fent servir la partició [:] es crea una còpia nova de la llista. De totes maneres només copia la llista exterior. Qualsevol llista secundària a l'interior segueix sent una referència a la subllista, a la llista original. Per tant, quan la llista conté llistes, les llistes internes s'han de copiar també. Es podria fer manualment però Python ja té un mòdul per a fer-ho. S'ha de fer servir la funció deepcopy del mòdul copy:

>>> import copy
>>> a = [[1, 2, 3], [4, 5, 6]]
>>> b = a[:]
>>> c = copy.deepcopy(a)
>>> b[0][1] = 10
>>> c[1][1] = 12
>>> print(a)
[[1, 10, 3], [4, 5, 6]]
>>> print(b)
[[1, 10, 3], [4, 5, 6]]
>>> print(c)
[[1, 2, 3], [4, 12, 6]]

Abans de res cal fixar-se en que a és una llista de llistes. I després en que quan s'executa b[0][1] = 10, s'aplica tant a a com a b, i no a c. Això passa perquè les matrius interiors segueixen sent referències quan es fa servir l'operador de partició. Tot i això amb deepcopy c és una còpia d'a.

Per tant, ens hem de preocupar per les referències cada vegada que es fa servir una funció o = ? La bona notícia és que aquesta preocupació només hi és quan es fan servir diccionaris i llistes. Els números i les cadenes de text creen referències quan s'assignen, però cada operació matemàtica o sobre cadenes de text crea una còpia nova, de manera que mai quedaran modificats de manera inesperada. Només s'ha de tenir en compte la qüestió de les referències quan s'estigui modificant una llista o diccionari.

A hores d'ara probablement la pregunta deu ser, perquè es fan servir les referències? El motiu bàsic és la velocitat. És molt més ràpid fer una referència a una llista amb un miler d'elements que no pas copiar-los tots. L'altre motiu és que això permet tenir una funció que modifiqui una llista que aparegui en un formulari o un diccionari. Només recordarem les referències quan aparegui algun problema estrany amb dades canviades quan no s'haurien d'haver canviat.