Introdução à Programação em Python

Notebook 03 - Listas e outros tipos iteráveis

Carlos Caleiro, Jaime Ramos

Dep. Matemática, IST - 2016

(actualizado em 23 de Setembro de 2020)

Listas

Por vezes, é necessário trabalhar com colecções de objectos. Tal pode ser conseguido em Python recorrendo a listas, o paradigma dos objectos ditos iteráveis. Uma lista é uma coleção de objectos de tipos arbitrários, sem comprimento fixo. Recorde-se que listas são objectos mutáveis, facto que traz também algumas consequências interessantes.

Apresentam-se de seguida alguns exemplos que pretendem ilustrar as operações básicas sobre listas. Algumas destas operações são semelhantes a operações sobre cadeias de caracteres, também iteráveis, apresentadas anteriormente.

In [1]:
a=[]

O comando anterior atribui à lista vazia, denotada por [], o nome a.

In [2]:
b=[1, 2, 3]

Neste caso, atribuiu-se o nome b à lista formada pelos números 1, 2 e 3.

In [3]:
c=[4, 5]
In [4]:
b+c
Out[4]:
[1, 2, 3, 4, 5]

A operação + permite concatenar listas. Também pode ser usada para repetição:

In [5]:
b+b
Out[5]:
[1, 2, 3, 1, 2, 3]

A lista anterior também pode ser obtida através da expressão:

In [6]:
b*2
Out[6]:
[1, 2, 3, 1, 2, 3]
In [7]:
b*3
Out[7]:
[1, 2, 3, 1, 2, 3, 1, 2, 3]

Note-se que as operações anteriores não alteraram o valor das listas b e c.

In [8]:
b
Out[8]:
[1, 2, 3]
In [9]:
c
Out[9]:
[4, 5]

O comprimento de uma lista pode ser obtido usando a função len:

In [10]:
len(b)
Out[10]:
3
In [11]:
len(c)
Out[11]:
2
In [12]:
len(a)
Out[12]:
0

Numa lista, a posição que cada objecto ocupa é relevante (ao contrário, por exemplo, de um conjunto). As posições da lista contam-se da esquerda para a direita. A primeira posição é a posição 0. A expressão seguinte permite determinar o primeiro objecto da lista b:

In [13]:
b[0]
Out[13]:
1

Os restantes objectos da lista podem ser obtidos de forma óbvia.

In [14]:
b[1]
Out[14]:
2
In [15]:
b[2]
Out[15]:
3

Se tentarmos aceder a uma posição fora dos limites da lista, o resultado é um erro de avaliação:

In [16]:
b[3]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-16-0a402005bccb> in <module>()
----> 1 b[3]

IndexError: list index out of range

Também podemos aceder às posições da lista a partir do fim. A posição -1 corresponde ao último elemento da lista, a posição -2 corresponde ao penúltimo elemento, e assim sucessivamente.

In [17]:
b[-1]
Out[17]:
3
In [18]:
b[-2]
Out[18]:
2
In [19]:
b[-3]
Out[19]:
1

Formalmente, uma posição negativa corresponde a adicionar o valor dessa posição ao comprimento da lista:

In [20]:
b[len(b)-1]
Out[20]:
3

Como o exemplo anterior ilustra, podemos usar expressões arbitrárias dentro dos parênteses rectos, desde que os valores dessas expressões correspondam a posições da lista. É também possível especificar uma secção da lista através da expressão [i:j]. Com esta expressão seleccionam-se todos os elementos desde a posição i (inclusive) até à posição j (exclusive). Por exemplo, com a expressão seguinte pretendemos seleccionar os elementos desde a posição 0 até à posição 2, excluindo esta última, ou seja, pretendemos os elementos na primeira e na segunda posições:

In [21]:
b[0:2]
Out[21]:
[1, 2]
In [22]:
b[1:3]
Out[22]:
[2, 3]
In [23]:
b[1:2]
Out[23]:
[2]
In [24]:
b[1:1]
Out[24]:
[]
In [25]:
b[0:len(b)]
Out[25]:
[1, 2, 3]

Caso seja omitido um dos limites da expressão i:j (ou ambos), o valor por defeito para o limite esquerdo é 0 e para o limite direito é o comprimento da lista.

In [26]:
b[:2]
Out[26]:
[1, 2]
In [27]:
b[1:]
Out[27]:
[2, 3]
In [28]:
b[:]
Out[28]:
[1, 2, 3]

Os objectos de uma lista não precisam de ser todos do mesmo tipo. As listas podem ser heterogéneas:

