Expressões Regulares (regex)


Expressões regulares, também chamadas de regex (de regular expression), são meios de descrever padrões que podem ser encontrados dentro de um texto. Os padrões podem ser simples, como a busca de um ou dois dígitos especificados, ou padrões complexos que incluem a posição do padrão no texto, o número de repetições, etc.

Regex são usados basicamente para a busca de um padrão, substituição de texto, validação de formatos e filtragem de informações. Praticamente todas as linguagens de programação possuem ferramentas de uso de regex, assim como grande parte dos editores de texto. As diferentes linguagens de programação e aplicativos que incorporam funcionalidade de regex possuem sintaxes ligeiramente diferentes, cada uma, mas há uma embasamento geral que serve a todas. Existem aplicativos de teste de regex em diversas plataformas e aplicativos online para o mesmo fim.
Exemplos de uso são:

  • filtragem de linhas de um texto longo com determinados caracteres no início, meio ou fim da linha;
  • remoção de tags de um texto escrito em html, com possível seleção dentro de uma tag determinada;
  • validação de texto para representar datas e horas;
  • validação de texto digitado pelo usuário para representar CPF, email, URL, CEP, cartão de crédito, etc;
  • extração de dados de um arquivo csv, json, XML, markdown ou outro formato estruturado qualquer.

Usaremos, por concisão, a expressão sensível (insensivel) ao caso significando sensível (insensivel) à minúsculas e maiúsculas.

Tags do HTML são comandos de formatação de texto envolvidos por dois sinais < >. Alguns exemplos são
<p>um parágrafo</p>, <b>letras em negrito</b>, etc.

Dizemos que um determinado padrão regex é procurado no texto até que ocorra um ou mais “casamentos” (match, em inglês). Por exemplo, o padrão ab é encontrado (e, portanto, casa com) os textos abraço, absolvido ou aberração, mas não casa com asbestos. Se for pedida uma busca insensível ao caso, ele também casa com Abraço ou ABROLHOS. Em todo esse artigo marcaremos as partes “casadas” pela essa formatação especial.

Nos exemplos e tabelas desse artigo há uma certa quantidade de repetições, buscando facilitar o aprendizado. Tabelas enxutas para consultas serão listadas em outra parte.

Caracteres simples ou conjuntos de caracteres (strings comuns) são casados literalmente.

padrão significado exemplo: casa ou não com
a caracter comum a casa Afazer aaa
ab grupos de caracteres comuns ab absoluto abraço Abcd trabalho
em 1945 caracteres alfanuméricos em 1945 a segunda guerra mundial terminou em 1945.

Metacaracteres

Claro que não precisaríamos de nenhuma tecnologia especial para encontrar letras ou conjuntos de letras. Para construir padrões mais complexos usamos os metacaracteres (o que significa que têm significados especiais):
. * ^ $ + ? { } [ ] \ | ( ).

O ponto (.) representa (substitui ou casa com) qualquer caracter único.

padrão casa com não casa com
p.ata prata, plata pata pirata
lu.a luta, luma lua lucca
phylos.net phylos.net, phylos-net (1) phylosnet

(1): O ponto (.) casa com todos os caracteres, inclusive o próprio ponto.

Quantificadores

Os quantificadores * ? + {} permitem o controle de quantas repetições de um padrão ocorrem.

O asterisco * representa zero, uma ou várias repetições do caracter que o precede.

padrão casa com não casa com
Jo*e Je, Joe, Jooe Jose
1*23 23, 123, 1123 1021
.* qualquer número de qualquer caracter

A interrogação ? representa zero ou uma repetição do caracter que o precede.

padrão casa com não casa com
Jo?e Je, Joe Jooe
1?23 23, 123 1123 1021
.? nenhum ou uma ocorrência de qualquer caracter

O sinal de mais + representa uma ou mais repetições do caracter que o precede.

padrão casa com não casa com
Jo+e Joe, Jooe Je
1+23 123, 1123 23 1021
.+ 1 ou mais ocorrências de qualquer caracter


As chaves {n} indicam n repetições do caracter precedente.
{n,} significa mínimo de n repetições do caracter precedente.
{n,m} significa mínimo de n, máximo de m repetições do caracter precedente.

padrão casa com não casa com
A{3} AAA, GAAAA AA AAB
Ot{2}o Otto Oto
1{2}9{3} 11999, A:11999 119, 11199
s{2,} passar, asssb casa, sapo
s{2,4} passar, asssb , sssss casa, sapo
.{5} 5 repetições de qualquer caracter

Os marcadores de posição ou âncoras ^ $ \b \B permitem o controle da posição onde o padrão ocorre.

O circunflexo ^ indica que o padrão seguinte está no início da linha.
O cifrão $ indica que o padrão prévio está no final da linha.
A marca \b indica início ou fim de uma palavra (uma sequência contínua de caracteres). \b marca a passagem de um caracter alfanumérico ou sublinhado \w para um não-caracter \W.
A marca \B, a negação de \b, representa início ou fim de uma palavra envolta em um não-caracter \W.

padrão casa com não casa com
^f filho afazia
^3{2} 333-123 133-7654
s$ bois essência
m{3}$ booommm mmmassa
\bPedro PedroPedro .Pedro LPedro _Pedro
Pedro\b Pedro PedroPedro. PedroL Pedro_
\BMaria LMaria _Maria 3Maria Maria -Maria
Maria\b Maria MariaMaria. MariaL Maria_

Observe que o sinal ^ serve como âncora, para marcar início da string, ou como sinal de negação.
A negação ocorre quando ^ é usado como primeiro sinal dentro de uma lista. Nesse caso ele nega todos os caracteres da lista.

padrão casa com não casa com
^[0-9].* 1989, 5G (1) G5
^[^0-9].* G7, I-99 (2) 5G
[0-9]^ 0^, 1^ (3) 55
^^.* uma linha iniciada por ^
^$ uma linha vazia
.{5}$ casa 5 últimos caracteres de uma linha
^.{15,30}$ casa linhas com 15 até 30 caracteres

(1): O padrão casa com textos que possuem um dígito no início.
(2): O padrão casa com textos que não se iniciam com um dígito.
(3): Qualquer dígito seguido do circunflexo literal ^

Classes de caracteres


Classes de caracteres são marcações que funcionam como “shortcuts”, representando um grupo de caracteres ou controles.
\s representa um espaço simples.
\S representa espaço negado (não é um espaço).
\d representa qualquer dígito. O mesmo que [0-9].
\D \d negado. Qualquer não dígito. O mesmo que [^0-9].
\w caracter alfanumérico e sublinhado.
\W \w negado. Qualquer sinal exceto caracter alfanumérico e sublinhado.

