Python intermediário - I

Visão geral sobre sequencias

A linguagem Python oferece diversas estruturas de dados caracterizadas como sequencias. As sequencias são implementadas com eficiência nos moldes da linguagem C.

Sequencias podem ser classificadas como:

  • Sequências contêinerizadas (container sequences): guardam referências aos objetos que elas contêm, que podem ser de qualquer tipo. Exemplos são: list, tuple e collections.deque.

  • Sequências rasas (flat sequences): armazenam fisicamente o valor de cada item dentro de seu próprio espaço na memória, e não como objetos distintos. Exemplos são: str,bytes e array.array.

Sequências rasas são mais compactas do que as contêinerizadas, mas são limitadas para armazenar valores primitivos como caracteres, bytes e números.

Outra forma de agrupar tipos de sequências é por mutabilidade:

  • Sequências mutáveis: podem ter seus elementos alterados. Exemplos: list,bytearray,memoryview.

  • Sequências imutáveis: seus elementos são inalteráveis. Exemplos: tuple,str,bytes.

Tuplas

Tuplas são sequências imutáveis de comprimento fixo.

Exemplo: Crie uma tupla com 4 elementos e desempacote-a em 4 variáveis.

tup1 = -1,'a',12.23,'$'
a,b,c,d = tup1

tup2 = (-1,'a',12.23,'$')
e,f,g,h = tup2

print(tup1)
print(tup2)
(-1, 'a', 12.23, '$')
(-1, 'a', 12.23, '$')

Discussão:

  • tup1 é uma tupla criada pelo sequenciamento de elementos, sem parênteses.

  • tup2 é uma tupla idêntica criada pelo sequenciamento de elementos, com parênteses.

  • Em a,b,c,d há um processo de desempacotamento (unpacking).

  • Em e,f,g,h temos um desempacotamento equivalente.

  • Ambas as tuplas são equivalentes.

Exemplo: Crie uma tupla a partir de objetos existentes.

tuple(tup1)
(-1, 'a', 12.23, '$')

Discussão:

  • Este processo é também conhecido como type casting.

Exemplo: Construa tuplas aninhadas.

nums = (0,2,4,6,8),(1,3,5,7,9)
nums
((0, 2, 4, 6, 8), (1, 3, 5, 7, 9))

Exemplo: Altere elementos de uma tupla in-place.

t = tuple(['a',[1,2,3],False])

# imutabilidade!
t[2] = True
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-cca77f8062f1> in <module>
      2 
      3 # imutabilidade!
----> 4 t[2] = True

TypeError: 'tuple' object does not support item assignment
# alteração in-place
t[1].append(4)
t
('a', [1, 2, 3, 4], False)

Discussão:

  • A operação t[2] = True retorna um erro porque a tupla não pode ser alterada.

  • Por outro lado, a lista armazenada na segunda posição pode ser modificada in-place.

Exemplo: Crie tuplas por concatenação.

('a',1) + ('b',2) + ('c',3)
('a', 1, 'b', 2, 'c', 3)
('+','-')*3
('+', '-', '+', '-', '+', '-')

Exemplo: Imprima tabela de valores usando desempacotamento e iteração.

s = [(1,2,3),(4,5,6),(7,8,9)]
s

for (m,n,p) in s:
    print('a={0}, b={1}, c={2}'.format(m,n,p))
a=1, b=2, c=3
a=4, b=5, c=6
a=7, b=8, c=9

Exemplo: Desempacote elementos em iteráveis de comprimento arbitrário.

O desempacotamento de uma tupla (ou sequência) em objetos iteráveis que tenham comprimento arbitrário pode ser feito por meio de uma expressão estrela (star expression).

# desempacota por star expression e ignora primeiro item
_,*restante = tup1
restante
['a', 12.23, '$']

Discussão:

  • Usamos 2 variáveis para desempacotar uma tupla de 4 elementos.

  • A variável p é uma lista contendo os últimos elementos de tup1.

  • A variável _ possui valor, mas é ignorada.

Hint

Use um underscore (_) para representar uma variável que, devido ao desempacotamento, você considere “indesejada”.

Comentário:

  • Outros casos em que star expressions são úteis envolvem dados que possuem mais de um valor atribuível a um mesmo registro (p.ex.: 1 pessoa com 2 números de telefone), ou quando se quer quebrar iteráveis em comprimentos arbitrários.

  • Star expressions produzem listas.

Por exemplo:

_,*m4,_ = (3,4,8,12,16,10)
m4 # múltiplos de 4
[4, 8, 12, 16]
_,_,_,*m5 = (4,8,12,5,10)
m5 # múltiplos de 5
[5, 10]
*m6,_,_ = (6,12,5,10)
m6 # múltiplos de 6
[6, 12]
# 2 star expressions não são permitidas
*m6,*m5 = (6,12,5,10)
  File "<ipython-input-13-5c1c52953256>", line 2
    *m6,*m5 = (6,12,5,10)
    ^
SyntaxError: two starred expressions in assignment

Note

Ao usar uma star expression, certifique-se que o número de variáveis usadas no desempacotamento é consistente com os seus objetivos.

Métodos de tupla

Métodos são funções atribuídas a alguns objetos. Embora tuplas sejam imutáveis, elas possuem métodos bastante úteis. Aqui, consideraremos dois:

  • count: conta quantas vezes um dado valor aparece na tupla.

  • index: localiza o índice de um valor dado.

Exemplo: Contando valores em tupla.

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

print(tupla.count(0))
print(tupla.count(3))
print(tupla.count(4))
0
8
4

Exemplo: Localizando posição.

tupla.index(3)
1

Comentário:

  • Caso haja valores repetidos na tupla, o método index retornará o índice da relativo à primeira aparição. Como o número 3 aparece 8 vezes na tupla e seu primeiro registro ocorre na segunda posição, o valor de retorno de index é 1.

Swap de variáveis

O modo Pythônico de realizar uma troca de variáveis (swap) é o seguinte:

x,y = 1,2
y,x = x,y
x,y
(2, 1)

Tuplas como registros

Tuplas servem para armazenar registros: cada item na tupla é o dado de um campo e a posição do item dá o significado.

Exemplo: Tuplas como registros.

# latitute e longitude da Torre Eiffel
eiffel_ll = (48.85844772530444, 2.2948031631859886)

# capitais do mundo
caps = [('Afeganistão','Cabul','Ásia'),
        ('Barbados','Bridgetown','América'),
        ('Chade','Jamena','África')]

# população indígena no Brasil por decênio
popind = [(1991,294131),(2000,734127),(2010,817963)]

for a,p in popind:
    print(f'Em {a}, o Brasil possuía {p} habitantes indígenas.')
Em 1991, o Brasil possuía 294131 habitantes indígenas.
Em 2000, o Brasil possuía 734127 habitantes indígenas.
Em 2010, o Brasil possuía 817963 habitantes indígenas.

Comentários:

  • Imprimimos os valores dos registros por desempacotamento.

print('{:14} | {:14} | {:14}'.format('PAÍS','CAPITAL','CONTINENTE'))
print('-'*(14*3+2))
fmt = '{:14} | {:14} | {:14}'
for pais, cap, cont in caps:
    print(fmt.format(pais,cap,cont))
PAÍS           | CAPITAL        | CONTINENTE    
--------------------------------------------
Afeganistão    | Cabul          | Ásia          
Barbados       | Bridgetown     | América       
Chade          | Jamena         | África        

Comentários:

  • Neste exemplo, simulamos uma tabela usando um formato-base e criamos uma linha de cabeçalho por concatenação de string.

Dicionários

Um dicionário (dict) é uma estrutura conhecida como tabela hash ou array associativo. É uma coleção de tamanho flexível composta de pares do tipo chave:valor. Criamos um dict por diversas formas. A mais simples é usar chaves e pares explícitos.

