Variáveis de Tipo Avançado#

Estas notas seguem o tutorial oficial do Python de forma bastante próxima: http://docs.python.org/3/tutorial/

Listas#

Listas agrupam dados. Muitas linguagens têm arrays (vamos olhar para eles em breve no Python). Mas, ao contrário dos arrays na maioria das linguagens, as listas podem conter dados de todos os tipos diferentes — não precisam ser homogêneas. Os dados podem ser uma mistura de inteiros, números de ponto flutuante ou complexos, strings ou outros objetos (incluindo outras listas).

Uma lista é definida usando colchetes:

a = [1, 2.0, "minha lista", 4]
a
[1, 2.0, 'minha lista', 4]

Obtendo o tamanho da lista#

A função len() retorna o comprimento de uma lista.

len(a)
4

Acessando elementos da lista#

Listas tem acesso direto aos seus elementos por meio de índices inteiros— lembre-se de que o Python começa a contagem em 0:

a[2]
'minha lista'

Listas permitem acessar fatias (slices) dos seus valores, e.g para obter a fatia da lista a compreendida entre os índices [1, 3):

a[1:3]
[2.0, 'minha lista']

Note que o lado direito do intervalo dos índices é aberto. Também é possível definir o intervalo da fatia com início, fim e incremento. O código abaixo seleciona os índices {1, 3 e 5}.

s = [2, 4, 6, 8, 10, 12, 14, 16, 18]
s[1:7:2]
[4, 8, 12]

Nenhum dos três valores para definição da fatia é obrigatório. Se o início não for fornecido, a fatia irá iniciar do índice 0. Se o fim não for definido, a fatia irá até a última posição da lista. Por fim, se o incremento não for informado, ele receberá o valor 1.

s[:3]
[2, 4, 6]
s[2:]
[6, 8, 10, 12, 14, 16, 18]

Python também permite que os elementos sejam acessados usando índices negativos. Para entender esses índices, basta imaginar que a origem deixa de ser o 0 e passa a ser o tamanho da lista, len(s). Por exemplo, o índice -1 corresponderá a len(s) - 1, ou seja, a última posição da lista. O índice -2 equivalerá à penúltima posição da lista, i.e. len(s) - 2. Além disso, incrementos de fatias também podem ser negativos.

s[-1]
18
s[-2]
16
s = [4, 5, 0, 2, 3, 9, 7, 1, 12]

s[5:2:-1]
[9, 3, 2]

Verificando se um item está presente#

Para verificar se um determinado valor x existe em uma lista ou vetor s, pode-se utilizar o operador in, que funciona de forma similar à notação matemática \(x \in \vec{s}\), conforme exemplificado no código abaixo.

s = [1, 2, -1]

2 in s
True

Por outro lado, para verificar se o elemento não pertence à lista, i.e. \(x \notin \vec{s}\):

3 not in s
True

Modificando a lista#

Ao contrário das strings, as listas são mutáveis, o que significa que seus elementos podem ser alterados, excluídos ou novos elementos podem ser inseridos. Para alterar um valor em uma lista, basta atribuir um novo valor à posição desejada:

print(a)
[1, 2.0, 'minha lista', 4]
a[1] = -2.0
print(a)
[1, -2.0, 'minha lista', 4]

Uma fatia inteira pode ser substituída pelos valores de uma outra lista do mesmo tamanho, como mostra o código abaixo.

a[0:1] = [-1, -2.1]   # isso colocará dois itens no lugar onde 1 existia antes
a
[-1, -2.1, -2.0, 'minha lista', 4]

Note que as listas podem até conter outras listas:

a[1] = ["outra lista", 3]
a
[-1, ['outra lista', 3], -2.0, 'minha lista', 4]

O operador del permite remover uma fatia inteira da lista, reduzindo seu tamanho:

del s[2:7:2]

s
[1, 2]

Assim como com strings, operadores matemáticos são definidos em listas:

a+["hello"]
[-1, ['outra lista', 3], -2.0, 'minha lista', 4, 'hello']
a*2
[-1,
 ['outra lista', 3],
 -2.0,
 'minha lista',
 4,
 -1,
 ['outra lista', 3],
 -2.0,
 'minha lista',
 4]