In [29]:
w=[1, 2, [3, 2, 1], 'ola']
In [30]:
w[0]
Out[30]:
1
In [31]:
w[1]
Out[31]:
2
In [32]:
w[2]
Out[32]:
[3, 2, 1]
In [33]:
w[3]
Out[33]:
'ola'
In [34]:
w[2][1]
Out[34]:
2

Esta última expressão permite determinar o elemento na posição 1 (segunda posição) na lista que se encontra na posição 2 (terceira posição) da lista w.

Listas e métodos

As operações anteriores podem ser usadas sobre objectos de outros tipos iteráveis (como por exemplo strings). As operações que se apresentam de seguida são operações específicas (métodos) das listas.

In [ ]:
list.
In [35]:
from IPython.display import Image
Image("listmethods.png")
Out[35]:

A primeira destas operações permite adicionar um elemento no fim de uma lista. Atente-se na sintaxe das operações apresentadas nesta secção.

In [6]:
list.append(b,10)
In [4]:
b=[1,2,3]
In [7]:
b
Out[7]:
[1, 2, 3, 10]
In [8]:
id(b)
Out[8]:
4399999944
In [9]:
a=5
In [13]:
id(a)
Out[13]:
4297515072
In [11]:
a=a+1
In [12]:
a
Out[12]:
6

Como podemos observar no exemplo anterior, o valor associado ao nome b foi afectado por este método. Trata-se de um efeito colateral possível devido ao facto de as listas serem um tipo mutável. Vale a pena verificar que se trata do mesmo objecto, apenas com um valor diferente.

Alternativamente, mas com o mesmo efeito, podemos invocar directamente o método sobre um objecto do tipo lista.

In [38]:
a=[]
In [39]:
a.append(5)
In [40]:
list.append(a,[6, 6, 6])
In [41]:
a
Out[41]:
[5, [6, 6, 6]]
In [42]:
a[0]
Out[42]:
5
In [43]:
a[1]
Out[43]:
[6, 6, 6]
In [44]:
list.append(a,7)
In [45]:
list.append(a,12)
In [46]:
list.append(a,'ola')
In [47]:
a
Out[47]:
[5, [6, 6, 6], 7, 12, 'ola']
In [48]:
a.pop()
Out[48]:
'ola'
In [49]:
a
Out[49]:
[5, [6, 6, 6], 7, 12]

O método pop apaga um elemento da lista e devolve-o como resultado. Admite mais um argumento opcional referente à posição que se pretende apagar. No caso de não ser especificado nenhum valor, apaga e devolve a última posição da lista.

In [50]:
list.pop(a,0)
Out[50]:
5
In [51]:
a
Out[51]:
[[6, 6, 6], 7, 12]
In [52]:
list.pop(a,-2)
Out[52]:
7

Tal como anteriormente, o índice da posição a apagar pode ser negativo, contando as posições do fim para o princípio. Para acrescentar um elemento numa qualquer posição da lista utiliza-se o método insert. Este método tem três argumentos: o primeiro é a lista, o segundo é o índice da posição antes da qual se pretende inserir o novo elemento, e o terceiro é o elemento a inserir.

In [53]:
a
Out[53]:
[[6, 6, 6], 12]
In [54]:
list.insert(a,1,5)
In [55]:
a
Out[55]:
[[6, 6, 6], 5, 12]
In [56]:
list.insert(a,0,5)
In [57]:
a
Out[57]:
[5, [6, 6, 6], 5, 12]
In [58]:
list.insert(a,len(a),20)
In [59]:
a
Out[59]:
[5, [6, 6, 6], 5, 12, 20]
In [60]:
list.insert(a,-1,30)
In [61]:
a
Out[61]:
[5, [6, 6, 6], 5, 12, 30, 20]

O resultado da última expressão pode parecer surpreendente mas é coerente com o que temos vindo a estudar. Recorde-se que quando se usa um valor negativo -k para o índice de uma posição numa lista a esse valor corresponde de facto ao índice len(a)-k. Antes de avaliarmos a expressão list.insert(a,-1,30), o comprimento da lista a é 5. Logo, esta expressão é equivalente a list.insert(a,4,30), que corresponde a inserir o elemento 30 antes da posição com índice 4, isto é, antes do elemento 20 (recorde-se que os índices das posições começam em 0).

Também podemos atribuir directamente um valor a uma determinada posição de uma lista.

In [14]:
a=[1,2,3]
In [15]:
a[1]=[7, 7, 7]
In [16]:
a
Out[16]:
[1, [7, 7, 7], 3]