d = {} # dict vazio
d
{}

Os pares chave-valor incorporam quaisquer tipos de dados.

d = {'par': [0,2,4,6,8], 'ímpar': [1,3,5,7,9], 'nome':'Meu dict', 'teste': True}
d
{'par': [0, 2, 4, 6, 8],
 'ímpar': [1, 3, 5, 7, 9],
 'nome': 'Meu dict',
 'teste': True}

Acesso a conteúdo

Para acessar o conteúdo de uma chave, indexamos pelo seu nome.

d['par'] 
[0, 2, 4, 6, 8]
d['nome']
'Meu dict'

Exemplo: construindo soma e multiplicação especial.

# dict
op = {'X' :[1,2,3], 'delta' : 0.1}

# função
def sp(op):
    s = [x + op['delta'] for x in op['X']]
    p = [x * op['delta'] for x in op['X']]
    
    return (s,p) # retorna tupla

soma, prod = sp(op) # desempacota

for i,s in enumerate(soma):
    print(f'pos({i}) | Soma = {s} | Prod = {prod[i]}')
pos(0) | Soma = 1.1 | Prod = 0.1
pos(1) | Soma = 2.1 | Prod = 0.2
pos(2) | Soma = 3.1 | Prod = 0.30000000000000004

Inserção de conteúdo

# apensa variáveis
op[1] = 3 
op['novo'] = (3,4,1) 
op
{'X': [1, 2, 3], 'delta': 0.1, 1: 3, 'novo': (3, 4, 1)}

Alteração de conteúdo

op['novo'] = [2,1,4] # sobrescreve
op
{'X': [1, 2, 3], 'delta': 0.1, 1: 3, 'novo': [2, 1, 4]}

Deleção de conteúdo com del e pop

del op[1] # deleta chave 
op
{'X': [1, 2, 3], 'delta': 0.1, 'novo': [2, 1, 4]}
novo = op.pop('novo') # retorna e simultaneamente deleta
novo
[2, 1, 4]
op
{'X': [1, 2, 3], 'delta': 0.1}

Listagem de chaves e valores

Usamos os métodos keys() e values() para listar chaves e valores.

arit = {'soma': '+', 'subtr': '-', 'mult': '*', 'div': '/'} # dict

k = list(arit.keys())
print(k)
val = list(arit.values())
print(val)
for v in range(len(arit)):
    print(f'A operação \'{k[v]}\' de "arit" usa o símbolo \'{val[v]}\'.')
['soma', 'subtr', 'mult', 'div']
['+', '-', '*', '/']
A operação 'soma' de "arit" usa o símbolo '+'.
A operação 'subtr' de "arit" usa o símbolo '-'.
A operação 'mult' de "arit" usa o símbolo '*'.
A operação 'div' de "arit" usa o símbolo '/'.

Combinando dicionários

Usamos update para combinar dicionários. Este método possui um resultado similar a extend, usado em listas.

pot = {'pot': '**'}
arit.update(pot)
arit
{'soma': '+', 'subtr': '-', 'mult': '*', 'div': '/', 'pot': '**'}

Dicionários a partir de sequencias

Podemos criar dicionários a partir de sequencias existentes usando zip.

arit = {'soma', 'subtr', 'mult', 'div', 'pot'} 
ops = {'+', '-', '*', '/', '**'}

dict_novo = {}

for chave,valor in zip(arit,ops):
    dict_novo[chave] = valor
    
dict_novo
{'div': '**', 'mult': '+', 'subtr': '-', 'pot': '/', 'soma': '*'}

Visto que um dict é composto de várias tuplas de 2, podemos criar um de maneira ainda mais simples.

dict_novo = dict(zip(arit,ops)) # visto que dicts
dict_novo
{'div': '**', 'mult': '+', 'subtr': '-', 'pot': '/', 'soma': '*'}

Conjuntos

Um conjunto (set) é uma coleção não ordenada de elementos únicos. Eles podem ser criados através da função set ou de uma expressão literal com um par de chaves {}. Eles são parecidos com dicts, porém não possuem chaves (keys).

Exemplo: Crie o conjunto dos números pares positivos e menores do que 15.

pq1 = set(range(0,15,2))
pq2 = {0,2,4,6,8,10,12,14}

Discussão:

  • Em pq1, criamos o conjunto por função, usando range para gerar os números.

  • Em pq2, criamos o conjunto literalmente, por extensão.

{1,2,2,3,3,4,4,4} # 'set' possui unicidade de elementos
{1, 2, 3, 4}

Operações com conjuntos

A seguir mostraremos uma série de operações com conjuntos. Considere os seguintes conjuntos.

A = {1,2,3}
B = {3,4,5}
C = {6}

União de conjuntos

A.union(B) # união
{1, 2, 3, 4, 5}
A | B # união com operador alternativo ('ou')
{1, 2, 3, 4, 5}

Atualização de conjuntos (união)

A união in-place de dois conjuntos pode ser feita com update.

C
{6}
C.update(B) # C é atualizado com elementos de B
C
{3, 4, 5, 6}
C.union(A) # conjunto união com A
{1, 2, 3, 4, 5, 6}
C # os elementos de A não foram atualizados em C
{3, 4, 5, 6}

A atualização da união possui a seguinte forma alternativa com |=.

C |= A # elementos de A atualizados em C
C
{1, 2, 3, 4, 5, 6}

Interseção de conjuntos

A.intersection(B) # interseção
{3}
A & B # interseção com operador alternativo ('e')
{3}

Atualização de conjuntos (interseção)

A interseção in-place de dois conjuntos pode ser feita com intersection_update.

D = {1, 2, 3, 4}
E = {2, 3, 4, 5}
D.intersection(E) # interseção com E
{2, 3, 4}
D # D inalterado
{1, 2, 3, 4}
D.intersection_update(E) 
D # D alterado
{2, 3, 4}

A atualização da interseção possui a seguinte forma alternativa com &=.

D &= E
D
{2, 3, 4}

Diferença entre conjuntos

A
{1, 2, 3}
D
{2, 3, 4}
A.difference(D) # apenas elementos de A
{1}
D.difference(A) # apenas elementos de D
{4}
A - D # operador alternativo 
{1}
D - A 
{4}

Atualização de conjuntos (diferença)

A interseção in-place de dois conjuntos pode ser feita com difference_update.

D = {1, 2, 3, 4}
E = {1, 2, 3, 5}
D
{1, 2, 3, 4}
D.difference(E)
D
{1, 2, 3, 4}
D.difference_update(E)
D
{4}

A atualização da diferença possui a seguinte forma alternativa com -=.

D -= E
D
{4}

Adição ou remoção de elementos

A
{1, 2, 3}
A.add(4) # adiciona 4 a A
A
{1, 2, 3, 4}
B
{3, 4, 5}
B.remove(3) # remove 3 de B
B
{4, 5}

Reinicialização de um conjunto (vazio)

Podemos remover todos os elementos de um conjunto com clear, deixando-o em um estado vazio.

A
{1, 2, 3, 4}
A.clear()
A # A é vazio
set()
len(A) # 0 elementos
0

Continência

Podemos verificar se um conjunto \(A\) é subconjunto de (está contido em) outro conjunto \(B\) (\(A \subseteq B\)) ou se \(B\) é um superconjunto para (contém) \(A\) (\(B \supseteq A\)) com issubset e issuperset.

B
{4, 5}
C
{1, 2, 3, 4, 5, 6}
B.issubset(C) # B está contido em C
True
C.issuperset(B) # C contém B
True

Subconjuntos e subconjuntos próprios