Existem muitos métodos que funcionam em listas. Dentre eles temos: Dois dos mais úteis são append, para adicionar ao final de uma lista, e pop, para remover o último elemento:

Outras maneiras para adicionar novos elementos a uma lista, você pode usar as seguintes operações:

  • append: Adiciona um elemento ao final da lista.

  • insert: Adiciona um elemento na posição específica desejada.

  • extend: Adiciona os elementos de outra lista ao final da lista.

a = [1, 2.0, "minha lista", 4]
a.append(6)
a
[1, 2.0, 'minha lista', 4, 6]
a.insert(3, 5)

a
[1, 2.0, 'minha lista', 5, 4, 6]
a.extend([7, 8])

a
[1, 2.0, 'minha lista', 5, 4, 6, 7, 8]

Para remover elementos de uma lista, o Python oferece o método pop, que remove e retorna o elemento na posição especificada. Se você não passar um índice, o pop remove o último elemento da lista.

a.pop()
8
a
[1, 2.0, 'minha lista', 5, 4, 6, 7]
elemento_removido = a.pop(3)  # Remove o elemento na posição 3 (número 5)
print(a)  
print(elemento_removido)  
[1, 2.0, 'minha lista', 4, 6, 7]
5

Exercício Rápido:

Uma operação que veremos muito é começar com uma lista vazia e adicionar elementos a ela. Uma lista vazia é criada como:

a = []

  • Crie uma lista vazia

  • Adicione os inteiros de 1 a 10 a ela.

  • Agora remova-os da lista um por um.

Copiando listas#

Copiar pode parecer um pouco contra-intuitivo no início. A melhor maneira de pensar sobre isso é que sua lista vive em algum lugar na memória e, quando você faz

a = [1, 2, 3, 4]

então a variável a é configurada para apontar para aquele local na memória, então ela se refere à lista.

Se então fizermos

b = a

então b também apontará para aquele mesmo local na memória — o exato mesmo objeto de lista.

Como ambos estão apontando para o mesmo local na memória, se alterarmos a lista através de a, a mudança será refletida em b também:

a = [1, 2, 3, 4]
b = a  # tanto a quanto b se referem ao mesmo objeto de lista na memória
print(a)
a[0] = "alteração"
print(b)
[1, 2, 3, 4]
['alteração', 2, 3, 4]

Se você quiser criar um novo objeto na memória que seja uma cópia de outro, você pode indexar a lista, usando : para obter todos os elementos, ou usar a função list():

c = list(a)   # você também pode fazer c = a[:], que basicamente fatiará toda a lista
a[1] = "dois"
print(a)
print(c)
['alteração', 'dois', 3, 4]
['alteração', 2, 3, 4]

As coisas ficam um pouco complicadas quando uma lista contém outro objeto mutável, como outra lista. Então, a cópia que analisamos acima é apenas uma cópia rasa. Faremos isso com mais cuidado na próxima vez.

Quando estiver em dúvida, use a função id() para descobrir onde na memória um objeto está localizado (você não deve se preocupar com o que os valores dos números que você obtém de id significam, mas apenas se eles são os mesmos que os de outro objeto).

print(id(a), id(b), id(c))
140679318602240 140679318602240 140679318613312

ou use o operador is

a is b
True
a is c
False

Existem muitos outros métodos que funcionam em listas

my_list = [10, -1, 5, 24, 2, -1, 9]
my_list.sort() # Ordena a lista em ordem crescente
my_list
[-1, -1, 2, 5, 9, 10, 24]
my_list.count(-1) # Conta quantas vezes o valor -1 aparece na lista
2

Podemos também inserir elementos.

a.insert(3, "meu elemento inserido")
a
['alteração', 'dois', 3, 'meu elemento inserido', 4]

Abaixo estão alguns métodos comuns utilizados com listas em Python, com suas respectivas descrições de efeitos:

Tabela 1.1: Métodos de Listas#

Método

Efeito

L.append(objeto)

Adiciona um elemento ao final da lista.

L.count(elemento)

Retorna o número de ocorrências do elemento.

L.extend(list)

Concatena listas.

L.index(value)

Retorna índice da primeira ocorrência do valor.

L.insert(i, o)

Insere objeto (o) antes de índice (i).

L.pop([indice])

Remove e retorna objeto no índice ou o último elemento.