Neste caso o elemento na posição 1 da lista, a lista [6, 6, 6], foi substituído pelo elemento [7, 7, 7]. A lista mantém o mesmo comprimento.

O método remove tem dois argumentos, uma lista e um elemento, e apaga a primeira ocorrência do elemento na lista. No caso de o elemento não estar presente na lista o resultado é um erro de avaliação.

In [64]:
list.remove(a,5)
In [65]:
a
Out[65]:
[[7, 7, 7], 5, 12, 30, 20]

O método index tem dois argumentos, uma lista e um elemento, e devolve o índice da primeira ocorrência do elemento na lista. No caso de o elemento não estar presente na lista o resultado é um erro de avaliação.

In [66]:
list.index(a,12)
Out[66]:
2
In [67]:
list.index(a,[7, 7, 7])
Out[67]:
0

O método count tem dois argumentos, uma lista e um elemento, e devolve o número de ocorrências do elemento na lista.

In [68]:
list.count(a,12)
Out[68]:
1
In [69]:
list.count(a,7)
Out[69]:
0
In [70]:
list.append(a,20)
In [71]:
list.count(a,20)
Out[71]:
2
In [72]:
list.reverse(a)
In [73]:
a
Out[73]:
[20, 20, 30, 12, 5, [7, 7, 7]]
In [74]:
list.pop(a)
Out[74]:
[7, 7, 7]
In [75]:
a
Out[75]:
[20, 20, 30, 12, 5]

O método sort permite ordenar listas (que sejam ordenáveis, ou seja, que os seus elementos sejam comparáveis).

In [76]:
list.sort(a)
In [77]:
a
Out[77]:
[5, 12, 20, 20, 30]

Este método admite um argumento adicional que permite escolher a forma como a lista é ordenada, se por ordem crescente, ou por ordem decrescente.

In [78]:
list.sort(a,reverse=True)
In [79]:
a
Out[79]:
[30, 20, 20, 12, 5]

Este método não serve exclusivamente para listas de números. Pode ser usado com listas com outros tipos de elementos, desde que estes sejam comparáveis.

In [80]:
b=['a', 'i', 'o', 'u', 'e']
In [81]:
list.sort(b)
In [82]:
b
Out[82]:
['a', 'e', 'i', 'o', 'u']
In [83]:
list.sort(b,reverse=True)
In [84]:
b
Out[84]:
['u', 'o', 'i', 'e', 'a']
In [85]:
c=[[4], [2], [3],[1]]
In [86]:
list.sort(c)
In [87]:
c
Out[87]:
[[1], [2], [3], [4]]
In [88]:
d=[[1,2],[4],[2,6],[2,6,7],[4,5]]
In [89]:
list.sort(d)
In [90]:
d
Out[90]:
[[1, 2], [2, 6], [2, 6, 7], [4], [4, 5]]

No entanto, se tentarmos ordenar objectos de tipos diferentes, o resultado é um erro de avaliação.

In [91]:
x=[1, [1], '1']
In [92]:
list.sort(x)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-92-54b4868da927> in <module>()
----> 1 list.sort(x)

TypeError: unorderable types: list() < int()
In [93]:
list.clear(a)
In [94]:
a
Out[94]:
[]

Listas definidas por compreensão

A linguagem Python disponibiliza um conjunto de operações que permitem definir listas de forma concisa e expedita. A primeira destas operações, já utilizada anteriormente, é a operação range. Seguem-se alguns exemplos que ilustram a utilização desta operação.

In [95]:
list(range(10))
Out[95]:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [96]:
list(range(0,5))
Out[96]:
[0, 1, 2, 3, 4]
In [97]:
list(range(3,10))
Out[97]:
[3, 4, 5, 6, 7, 8, 9]
In [98]:
list(range(0,10,2))
Out[98]:
[0, 2, 4, 6, 8]
In [99]:
list(range(10,0,-1))
Out[99]:
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

De facto, a operação range produz aquilo a que se chama um iterador, conceito que aprofundaremos mais adiante, que é transformado numa lista pela operação list.

A operação range permite definir outras listas mais complexas. No exemplo seguinte, define-se a lista de todos os quadrados dos números entre 0 e 9.

In [100]:
[x**2 for x in range(10)]
Out[100]:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [101]:
[[x, y] for x in range(5) for y in range(2)]
Out[101]:
[[0, 0],
 [0, 1],
 [1, 0],
 [1, 1],
 [2, 0],
 [2, 1],
 [3, 0],
 [3, 1],
 [4, 0],
 [4, 1]]