Podemos usar operadores de comparação entre conjuntos para verificar continência.

  • \(A \subseteq B\): \(A\) é subconjunto de \(B\)

  • \(A \subset B\): \(A\) é subconjunto próprio de \(B\) (\(A\) possui elementos que não estão em \(B\))

{1,2,3} <= {1,2,3} # subconjunto
True
{1,2} < {1,2,3} # subconjunto próprio
True
{1,2,3} > {1,2}
True
{1,2} >= {1,2,3}
False

Disjunção

Dois conjuntos são disjuntos se sua interseção é vazia. Podemos verificar a disjunção com isdisjoint

E
{1, 2, 3, 5}
G = {1,2,3,4}
G
{1, 2, 3, 4}
E.isdisjoint(G) # 1,2,3 são comuns
False

Igualdade entre conjuntos

Dois conjuntos são iguais se contém os mesmos elementos.

H = {3,'a', 2}
I = {'a',2, 3}
J = {1,'a'}
H == I
True
H == J
False
{1,2,2,3} == {3,3,3,2,1} # lembre-se da unicidade
True

Compreensões de lista

As listas são as sequências mais comuns em Python. Listas são objetos mutáveis de comprimento variável. Dominar este tipo de dado é fundamental para compreender o preenchimento de sequências de praticamente qualquer outro tipo. Começaremos aprendendo sobre compreensões de lista, ou, no jargão do Python, listcomps.

Exemplo: Construa uma lista dos códigos Unicode correspondentes dos caracteres de uma string.

palavra = '!@#$%'
cods = []
for c in palavra:
    cods.append(ord(c))
cods    
[33, 64, 35, 36, 37]

Discussão:

  • ord produz o número ordinal correspondente de um caracter segundo o padrão Unicode. Para saber o código hexadecimal correspondente, pode-se fazer hex(ord(c)).

Hint

Caracteres, emojis e símbolos são números codificados! Para saber mais sobre o padrão Unicode, veja este site.

Exemplo: Construa uma listcomp correspondente.

palavra = '!@#$%'
cods = [ord(c) for c in palavra]
cods
[33, 64, 35, 36, 37]

Comentário:

  • Uma listcomp é mais legível e sua intenção é explícita. Todavia, é preciso ser cauteloso para não criarmos códigos ilegíveis abusando de listcomps.

  • Como quebras de linha são ignoradas dentro de pares [],() ou {}, é possível quebrar uma instrução de listcomp sem usar o caracter de escape para quebra de linha \.

# listcomp com quebra de linha sem `\`
[ord(c) 
 for c 
 in palavra]
[33, 64, 35, 36, 37]

Exemplo: Construa um produto cartesiano de pares \((a,b)\), onde \(a\) é um tipo de sangue de acordo com o sistema ABO e \(b\) é o Fator Rh.

A = ['A','B','O','AB']
Rh = ['+','-']
sangue = [(a,b) for a in A for b in Rh]
sangue
[('A', '+'),
 ('A', '-'),
 ('B', '+'),
 ('B', '-'),
 ('O', '+'),
 ('O', '-'),
 ('AB', '+'),
 ('AB', '-')]

Discussão:

  • A listcomp anterior usa dois for que iteram sobre as sequências A e Rh e constroi uma lista de tuplas do tipo (a,b).

Exemplo: Construa um produto cartesiano de pares \((a,b)\), onde \(a\) é o FatorRh e \(b\) é um tipo de sangue de acordo com o sistema ABO.

[(b,a) for a in A for b in Rh]
[('+', 'A'),
 ('-', 'A'),
 ('+', 'B'),
 ('-', 'B'),
 ('+', 'O'),
 ('-', 'O'),
 ('+', 'AB'),
 ('-', 'AB')]

Comentário:

  • Note que, neste caso, basta alterarmos a ordem dos elementos da tupla.

Exemplo: Use listcomps com condicionais para criar filtros.

# equipes da fórmula 1
f1 = ['Red Bull Racing','Mercedes',
 'McLaren','Ferrari','AlphaTauri',
 'Aston Martin','Alpine','Alfa Romeo Racing',
 'Williams','Haas F1 Team']

# equipes que começam com A, em maiúsculas
[team.upper() for team in f1 if team[0] == 'A']
['ALPHATAURI', 'ASTON MARTIN', 'ALPINE', 'ALFA ROMEO RACING']
# equipes cujo nome não inicia com A e tem menos do que 10 caracteres
[team for team in f1 
 if team[0] != 'A' and len(team) < 10]
['Mercedes', 'McLaren', 'Ferrari', 'Williams']

Compreensões de conjuntos

Semelhantemente a listcomps, podemos realizar “setcomps”.

# comprimentos de nomes de equipes (únicos)
{len(team) for team in f1}
{6, 7, 8, 10, 12, 15, 17}
[len(team) for team in f1]
[15, 8, 7, 7, 10, 12, 6, 17, 8, 12]

Comentários:

  • Note que a listcomp para este caso possuiria elementos repetidos.

# comprimento de nomes de equipes que terminam com "ing"
{len(team) for team in f1 if team.endswith('ing')}
{15, 17}

Compreensões de dicionários

Compreensões de dicionários, ou “dictcomps”, possuem resultados similares às anteriores e são convenientes para criar dicts.

{k:v for k,v in enumerate(f1)}
{0: 'Red Bull Racing',
 1: 'Mercedes',
 2: 'McLaren',
 3: 'Ferrari',
 4: 'AlphaTauri',
 5: 'Aston Martin',
 6: 'Alpine',
 7: 'Alfa Romeo Racing',
 8: 'Williams',
 9: 'Haas F1 Team'}

Comentário:

  • enumerate é um objeto que constroi uma enumeração para um iterável.

# enumera a partir do índice 7, mas percorre toda a lista
{k:v for k,v in enumerate(f1,7)}
{7: 'Red Bull Racing',
 8: 'Mercedes',
 9: 'McLaren',
 10: 'Ferrari',
 11: 'AlphaTauri',
 12: 'Aston Martin',
 13: 'Alpine',
 14: 'Alfa Romeo Racing',
 15: 'Williams',
 16: 'Haas F1 Team'}

Sorted

Listas em Python possuem um método chamado sort com o qual podemos realizar uma ordenação in-place de uma lista. Por outro lado, a função sorted permite que façamos o mesmo criando uma nova lista e, dessa maneira, não alterando a lista original.

# copia lista f1
f1c = f1.copy()
f1c
['Red Bull Racing',
 'Mercedes',
 'McLaren',
 'Ferrari',
 'AlphaTauri',
 'Aston Martin',
 'Alpine',
 'Alfa Romeo Racing',
 'Williams',
 'Haas F1 Team']
# lista ordenada
f1c.sort()
f1c
['Alfa Romeo Racing',
 'AlphaTauri',
 'Alpine',
 'Aston Martin',
 'Ferrari',
 'Haas F1 Team',
 'McLaren',
 'Mercedes',
 'Red Bull Racing',
 'Williams']
# lista f1 ordenada
f1c2 = sorted(f1)
f1c2
['Alfa Romeo Racing',
 'AlphaTauri',
 'Alpine',
 'Aston Martin',
 'Ferrari',
 'Haas F1 Team',
 'McLaren',
 'Mercedes',
 'Red Bull Racing',
 'Williams']
# lista original intacta
f1
['Red Bull Racing',
 'Mercedes',
 'McLaren',
 'Ferrari',
 'AlphaTauri',
 'Aston Martin',
 'Alpine',
 'Alfa Romeo Racing',
 'Williams',
 'Haas F1 Team']

Para realizar a ordenação reversa, usamos reverse=True na função sorted.