padrão casa com não casa com
\s casadamãeJoana (espaços) casa_da_mãe_Joana
123\s456 123 456 99.123 456.90 123.456__123_456
\S casa “   ”
\d{3}/\d{2} 123/56 987/78 22/3 aaa/bb
\D+ basta 123 0000
\D{3}-\D{2} abc-de aAs-fg1 a2c-de 123-00
\w+ atr3v1d0_b3sta phylos.net — @@@ %%%
\W+ --- @@@ %%% phylos.net atr3v1d0_b3sta
[\b] caracter de backspace
\c caracter de controle

Para procurar por um dos metacaracteres como um literal dentro do texto usamos o escape \.

padrão casa com não casa com
\\s No regex \s casa espaço, número \s987 s ss “ ”
R\$45 R$45 R45
(U\$5|R\$25) U$5, R$25 U$ 5
\\d\(\d{2}\) \d(69), \d(00) \d(123), \d12

Agrupamentos

É possível encontrar um dos caracteres dentro de um conjunto de caracteres usando chaves []. Essa notação permite a descrição de intervalos como [a-z], significando todas as letras minúsculas de a até z, ou [0-9], todos os dígitos.

padrão casa com não casa com
pr[ae]to prato, preto prto, proto
[a-e]rm arm, permanente, ermitão frm, rm
[5-7]00 500, 600, 700 800, 00
c[ep]* c, ce, cp, cep, ceep, ceepp bep, ep
</[bi]*> </b>, </i>, </>, </bi>, /b, /i>
</[bi]{1}> </b>, </i> </>, </bi>


Outros agrupamentos: alguns caracteres de controle permitem o agrupamento de texto e padrões.
(abc) permite o agrupamento simples de caracteres (no caso ‘abc’).
[abc] quaisquer dos caracteres (no caso abc).
[a-z]: qualquer caracter no intervalo de a até z.
[^a-z]: negação de [a-z]. Todos os caracteres exceto aqueles entre a e z.
[0-9]: qualquer dígito entre 0 e 9.
[^0-9]: negação de [0-9]. Tudo o que não é dígito.
a|b: opcional, a ou b.
(padrao1|padrao1): busca por um ou outro padrão.

padrão casa com não casa com
(est) muito estudo, sem estado Estimo, set
[est]ato sato, tato mato
[f-h]ato fato, hato, gato mato, rato, feto
[^f-h]ato mato, rato, lato fato, gato
[^a-c]123 r123, M-123, s/123 a123, b123
[1-5]-[6-9] 1-6, 123-456, A5-6 a1-b2, 6-1
[^1-5]6789 06789, 12345678900, R56789 16789
p(l|r)at platão, prata pato, piato
p(at|len)o pato, pleno patleno, po, plo
p(ratic|ublic|enal)idade praticidade, publicidade, penalidade pcidade, pibcidade
(s{2}|r{2})os carros passos nossos erros casos, cappos, cassas
[0-2][0-9]:[0-5][0-9] 04:20, 12:50 34:30, 11:65