Os exemplos anteriores podem ser usados com outras listas.

In [102]:
a=[1, 3, 5, 8]
In [103]:
[x**2 for x in a]
Out[103]:
[1, 9, 25, 64]
In [105]:
b=[[1, 2, 3],[4, 5, 6],[7, 8, 9]]
In [106]:
[list.reverse(w) for w in b]
Out[106]:
[None, None, None]

Convém recordar que o método reverse não devolve valor. Daí o resultado [None, None, None]. No entanto, este foi aplicado a cada uma das sublistas da lista b, como se pode constatar consultando o valor de b.

In [107]:
b
Out[107]:
[[3, 2, 1], [6, 5, 4], [9, 8, 7]]

Podemos também usar construções mais ricas na definição de listas. Por exemplo, podemos construir uma lista de pares ordenados com valores entre 1 e 3, desde que a primeira posição seja diferente da segunda.

In [108]:
[[x,y] for x in range(1,4) for y in range(1,4) if x!=y]
Out[108]:
[[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]

Podemos seleccionar de uma lista de números inteiros, todos os números pares. Apresentamos a função pares que recebe como argumento uma lista w de números inteiros e devolve a lista contendo todos os números pares em w.

In [109]:
def pares(w):
    return [y for y in w if y%2==0]
In [110]:
a=[1, 2, 10, 44, 33, 1, 2, 45, 71]
In [111]:
pares(a)
Out[111]:
[2, 10, 44, 2]

A função seguinte permite determinar quantos números pares existem numa lista.

In [112]:
def contapares(w):
    return len([y for y in w if y%2==0])
In [113]:
contapares(a)
Out[113]:
4

Na lista anterior existem 4 números pares.

No exemplo seguinte, define-se uma função para aplanar listas de listas.

In [114]:
def aplana(w):
    return [x for elem in w for x in elem]
In [115]:
b=[[1, 2, 3], [4, 5], [6], [7, 8, 9]]
In [116]:
aplana(b)
Out[116]:
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Nos exemplos seguintes generalizamos a função contapares para contar outros tipos de elementos. Começamos por definir alguns predicados sobre números inteiros.

In [117]:
def par(n):
    return n%2==0
In [118]:
par(2)
Out[118]:
True
In [119]:
par(3)
Out[119]:
False
In [120]:
def impar(n):
    return n%2!=0
In [121]:
impar(2)
Out[121]:
False
In [122]:
impar(3)
Out[122]:
True

A função conta recebe dois argumentos, uma lista e um predicado, e devolve o número de elementos da lista para os quais o predicado é verdadeiro.

In [123]:
def conta(w,p):
    return len([y for y in w if p(y)])
In [124]:
conta(a,par)
Out[124]:
4
In [125]:
conta(a,impar)
Out[125]:
5

Para seleccionar elementos de uma lista que verifiquem uma determinada condição (definida por um predicado), podemos definir a função seguinte.

In [126]:
def selecciona(w,p):
    return [x for x in w if p(x)]
In [127]:
selecciona(a,par)
Out[127]:
[2, 10, 44, 2]
In [128]:
selecciona(a,impar)
Out[128]:
[1, 33, 1, 45, 71]

Deixámos para o fim a cópia de listas. Considere-se o seguinte exemplo.

In [129]:
w=[1, 3, 2]
In [130]:
w1 = w
In [131]:
w1
Out[131]:
[1, 3, 2]
In [132]:
list.sort(w)
In [133]:
w
Out[133]:
[1, 2, 3]
In [134]:
w1
Out[134]:
[1, 2, 3]

Embora possa parecer surpreendente, o exemplo anterior serve para ilustrar a forma como o mecanismo de atribuição em Python interage com os tipos mutáveis. Ao contrário de outras linguagens de programação, na atribuição em Python os objectos não são copiados da expressão (origem) para a variável (destino). Em vez disso, é criada uma ligação entre a expressão origem e o nome destino. No caso de objectos mutáveis, isto pode conduzir a resultados inesperados, como se ilustrou acima. De facto, w e w1 são dois nomes (aliases) do mesmo objecto.

In [135]:
id(w)==id(w1)
Out[135]:
True

A solução mais simples consiste em copiar o valor para outro objecto.

In [136]:
del w, w1
In [137]:
w = [1, 3, 2]
In [138]:
w1 = w[:]
In [139]:
id(w)==id(w1)
Out[139]:
False
In [140]:
w1
Out[140]:
[1, 3, 2]
In [141]:
list.sort(w)
In [142]:
w
Out[142]:
[1, 2, 3]
In [143]:
w1
Out[143]:
[1, 3, 2]

Matrizes

Como foi referido anteriormente, em Python, uma matriz é representada por uma lista de listas (a lista das suas linhas), todas com o mesmo comprimento. Apresentamos de seguida alguns exemplos de funções para manipulação de matrizes. Estas funções pressupõem que algumas das funções definidas acima estão disponíveis. Começamos com uma função para seleccionar de uma matriz, todas as linhas crescentes, isto é, todas as linhas cujos elementos se encontram ordenados por ordem crescente. Para tal, definimos um predicado que verifica se uma lista tem os elementos pela ordem pretendida e, em seguida, seleccionamos todas as linhas que satisfazem este predicado, recorrendo à função selecciona.

In [144]:
def listacresc(w):
    w1 = w[:]
    list.sort(w1)
    return w == w1
In [145]:
listacresc([1, 2, 3])
Out[145]:
True
In [146]:
listacresc([2, 1, 2])
Out[146]:
False

Ordenar uma lista para verificar se está ordenada é certamente demasiado. Deixa-se como exercício a implementação de uma definição alternativa, logicamente mais sensata, da definição anterior, que não recorra ao método sort.

In [147]:
def linhascresc(m):
    return selecciona(m,listacresc)
In [148]:
matriz=[[1, 2, 3], [4, 2, 1], [5, 2, 7], [4, 5, 6], [1, 1, 1]]
In [149]:
linhascresc(matriz)
Out[149]:
[[1, 2, 3], [4, 5, 6], [1, 1, 1]]

As definições por compreensão também podem ser usadas sobre matrizes. Suponha-se que se pretende encontrar a diagonal de uma matriz quadrada.

In [150]:
matriz=[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
In [151]:
[matriz[i][i] for i in range(len(matriz))]
Out[151]:
[1, 5, 9]

Apresentamos de seguida, a função que permite determinar a diagonal de uma matriz quadrada.

In [152]:
def diagonal(m):
    return [m[i][i] for i in range(len(m))]
In [153]:
diagonal(matriz)
Out[153]:
[1, 5, 9]

Podemos também verificar se a diagonal de uma matriz é uma lista crescente.

In [154]:
def diagonalcresc(m):
    return listacresc(diagonal(m))
In [155]:
diagonalcresc(matriz)
Out[155]:
True

Podemos encadear definições por compreensão. No exemplo seguinte, apresenta-se uma definição por compreensão para transpor matrizes. Embora esta operação seja disponibilizada em Python, destina-se a ilustrar como se encadeiam definições por compreensão.

In [156]:
m=[[1, 2], [3, 4], [5, 6]]
In [157]:
[[linha[i] for linha in m] for i in range(len(m[1]))]
Out[157]:
[[1, 3, 5], [2, 4, 6]]

Esta expressão constrói uma lista de listas. Para cada valor de i, constrói-se uma lista com as posições i de cada uma das linhas da matriz original, ou seja, com a coluna i. Os valores que i pode assumir correspondem ao comprimento das linhas, ou seja, ao número de colunas.

Podemos promover esta expressão a função, definindo uma função para transpor matrizes.

In [158]:
def transposta(m):
    return [[linha[i] for linha in m] for i in range(len(m[1]))]
In [159]:
transposta(m)
Out[159]:
[[1, 3, 5], [2, 4, 6]]

Recorrendo a esta função, é imediato definir uma função que selecciona de uma matriz, todas as colunas crescentes.

In [160]:
def colunascresc(m):
    return selecciona(transposta(m),listacresc)
In [161]:
m = [[1, 5, 2, 6], [1, 4, 3, 6], [1, 7, 4, 2]]
In [162]:
colunascresc(m)
Out[162]:
[[1, 1, 1], [2, 3, 4]]

Considere-se a função pospares que recebe como argumento uma lista de números inteiros e devolve a lista das posições onde ocorrem números pares. Começamos por definir algumas funções auxiliares. Recorde que a primeira posição de uma lista é a posição 0.

In [163]:
def grafo(w):
    return [[i,w[i]] for i in range(len(w))]
In [164]:
a=[1, 12, 14, 32, 5, 1, 10]
In [165]:
grafo(a)
Out[165]:
[[0, 1], [1, 12], [2, 14], [3, 32], [4, 5], [5, 1], [6, 10]]
In [166]:
def segpospar(p):
    return p[1]%2==0
In [167]:
segpospar([0, 1])
Out[167]:
False
In [168]:
segpospar([1, 12])
Out[168]:
True
In [169]:
def pospares(w):
    return transposta(selecciona(grafo(w),segpospar))[0]
In [170]:
pospares(a)
Out[170]:
[1, 2, 3, 6]

Vejamos passo a passo o funcionamento desta função. Num primeiro passo, constrói-se o grafo da lista a, isto é, uma lista de pares em que o primeiro elemento é a posição e o segundo é o elemento da lista original.

In [171]:
grafo(a)
Out[171]:
[[0, 1], [1, 12], [2, 14], [3, 32], [4, 5], [5, 1], [6, 10]]

Em seguida, seleccionam-se desta lista todos os elementos cuja segunda posição seja par, recorrendo ao predicado segpospar e à função selecciona.

In [172]:
selecciona(grafo(a),segpospar)
Out[172]:
[[1, 12], [2, 14], [3, 32], [6, 10]]

A lista anterior contém todos os elementos pares e respectivas posições. O passo seguinte consiste em transpor esta lista, usando a função transposta.

In [173]:
transposta(selecciona(grafo(a),segpospar))
Out[173]:
[[1, 2, 3, 6], [12, 14, 32, 10]]

Obtemos uma lista com duas listas. A primeira lista é a lista das posições e a segunda lista é a lista dos elementos pares. Basta, por isso, escolher a primeira posição para resultado da função pospares.

In [174]:
transposta(selecciona(grafo(a),segpospar))[0]
Out[174]:
[1, 2, 3, 6]

Deixa-se como exercício generalizar a função anterior para calcular a lista das posições dos elementos de uma lista de inteiros que satisfaçam um particular predicado sobre números inteiros.

Para terminar, apresentamos uma função para calcular o índice das linhas crescentes de uma matriz. Começamos por definir algumas funções auxiliares.

In [175]:
def seglistacresc(w):
    return listacresc(w[1])
In [176]:
seglistacresc([0,[1, 2, 3]])
Out[176]:
True
In [177]:
seglistacresc([1, [3, 2, 4]])
Out[177]:
False
In [178]:
def indlinhascresc(m):
    return transposta(selecciona(grafo(m),seglistacresc))[0]
In [179]:
m = [[1, 2, 3], [3, 4, 1], [1, 2, 2], [4, 2, 1]]
In [180]:
indlinhascresc(m)
Out[180]:
[0, 2]

O funcionamento desta função é em tudo semelhante ao da função pospares.

Tipos mutáveis, passagem de parâmetros e efeitos colaterais

Considere-se a seguinte definição.

In [34]:
a=[1,2,3]
In [35]:
b=[1,a,0]
In [36]:
c=b[:]
In [37]:
a[1]=5
In [38]:
b
Out[38]:
[1, [1, 5, 3], 0]
In [39]:
c
Out[39]:
[1, [1, 5, 3], 0]
In [181]:
def fun(x):
    x=x+x
    return x
In [182]:
a=5
fun(a),a
Out[182]:
(10, 5)
In [183]:
a=[4,5,6]
fun(a),a
Out[183]:
([4, 5, 6, 4, 5, 6], [4, 5, 6])

Obviamente, como esperado, a invocação da função não altera o valor do objecto a dado como input.

Mas o caso poderia ser (inesperadamente?) diferente se a função usasse métodos de uma classe mutável, como é o caso das listas.

In [184]:
def newfun(w,x):
    w.append(x)
    return w
In [185]:
lista=[1,2,3]
valor=5
newfun(lista,valor),lista,valor
Out[185]:
([1, 2, 3, 5], [1, 2, 3, 5], 5)

Este tipo de fenómeno é uma idiossincrasia da linguagem Python à qual devemos estar atentos. Na terminologia das linguagens de programação, é como se o mecanismo de passagem de parâmetros a procedimentos funcionasse por valor no caso de tipos imutáveis e por referência no caso de tipos mutáveis. Em rigor, no entanto, a passagem de parâmetros dá-se sempre por valor sendo o efeito colateral provocado pelo particular significado da atribuição em Python, dado o seu carácter OO.

Outros tipos iteráveis

As listas, que vimos acima, e as cadeias de caracteres (strings), que já tínhamos visto antes, são exemplos daquilo em que na linguagem Python se denominam tipos iteráveis. A ideia é essencialmente a de agrupar valores de forma sequencial. Outros tipos iteráveis disponíveis são tuples (tuplos de valores), dictionaries (dicionários), ranges (progressões aritméticas limitadas), sets (conjuntos) e files (ficheiros).

Já abordámos antes range, a propósito da definição de listas por compreensão, e ficheiros, a propósito das operações de leitura/escrita.

Formalmente, um tipo é iterável quando é conforme com o chamado protocolo de iteração, isto é, se obj é um objecto iterável então iter(obj) retorna um iterador (objecto cuja sequência de valores, finita ou infinita, pode ser obtida, sequencialmente, usando o método next()). Outras formas de construir iteradores incluem a definição de geradores usando a construção yield (que veremos mais tarde, no contexto da programação orientada a objectos), ou a utilização da primitiva enumerate. Estas formas genéricas estão na base da construção de ciclos for, uma forma particular de composição iterativa de instruções que é extremamente útil, e que estudaremos no contexto da programação imperativa.

Vale a pena atentarmos, sucintamente, na manipulação de tuplos, dicionários e conjuntos.

Tuplos

Os tuplos constituem um tipo imutável, que de resto é em tudo semelhante às listas. Notacionalmente, é usual utilizar parênteses curvos para representar tuplos em Python, em vez dos parênteses rectos usados para representar listas.

In [186]:
w=(1,2,3)
In [187]:
w+w
Out[187]:
(1, 2, 3, 1, 2, 3)
In [188]:
len(w)
Out[188]:
3
In [189]:
tuple(2*i for i in range(5))
Out[189]:
(0, 2, 4, 6, 8)

Obviamente, os métodos específicos das listas não são aplicáveis a tuplos.

Utilizámos tuplos, acima, quando ilustrámos a passagem de parâmetros, para devolver vários valores em simultâneo (apesar de tudo, os parênteses curvos são delimitadores e portanto opcionais). Atente-se, por exemplo, na seguinte definição.

In [190]:
def sumprod(x,y):
    return x+y,x*y
In [191]:
sumprod(5,6)
Out[191]:
(11, 30)

Em Python é possível fazer atribuições directamente a tuplos de nomes.

In [ ]:
x=2
y=9
x,y=x+y,x*y
x,y

Esta forma de atribuição simultânea é particularmente útil.

In [193]:
x,y=y,x
x,y
Out[193]:
(18, 11)

Usando atribuições a nomes só seria possível obter o mesmo efeito, a troca de valores entre os dois nomes, usando um nome auxiliar.

In [194]:
aux=x
x=y
y=aux
x,y
Out[194]:
(11, 18)

Dicionários

Os dicionários (ou registos) são semelhantes a listas, mas com as posições nomeadas por um referente ou chave (key). O tipo dos dicionários é mutável, tal como o tipo das listas. Notacionalmente, usam-se chavetas para representar dicionários.

In [23]:
notas={'joao':10,'maria':14,'jack':9,'ana':18}
In [24]:
notas['joao']
Out[24]:
10
In [25]:
notas.pop('joao')
Out[25]:
10
In [26]:
notas
Out[26]:
{'maria': 14, 'jack': 9, 'ana': 18}
In [29]:
notas['john']=10
notas
Out[29]:
{'maria': 14, 'jack': 9, 'ana': 18, 'john': 10}

As chaves, os valores, ou todo o conteúdo de um dicionário pode ser obtido usando, respctivamente, os métodos keys, values, ou items.

In [30]:
notas.keys()
Out[30]:
dict_keys(['maria', 'jack', 'ana', 'john'])
In [31]:
notas.values()
Out[31]:
dict_values([14, 9, 18, 10])
In [32]:
notas.items()
Out[32]:
dict_items([('maria', 14), ('jack', 9), ('ana', 18), ('john', 10)])
In [34]:
for nome in notas.keys():
    if notas[nome]<10:
        print(nome+" RE")
    else:
        print(nome+" "+str(notas[nome]))
maria 14
jack RE
ana 18
john 10

Atente-se noutro exemplo.

In [19]:
def f(x):
    return 2**x

def g(x):
    return x**2

dados={x:[f(x),g(x)] for x in range(6)}
dados
Out[19]:
{0: [1, 0], 1: [2, 1], 2: [4, 4], 3: [8, 9], 4: [16, 16], 5: [32, 25]}
In [21]:
for (i,[x,y]) in dados.items():
    print(x>y)
True
True
False
False
False
True

Vale a pena explorar os métodos específicos disponíveis para a manipulação de dicionários.

Conjuntos

Os conjuntos em Python (tal como é usual em matemática) correspondem a colecções de valores, sem ordem especificada, e sem duplicações. São tratados na linguagem de forma semelhante aos dicionários, mas sem chaves.

In [11]:
a={0,1,2,0}
b=set()
c=set([3,4])

a
Out[11]:
{0, 1, 2}
In [12]:
a.add(3)
a
Out[12]:
{0, 1, 2, 3}

Vale a pena explorar os métodos específicos disponíveis para a manipulação de conjuntos que, para além de add ilustrado acima, incluem as operações habituais sobre conjuntos como união, intersecção e complementação.

In [13]:
a.intersection(c)
Out[13]:
{3}
In [14]:
dir(set)
Out[14]:
['__and__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__iand__',
 '__init__',
 '__init_subclass__',
 '__ior__',
 '__isub__',
 '__iter__',
 '__ixor__',
 '__le__',
 '__len__',
 '__lt__',
 '__ne__',
 '__new__',
 '__or__',
 '__rand__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__ror__',
 '__rsub__',
 '__rxor__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__sub__',
 '__subclasshook__',
 '__xor__',
 'add',
 'clear',
 'copy',
 'difference',
 'difference_update',
 'discard',
 'intersection',
 'intersection_update',
 'isdisjoint',
 'issubset',
 'issuperset',
 'pop',
 'remove',
 'symmetric_difference',
 'symmetric_difference_update',
 'union',
 'update']

Os conjuntos são também passíveis de definições por compreensão, como se ilustra de seguida.

In [15]:
{x for x in "as armas e os baroes assinalados" if x not in " "}
Out[15]:
{'a', 'b', 'd', 'e', 'i', 'l', 'm', 'n', 'o', 'r', 's'}

Definição de funções com argumentos variáveis

Já vimos antes o mecanismo básico de definição de funções em Python. Vale a pena neste ponto atentar na seguinte generalização. É por vezes útil definir funções com um número variável de argumentos, ou com argumentos possivelmente desordenados. A linguagem suporta tal possibilidade recorrendo, a tuplos e dicionários, e às construções usualmente conhecidas por *args e **kwargs.

A definição seguinte corresponde a uma função com pelo menos um argumento, mas potencialmente muitos, que devolve a sua soma.

In [6]:
def f(x,*ys):
    for y in ys:
        x=x+y
    return x

f(5,6,7,10)
Out[6]:
28

O parâmetro *ys da definição de f é na verdade interpretado como um tuplo. Podemos inclusive invocar a função a partir de tuplos de argumentos.

In [7]:
t=(1,2,3)
f(5,*t)
Out[7]:
11

Quando a ordem, ou o nome dos argumentos, é relevante, podemos usar dicionários, como se ilustra.

In [8]:
def g(x,*ys,**zs):
    for y in ys:
        x=x+y
    return zs["a"]+x
            
g(1,1,a=8,b=23)
Out[8]:
10
In [9]:
t=(1,1)
d={"b":100,"c":10,"a":1}
g(1,*t,**d)
Out[9]:
4

Esta ordem deve ser respeitada na definição: primeiro os parâmetros fixos, depois *args e finalmente **kwargs.

Sumário

  • Há vários tipos iteráveis que permitem trabalhar com sequências de valores (objectos)
  • As listas são o mais comum dos tipos iteráveis; para além das usuais operações sobre listas, tratando-se de um tipo mutável, as listas dispõem ainda de diversos métodos que permitem alterar o seu valor
  • Uma matriz é representada em Python como uma lista de listas (a lista das suas linhas)
  • As listas, tal como os outros tipos iteráveis, podem ser definidas por compreensão recorrendo a iteradores

Bibliografia

Introdução à Programação em Mathematica (3a edição): J. Carmo, A. Sernadas, C. Sernadas, F. M. Dionísio, C. Caleiro, IST Press, 2014.

Think Python: How to think like a computer scientist: A. Downey, Green Tea Press, 2012.

Introduction to Computation and Programming Using Python (revised and expanded edition): J. V. Guttag, MIT Press, 2013.

The Art of Computer Programming: D. E. Knuth, Addison-Wesley (volumes 1--3, 4A), 1998.

Learning Python (fifth edition): M. Lutz, O'Reilly Media, 2013.

Programação em Python: Introdução à programação utilizando múltiplos paradigmas: J. P. Martins, IST Press, 2015.

Introdução à Programação em MatLab: J. Ramos, A. Sernadas e P. Mateus, DMIST, 2005.

Learning IPython for Interactive Computing and Data Visualization: C. Rossant, Packt Publishing, 2013.

Programação em Mathematica: A. Sernadas, C. Sernadas e J. Ramos, DMIST, 2003.