# ordenação re
sorted(f1,reverse=True)
['Williams',
 'Red Bull Racing',
 'Mercedes',
 'McLaren',
 'Haas F1 Team',
 'Ferrari',
 'Aston Martin',
 'Alpine',
 'AlphaTauri',
 'Alfa Romeo Racing']

Aleatoriedade

Números aleatórios, ou também chamados de randômicos, são úteis para fabricar experimentos estatísticos, realizar amostragem e desenvolver uma série de estudos envolvendo probabilidade.

Em Python, podemos usar o módulo random para atingir uma diversidade de propósitos em ciência de dados. Vamos explorar algumas funções para geração aleatória: choice, random, randint, shuffle e sample.

import random

Exemplo: Crie um experimento de lançamento de dados e realize uma escolha aleatória em \(n\) lançamentos.

dado = list(range(1,7))

n = 4
for _ in range(n):
    print(random.choice(dado))
4
1
5
2

Comentários:

  • O loop será repetido \(n\) vezes.

  • O método choice escolhe um valor ao acaso entre os disponíveis na lista.

Exemplo: Crie um experimento de probabilidade aleatório que determine a salinidade presente em 1 litro de água marinha (3 - 5%).

na = 5
saly = []
for _ in range(na):
    saly.append(0.03 + (0.05-0.03)*random.random())
saly    
[0.045050420686575235,
 0.03604191301033581,
 0.043942882418974434,
 0.04297061363142192,
 0.035321529319922725]

Comentários:

  • Este experimento equivale a coletar 5 amostras de água marinha com salinidade variável entre 3 e 5%.

  • O método random determina um número aleatório no intervalo [0,1).

Exemplo: Crie um experimento que ordena 15 arremessadores para cestas de 3 pontos durante um treinamento do time do Los Angeles Lakers (0 a 99) excluindo a lista de números reservados. Considere que se o número sorteado não estiver atribuído a nenhum jogador atual ou se já tiver sido sorteado, o técnico escolherá o próximo da lista.

Lista de números reservados: 8,13,17,19,22,24,25,32,33,34,42,44,52,99.

reservados = [8,13,17,19,22,24,25,32,33,34,42,44,52,99]

shooter = []
while len(shooter) != 15:
    x = random.randint(0,99)
    if x not in reservados and x not in shooter:
        shooter.append(x)    

print('Los Angeles Lakers :: 3-point shooters (Training Schedule):')
for s in shooter:
    print(s)
        
Los Angeles Lakers :: 3-point shooters (Training Schedule):
40
71
23
97
3
93
68
74
27
0
39
47
30
96
88

Comentários:

  • O loop while encerrará quando uma lista com 15 arremessadores estiver completa.

  • A estrutura condicional previne que números já sorteados entrem na lista.

  • O método randint escolhe um número inteiro aleatório entre 0 e 99.

Exemplo: Crie uma alteração no schedule de treinamento anterior a partir da última lista disponível.

print(random.shuffle(shooter))
shooter
None
[30, 93, 74, 0, 23, 47, 96, 39, 68, 40, 27, 97, 71, 3, 88]

Comentário:

  • O método shuffle reordena a lista shooter randomicamente e produz um valor None como saída. Dessa forma, geramos uma nova lista com os mesmos arremessadores pré-selecionados, mas em ordem distinta.

Exemplo: Construa uma tabela aleatória de 3 jogos de beisebol entre times da MLB.

# lê arquivo com nome de times da MLB
with open('../database/mlb-teams.csv') as f:
    mlb = f.read().splitlines()
    
for _ in range(3):
    a,b = random.sample(mlb,2)
    print(f'Game {_+1}: {a} x {b}')        
Game 1: Pittsburgh Pirates x Baltimore Orioles
Game 2: San Diego Padres x Chicago White Sox
Game 3: Kansas City Royals x New York Mets

Comentários:

  • O método sample realiza uma amostragem de \(k\) valores disponíveis na lista. Aqui, escolhemos \(k=2\), de modo que 2 times são escolhidos ao acaso para comporem 3 jogos.

Expressões regulares

Expressões regulares (regular expressions), também conhecidas como RE, regexes, ou regex pattern são regras estabelecidas para localizar padrões desejados em strings. REs, na verdade, formam uma pequena linguagem de programação dentro de uma linguagem maior.

Em Python, REs funcionam com base em códigos escritos na linguagem C. Nossa intenção aqui é fazer uma introdução sobre a utilidade e aplicabilidade de expressões regulares. Para entender este tema com profundidade, é necessário estudar aspectos internos de compiladores.

Para que usamos regex?

Expressões regulares são implicitamente usadas por nós quase que diariamente quando utilizamos comandos do tipo Localizar para buscar uma palavra em uma página da internet ou dentro de um arquivo PDF. Porém, elas nos permitem fazer muito mais do que uma simples busca por palavras.

O texto a seguir é um trecho do livro Memórias Póstumas de Brás Cubas, de Machado de Assis, que pode ser recuperado pelo arquivo bras-cubas.txt:

Não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado. Talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas. O que por agora importa saber é que Virgília — chamava-se Virgília — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito. O estranho levantou-se e saiu. Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo. Saiu; Virgília deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro, sem articular palavra. Quem diria? De dois grandes namorados, de duas paixões sem freio, nada mais havia ali, vinte anos depois; havia apenas dois corações murchos, devastados pela vida e saciados dela, não sei se em igual dose, mas enfim saciados. Virgília tinha agora a beleza da velhice, um ar austero e maternal; estava menos magra do que quando a vi, pela última vez, numa festa de São João, na Tijuca; e porque era das que resistem muito, só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata.

Suponha que você quisesse localizar quantas vezes a palavra “Vírgilia” aparece neste trecho do livro. Com a ajuda de um editor de textos munido de um comando de busca, facilmente encontraríamos a resposta. São 4 vezes.

Agora, suponha que você quisesse localizar se, neste mesmo texto, há alguma ocorrência do seguinte padrão: palavra que se inicia com a letra “p”, maiúscula ou minúscula, possui pelo menos uma letra “i” e a letra “i” deve ser, no máximo, o quarto caracter. Claramente, não conseguiríamos cumprir esta exigência tão facilmente como encontrar “Virgília”. Com algum esforço visual, conseguimos verificar no texto que “paixões” atende este critério. Como faríamos isto usando Python? Ou melhor, como faríamos isto usando Python em um arquivo com 550.000 palavras para serem analisadas?

O padrão que acabamos de descrever pode ser procurado por meio de uma regex. Ou seja, regex são úteis quando desejamos buscar um determinado padrão em strings. A seguir, vamos usar o pequeno texto de Machado de Assis como base para aprender um pouco sobre regex.

Padrões simples, caracterese ordinários e metacaracteres

A tarefa mais simples que podemos executar com REs é combinar caracteres. Por exemplo, sabemos que a string 'murchos' equivale à string 'murchos', mas não equivale à string 'moribundos'. No primeiro caso, todos os caracteres se combinam e estão posicionados nos mesmos lugares; no segundo caso, embora alguns caracteres se combinem, a exemplo de 'm', 'r', 'o' e 's', e estejam posicionados aparentemente em mesmos lugares (no início, terceira posição e final da string), obviamente uma string não combina exatamente com a outra.

Há muitos caracteres que podem ser usados para combinações simples. Por exemplo, quando em case insensitive, 'mur' é um padrão encontrado tanto em 'Murcho', quanto em 'muralha' ou em 'HAMURABI'. Entretanto, há caracteres que não podem ser combinados da forma como o lemos. Por exemplo, para buscarmos o padrão '\textsuperscript' em uma string, o caracter '\' não pode ser usado de forma literal. Ele é um metacaracter.

Caracteres ordinários são basicamente todos os caracteres ASCII alfanuméricos, tais como 'b', 'B' e '1'.