L.remove(value)

Remove a primeira ocorrência do valor.

L.reverse()

Inverte lista. In situ.

L.sort()

Ordena lista. In locus.

In situ significa «no local», ou seja, a ação é realizada diretamente no objeto ou estrutura de dados, sem criar uma nova cópia. Um exemplo disso é a função L.reverse(), que inverte a lista no próprio local. Já in locus também significa «no local», mas é mais comumente usado para descrever operações feitas diretamente em estruturas de dados, como a função L.sort(), que ordena a lista sem criar uma nova.

Por exemplo, imagine que você tem uma lista L = [1, 2, 3]. Quando você usa o método L.reverse(), ele inverte a lista no próprio local, ou seja, a lista original é modificada para L = [3, 2, 1] e não cria uma nova lista. Isso é um exemplo de in situ. Já se você usar L.sort(), a lista L será ordenada no próprio local também, ou seja, a lista é modificada diretamente e não cria uma nova lista ordenada. Isso é in locus.

Em resumo, tanto o in situ quanto o in locus indicam que a operação ocorre diretamente no local da estrutura de dados, sem criar uma nova instância. A diferença está no uso dos termos em contextos ligeiramente diferentes, mas ambos envolvem a modificação da estrutura original.

Concatenando listas#

Dadas duas listas s1 e s2, pode-se concatená-las, produzindo uma terceira lista. Assim como com strings, o operador + concatena:

b = [1, 2, 3]
c = [4, 5, 6]
d = b + c
print(d)
[1, 2, 3, 4, 5, 6]

Verificando a presença de um item#

Para verificar se um valor x pertence à lista ou vetor s, usa-se o operador in, que equivale à notação matemática \(x \in \vec{s}\), como mostra o código abaixo.

s = [1, 2, -1]

2 in s
True

Intervalos#

O tipo de coleção range é imutável e contém sequências de números. Ele é definido por três valores: início, fim (intervalo aberto) e incremento. Caso o início ou o incremento não sejam informados, eles assumem, respectivamente, os valores 0 e 1.

Por exemplo, um range definido com início \(j\), fim \(k\) e incremento \(t\) gera a sequência \(r = \left\{j \leq r_i < k~|~r_i =j + t * i, i \geq 0\right\}\).

Vamos analisar cada parte da coleção range:

  • range(start, stop, step): A função range() gera uma sequência de números. Ela aceita três argumentos:

    • start: O valor inicial da sequência (inclusivo).

    • stop: O valor final da sequência (exclusivo). Isso significa que a sequência irá parar antes de chegar no valor stop.

    • step: O incremento entre cada número na sequência.

Uma característica importante do range é que ele ocupa a mesma quantidade de memória, independentemente do tamanho do intervalo, pois só armazena os valores de início, fim e incremento. Os valores da sequência são gerados apenas quando solicitados, o que significa que, ao criar um range, ele não imprime automaticamente todos os elementos.

Por exemplo, o comando abaixo cria um range de 0 até 10, mas não exibe todos os valores de imediato.

range(10)
range(0, 10)

Para imprimir todos os elementos, pode-se criar uma lista a partir do intervalo:

