Listas
E mais: tuplas
Até aqui aprendemos vários tipos de dados: int
, float
, bool
,
str
. O que todos esses tipos tem em comum é que eles são simples,
básicos. Hoje vamos aprender tipos de dados compostos: tuplas e
listas. Chamamos tuplas e listas de tipos compostos porque
teoricamente elas não são um tipo sozinhas: elas são sempre
tuplas/listas de alguma coisa (listas de números, listas de listas de
strings, etc).
1. Tuplas
Tuplas são uma generalização de duplas/pares; podemos ter triplas, quádruplas, etc. Representamos tuplas por parênteses, com cada elemento da tupla separado por vírgulas:
empty_tuple = () interval = (0, 1)
Acessamos valores de tuplas de duas formas. Uma delas nós já conhecemos de outra aula: quando retornamos múltiplos resultados de uma função é como se estivéssemos retornando uma tupla, e portanto podemos usar a mesma sintaxe de assignment para desestruturar (‘desmontar’) a tupla.
interval = (0, 1) beg, end = interval
A outra forma é por meio de indexação, especificando diretamente o índice do elemento que queremos. Como de praxe, a contagem do índice começa do zero (e portanto o índice do último elemento de uma tupla de \(n\) elementos é \(n-1\)):
full_name = ("Guido", "van Rossum") first_name = full_name[0] surname = full_name[1]
Como se vê, para fazer uma indexação nós colocamos colchetes depois do que queremos indexar, com o número do índice dentro dos colchetes.
Nada impede que usemos uma variável para indexar, mas é preciso que o valor dela seja um inteiro:
i = 2 student_info = ("Jack Black", "5906-94", 67.9) grade = student_info[i]
Uma grande utilidade de tuplas é em fazer assignments múltiplos. Quando queremos trocar o valor de duas variáveis (um swap) sem usar tuplas, precisamos criar uma terceira variável não relacionada:
a = 0 b = 1 # swap! tmp = b b = a a = tmp
Com tuplas e assignment múltiplo, podemos fazer simplesmente:
a = 0 b = 1 # swap! (b, a) = a, b
Isso funciona pois do lado direito do =
ainda temos os valores
antigos de a
e b
. Se tentássemos separar os assignments
perderíamos um dos valores:
a = 0 b = 1 # swap! b = a a = b # aqui a == b == 0
2. Listas
O tipo de dados lista também é intuitivo, sendo uma sequência de elementos acessível por índice (indexável). Listas em geral contém um tipo só (listas de inteiros, listas de tuplas de string e números, etc).
empty_list = [] finite_fibonacci = [0, 1, 1, 2, 3, 5, 8] len(finite_fibonacci) # calcula o ‘comprimento’ da lista, seu número # de elementos finite_fibonacci[4] # == 3
Como podemos ver, uma lista é declarada por um par de colchetes, e seus elementos vão dentro dos colchetes, separados por vírgulas. Não há confusão possível com a sintaxe de indexação, veja:
grades = [8.8, 7.5, 0, 9.4] print(grades[2]) # alguém faltou à prova… print(["hey", "you"][1])
O grande diferencial de listas é que elas são mutáveis (assim como variáveis), e extensíveis (seu tamanho pode crescer ou diminuir).
grades = [8.8, 7.5, 0, 9.4] grades[2] = 6.7 # substituindo com a nota da segunda chamada print(grades) grades = grades + [5.5, 7.1, 8.3] # adicionando notas de outra turma # da mesma disciplina print(grades)
Outro ponto interessante de listas é que podemos iterar sobre elas:
sum_total = 0 grades = [8.8, 7.5, 0, 9.4] for i in range(len(grades)): sum_total = sum_total + grades[i] average = sum_total / len(grades)
Mas na verdade, a melhor forma de iterar sobre uma lista é sem usar range:
sum_total = 0 grades = [8.8, 7.5, 0, 9.4] for grade in grades: sum_total = sum_total + grade average = sum_total / len(grades)
Desta forma não precisamos de usar os índices desnecessariamente, e
portanto é impossível de errá-los, começando a iteração do índice
errado, ou acabando antes do último elemento, ou tentando acessar um
elemento que não existe (tente fazer ["hello", "there"][2]
).
2.1. Exercício: variância
Crie uma função chamada variance
que recebe uma lista de números e
calcula a variância populacional deles, dada por \(\sigma^2\).
\[\sigma^2 = \frac{1}{N}\sum_{i=1}^{N} (x_i - \mu)^2 \qquad \mu = \frac{1}{N}\sum_{i=1}^{N} x_i\]
3. Operadores e funções de listas
Como vimos em exemplos anteriores, podemos usar a função len
para
determinar o tamanho ou comprimento de uma lista (seu número de
elementos). Também podemos usar o operador de adição (+
) para fazer
uma lista maior. Além disso, o operador in
que usamos para fazer um
loop sob uma lista (for element in my_list
) também pode ser usado
sozinho (sem o for
) para checar se um valor está presente na lista,
retornando o valor booleano (True
ou False
) correspondente:
if "needle" in ["hay", "more hay", "needle", "even more hay"]: print("Found it!") print(3.141592 in [3, 3.1, 3.14, 10])
Nem len
nem in
são operadores exclusivos de listas (mais
para a frente veremos mais usos para eles), mas existem uma série de
funções que podem ser aplicadas somente à listas:
fila = ["Michael", "Dwight", "Angela"] list.pop(fila) # remove e retorna o último elemento da lista list.sort(fila) # ordena os elementos da lista list.append(fila, "Jim") # adiciona um novo elemento ao final da lista
Como se vê, essas funções todas são começam com list.
, por conta de
sua exclusividade para listas. Para economizar digitação, Python nos
permite omitir o prefixo list
da seguinte forma:
fila = ["Michael", "Dwight", "Angela"] fila.pop() # remove e retorna o último elemento da lista fila.sort() # ordena os elementos da lista fila.append("Jim") # adiciona um novo elemento ao final da lista
Isso só funciona se o que vier antes do ponto de fato for uma lista, e
nesse caso fila.append("Jim")
e list.append(fila, "Jim")
são
equivalentes (se fila
não for uma lista, teste e veja o que acontece
em cada caso).
Ao rodar esse pedaço de código, o que você espera que seja o valor
final de lista fila
? Até aqui, se nós fizermos uma operação como
x + 1
e não fizermos o reassignment (como em x = x + 1
), o valor
calculado é perdido, descartado. Mas ao averiguarmos o valor de fila
depois das operações acima, ele mudou.
3.1. Exercício: produto cartesiano
Crie uma função chamada cartesian_product
que recebe duas listas e
produz uma nova lista que seja o produto cartesiano entre elas. Para
fazer o produto em si, utilize tuplas; de modo que se \(n\) é membro da
primeira lista e \(m\) é membro da segunda lista, então \((m, n)\) é
membro da lista-resultado.
Por exemplo, uma implementação correta de cartesian_product
teria o
seguinte comportamento (mas a ordem dos elementos da lista pode ser
diferente):
print(cartesian_product([1,2], [3,4])) # [(1,3), (1,4), (2,3), (2,4)] print(cartesian_product([], [3,4])) # [] print(cartesian_product([1], [2, 3])) # [(1,2), (1,3)]
4. Funções puras e funções com efeito colateral
def append_pure(ls, elem): return ls + [elem] def append_impure(ls, elem): ls.append(elem) return ls # compare: languages_i_love = [] print(append_pure(languages_i_love, "Spanish")) print(languages_i_love) print(append_impure(languages_i_love, "Python")) print(languages_i_love)
Em Python e em outras linguagens de programação, algumas funções e
comandos são puros, ao passo que outros são impuros ou tem
efeitos colaterais. Calcular languages_i_love + ["Spanish"]
não
tem nenhum efeito colateral — nenhuma variável é modificada, não há
nada printado na tela, etc. Por conta disso, se não fizermos um
assignment languages_i_love = languages_i_love + ["Spanish"]
o valor
calculado será descartado. Funções como list.append
tem efeito
colateral; além de avaliarmos seus argumentos (uma lista e um valor a
ser inserido nela), modifica-se a lista como ‘efeito’. print
é
impura, pois tem o efeito colateral de mostrar algo na tela.
Uma função pura, assim como uma função matemática, sempre tem o mesmo
resultado para as mesmas entradas, e não modifica nem as entradas nem
nenhuma outra variável. Se languages_i_love = []
, podemos invocar
append_pure(languages_i_love, "Spanish")
5 vezes e o resultado será
sempre ["Spanish"]
, ao passo que languages_i_love
continua sendo
[]
. Para uma função impura, as mesmas entradas de uma função podem
trazer resultados diferentes; basta invocar
append_impure(languages_i_love, "Python")
mais de uma vez e ver como
o resultado é sempre diferente; isto acontece porque o valor de
languages_i_love
está sendo modificado a cada chamada da
função. Teste você mesmo!
4.1. Descobrindo mais sobre uma função
Uma pergunta natural é como saber se uma função tem efeito
colateral. Se você está usando uma função que não foi você que
escreveu (por exemplo uma função que é built-in do Python como
list.sort
), quem escreveu a função deve ter informado se as
propriedades da função, se ela tem efeitos colaterais, se modifica
seus argumentos, etc. Podemos usar a função help
no shell Python
para ver a documentação de funções Python:
help(list.sort)
nos diz (entre outras coisas)
The sort is in-place (i.e. the list itself is modified) and stable (i.e. the order of two equal elements is maintained).
O que nos esclarece que list.sort
modifica a lista. Se fizermos
help(sorted)
, vemos que
Return a new list containing all items from the iterable in ascending order.
Ao dizer que retorna uma lista nova, entende-se que a anterior não é modificada, e portanto a função é pura.
Ao escrever nossas próprias funções nós podemos escolher se queremos que elas sejam puras ou não. Podemos informar isso e outras propriedades da função (além de explicações em geral) colocando uma string (chamada docstring) após a definição do nome da função e de seus argumentos, como no exemplo abaixo:
def append_pure(ls, elem): """Retorna um nova lista com um elemento adicionado à seu fim. Argumentos: ls -- lista elem -- elemento a ser adicionado ao final da lista """ return ls + [elem] help(append_pure)
Como se pode ver, help
pode ser usado em funções que nós mesmos
definimos, e o texto que adicionamos como docstring será também
mostrado. help
é prática de ser usada, mas também podemos encontrar
essas e outras informações sobre uma função built-in de Python na
documentação oficial (exemplo para sorted
).
5. Mutabilidade e imutabilidade
Como mencionamos anteriormente, listas são mutáveis. Mas além de
podermos modificar seus elementos, há uma outra consequência; veja a
comparação abaixo com um tipo imutável (int
).
n = 42 answer = n n = n + 1 print(n, answer) languages_i_love = [] languages_i_like = languages_i_love languages_i_love.append("Python") print(languages_i_love, languages_i_like) languages_i_like = languages_i_like + ["Dothraki"] # O que você acha que será o resultado? print(languages_i_love, languages_i_like)