Note

ASCII, de American Standard Code for Information Interchange, é um padrão de codificação de caracteres que foi predominantemente usado desde a década de 1960 e tornou-se um padrão da internet em 2015 contendo apenas 128 caracteres. Em 2007, ele foi suplantado pelo padrão UTF-8 (Unicode Transformation Format - 8 bit), gerido pela iniciativa Unicode (Universal Coded Character Set). O UTF-8 é um padrão mais novo que comporta 1.112.064 caracteres e também é o usual no Python 3. Os 128 primeiros caracteres do padrão UTF-8 são os 128 primeiros da lista do UTF-8, dos quais alguns tornaram-se obsoletos.

Caracteres especiais são todos os metacaracteres. A lista dos metacaracteres é a seguinte:

. ^ $ * + ? { } [ ] \ | ( )

Todos eles exercem papéis especiais na linguagem das REs como veremos a seguir.

Para efeito de curiosidade, o seguinte código imprime os caracteres ASCII mais comuns. A partir do fatiamento da lista, podemos gerar as sublistas que contêm apenas números e letras. Deixaremos isto como exercício. Cabe enfatizar, entretanto, que em Python 3, o padrão de caracter

# imprime caracteres ASCII mais comuns 
# end = ' ' altera a terminação padrão '\n'
for i in range(33,127):
    print(chr(i), end = ' ')
! " # $ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ 

O módulo re

Em Python, temos à disposição o módulo re para trabalhar com REs. Vejamos o que ele pode nos proporcionar. Antes de prosseguir, vamos ler o arquivo e salvar o texto em uma string.

with open('../database/bras-cubas.txt', 'r') as f:
    cubas = f.read()
cubas    
'Não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado. Talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas. O que por agora importa saber é que Virgília — chamava-se Virgília — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito. O estranho levantou-se e saiu. Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo. Saiu; Virgília deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro, sem articular palavra. Quem diria? De dois grandes namorados, de duas paixões sem freio, nada mais havia ali, vinte anos depois; havia apenas dois corações murchos, devastados pela vida e saciados dela, não sei se em igual dose, mas enfim saciados. Virgília tinha agora a beleza da velhice, um ar austero e maternal; estava menos magra do que quando a vi, pela última vez, numa festa de São João, na Tijuca; e porque era das que resistem muito, só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata.'
# importa o módulo
import re

As principais funções do módulo re podem ser resumidas ao seguinte modelo:

re.f(pattern,string)

onde f é o nome da função, pattern é o padrão, isto é, a regex, e string é a string na qual o padrão é procurado. Exploraremos algumas delas a seguir compreendendo, simultaneamente, o papel desempenhado pelos metacaracteres.

Exemplo: Procure pelo padrão “que” no texto-base e conte quantas vezes ele aparece.

# busca todas as combinações
que = re.findall('que',cubas)
print(que)
print(len(que))
['que', 'que', 'que', 'que', 'que', 'que', 'que']
7

Discussão:

  • O método findall procura em cubas todas as combinações do padrão “que”

  • len permite que contemos o número de elementos da lista e, consequentemente, o número de vezes que o padrão aparece. Entretanto, observemos que o padrão poderia ser algo que não formasse uma palavra inteligível. Vejamos o próximo exemplo:

# padrão presente, mas não é palavra inteligível
re.findall('xou',cubas)
['xou']

Buscas por caracteres ordinários são sempre exatas, no sentido de que o caracter é combinado como ele é. Vejamos um exemplo que conta as vogais que aparecem no texto.

for vogal in ['a','e','i','o','u']:    
    print(f'A vogal \'{vogal}\' foi \
localizada {len(re.findall(vogal,cubas))} vezes no texto.')
A vogal 'a' foi localizada 143 vezes no texto.
A vogal 'e' foi localizada 104 vezes no texto.
A vogal 'i' foi localizada 65 vezes no texto.
A vogal 'o' foi localizada 83 vezes no texto.
A vogal 'u' foi localizada 43 vezes no texto.

Exemplo: Busque pelo padrão “Tijuca” no texto e determine suas posições de início e término.

# busca por combinação retornado posição
p = re.search('Tijuca',cubas)
p.start(), p.end(), p.group()
(1042, 1048, 'Tijuca')

Discussão:

  • O método search procura pelo padrão 'Tijuca' em cubas e retorna três informações:

    • start: a posição inicial do padrão

    • end: a posição final do padrão

    • group: o padrão localizado

  • O padrão aparece apenas uma vez no texto na fatia cubas[1042:1048].

Comentários:

  • Caso houvesse repetição do padrão, apenas os índices da primeira aparição seriam retornados. Vejamos:

p2 = re.search('que',cubas)
p2.start(), p2.end(), p2.group()
(179, 182, 'que')

Vimos anteriormente que há 7 aparições do padrão “que” no texto-base. Porém, apenas a primeira é retornada por search. Vejamos:

# uma maneira de mostrar que há apenas
# um 'que' até o caracter 182 de 'cubas'
# por meio de split e interseção de conjuntos
set(cubas[0:182].split(' ')).intersection({'que'})
{'que'}

Como este conjunto é unitário, há apenas um padrão ‘que’ até a posição 182 de cubas.

Exemplo: Busque pelo padrão “Não” no texto e verifique se ele aparece no início da string.

p3 = re.match('Não',cubas)
p3.start()
0

Discussão:

  • O método match busca por um determinado padrão que aparece somente no início de uma string. Quando tal combinação de caracteres é encontrada o método retorna um objeto. Caso contrário, retorna NoneType.

  • No exemplo anterior, o método start mostra que o primeiro caracter do padrão 'Não', qual seja, 'N' , ocupa a primeira posição da string, isto é, a posição 0.

Comentários:

  • Enquanto re.search realiza a busca por um padrão em qualquer posição da string, re.match busca se esse mesmo padrão ocorre, exclusivamente, no início da string.

  • Há muitas situações em que match será a escolha a fazer em seu programa. Em uma string de várias linhas, por exemplo, match procurará pelo padrão apenas na primeira linha e ignorará a ocorrência dele nas demais. Opostamente, search procurará por ele em todas as linhas. Se forem detectadas mais de uma ocorrência, retornará a posição da primeira ocorrência.

  • Em ambos os métodos, quando o padrão não existe na string, o objeto retornado é um NoneType.

Exemplo: Substitua o padrão “Virgília” por “Ofélia” em toda a string do texto.

cubas_novo = re.sub('Virgília','Ofélia',cubas)
cubas_novo
'Não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado. Talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas. O que por agora importa saber é que Ofélia — chamava-se Ofélia — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito. O estranho levantou-se e saiu. Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo. Saiu; Ofélia deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro, sem articular palavra. Quem diria? De dois grandes namorados, de duas paixões sem freio, nada mais havia ali, vinte anos depois; havia apenas dois corações murchos, devastados pela vida e saciados dela, não sei se em igual dose, mas enfim saciados. Ofélia tinha agora a beleza da velhice, um ar austero e maternal; estava menos magra do que quando a vi, pela última vez, numa festa de São João, na Tijuca; e porque era das que resistem muito, só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata.'

Discussão:

  • O método sub realiza a substituição de um padrão por outro na string passada e sua sintaxe mais genérica é do tipo re.sub(p,r,s), onde p é o padrão que se deseja substituir por r na string s.

  • No exemplo dado, a operação com sub simplesmente substituirá todas as entradas da palavra “Vírgília” por “Ofélia” em cubas, resultando em uma string nova chamada cubas_novo.

Exemplo: Substitua o padrão “Virgília” por “Ofélia” apenas nas duas primeiras aparições