list(range(0, 10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Uma fatia de um range também é um range, mantendo a vantagem de usar pouca memória. Por outra lado, valores únicos podem ser acessados diretamente usando seus índices.

range(1,12)[2:6:2]
range(3, 7, 2)
list(range(1,12)[2:6:2])
[3, 5]
range(1,12)[4]
5

Note que range() pode receber um passo.

list(range(2, 10, 2))
[2, 4, 6, 8]
for n in range(2, 10, 2):
    print(n)
2
4
6
8

Operações em Lista e Intervalos#

As operações em lista e intervalo fornecem uma maneira compacta de inicializar realizar operações

list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
squares = [x**2 for x in range(10)]
squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Aqui, usamos outro tipo de Python, a tupla, para combinar números de duas listas em um par.

[(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

Exercício Rapído

Use uma operação de lista para criar uma nova lista a partir de squares contendo apenas os números pares. Pode ser útil usar o operador de módulo, %.

Tuplas#

As tuplas são imutáveis — elas não podem ser alteradas, mas são úteis para organizar dados em algumas situações. Usamos () para indicar uma tupla:

a = (1, 2, 3, 4)
a
(1, 2, 3, 4)

Podemos desempacotar uma tupla:

w, x, y, z = a
w
1

Como uma tupla é imutável, não podemos alterar um elemento:

a[0] = 2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[52], line 1
----> 1 a[0] = 2

TypeError: 'tuple' object does not support item assignment

Mas podemos transformá-la em uma lista, e então podemos alterá-la.

z = list(a)
z[0] = "novo"
z
['novo', 2, 3, 4]

Frequentemente, não está claro como as tuplas diferem das listas. A diferença mais óbvia é que elas são imutáveis (use isso a seu favor para evitar bugs!).

Muitas vezes, veremos tuplas usadas para armazenar dados relacionados que devem ser interpretados juntos. Um bom exemplo é um ponto cartesiano, (x, y).

points = []
points.append((1,2))
points.append((2,3))
points.append((3,4))
points
[(1, 2), (2, 3), (3, 4)]

Conjuntos#

Coleções do tipo conjunto (set) são exatamente o que se espera: uma coleção não-ordenada de elementos únicos que suporta operações matemáticas em conjuntos, como união, interseção, diferença, etc.

S = set('Unesp')

S
{'U', 'e', 'n', 'p', 's'}

Por não ser uma coleção ordenada, não é possível acessar elementos por seus índices, mas naturalmente é possível testar se um elemento pertence a um conjunto.

'b' in S
False
'b' not in S
True
'p' in S
True

Comparações de conjuntos#

Os operadores relacionais <=, <, >= e > tem outros significados quando usados para comparar conjuntos permitindo testar se \(S_1 \subseteq S_2\), \(S_1 \subset S_2\), \(S_1 \supseteq S_2\) e \(S_1 \supset S_2\), respectivamente.

Descrição da Imagem
S1 = {1, 2}
S2 = {1, 2, 3, 4}

S1 <= S2
True
S1 < S2
True
S1 >= S2
False
S1 > S2
False

A comparação de igualdade de dois conjuntos só retorna True sse \(S_1 \subseteq S_2\) e \(S_1 \supseteq S_2\), i.e. se ambos os conjuntos possuem exatamente os mesmos elementos:

{1, 2} == {1, 2}
True

Também pode ser útil verificar se dois conjuntos são disjuntos:

Descrição da Imagem
S1 = {0, 8}
S2 = {1, 2, 3, 4}

S1.isdisjoint(S2)
True

Operações em dois ou mais conjuntos#

Python também oferece operadores para realizar a união (|), interseção (&) e a diferença (-) de dois ou mais conjuntos e a diferença simétrica (^) entre dois conjuntos.

S1 = {1, 2, 3}
S2 = {2, 3, 4}
S3 = {5, 3, 4}

S1 | S2 | S3
{1, 2, 3, 4, 5}
S1 & S2 & S3
{3}
S1 - S2 - S3
{1}
S1 ^ S2
{1, 4}

Modificando um conjunto#

Conjuntos são coleções mutáveis (há um outro tipo de conjunto, chamado frozenset que é imutável e pode ser útil em algumas situações), portanto suportam operações pra adicionar e remover elementos, no entanto os operadores append, insert e del que podem ser usados para as listas não são suportados, pois assumem coleções ordenadas. Assim, para adicionar novos elementos a um conjunto, deve-se usar a operação add e para remover, pode-se usar remove (resulta em erro do tipo KeyError, se o elemento não pertencer ao conjunto) ou discard (executa apenas se o elemento pertencer ao conjunto).

S1.add(4)

S1
{1, 2, 3, 4}
S1.remove(2)

S1
{1, 3, 4}
S1.remove(2)
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[73], line 1
----> 1 S1.remove(2)

KeyError: 2
S1.discard(3)

S1
{1, 4}
S1.discard(3)

S1
{1, 4}

Dicionários#

Um dicionário armazena dados como um par chave:valor. Ao contrário de uma lista, onde você tem uma ordem específica, as chaves em um dicionário permitem que você acesse informações facilmente de qualquer lugar:

my_dict = {"chave1":1, "chave2":2, "chave3":3}
my_dict["chave1"]
1

Você pode adicionar uma nova chave:valor facilmente, e ela pode ser de qualquer tipo.

my_dict["novachave"] = "nova"
my_dict
{'chave1': 1, 'chave2': 2, 'chave3': 3, 'novachave': 'nova'}

Você também pode obter facilmente a lista de chaves que estão definidas em um dicionário. A operação keys retorna todas as chaves de um dicionário em um tipo de coleção (dict_keys).

chaves = my_dict.keys()
chaves
dict_keys(['chave1', 'chave2', 'chave3', 'novachave'])

É importante salientar que essa operação é uma lista que atualiza automaticamente se as chaves do dicionário forem modificadas.

chaves = my_dict.keys()
print(chaves)

my_dict['chave4'] = 4

print(chaves)
dict_keys(['chave1', 'chave2', 'chave3', 'novachave'])
dict_keys(['chave1', 'chave2', 'chave3', 'novachave', 'chave4'])

Para converter em uma lista basta usar o comando list()

chaves = list(my_dict.keys())
chaves
['chave1', 'chave2', 'chave3', 'novachave', 'chave4']

A operação values é similar à operação keys, mas retorna os valores dos mapeamentos do dicionário em um tipo de coleção (dict_values) que também se mantém atualizado quando o dicionário é modificado.

valores = my_dict.values()
print(valores)

my_dict['chave5'] = 5

print(valores)
dict_values([1, 2, 3, 'nova', 4])
dict_values([1, 2, 3, 'nova', 4, 5])

Por fim, a operação items retorna os mapeamentos do dicionário em forma de tuplas em um tipo de coleção (dict_items) que também se mantém atualizado quando o dicionário é modificado.

itens = my_dict.items()
print(itens)

my_dict['chave6'] = 6

print(itens)
dict_items([('chave1', 1), ('chave2', 2), ('chave3', 3), ('novachave', 'nova'), ('chave4', 4), ('chave5', 5)])
dict_items([('chave1', 1), ('chave2', 2), ('chave3', 3), ('novachave', 'nova'), ('chave4', 4), ('chave5', 5), ('chave6', 6)])

Pode-se verificar facilmente se uma chave existe no dicionário usando os operadores in e not in.

my_dict = {"chave1":1, "chave2":2, "chave3":3}
print("chave1" in my_dict)
print("ChaveInvalida" not in my_dict)
True
True

Podemos também checar se a chave existe na operação keys

print("chave1" in chaves)
print("ChaveInvalida" in chaves)
True
False

Podemos fazer operações com os valores das chaves, e redefini-lás

my_dict["chave3"] = my_dict["chave3"] + 1
my_dict
{'chave1': 1, 'chave2': 2, 'chave3': 4}

Podemos usar o comando del para remover chaves do nosso dicionário

del my_dict["chave3"]
my_dict
{'chave1': 1, 'chave2': 2}

O erro KeyError também acontece ao tentar acessar uma chave que não existe no dicionário. Para evitar esse erro, pode-se usar a operação get, que retorna um valor padrão, caso a chave não esteja disponível.

my_dict['chave4']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[89], line 1
----> 1 my_dict['chave4']

KeyError: 'chave4'
my_dict.get('chave4', 0)
0

Caso o valor padrão não seja informado, get retorna o valor None, também conhecido como null em outras linguagens de programação.

print(my_dict.get('chave4'))
None

Abaixo estão alguns métodos comuns utilizados com dicionários em Python, com suas respectivas descrições de efeitos:

Método

Efeito

D.clear()

Remove todos os itens do dicionário.

D.copy()

Cria uma cópia de um dicionário.

D.get(k[, d])

Retorna D[k], se a chave k existir. Senão, d.

D.has_key(k)

Retorna 1 se D possuir a chave k.

D.items()

Retorna lista de tuplas (chave:valor).

D.iteritems()

Retorna objeto iterador para D.

D.iterkeys()

Idem para chaves.

D.itervalues()

Idem para valores.

D.keys()

Retorna lista com todas as chaves.

D.popitem()

Remove e retorna um ítem (chave:valor).

D.update(E)

Copia itens de E para D.

D.values()

Retorna lista com todos os valores.

Exercício Rapído

Crie um dicionário onde as chaves são os nomes em string dos números de zero a nove e os valores são suas representações numéricas (0, 1, … , 9).