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.