cubas_novo2 = re.sub('Virgília','Ofélia',cubas,count=2)
cubas_novo2
'Não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado. Talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas. O que por agora importa saber é que Ofélia — chamava-se Ofélia — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito. O estranho levantou-se e saiu. Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo. Saiu; Virgília deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro, sem articular palavra. Quem diria? De dois grandes namorados, de duas paixões sem freio, nada mais havia ali, vinte anos depois; havia apenas dois corações murchos, devastados pela vida e saciados dela, não sei se em igual dose, mas enfim saciados. Virgília tinha agora a beleza da velhice, um ar austero e maternal; estava menos magra do que quando a vi, pela última vez, numa festa de São João, na Tijuca; e porque era das que resistem muito, só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata.'

Discussão:

  • É possível usar a keyword count e especificar quantas vezes, em ordem, o padrão deverá ser substituído.

  • Como count=2, o método sub fará a substituição de “Virgília” por “Ofélia” apenas nas duas primeiras ocorrências do padrão, das quatro existentes no texto.

Comentários:

  • Note que se usássemos re.findall para fazer uma breve verificação nas strings cubas, cubas_novo e cubas_novo2, o número de ocorrências de “Virgília” diferiria em cada caso.

# original
print(len(re.findall('Virgília',cubas)))

# "Virgília" -> "Ofélia" integralmente
print(len(re.findall('Virgília',cubas_novo)))

# "Virgília" -> "Ofélia" em 2 ocorrências apenas
print(len(re.findall('Virgília',cubas_novo2)))
4
0
2

Exemplo: Seccione o texto em várias substrings usando a vírgula (“,”) como ponto de quebra:

cubas_sep = re.split(',',cubas)
cubas_sep
['Não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado. Talvez eu exponha ao leitor',
 ' em algum canto deste livro',
 ' a minha teoria das edições humanas. O que por agora importa saber é que Virgília — chamava-se Virgília — entrou na alcova',
 ' firme',
 ' com a gravidade que lhe davam as roupas e os anos',
 ' e veio até o meu leito. O estranho levantou-se e saiu. Era um sujeito',
 ' que me visitava todos os dias para falar do câmbio',
 ' da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo. Saiu; Virgília deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro',
 ' sem articular palavra. Quem diria? De dois grandes namorados',
 ' de duas paixões sem freio',
 ' nada mais havia ali',
 ' vinte anos depois; havia apenas dois corações murchos',
 ' devastados pela vida e saciados dela',
 ' não sei se em igual dose',
 ' mas enfim saciados. Virgília tinha agora a beleza da velhice',
 ' um ar austero e maternal; estava menos magra do que quando a vi',
 ' pela última vez',
 ' numa festa de São João',
 ' na Tijuca; e porque era das que resistem muito',
 ' só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata.']

Na próxima sessão, vamos incorporar sintaxes especiais nos métodos que aprendemos para realizar propósitos bem mais avançados.

Discussão:

  • O método split quebra a string em partes (substrings) substituindo usando o padrão passado ao método como ponto de quebra. Neste exemplo, as vírgulas são removidas e uma lista de strings é retornada.

  • Ao utilizar re.split(a,b), lembre-se que a é um padrão a ser encontrado na string b. Ele não é necessariamente um caracter único, mas uma expressão regular.

Exemplo: Separe a string do texto em no máximo 3 substrings usando o padrão “ão” como ponto de quebra.

# maxsplit = n; logo, n + 1 substrings
cubas_sep3 = re.split("ão",cubas,maxsplit=2)
cubas_sep3
['N',
 ' durou muito a evocaç',
 '; a realidade dominou logo; o presente expeliu o passado. Talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas. O que por agora importa saber é que Virgília — chamava-se Virgília — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito. O estranho levantou-se e saiu. Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo. Saiu; Virgília deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro, sem articular palavra. Quem diria? De dois grandes namorados, de duas paixões sem freio, nada mais havia ali, vinte anos depois; havia apenas dois corações murchos, devastados pela vida e saciados dela, não sei se em igual dose, mas enfim saciados. Virgília tinha agora a beleza da velhice, um ar austero e maternal; estava menos magra do que quando a vi, pela última vez, numa festa de São João, na Tijuca; e porque era das que resistem muito, só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata.']

Discussão:

  • A keyword maxsplit permite que limitemos o número de quebras. Neste exemplo, como maxsplit=2, há dois pontos de quebra e, portanto, 3 substrings.

  • Note que, neste exemplo, a substring 1 é formada por apenas um caracter, ao passo que a substring 3 comporta todos os caracteres além da segunda ocorrência do padrão “ão”.

  • Perceba, adicionalmente, que o padrão não é preservado durante o split.

Sintaxes especiais

Os metacaracteres podem ser usados para criar REs das formas mais variadas (e criativas) possíveis. Vejamos exemplos.

Metacaracter .

Serve para combinar qualquer caracter, exceto o newline (\n), se usado no modo padrão. Em uso não padrão, pode também combinar-se com \n.

Exemplo: Localize padrões com 5 caracteres contendo “lh” na 3a. e 4a. posições.

re.findall('..lh.',cubas)
['e lhe', ' olha', 'velhi']

Comentário:

  • Espaço também é um caracter. Por isso, na lista, temos também o padrão possível 'e lhe'.

Exemplo: Localize padrões iniciando com a letra T maiúscula e contendo, ao todo, 3 caracteres.

re.findall('T...',cubas)
['Talv', 'Tiju']

Metacaracter \

Serve para criar escapes de caracteres especiais ou sinalizar uma sequência especial.

Exemplo: Quebre a string usando o ponto final (“.”) como ponto de quebra.

# escape do metacaracter '.'
mt1 = re.split('\.',cubas)
mt1
['Não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado',
 ' Talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas',
 ' O que por agora importa saber é que Virgília — chamava-se Virgília — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito',
 ' O estranho levantou-se e saiu',
 ' Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo',
 ' Saiu; Virgília deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro, sem articular palavra',
 ' Quem diria? De dois grandes namorados, de duas paixões sem freio, nada mais havia ali, vinte anos depois; havia apenas dois corações murchos, devastados pela vida e saciados dela, não sei se em igual dose, mas enfim saciados',
 ' Virgília tinha agora a beleza da velhice, um ar austero e maternal; estava menos magra do que quando a vi, pela última vez, numa festa de São João, na Tijuca; e porque era das que resistem muito, só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata',
 '']

Comentário:

  • Note que, todas as substrings intermediárias passaram a iniciar-se com um caracter de espaço. Como isto é pouco útil para nós, poderíamos ter utilizado o padrão . (com espaço) para realizar a quebra.

# considera um espaço no padrão
mt2 = re.split('\. ',cubas)
mt2
['Não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado',
 'Talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas',
 'O que por agora importa saber é que Virgília — chamava-se Virgília — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito',
 'O estranho levantou-se e saiu',
 'Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo',
 'Saiu; Virgília deixou-se estar de pé; durante algum tempo ficamos a olhar um para o outro, sem articular palavra',
 'Quem diria? De dois grandes namorados, de duas paixões sem freio, nada mais havia ali, vinte anos depois; havia apenas dois corações murchos, devastados pela vida e saciados dela, não sei se em igual dose, mas enfim saciados',
 'Virgília tinha agora a beleza da velhice, um ar austero e maternal; estava menos magra do que quando a vi, pela última vez, numa festa de São João, na Tijuca; e porque era das que resistem muito, só agora começavam os cabelos escuros a intercalar-se com alguns fios de prata.']

Metacaracter ^

Também chamado caret, serve para buscar padrões no início de uma string.

Exemplo: Use as substrings do exemplo anterior para buscar quais delas iniciam-se por 'T'.