Valores de tipos imutáveis não mudam, então quando fazemos alguma
operação sobre eles (como somar 1) na verdade primeiro copiamos o
valor e então o modificamos. Operações sobre valores imutáveis inclui
o assignment, então ao fazermos answer = n
, copia-se o valor de n
daquele momento para answer
, e quaiquer modificações futuras de n
não se refletem de maneira alguma em answer
.

n = n + 1
, languages_i_love.append("Python")
) (visualização por Python Tutor)
No caso de valores de tipos mutáveis, ao fazermos languages_i_like =
languages_i_love
, não há cópia alguma; é como se fossem nomes
diferentes para a mesma coisa. Quaisquer modificações da lista que é o
objeto a que os nomes se referem se refletem em ambos. Contudo, ao
fazer languages_i_like = languages_i_like + ["Dothraki"]
, como a
operação +
em listas é pura, retornando uma nova lista, o assignment
quebra a ligação entre languages_i_love
e languages_i_like
: agora
uma se refere a uma lista e a outra se refere a uma outra lista, e
portanto modificações de uma lista não afetam a outra.

languages_i_like = languages_i_like + ["Dothraki"]
(visualização por Python Tutor)6. Tuplas nomeadas
Tuplas são úteis para guardar informações, mas se tiverem muitos elementos seu uso começa a ficar desajeitado. É difícil de gravar a ordem correta dos elementos, e se trocarmos elementos de tipos iguais é difícil de perceber o erro.
# employee information is (name, age, title, department, paygrade) employee1 = ("Meredith Grey", 30, "Surgical resident", "Surgery", 110000.00) employee2 = ("Shonda Rhimes", 45, "Storyteller", "Well-Being", 200000.00)
Além disso, acessar os elementos também pode ser desajeitado, especialmente se não quisermos todos os valores de uma vez só.
drgrey = ("Meredith Grey", 30, "Surgical resident", "Surgery", 110000.00) name, age, jobtitle, department, paygrade = drgrey # ok jobtitle = drgrey[2] # não é muito informativo
Um grande problema é se decidirmos mudar o formato das tuplas. Digamos que queiramos incluir um novo elemento na tupla; se não colocarmos este elemento como último elemento da tupla, qualquer indexação anterior pode se tornar inválida:
# employee information now is (name, age, title, department, room_number, paygrade) drgrey = ("Meredith Grey", 30, "Surgical resident", "Surgery", "404A", 110000.00) drgrey[4] # era paygrade, agora é room_number
Da mesma forma, uma desestruturação da tupla antiga agora é inválida:
# ValueError: too many values to unpack (expected 5) name, age, jobtitle, department, paygrade = drgrey
Tudo isso indica que ao fazermos uma pequena modificação do nosso código (modificar o número de elementos de uma tupla) temos uma série de outras modificações em cascata que precisamos fazer para não criar bugs (em todo uso da tupla precisamos ajustar índices e desestruturação).
Felizmente, há uma forma melhor de usar tuplas para armazenar dados
estruturados: tuplas nomeadas. Cada tupla nomeada é na verdade um tipo
diferente, que precisamos criar com a função namedtuple
. A função
retorna então um construtor, uma nova função que cria elementos do
tipo da tupla nomeada que acabamos de criar, e cujos argumentos são os
nomes que informamos como sendo os nomes da tupla nomeada. Veja o
exemplo:
from collections import namedtuple EmployeeRecord = namedtuple('EmployeeRecord', ['name', 'age', 'title', 'department', 'paygrade']) drgrey = EmployeeRecord(name="Meredith Grey", age=30, title="Surgical resident", department="Surgery", paygrade=110000) print(drgrey)
Nomear tuplas torna seu uso mais fácil, em parte porque temos uma
sintaxe especial para acessar suas componentes: ao invés de
drgrey[4]
podemos fazer drgrey.paygrade
. Outra facilidade é que se
adicionarmos um novo elemento nomeado à tupla, não precisamos de
modificar o código que usa os outros elementos, somente o que código
que cria tuplas.
EmployeeRecord = namedtuple('EmployeeRecord', ['name', 'age', 'title', 'department', 'room_number', 'paygrade']) drgrey = EmployeeRecord(name="Meredith Grey", age=30, title="Surgical resident", department="Surgery", paygrade=110000, room_number="404A") print(drgrey.name, drgrey.paygrade, drgrey.room_number)
6.1. Exercício: calculando saldo de transações
O aplicativo Tricount (e vários outros concorrentes) gerencia contas em grupo. Cada pessoa adiciona gastos compartilhados entre pessoas no grupo (por exemplo: pessoas em uma viagem, em que uma pagou o hotel, a outra alugou um veículo, outra contratou um guia), e no final o aplicativo calcula o quanto cada uma deve pagar ou receber para que todos fiquem quites.
Neste exercício, implemente uma das funcionalidades principais do
aplicativo: escreva a função calculate_balance
, que dado o nome de
uma pessoa e uma lista de gastos, retorna o saldo da pessoa em relação
ao grupo. Se a pessoa pagou mais gastos do que ‘recebeu’ gastos de
outras pessoas, seu saldo é positivo, e caso contrário seu saldo é
negativo.
O nome das pessoas do grupo é sempre uma string, e o gasto será modelado como uma tupla nomeada:
Expense = namedtuple('Expense', ['name', 'payer', 'amount', 'beneficiaries']) example_expenses = [Expense(name="Breakfast", payer="Sam", amount=15.90, beneficiaries=["Frodo", "Sam", "Pippin"]), Expense(name="Breakfast (again)", payer="Pippin", amount=18.30, beneficiaries=["Frodo", "Sam", "Pippin"])]
em que name
é um nome para o gasto (string), payer
é o pagador
(string), amount
é o valor que foi pago (float), e beneficiaries
é
a lista dos nomes das pessoas por quem o gasto será dividido (lista de
strings).
Se você implementou calculate_balance
corretamente,
calculate_balance("Frodo", example_expenses)
deve retornar -11.4
,
já que Frodo não pagou nada para ninguém e recebeu dois cafés da manhã
comprados por Sam e Pippin (nos valores totais de 15.90 e 18.30 cada,
divididos por três pessoas isso significa que Frodo deveria ter pago
11.40 se cada um tivesse pagado a sua parte de uma vez).