Uma observação importante: dentro de um agrupamento por colchetes ([]) o ponto e todos os demais metacaracteres têm valor literal e não precisam ser “escapados”. A única exceção é o hífen, - que serve para representar intervalos. Para inserir um hífen no padrão agrupado devemos colocá-lo como o último do grupo. Para usar o caracter ] ele deve ser posto como o primeiro elemento para não ser confundido com o fechamento do grupo. Não há restrição sobre onde colocar o [.

padrão casa com não casa com
12[:. ]45 12:45, 12.45, 12 45 1245, 12-45, 12/45
[$€¥]137 $137, €137, ¥137 137, R137
876[/_-]543 876/543, 876_543, 876-543 876 543, 876|543
doid[]oa] doid], doido, doida doid[, doide, doid*
doid[[oa] doid[, doido, doida doid], doide, doid*
9[1-5-][a-f] 92b, 9-b 95, 9-, 91g
[*ab]\d *9, a3, b5 64

Observe que [*] = \*.

Correspondência gananciosa ou preguiçosa

Por default as buscas quantificadas por * + {} são gananciosas (greddy), o que significa que englobam a maior porção de texto casada com o padrão.

Em inglês se usa os termos greddy e lazy, que significam literalmente “ganancioso” e “preguiçoso” para se referir à correspondência do maior ou menor intervalo possível de texto. Em português são comuns as traduções ganancioso e prequiçoso ou guloso e não-guloso.


O padrão <.+> significa ‘todos os caracteres entre os sinais < > e casa com o maior texto possível iniciado por < e finalizado por >, mesmo que o caracter finalizador ocorra nesse intervalo casado. Portanto, se quisermos extrair o texto circundado pela tag div devemos ser capazes de escrever um padrão que é interrompido no primeiro encontro do caracter finalizador >. Essas são as chamadas buscas preguiçosas, o que é possível se obter com o acréscimo do sinal ? após o quantificador.

padrão casa com não casa com
<.+> <div class="classe">Todo o texto dentro da TAG</div> texto sem tags
<.+?> <div class="classe">Todo o texto dentro da TAG</div> texto sem tags
.*(\sR) primeiro R segundo R fim (1) primeiro, segundo, fim
.*?(\sR) primeiro R segundo R fim (2) primeiro, segundo, fim
\d+ 1957 2021 258.1258 abcd
\d+? 1957 2021

(1): .*(\sR) significa “qualquer quantidade de caracteres seguido de espaço, depois um R”.
(2): A versão “prequiçosa” pára na primeira ocorrência de ” R”, depois casa um segundo grupo.

Observe que o padrão .*? significa “zero ou qualquer número de qualquer caracter” e portando não casa com coisa alguma.

Resumindo:

Ganancioso casa
ab* abbbb
ab+ abbb
ab? ab
ab{1,3} abbb
Preguiçoso casa
ab*? a
ab+? ab
ab?? a
ab{1,3}? ab

Grupos

Caracteres dentro de chaves ()são tratados como grupos e casados juntos. Todos os quantificadores e âncoras podem ser aplicadas aos grupos e podemos inclusive aninhá-los (usar um grupo dentro de outro).

padrão casa com não casa com
([a-z]+) casa com palavras de minúsculas
(bem|mal)\sfeito bem feito, mal feito
(bem|mal)?\sfeito bem feito, mal feito, feito
(in|con)?certo incerto, concerto, certo
(in|con)+certo incerto, concerto certo
(\.\d){2} .1.2, .4.5, .0.0 12, 1.2
(www\.)?phylos.net www.phylos.net, phylos.net
(hiper|hipo)(trofia|plasia) hipertrofia, hipotrofia, hiperplasia, hipoplasia plasia, hipo
((su|hi)per)?mercado supermercado, hipermercado, mercado plasia, hipo

Retrovisores

O grupos permitem o uso de retrovisores com a sintaxe (grupo)\1...9. Esse uso de \1...9 (\ seguido de um dígito, 1 – 9) não tem relação com escape mas denota um grupo casado. Ele serve para a reutilização do trecho casado para uma nova busca no texto alvo.

Por ex.:
([a-z]+) casa com palavras de uma ou mais letras minúsculas.
([a-z]+)- casa com palavras de uma ou mais letras minúsculas seguidas de hífen.
([a-z]+)-\1 armazena o texto casado e o procura novamente, uma vez.

padrão casa com não casa com
([a-z]+)-\1 quero-quero, mau-mau, asdfg-asdfg quero
([a-z]+)-?\1 quero-quero, queroquero, bombom, bombomzeiro bom
in(co)lo(r) = sem \1\2 incolor = sem cor
\b([a-z]+)-?\1\b quero-quero, queroquero, bombom (1) bombomzeiro
\b(bo(na|to))\1\b bonabona, botoboto (2) rbotoboto, bonabonas
(rapida)(mente) conseguimos uma \2 \1 rapidamente conseguimos uma mente rapida (3)
(su)d(ão) do \1l n\2 sudão do sul não
(AA)(99)(hh) \3 \1 \2 AA99hh hh AA 99 (4) AA99hhhhAA99
(AA)(99)(hh)\3\1\2 AA99hhhhAA99 (5) AA99hh hh AA 99
(\d{2})(\d{3})(\d{2}) \1-\2-\3 1122233 11-222-33, 9988877 99-888-77 998877 99-88-77
((band)eira)nte \1 \2alheira bandeirante bandeira bandalheira (6)

(1): \b inicial e final significa que o padrão circundado é uma “palavra”.
(2): O padrão casado e repetido é bona ou boto no início e no fim da “palavra”.
(3): o 1º grupo é rapida, o 2º é mente.
(4): os grupos 1, 2 e 3 são capturados na ordem, e repetidos em outra ordem, separados com espaços.
(5): o mesmo que (4), exceto que os grupos são repetidos sem espaços.
(6): ilustrado na figura abaixo.

O retrovisor serve para procurar grupos repetidos. Os grupos são numerados de 1 até 9, contando-se da esquerda para a direita, sendo que o primeiro parêntese encontrado define a ordem do grupo.

Outras técnicas de grupos

Algumas técnicas mais sofisticadas foram implementadas nas expressões regulares, nem todas reconhecidas por todos os editores, IDEs e linguagens de programação. Para isso o metacaracter ainda não utilizado, (?..) ganhou significado de operador em regex.

(?#texto de comentário)
(?:regex): grupo casado mas não armazenado nem incluído na contagem dos grupos.

padrão casa com não casa com
(?#nome)(pa)(pi) \2\1 papi pipa papi papi
(Jó) (?:Alto)- (Rui) \1 \2 Jó Alto Rui- Jó Rui
(?:Z)-(\d{2})-(\d{4}):\2:\1 Z-11-2222:2222:11, Z-45-9876:9876:45
(?:\w)-(\d{3}) \1-\1 a-123 123-123, b-456 456-456 a-123 123-121

padrao(?=regex): não é casado mas determina regex que deve existir após padrao.
(?<=regex)padrao: não é casado mas determina regex que deve existir antes de padrao.

padrão casa com não casa com
casa (?=\d{2}) casa 23, casa 899 casa dez, 852 casa
Pedro(?=\sCa) Pedro Cabral, Pedro Camilo Pedro Barata, Pedroca
\d{4}(?=[A-Z]) , 1234A, 0987H, 6666GGG 354W, G5432, 987G
(?<=Albert) Einstein Albert Einstein Alberto Einstein
(?<=\d{3}) [a-r.]{5} 123 roman, 987 coma. 123 ruela

(?!regex): não é casado mas determina regex que não deve existir após outro padrão.
(?<!regex): não é casado mas determina regex que não deve existir antes de outro padrão.

padrão casa com não casa com
casa (?!\d{2}) casa dez, casa verde casa 12, casa 123
Pedro(?!\sCa) Pedro Bernardo, Pedro Bento, Pedroca Pedro Cabral
\d{4}(?![A-Z]) , 1234890, 0987-987 3354W, 5432H, 987G
(?<!\d{2}) casa naquela casa, outra casa 12 casa, 123 casa
(?<!Pedro) Cabral José Cabral Pedro Cabral
(?<![A-Z])-\d{4} 987-1234, 4-0987 A-3354, H-5432

(?P<nome>regex): grupo casado e nomeado com nome, ao invés de numerado com \1…\9.
Obs.: Essa é a sintaxe usado no Python. Ela pode variar em outros ambientes.

» data= '23 de junho de 2021'
» regex= '^(?P\d{1,2})\sde\s(?P\w+)\sde\s(?P\d{4})'
» matches= re.search(regex, data)

» print('Dia: ', matches.group('dia'))
» print('Mês: ', matches.group('mes'))
» print('Ano: ', matches.group('ano'))
↳ Dia:  23
↳ Mês:  junho
↳ Ano:  2021

(?modificador): modificador é uma ou mais letras que ativam uma funcionalidade, sendo:

Modificador Significado
i busca insensível ao caso
m força o metacaracter . a casar com \n
s obriga as âncoras ^ e $ a casarem com \n
x permite a inclusão de espaços e comentários
L força o uso da localização do sistema (só Python)
u considera a tabela Unicode (só Python)
padrão casa com não casa com
(?i)[a-z]* Pedro, aLLana 654-654
(?i)[A-Z]* Pedro, aLLana 654-654
(?i)\d+\.png 1234.png, 1234.PNG foto.png, 987000.jpg

Precedência de metacaracteres

Quando vários metacaracteres aparecem juntos eles obedecem a uma ordem definida de precedência, definida pela ordem na tabela.

Ordem Tipo Exemplo Significado
0 () (grupo) grupos não quebrado
1 quantificador abc+ ab seguidos de c em qualquer quantidade
2 concatenar abc abc simples
3 | ab|c ab ou c
3 | ab|c ab ou c

Alguns exemplos dessas regras de precedência:

padrão casa com significado
abc+ abc abcc abccc “ab” seguido de 1 ou mais “c”
abc abc abcc abccc “abc”, juntos
(abc) abc abcc abccc “abc”, juntos, em grupo
ab|c abc abc “ab” ou “c”
a(b|c) ab ac abc a abcc accc “a” seguido de “b” ou “c”
ab|cd* ab cd cddd abcdddddddd (1) o mesmo que (ab)|(c(d*))
s/ n/|número \d* s/ n/ número 19000 o mesmo que (s/ n/)|(número (\d*))

Para forçar uma união de caracteres em um grupo inquebrável usamos ().
(1): A concatenação em ab tem prioridade sobre a alternância |. d* ocorre antes da concatenação com c. Portanto ab|cd* é o mesmo que (ab)|(c(d*)).

Caracteres acentuados

Em português e outras línguas européias precisamos criar padrões que incluem caracteres com acentos. Uma alterniva é usar as classes POSIX listadas abaixo. Alternativamente podemos extender grupos de acordo com a tabela ASCII, o que é útil quando POSIX não está disponível.

POSIX alternativa significado
[[:lower:]] [a-zà-ü] minúsculas, acentuadas ou não
[[:upper:]] [A-ZÀ-Ü] maiúsculas, acentuadas ou não
[[:alpha:]] [A-Za-zÀ-ü] minúsculas e maiúsculas, acentuadas ou não
[[:alnum:]] [A-Za-zÀ-ü0-9] todas as letras, acentuadas ou não, e dígitos
padrão casa com significado
(ção)|(ções) noção noções
[à-ü] estúpido eqüinócio
[a-zà-ü]* retratação RETRATAÇÃO
[A-Za-zÀ-ü0-9]* retratação RETRATAÇÃO 2001

Classes POSIX

Nem todas as linguagens de programação aceitam as classes POSIX. Java e C dão suporte a POSIX e existem bibliotecas Python para o mesmo resultado.

Classe Descrição
[:digit:] dígito, \d; equivalente a [0-9]
^[:digit:] não dígito, \D; equivalente a [^0-9]
[:alnum] letras e números ; equivalente a [A-Za-z0-9]
[:space:] caracteres brancos ; equivalente a [ \t\n\r\f\v]
^[:space:] não espaço: \S
[:alpha:] letras; equivalente a [A-Za-z]
[:lower:] minúsculas; equivalente a [a-z]
[:upper:] maiúsculas; equivalente a [A-Z]
[:xdigit:] números hexadecimais; equivalente a [0-9A-Fa-f]
[:word:] \w qualquer caractere alfanumérico mais underscore (_); equivalente a [[:alnum:]_]
^[:word:] \W, negação de \w
[:blank:] espaço em branco e TAB; equivalente a [\t]
[:punct:] pontuação; equivalente a [!”\#$%&'()*+,\-./:;<=>?@\[\\\]^_‘{|}~]

Exemplos de validações com Regex

Alguns exemplos de validações com Expressões Regulares

Tipo de Validação regex
Dígitos ^\d+$
Letras ^\w+$
Decimal ^[+-]?((\d+|\d{1,3}(\.\d{3})+)(\,\d*)?|\,\d+)$ ^[-+]?([0-9]*\,[0-9]+|[0-9]+)$
URL ^((http)|(https)|(ftp)):\/\/([\- \w]+\.)+\w{2,3}(\/ [%\-\w]+(\.\w{2,})?)*$
E-mail ^([\w\-]+\.)*[\w\- ]+@([\w\- ]+\.)+([\w\-]{2,3})$
Endereço IP \b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b
Tempo (24 horas) ^([0|1|2]{1}\d):([0|1|2|3|4|5]{1} \d)$
Data (dd/mm/aaaa) ^((0[1-9]|[12]\d)\/(0[1-9]|1[0-2])|30\/(0[13-9]|1[0-2])|31\/(0[13578]|1[02])) \/\d{4}$
Telefone ^\(\d{3}\)-\d{4}-\d{4}$
Senha ^\w{4,10}$ ^[a-zA-Z]\w{3,9}$ ^[a-zA-Z]\w*\d+\w*$
🔺Início do artigo

Bibliografia

Python: Escopos e namespaces


Escopos no Python

O escopo de uma variável é todo trecho de execução do código onde aquela variável pode ser encontrada, lida e alterada. Uma vez criada a variável fica disponível dentro desses trechos e só será apagada quando nenhuma referência á feita a ela. Fora de seu escopo a variável não existe e, por isso, o mesmo nome pode ser atribuído à outra variável, sem conflito.

A definição de uma função cria um novo escopo. Alguns exemplos abaixo demonstram escopos em relação a funções definidas na área principal do código (que já definiremos com mais rigor). Os casos são explicados depois do código.

» # Caso 1:
» def teste():
»     print(i)
» i = 42
» teste()
↳ 42

» #  Caso 2:
» def identidade(i):
»     return i
» i = 42
» print(identidade(5))
↳ 5

» # a variável externa à função não foi alterada
» print(i)
↳ 42

» #  manipulando i internamente
» def soma10(i):
»     i += 10
»     print(i)
» i = 42
» soma10(5)
↳ 15

» print(i)
↳ 42

» #  Caso 3:
» del h        # caso h já exista, delete a variável
» def func():
»     h=200
»     print(h)
» func()
↳ 200
» print(h)
↳ NameError: name 'h' is not defined

» # função definida dentro de outra (aninhada)
» def func():
»     h=200
»     def func2():
»         print(h)
» func2()
↳ NameError: name 'func2' is not defined

» # mas pode ser chamada na área onde foi definida
» def func():
»     h=800
»     def func2():
»         print(h)
»     func2()

» func()
↳ 800
  • Caso 1: a variável i definida fora da função pode ser lida internamente à função teste.
  • Caso 2: se o mesmo nome i é usado como parâmetro da função uma nova variável independente é criada. identidade.i não é a mesma variável que i fora da função. Aterações dentro do corpo da função não se propagam para fora dela.
  • Caso 3: uma variável ou uma função definida dentro de uma função não estão disponíveis fora dela.

Namespaces

“Namespaces are one honking great idea. Let’s do more of those!”
— The Zen of Python, Tim Peters
Em um projeto com código razoavelmente complexo muitos objetos são criados e destruídos. Namespaces é a forma usada para que conflitos entre esses nomes não ocorram. Definiremos como código, bloco ou programa principal a parte da execução do código por onde o interpretador (ou compilador) se inicia.

Um conceito importante, associado ao de escopo, é o de namespaces. Namespaces são as estruturas usadas para organizar os nomes simbólicos que servem para referenciar objetos em um programa. Uma atribuição cria um objeto na memória e associa a ele seu nome simbólico dado à variável, função ou método. Namespaces são coleções de nomes associados às informações sobre o objeto referenciados. Uma analogia pode ser feita com um dicionário onde os nomes dos objetos são as chaves os valores são os próprios objetos. Ele é um mapeamento entre nome e objeto na memória.

A existência de vários namespaces distintos significa que o mesmo nome pode ser usado em locais diferentes do código, desde que esteja em um namespace diferente.

Existem quatro tipos de namespaces no Python:

  • Interno (built-in),
  • global,
  • envolvente e
  • local.

Cada um deles é criado e existe por um tempo próprio, e destruído quando não mais necessário.

Namespace Interno (built-in)

O Namespace Interno (built-in) contém nomes e objetos criados internamente, antes mesmo que nada tenha sido importado e nem definido pelo usuário. Os nomes de váriáveis, funcões e métodos nele contidos podem ser vistos com o comando dir(__builtins__). Esse espaço é criado pelo interpretador quando é iniciado e existe enquanto ele não for encerrado.

» # No output abaixo (que está truncado) estão incluídos muitos nomes que já conhecemos
» dir(__builtins__)
↳ [ ...
↳    'chr', 'complex', 'dict', 'dir', 'divmod', 'enumerate', 'float', 'format', 'frozenset',
↳   'help', 'input', 'int', 'len', 'list', 'map', 'max', 'min', 'next', 'object', 'open', 'ord',
↳    'pow', 'print', 'range', 'reversed', 'round', 'set', 'slice', 'sorted', 'str', 'sum',
↳    'tuple', 'type', 'vars', 'zip'
↳ ]

Esses objetos estão disponíveis no ambiente mais geral e, portanto, em todos os setores do código, mesmo que distribuído em vários módulos.

Para ver onde residem esses valores em __builtins__ usamos:

» __builtins__.str.__module__
↳ 'builtins'

Isso significa que todas essas definições estão no módulo builtins.py.

Namespace global

O namespace global contém todos os nomes definidos no nível básico, abaixo apenas do namespace buil-in. Ele é criado quando o módulo é iniciado e só deixa de existir quando o interpretador é encerrado. Quando um novo módulo é importado com a instrução import um novo namespace é designado para ele. Além desses é possível criar nomes globais através palavra chave global, dentro da cada módulo.

Para ver o conteúdo do namespace global inciamos uma nova sessão do Jupiter Notebook, teclando 0-0 em qualquer célula, no modo de controle, para zerar as referências já criadas.

» # a função globals() retorna um dicionário
» type(globals())
» dict
» # para ver o conteúdo desse dicionário (output truncado)
» print(globals())
↳ {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', ...}

» # inserir uma variável é o mesmo que inserir um par no dicionário
» i = 23
» print(globals())
↳ {'__name__': '__main__', '__doc__': 'Automatically created module for IPython interactive environment', ..., 'i': 23}

» # a variável 'i' pode ser acessada e alterada diretamente no dicionário
» globals()['i']
↳ 23

» globals()['i'] = 789
» globals()['i']
↳ 789

# uma nova variável pode ser inserida no dicionário
» globals()['letras'] = 'Aa'
» print(letras)
↳ Aa

Os exemplos acima mostram que manipular o dicionário acessado por globals() é o mesmo que inserir, editar ou remover variáveis no namespaces global. Se um módulo externo for importado uma referência é feita à esse módulo, mas suas propriedades e métodos não são incluídos no dicionário.

» # importando o módulo datetime    
» import datetime
» globals()
↳ {'__name__': '__main__',  '__doc__': 'Automatically created module for IPython interactive environment', ...,
↳   'datetime': <module 'datetime' from '/home/guilherme/.anaconda3/lib/python3.8/datetime.py'>, ...}

Namespaces envolventes e locais

Quando uma nova função ou classe é inicializada o interpretador (ou compilador) cria num novo namespace reservado para aquela função ou classe. Objetos criados dentro deles não estarão disponíveis no ambiente externo (global), nem dentro de outras funcões no escopo global.

Isso significa que variáveis e funções podem ser definidas e usadas dentro de uma função com o mesmo nome de objetos em outras funções ou no programa principal, não ocorrendo confusão ou interferência porque são mantidos em namespaces separados. Isso contribui para que menos erros ocorram no código.

No exemplo seguinte o código exibe 4 diferentes namespaces:

» a = 1
» def f():
»     b = 2
»     c = 3
»     global g
»     def g():
»         c = 4
»         d = 5
»         print('a =%d, b=%d, c=%d, d=%d.'% (a, b, c, d))
        
» # f retorna None mas deve ser executada para que g() seja definida
» f()
» c = 3
» g()
↳ a = 1 , b = 2 , c = 4, d = 5

Em cada ambiente estão disponíveis:

  • built-in: a função print(), (por exemplo),
  • global: variável a, a função f() e a função g(),
  • ambiente de f(): as variáveis a, b, c e a função g(),
  • ambiente de g(): as variáveis a, b, c, d.

A função f() é global. A função g() foi tornada global devido à atribuição global.

Quando o módulo chama f() um novo namespace é criado. De dentro de f() a função g() é chamada, com seu namespace próprio.

O nome (ou identificador) g foi definido como global. Desta forma a função g(), embora tenha sido criada no namespace de f é global e pode ser chamada do módulo principal. Apesar disso sua variável interna d continua tendo escopo restrito a g. A variável c dentro de g não é a mesma que c em f, como se vê nos outputs do código.

Análogo ao dicionário para namespace global, podemos acessar o dicionário local através da função locals(). O código abaixo mostra o estado do namespace local para pontos diferentes de um código com a função g() aninhada em f().

» def f(x,y):
»     a = 'boa'
»     print('posição 1: ',locals())
»     def g():
»         b = 'tarde'
»         print('posição 2: ',locals())
»     g()
»     print('posição 3: ',locals())

» # executamos a função com x = 10, y = 55
» f(10, 55)
↳ posição 1:  {'x': 10, 'y': 55, 'a': 'boa'}
↳ posição 2:  {'b': 'tarde'}
↳ posição 3:  {'x': 10, 'y': 55, 'a': 'boa', 'g': <function f.<locals>.g at 0x7fc174050a60>}

No entanto, se tentarmos usar a variável a dentro de g() o interpretador a busca no namespace superior. Se ela for modificada dentro de namespace de g() uma nova variável é criada com aquele nome, preservando a variável do ambiente superior.

» def f(x,y):
»     a = 'boa'
»     def g():
»         b = 'tarde'
»         print('posição 1: ', a , b)
»         print('posição 2: ', locals())
»     g()
    
» f(10,55)
↳ posição 1:  boa tarde
↳ posição 2:  {'b': 'tarde', 'a': 'boa'}   

Uma forma diferente de se criar um novo namespace é através da criação de classes. Cada classe carrega seu próprio ambiente e é, portanto, seu próprio escopo.

» um = 1000
» tres = 3
» class Unidade:
»     um = 1
»     dois = 2000

» class Dezena:
»     um = 10
 
» class Soma:
»     um = 1 + tres

» print(um)
↳ 1000

» print(Unidade.um)
↳ 1

» print(Dezena.um)
↳ 10

» # a variável global tres existe dentro da classe Soma
» print(Soma.um)
↳ 4

Ao encontrar a requisição de um nome (uma referência à um objeto) o interpretador procura primeiro no ambiente local. Se não encontrar passa consecutivamente para os namespaces local, envolvente, global e built-in. Variáveis definidas em níveis mais externos podem ser acessadas nos níveis mais internos, mas não vice-versa. Em inglês é costume se referir a esse comportamento como Regra LEGB (Local, Enclosing, Global, Built-in).

A terminologia de namespaces local e envolvente não aparecem nas especificações oficiais do Python mas tem sido usadas em manuais e cursos. Ele simplesmente busca expressar o fato de que cada ambiente pode abrigar um ambiente aninhado criando uma hierarquia de precedências onde um nome será buscado.

Escopo:

Como vimos uma variável criada no corpo principal do código de Python está no global namespace e é chamada de variável global. Essas variáveis estão disponíveis em qualquer escopo, global e local.

Se, dentro de um escopo restrito, o programador necessite criar uma variável global (que possa ser acessada em todas as partes do código) dentro de uma área restrita podemos usar a palavra chave global.

Embora funções carreguem seu próprio escopo local, laços for ou while não criam uma área própria de escopo. Variáveis definidas dentro do laço continuam existindo depois de seu término.

» # r, definida dentro do laço, continua existindo após o seu final
» for t in range(10):
»     r = t
» print('Último t = %d' % r)
↳ Último t = 9

» # idem para um laço while
» while True:
»     j = 0
»     while j < 9:
»         j+=1
»     break
» 
» print(j)
↳ 9
    
» # global keyword: G fica acessível de fora de seu escopo de definição
» def func3():
»     global G
»     G = 300
» 
» func3()
» print(G) 
↳ 300

» # um variável 
» k = 1000
» def func4():
»     k = k + 23
»     print(k)
» 
» func4()
↳ UnboundLocalError: local variable 'k' referenced before assignment

» # no entanto ela pode ser acessada de dentro da função
» k = 1000
» def func4():
»     w = k + 23
»     print(w)
» 
» func4()
↳ 1023

# o mais seguro a fazer é passar um parâmetro que será confinado ao escopo da função
» w = 1000
» def func6(i):
»     return(i + 23)
» print(func6(w))
↳ 1023

Da mesma forma a função g() a seguir só poderia ser acessada de dentro de f() se não fosse usada a declaração global.

» def externa():
»     print('dentro de externa')
»     # global interna
»     def interna():
»         print('dentro de interna') 

» # executamos interna() para que ocorra a definição de interna()
» externa()
↳ dentro de externa

» interna()
↳ NameError: name 'interna' is not defined

» # mas, se definimos global interna
» def externa():
»     print('dentro de externa')
»     global interna
»     def interna():
»         print('dentro de interna') 

» externa()
↳ dentro de externa

» interna()
↳ dentro de interna

Sublinhados do Python

Sublinhados (_, __, underscores) tem significados específicos e diferentes no Python, de acordo com sua utilização.

Sublinhado simples:

No interpretador, seja no Idle, no prompt do Phyton ou no Jupyter Notebook, um sublinhado simples isolado (_) significa o valor da última expressão avaliada.

» pow(2,3)
↳ 8
» pow(_,3)
↳ 512

» 'cat' + 'egoria'
↳ 'categoria'
» _
↳ 'categoria'

» _ + 's'
↳ 'categorias'

Também, _ pode ser usado em lugar de uma variável que será ignorada. Isso pode tornar o código mais legível por mostrar que a tal variável não terá nenhum papel nas linhas a seguir. Outro uso é o de utilizar palavras chaves como nomes de variáveis, seguidos de um sublinhado.

Por exemplo:

» _, y = (1, 2)
» # ambos os valores são armazenados mas só y será usado (por convenção)
» print(y)
↳ 2

» for _ in range(10):
»     print('+', end='')
↳ ++++++++++

» # uma função pode retornar 4 valores mas apenas os 2 primeiros serão usados
» a, b, _, _ = funcao(parametro)

» # uma palavra chave (com sublinhado no final) pode ser usada como nome
» if_ = 1234
» print(if_)
↳ 1234

Essa última possibilidade pode tornar o código mais confuso e de difícil leitura.

Um sublinhado inicial, antes do nome da variável, função ou método é uma convenção usada para indicar que aquele objeto é apenas para uso interno e só deve ser acessado dentro da classe. Isso não previne, de fato, que ele seja acessado de fora da classe e, por isso o objeto é chamado de privado fraco. Mas, se o módulo em que a classe reside for importado com import * os nomes iniciados com _ não serão importados.

» # dentro de um módulo (gravado no arquivo classe.py) criamos 2 classes
» def metodo_publico():
»     print ("método público")
» def _metodo_privado():
»     print ("método privado")
» # ------ fim do módulo ------

» # importamos esse módulo
» from classe import *
» # uma chamada ao método público funciona normalmente
» metodo_publico()
↳ método público

» # uma chamada ao método privado não funciona
» _metodo_privado()
↳ NameError: name '_metodo_privado' is not defined
  
» # se o módulo inteiro for importado a classe _metodo_privado() pode ser chamada
» import classe
» classe._metodo_privado()
↳ método privado

Sublinhado duplo:

Uma variável ou método com duplo sublinhado inicial (__), como vimos antes, tem a função de reescrever em tempo de interpretação (ou compilação) o nome do objeto para evitar conflito com os mesmos nomes em subclasses.

» # O seguinte módulo está gravado como modulo.py
» class MinhaClasse():
»     def __init__(self):
»         self.__variavel = 987
» # ------ fim do módulo ------

» # importamos esse módulo e tentamos usar o acesso direto à variável __variavel
» import modulo
» obj = modulo.MinhaClasse()
» obj.__variavel
↳ AttributeError: Myclass instance has no attribute '__variavel'

» # para acessar a variável é necessário escrever um acesso público
» # e alterar o conteúdo de classe.py
» # O seguinte módulo está gravado como modulo.py
» class MinhaClasse():
»     def __init__(self):
»         self.__variavel = 987
»     def funcPublica(self)
»         print(self.___variavel)
» # ------ fim do módulo ------
» import modulo
» obj = modulo.MinhaClasse()
» obj.funcPublica()
↳ 987

Nomes iniciados e terminados com sublinhado duplo:

Métodos com nomes cercados por um duplo sublinhado, como __len__ e __init__, são considerados especiais no Python e servem para que o programador possa fazer sobrecarga ou overloading de métodos especiais das classes. Como vimos na seção sobre Métodos Especiais o método add() pode ser alterado pelo programador para executar função diferente da original.

» # O módulo seguinte está gravado no arquivo modulo2.py
» class MinhaClasse():
»     def __add__(self,a,b):
»         print (a*b)
» # ------ fim do módulo ------        

» import modulo2
» obj = modulo2.MinhaClasse()
» obj.__add__(5,2)
↳ 10

» # o operador + pode ser overloaded
» class novaSoma():
»     def __init__(self, valor):
»         self.valor = valor
»     def __add__(self, other):
»         return self.valor * other.valor

» a, b = novaSoma(56), novaSoma(10)
» print(a+b)
↳ 560

Resumindo

sublinhados em seu nome. Estas são as possibilidades:

  • Pública: significa que o membro pode ser acessado fora da classe onde foi definido, por outras instâncias ou objetos da mesma classe. Esses são nomes sem sublinhados. Por ex.: quantosAlunos = 367.
  • Protegida: o membro pode ser acessado pela classe onde ela está definida e seus filhos, outras classes que herdam dessa classe. Esses nomes são definidos iniciando com um sublinhado. Por ex.: _quantosAlunos = 367.
  • Privada: o membro só está acessível dentro da classe onde ela está definida. Esses são nomes iniciados com dois sublinhados. Por ex.: __quantosAlunos = 367.

Por default todos os membros de uma classe são públicos.

Argumentos de funções passados por valor e por referência

Terminologia:

Os valores usados na definição de uma função e manipulados por ela são chamados de parâmetros da função. Na chamada à função valores são fornecidos, normalmente chamado de argumentos. Na programação em geral (e não apenas no Python) argumentos podem ser passados por referência e por valor.

Um argumento passado por valor pode ser manipulado internamente na função e não tem seu valor alterado fora do escopo da função. Isso acontece porque a função manipula uma nova variável inicializada com aquele valor.

Argumentos passados por referência, se alterados no escopo interno da função, será alterado também fora do escopo da função. Nesse caso a função atua sobre o objeto em si, que é o mesmo que aquele do escopo de nível superior ao da função.

O comportamento dessas variáveis no Python é diferente se são referências a objetos mutáveis ou imutáveis.

São objetos imutáveis:

  • Números (Inteiros, Racionais, Ponto flutuante, Decimais, Complexos e Booleanos)
  • Strings
  • Tuplas
  • Conjuntos congelados (Frozen Sets)
  • Classes do usuário (desde que definida como imutável)

São objetos mutáveis:

  • Listas
  • Conjuntos (Sets)
  • Dicionários
  • Classes do usuário (desde que definida como mutável)

Um aspecto que pode ser difícil de debugar, em caso de erros, são as formas de tratamento dos parâmetros de uma função. Funções tratam de modo diferente argumentos mutáveis e imutáveis.

No Python, como as variáveis são nomes que fazem referências à objetos, toda variável é passada a uma função por referência. Se o objeto é mutável a variável original, fora do escopo da função, é alterado. Se o objeto é imutável a variável original fica inalterada e a função age sobre uma nova variável no seu próprio escopo, deixando inalterada a variável original.

Portanto, em comparação com outras linguagens, as funções agem como se variáveis mutáveis fossem passadas por referência e imutáveis por valor.

» # uma função que recebe um valor imutável trata seu parâmetro como passado por valor
» p = 'explícita'
» def concatena(p):
»     p = p.replace('í','i')
»     p +='mente'
»     return p

# a variável dentro da função está em escopo interno e não altera p global 
» print(concatena(a))
↳ explicitamente

» print(a)
↳ explícita

» # se o valor for mutável a função trata seu parâmetro como passado por referência
» alunos = {'Ana':28,'Julia':25,'José':32}
» def insere(alunos):
»     novos = {'Otto':30,'Mario':28}
»     alunos.update(novos)
»     print('Dentro da função:\n', alunos)

» insere(alunos)

» print('Fora da função:\n', alunos)
↳ Dentro da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}
↳ Fora da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}

» # forçando a função a se comportar "por valor"
» alunos = {'Ana':28,'Julia':25,'José':32}
» def byValue(alunos):
»     alunos2 = alunos.copy()
»     alunos2.update({'Otto':30,'Mario':28})
»     print("Dentro da função:\n", alunos2)
    
» byValue(alunos)
» print("Fora da função:\n", alunos)
↳ Dentro da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32, 'Otto': 30, 'Mario': 28}
↳ Fora da função:
↳  {'Ana': 28, 'Julia': 25, 'José': 32}

» # forçando função a se comportar "por referência"
» a = 34
» def byRef(a):
»     a = 78
»     print('Dentro da função: a=', a)
»     return a
» a = byRef(a)
↳ Dentro da função: a= 78

» print('Fora da função: a=', a) 
↳ Fora da função: a= 78

No penúltimo exemplo reconstruimos a função de modo a não alterar o objeto alunos no escopo global. Para isso criamos uma cópia de alunos em alunos2 = alunos.copy(), cuja alteração não implica em alteração na variável global.

Em seguida usamos uma solução paliativa para o caso de querermos tratar um valor imutável como passado por referência. Ele consiste em retornar o valor alternar e reatribuir a variável a.

Gerenciamento de memória

Cada um desses namespaces existe na memória até que sua função termine. O Python possui um processo interno recuperar a memória neles alocada. Mesmo que essa limpeza não seja imediata para esses namespaces quando suas funções terminam, mas todas as referências aos objetos que eles contêm deixam de ser válidas.

Estritamente dizendo, Python não possui variáveis e sim nomes (names) que são referências para objetos. Um objeto pode ter mais de um nome. Por exemplo, no código abaixo, experimento 1, a e b são referências para o mesmo objeto, o inteiro 1. A função id(objeto) retorna um id único para o objeto especificado. Todo objeto possuem seu próprio id que é atribuído a ela em sua criação. No teste vemos que a e b se referem ao mesmo objeto que tem id = 94153176063296. No experimento 2 o nomes, x e y, referenciam o mesmo objeto, a string “algo”. Mesmo sem associar as duas diretamente elas têm o mesmo id e x is y = True.

» # experimento 1
» a = 1
» b = a
» print(id(a))
↳ 94153176063296

» print(id(b))
↳ 94153176063296

# experimento 2
» x = 'algo'
» y = 'algo'

» print(id(x))
↳ 139723955294064

» print(id(y))
↳ 139723955294064

» print(x is y)
↳ True

Um nome é um label (uma etiqueta) que serve para disponibilizar no código um objeto, com suas propriedades e métodos. Mas nem todo objeto tem um nome. Existem objeto simples, como um inteiro ou uma string, e objetos compostos de outros objetos como containeres, como dicionários, listas e classes definidas pelo usuário, que armazenam referências para vários objetos simples ou mesmo outros containeres. Definimos como referência um nome, ou um objeto conteiner que apontam para outros objetos.

O Python usa um sistema de contagem de referências (reference counter) que mantém uma tabela atualizada de quantos nomes (ou referências) são associados a cada instante com um objeto. Quando um nome é associado à outro objeto a contagem decresce. Também podemos desfazer a ligação entre nome e objeto com o comando del.

» # instanciamos um objeto
» x = 700          # referências para o objeto 700 = 1
» y = 700          # referências para o objeto 700 = 2
» z = [700, 700]   # referências para o objeto 700 = 4
»                  # duas novas referências foram criadas: z[0] e z[1]
                 
» # a contagem decresce quando um nome passa a se referir a outo objeto
» x = None         # referências para o objeto 700 = 3
» y = 1            # referências para o objeto 700 = 2

# também podemos usar o comando del
» del z            # referências para o objeto 700 = 0

No código seguinte criamos duas variáveis com o mesmo valor (ou, melhor dizendo, associamos dois nomes com o mesmo objeto, 700) o verificamos seus ids, que são os mesmos. O teste x is y testa se são o mesmo objeto. No segundo teste a variável x é incrementada de 1. Isso faz com que novo objeto seja criado na memória (701) e a contagem de 700 seja diminuída de 1.

» def mem_test():
»     x = 700
»     y = 700
»     print(id(x))
»     print(id(y))
»     print(x is y)

» mem_test()
↳ 139800599228816
↳ 139800599228816
↳ True

» def mem_test2():
»     x = 700
»     y = 700
»     x += 1
»     print(id(x))
»     print(id(y))
»     print(x is y)

» mem_test()
↳ 139800599228688
↳ 139800599228656
↳ False

Objetos do Python, além de ter suas propriedades e código de seus métodos, possui sempre uma tabela com sua contagem de referência e seu tipo.

Como mencionamos o comando del desfaz a referência entre nome e objeto, reduzindo o contador de referências. Mas ele não apaga instantaneamente o objeto da memória.

A figura ilustra o ciclo de vida de um objeto criado dentro de uma função e referenciado uma vez. Quando a função termina sua contagem de referência está zerada e ele pode ser apagado.

No entanto, um objeto no namespace global não sai do escopo enquanto o programa inteiro não termina. Sua contagem de referência não se anula e, por isso, ele não será apagado, mesmo que se torne obsoleto. Essa é uma justificativa válida para não se usar variáveis globais em um projeto, exceto quando são realmente necessárias. E certamente se deve evitar referências para objetos grandes (em termos de requisição de memória) e complexos no escopo global.

Quando o contador de referências atinge o valor 0, significando que nenhuma referência está ativa para aquele objeto, ele pode ser apagado, liberando espaço de memória. Essa função é executada pelo coletor de lixo (garbage collector) que é uma forma de liberar memória para outros usos. Com frequência o uso total de memória de um programa em Python, visto pelo sistema, não decresce quando o coletor apaga objetos. No entanto a memória interna livre, alocada para aquele programa, aumenta. Não é muito difícil depararmos com situações de esgotamento de memória, o que causa lentidão do computador e eventual travamento.

Coletor de lixo geracional:

Um problema existente com o coletor de lixo ocorre com as chamadas referências cíclicas. Temos uma referência cíclica se um objeto faz referência a si mesmo, ou, por ex, um objeto A faz referência à B, que faz referência à C, que por sua vez contém referência à A. Mesmo que os objetos sejam extintos o contador de referências não fica zerado e esses objetos não são apagados, mesmo que nenhum nome se refira a eles.

Referência Cíclica

Por esse motivo o coletor de lixo geracional (generational garbage collector, GGC) foi inserido. Esse mecanismo faz uma verificação de objetos que não são acessíveis por nenhuma parte do código e os remove, mesmo que ainda estejam referenciados. O GGC é mais lento que o simples apagamento quando o contador de referências é zerado e por isso ele não é executado imediatamente após a remoção de uma referência.

Cada objeto no Python pertence a uma de três gerações de objetos. Ao ser criado ele é de primeira geração (geração 0). A cada execução do GGC, se o objeto não for removido, ele é promovido para a geração seguinte. Quando o objeto atinge a última geração (geração 2) ele permanece lá até ser alcançado pela remoção.

Configurando o GGC


A ação do GGC é configurável. Podemos especificar o número máximo de objetos que podem permanecer na geração 0 e quantos devem existir nas gerações 1 e 2 antes que a coleta seja ativada. É possível determinar o número de coletas que devem ser executadas sem que o processamento atinja geração 1 e 2.

Uma coleta de lixo é executada quando o número de objetos na geração 0 atinje o limite, removendo objetos inacessíveis e promovendo os demais para a próxima geração. Nesse caso o coletor de lixo atua apenas sobre geração 0. Quando o segundo limite é alcançado o coletor processa os objetos da geração 0 e 1. Quando o limite da geração 2 é alcançado, o coletor processa todos os objetos, das três gerações. Isso, no entanto, não acontece todas as vezes. A geração 2 (final) tem um limite adicional. Um GGC completo só ocorre quando o número total de objetos na primeira e segunda geração excede 25% de todos os objetos na memória (um limite não configurável).

Esses parâmetros foram inseridos para impedir a execução muito frequente do coletor, pois sua execução paraliza a execução do programa, tornando-os muito mais lentos. A maioria dos aplicativos usa objetos de “vida curta”, por exemplo criados e destruídos dentro de uma função, que nunca são promovidos à próxima geração. O programador pode usar desse fato para impedir “memory leaks” em seu código.

Por que isso importa no Jupyter Notebook

No Jupyter Notebook as células usam o escopo global. Variáveis criadas dentro de uma célula continuam existindo durante todo o ciclo de vida do próprio notebook. Desta forma ele não perde sua contagem de referência e não é excluída da memória, exceto se o programador atribuir aquele mesmo nome à outro objeto ou apague sua referência com o comando del. É fácil ter problemas com memória se você criar variáveis diferentes ​​para as etapas intermediárias de um processamento de dados.

Exemplos comuns desse procedimento ocorrem quando carregamos um dataframe de tamanho razoável (veja o artigo sobre dataframes). Em seguida podemos fazer várias etapas de tratamento de dados, tais como a remoção de valores inválidos, a inserção ou remoção de colunas, atribuindo o resultado de cada etapa à um novo nome.

Como já vimos uma solução seria o apagamento da referência com del, o que não é recomendado porque não há garantias de que o apagamento seria feito no momento esperado. Outra solução, mais eficaz, é a de só usar novos nomes para objetos dentro de funções ou classes, de forma que a referência é extinta ao fim do escopo. Um exemplo é mostrado no código a seguir. Se você não conhece pandas e dataframes simplesmente pule este exemplo.

» import pandas as pd

» def processar_dados(dados_brutos):
»     '''
»     codigo de remoção de valores nulos
»     alteração de nomes de colunas
»     inserção e remoção de dados
»     '''
»     return dados_depurados

» dados = pd.read_csv('file.csv')
» dados_processdos = processar_dados(dados)

Em linguagens de programação mais antigas o programador tinha que alocar um espaço de memória declarando a variável e seu tipo. Após o uso ele devia promover o apagamento da variável. Isso cria dois tipos de problemas: (a) o esquecimento de uma limpeza apropriada leva ao acúmulo de memória usada, particularmente em programas que rodam por longo tempo; (b) o apagamento prematuro de um recurso que ainda pode ser necessário, causando queda do programa. Por esses motivos as linguagens modernas usam o gerencimento automático de memória.

No lado negativo, o coletor de lixo deve armazenar suas informações (o contador de referência, no caso do Python) em algum lugar e precisa usar recursos de processamento para sua função, o que onera o sistema tanto em memória usada quanto de tempo de preocessamento. Ainda assim o gerenciamento automático torna mais fácil para o programador a sua tarefa.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.