for k,s in enumerate(mt2):
    aux = re.match('^T',s)
    if aux: # se lista não for vazia
        print(f'---> Padrão detectado na \
substring {k}:\n\"{s}\"')
---> Padrão detectado na substring 1:
"Talvez eu exponha ao leitor, em algum canto deste livro, a minha teoria das edições humanas"

Comentário:

  • Poderíamos ter usado re.search ou re.match também para atingir o mesmo propósito, visto que a expressão regular '^T' força o início com “T”.

Metacaracter $

Visto em sentido oposto a ^, $ serve para buscar padrões no final de uma string, ou logo antes de um caracter newline.

Exemplo: Use as substrings do exemplo anterior para buscar quais delas terminam por 'o'.

for k,s in enumerate(mt2):
    aux = re.findall('o$',s)
    if aux: # se lista não for vazia
        print(f'---> Padrão detectado na \
substring {k}:\n\"{s}\"')
---> Padrão detectado na substring 0:
"Não durou muito a evocação; a realidade dominou logo; o presente expeliu o passado"
---> Padrão detectado na substring 2:
"O que por agora importa saber é que Virgília — chamava-se Virgília — entrou na alcova, firme, com a gravidade que lhe davam as roupas e os anos, e veio até o meu leito"
---> Padrão detectado na substring 4:
"Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo"

Discussão:

  • Neste exemplo, identificamos todas as substrings que terminam com o caracter ‘o’.

  • Com expressões regulares mais exatas, restringimos as possibilidades. Vejamos o exemplo a seguir.

# busca substring terminando com 'undo'
for k,s in enumerate(mt2):
    aux = re.findall('undo$',s)
    if aux: # se lista não for vazia
        print(f'---> Padrão detectado na \
substring {k}:\n\"{s}\"')
---> Padrão detectado na substring 4:
"Era um sujeito, que me visitava todos os dias para falar do câmbio, da colonização e da necessidade de desenvolver a viação férrea; nada mais interessante para um moribundo"

Comentário:

  • Note que para buscar o caracter literal ‘$’ em uma string devemos usar o caracter de escape. Vejamos o próximo exemplo.

real = 'João me pagou R$ 150,00.'
v = re.search('\$',real)
print(f'Caracter $ identificado na posição {v.start()}.')
Caracter $ identificado na posição 15.

Metacaracter *

Serve para buscar 0 ou mais repetições de um caracter precedente. Este caracter especial é de repetição.

Exemplo:

re.findall('hav*.',cubas)
['ha ', 'ha ', 'ham', 'har', 'havi', 'havi', 'ha ']

Discussão:

  • A RE 'hav*.' procura por padrões que atendem às seguintes restrições:

    • o caracter ‘v’ aparece 0 ou mais vezes após ‘ha’.

    • quando não houver ‘v’, o terceiro caracter passa a ser qualquer um que vier na sequência por causa do metacaracter ‘.’.

    • quando houver um ‘v’, o quarto caracter é qualquer um

    • quando houver n repetições de ‘v’ após ‘a’, todos os n são considerados, e o n+1-ésimo caracter é qualquer um.

Comentários:

  • Descrever em palavras o que a expressão regular faz exatamente não é sempre muito fácil. Vejamos mais um exemplo.

c = 'ha hahaa hav havv havva-havvva; haiiaa havia'

re.findall('hav*.',c)
['ha ', 'hah', 'hav ', 'havv ', 'havva', 'havvva', 'hai', 'havi']

Metacaracter +

Serve para buscar 1 ou mais repetições de um caracter precedente. Assim como *, + também é um caracter especial de repetição.

Exemplo:

re.findall('hav+.',cubas)
['havi', 'havi']

Discussão:

  • A RE 'hav+.' é similar à anterior. Porém, pelo fato de ser obrigatória a ocorrência de pelo menos 1 caracter ‘v’ após ‘a’, os padrões ‘ha ‘, ‘ham’ e ‘har’ são excluídos.

  • Se encontradas n ocorrências de ‘v’, o padrão será encerrado pelo n+1-ésimo caracter arbitrário

  • Como no texto existe tal padrão com apenas uma ocorrência de ‘v’, o quarto caracter, sendo arbitrário, é encontrado como ‘i’.

  • O padrão ‘havia’, que forma uma palavra compreensível em Português, é ignorado por conter 5 caracteres.

Metacaracter ?

Serve para buscar 0 ou 1 repetição de um caracter precedente. Assim como *, + também é um caracter especial de repetição.

Exemplo:

re.findall('s?an.',cubas)
['ant', 'ana', 'ano', 'anh', 'ant', 'sant', 'ant', 'and', 'ano', 'and']

Comentários:

  • Esta RE procurará por 0 ou 1 repetição de ‘s’ seguida por ‘an’ e qualquer outro caracter.

  • Neste caso, a substring ‘ssan’, por exemplo, existente na palavra ‘interessante’ não seria coberta por ?.

Metacaracteres []

Os colchetes devem ser usados, juntos, em par, para indicar um conjunto de caracteres sobre os quais a expressão regular deve operar. Em geral, usa-se o par [] para os seguintes propósitos:

  • Listar caracteres individualmente

A RE [vnd] combinará os caracteres 'v', 'n' ou 'd'.

Exemplo:

# localiza todas as ocorrências 
# de 'v', 'n', ou 'd'
print(re.findall('[vnd]',cubas))
['d', 'v', 'd', 'd', 'd', 'n', 'n', 'd', 'v', 'n', 'n', 'd', 'v', 'n', 'd', 'd', 'n', 'v', 'n', 'n', 'v', 'v', 'd', 'd', 'd', 'v', 'n', 'v', 'n', 'v', 'n', 'v', 'v', 'd', 'd', 'd', 'd', 'n', 'd', 'n', 'd', 'd', 'd', 'd', 'n', 'v', 'v', 'v', 'n', 'd', 'n', 'n', 'n', 'd', 'd', 'd', 'd', 'n', 'v', 'd', 'd', 'n', 'd', 'n', 'd', 'd', 'd', 'n', 'd', 'v', 'v', 'n', 'n', 'd', 'v', 'n', 'd', 'd', 'v', 'd', 'v', 'd', 'd', 'd', 'n', 'd', 'n', 'd', 'n', 'd', 'v', 'n', 'v', 'n', 'd', 'n', 'd', 'v', 'v', 'n', 'd', 'n', 'd', 'v', 'n', 'n', 'd']
  • Localizar um intervalo de caracteres

Uma RE do tipo [a-d] combinará os caracteres de 'a' a 'd', ao passo que [a-z] combinará todas as letras minúsculas da tabela ASCII (e não Unicode!), ou seja, de 'a' a 'z'.

Do mesmo modo, [0-4] combinará os dígitos de 0 a 4, e [0-9] todos os dígitos decimais. Por outro lado, a expressão [0-3][3-8] combinará todos os números de dois dígitos de 03 a 38.

Se o símbolo - for escapado (p.ex. [a\-d]), ou se for colocado como primeiro ou último caracter (p.ex. [-b] ou [b-]), ele significará o hífen literal (‘-’).

Exemplo:

re.findall('[a-z]um',cubas)
['gum', 'hum', 'gum', 'num']

Discussão:

  • Esta RE procurará todos os padrões que iniciam por qualquer letra minúscula (de ‘a’ a ‘z’) seguidas por ‘um’.

Exemplo:

re.findall('[v-z][a-].',cubas)
['va-', 'va,', 'vam', 'van', 'va ', 'zaç', 'vas', 'za ', 'va ', 'vam']

Discussão:

  • Esta RE procurará todos os padrões que iniciam por uma letra minúscula entre ‘v’ e ‘z’ seguidas por ‘a’, ‘-’, ou ‘a-’ e um caracter qualquer adicional.

Exemplo:

# mesmo efeito que anterior
re.findall('[v-z][-a].',cubas)
['va-', 'va,', 'vam', 'van', 'va ', 'zaç', 'vas', 'za ', 'va ', 'vam']
  • Localizar caracteres especiais como literais

Os caracteres especiais perdem seu status quando aparecem entre colchetes. Por exemplo, a RE [(+*)] procurará por qualquer um dos literais (, +, * ou ).

ex = 'Quanto é (22 + 3)*1 - 6*1?'
re.findall('[0-9][)*]',ex)
['3)', '6*']

Discussão:

  • Esta RE busca padrões em que um dígito de 0 a 9 precede um ‘)’ ou ‘*’ literalmente.

re.findall('[0-9][?]',ex)
['1?']

Discussão:

  • Esta RE busca padrões em que um dígito de 0 a 9 precede um ‘?’ literal.

Metacaracteres {}

As chaves, assim como os colchetes, devem ser usadas juntas, em par, para localizar cópias ou repetições de uma expressão regular. Em geral, ela é usada para os seguintes objetivos:

  • Localizar exatamente m cópias de uma RE anterior

Exemplo:

# busca pelo padrão 'rr'
re.search('r{2}',cubas).group()
'rr'

Exemplo:

ex2 = '0110011000110011001010110100110111111000'
re.findall('0{2}',ex2)
['00', '00', '00', '00', '00', '00']
re.findall('1{3}',ex2)
['111', '111']
# busca 000000 - 111111
# com variações de 0,1 intermediárias
re.findall('[0-1]{6}',ex2)
['011001', '100011', '001100', '101011', '010011', '011111']
  • Localizar entre m e n repetições de uma RE anterior

Exemplos:

set(re.findall('10{2,3}',ex2))
{'100', '1000'}
set(re.findall('01{2,3}',ex2))
{'011', '0111'}
set(re.findall('[0-1]{2,3}',ex2))
{'001', '010', '011', '100', '101', '111'}

Metacaracter |

Usamos o pipe para significar “ou” com o objetivo de localizar mais de uma RE. Por exemplo, A|B buscará padrões determinados pela RE A ou por B. Pipes adicionais constroem sintaxes conjuntivas. Ou seja, A|B|C|D...|Z significaria “A ou B ou C ou … D”.

Exemplos:

# busca por 'rre' ou 'ssa'
re.findall('r{2}e|s{2}a',cubas)
['ssa', 'rre', 'ssa']
# busca por 'cha', 'cho' ou
# 'lha', 'lhi' ou 
# 'pra', 'pre', 'pri', 'pro', 'pru'
re.findall('ch[ao]|lh[ai]|pr[aeiou]',cubas)
['pre', 'cha', 'lha', 'cho', 'lhi', 'pra']

Metacaracteres ()

Servem para criar grupos de captura. Um grupo de captura é formado por uma RE confinada entre parênteses. Grupos de captura podem ser aplicados para extrair padrões que possuam uma certa estrutura.

Exemplo:

table = ['alfa00-LAX','beta22-PET', 'zeta92-XIR']

for t in table:
    pat = re.match("([a-z][a-z][a-z][a-z][0-9][0-9])-([A-Z]{3})", t)
    if pat:
        print(pat.groups())
('alfa00', 'LAX')
('beta22', 'PET')
('zeta92', 'XIR')

Discussão:

  • A RE acima é composta de dois grupos de captura que são separados pelo - literal.

  • O primeiro grupo de captura é ([a-z][a-z][a-z][a-z][0-9][0-9])

  • O segundo grupo de captura é ([A-Z]{3})

  • Tuplas com dois elementos são impressas com groups().

Comentários:

  • Como se vê, os grupos de captura permitiram que identificássemos padrões separados em uma lista de strings que continha uma estrutura predefinida.

  • Entretanto, as REs anteriores criadas como grupos de captura poderiam ser definidas de uma forma muito mais concisa através de identificadores.

Identificadores e tokens gerais

Ao trabalhar com REs, podemos utilizar identificadores e tokens diversos para significar padrões de maneira concisa.

O exemplo anterior sobre grupos de captura, por exemplo, poderia ser escrito de outra forma:

for t in table:
    pat = re.match("(\w+)\W(\w+)", t)
    if pat:
        print(pat.groups())
('alfa00', 'LAX')
('beta22', 'PET')
('zeta92', 'XIR')

Neste caso, \w e \W são chamados identificadores.

A tabela a seguir resume os principais identificadores e seu significado.

Identificador

Significado

\d

qualquer caracter que é um dígito decimal. Equivalente a [0-9]

\D

qualquer caracter exceto dígido (não-dígito). Equivalente a [^0-9]

\w

qualquer caracter Unicode. Se a flag ASCII for utilizada, equivale ao conjunto [a-zA-Z0-9_]

\W

o oposto de \w. Se a flag ASCII for utilizada, equivale ao conjunto [^a-zA-Z0-9_]

\s

qualquer caracter que é um espaço.

\S

qualquer caracter que não seja espaço.

A tabela a seguir resume tokens gerais e seu significado.

Token

Significado

\n

quebra de linha. Newline.

\r

carriage return

\t

TAB

\0

caracter nulo

Exemplo: Identificando padrões de data e hora.

dt = ['2021-04-06 10:32:00', '2020-12-03 01:12:58'] 
for m in dt:
    pat = re.match('(\d+)-(\d+)-(\d+)\s(\d+):(\d+):(\d+)',m)
    if pat:
        print(pat.groups())
('2021', '04', '06', '10', '32', '00')
('2020', '12', '03', '01', '12', '58')

Exemplo: Identificando cores em padrão hexadecimal.

cores = ['(1,23,43)','#3ed4f4', '#ffcc00','(C,M,Y,K)','#9999ff'] 

for c in cores:
    ok = re.match('#[a-fA-F0-9]{6}',c)
    if ok:
        print(ok.group())
#3ed4f4
#ffcc00
#9999ff

Exemplo: Identificando URLs no domínio Wikipedia Português.

urls = ['http://pt.wikipedia.org/wiki/Bras',
       'http://wikipedia.org/wiki/Bras',
       'https://pt.wikipedia.org/wiki/Bras',
       'http://pt.wikipedia.org/wik/Bras',
       'https://pt.wikipedia.org/wiki/Cubas']

for u in urls:
    ok = re.match('((http|https)://)?(pt.wikipedia.org/wiki/).+',u)
    if ok:
        print(ok.group())
http://pt.wikipedia.org/wiki/Bras
https://pt.wikipedia.org/wiki/Bras
https://pt.wikipedia.org/wiki/Cubas

Compilação e literais

Até agora abordamos REs de uma maneira direta, mostrando como podemos localizar padrões pela utilização de alguns métodos. Vale comentar que REs podem ser compiladas como objetos mais abstratos.

Para compilar expressões, podemos utilizar o método compile, no qual REs são manipuladas como strings.

Exemplo:

rex = re.compile('[p-t]a[b-d]a?')
rex.findall(cubas)
['sad', 'sab', 'rad', 'tad', 'sac', 'sac']

Aqui, rex é um objeto abstrato.

print(type(rex))
<class 're.Pattern'>

Se imprimirmos rex, veremos o seguinte:

rex
re.compile(r'[p-t]a[b-d]a?', re.UNICODE)

O prefixo r torna a string literal e serve para tratar problemas com escape. Todavia, deixaremos esta discussão para um momento posterior. Para saber mais, leia sobre a Praga do Backspace.

Recursos de aprendizagem

Para aprofundar seu entendimento sobre expressões regulares, recomendamos os seguintes sites: