Entendendo o modo de execução do Python

Igor Franca
Nerdzão/Nerdgirlz
Published in
11 min readMar 28, 2018

E se eu te falasse que tudo não passa de um “objeto”?

Bom dia, boa tarde ou boa noite! Acho que assim como eu, você já deve ter ouvido falar muito sobre Python ultimamente, Se você, assim como eu gosta de desmistificar a magia por trás das ferramentas que usamos diariamente, você deve ter ficado intrigadissimo de como seu simples print(objeto_qualquer) aparentemente não tem utilização nenhuma!

Comportamento BIZARRO!

Poxa, meu “objeto” tem um valor, não? Por que ele não imprime algo sobre ele e sim esse endereço de memória que não significa nada??

Na verdade, o que precisamos entender é o que a “váriavel” example realmente significa no Python!

Em muitas outras linguagens de programação, você vai ouvir que example é uma váriavel, e infelizmente, em muitos artigos sobre Python também. Porém, é extremamente necessário que saibamos que na verdade, a “váriavel” example é na verdade um “apelido” para aquele endereço de memória.

Em linguagens como C ou Java, a váriavel example seria um local para guardar algo, como uma caixinha como muitos se referem falando de ponteiros em C, então escrever algo assim:

// Podemos dizer que a váriavel 'example' contém o valor 42!
int example = 42;

Porém, em Python, não temos o mesmo comportamento, então se dissermos que em:

example = Example()

A “váriavel” example contém um objeto do tipo Example seria errado! Pois na verdade, (e isso já explica porque estou colocando aspas na palavra váriavel hahaha) example é um name com um binding para um objeto criado por Example() !

*Whaaaaaaaaat?*

Calmaaa, vamos analizar parte a parte dessa frase “ … example é um name com um binding para o objeto criado por Example()

A primeira conclusão que chegamos é que, Python não tem váriaveis (no sentido clássico), e sim tem names e bindings!

Esse output da função print() pode passar a fazer um pouco mais de sentido se pensarmos que ele diz que example aponta para um __main__.Example object que está no endereço de memória 0x7f36a0b5d780.

Então, temos um entendimento melhor sobre o porque que coisas bizarras acontecem quando fazemos algo como:

Olhaaa só! Continuando nosso exemplo pelo terminal interativo do Python, (ipython, que criou o projeto para “prototipação” mais legal hoje de Python na minha opinião, o Jupyter Notebook!)

O que podemos entender, lembrando do que aprendemos, é que o name another_example é na verdade também um endereço (na memória 😃) para o objeto criado por Example()! E como o objeto criando por Example() não tem a propriedade some_property acabamos com um erro!

Então chegamos a pergunta,

Qual a definição de um name no Python?

Names no Python…

São como os nomes na vida real! Sim, isso mesmo! Pra ilustrar, vamos a um exemplo!

Se meus amigos me chamam de “mano” e minha mãe de “Igor”, não significa que sejam duas pessoas distintas! Ambos se referem a mim, portanto, se eu mudar algo em mim, não importam como me chamam, a mudança será vista me chamando de “Igor” ou de “mano”!

Mudar como me chamam, não muda as propriedades que tenho!

Porém, se lembrar o que disse no subtítulo do artigo, “… tudo é um “objeto” ”

Sim! Eu juro! Tudo é um objeto!

Vamos analizar outro caso. Vamos pegar o tipo primitivo no Python, uma int simples!

answer_for_everything = 42

Se você colocar isso no terminal do Python, você pode achar que agora o name (😸 agora sabemos que não é uma váriavel!) answer_for_everything na verdade aponta pro numero 42. Mas na verdade, tem algumas coisinhas escondidas!

Agora, veja como a função print() nos ajuda a entender melhor como o Python trabalha! Lembrando de como podemos ler isso, vemos que na verdade o name answer_for_everythingna verdade aponta para um int object no endereço de memória 0x56270a369200 .

Mas e se printarmos só o 42 , o que temos? Como o inté um primitivo do Python, o interpretador sabe que acessando ele, você quer na verdade o que ele vale! Então:

Wow! Temos o real valor!

Para ver o objeto na real, podemos usar a função nativa do Python dir() para ajudar a vizualizar o objeto int:

Eitaaa!

Usei também a função print() para melhorar a vizualização. Porém só a função dir() já imprime no terminal os atributos do objeto!

Assim, podemos ver que o simples 42 é um objeto, com suas próprias propriedades!

Agora, vamos mais além?

E se eu dissesse que existem também 2 tipos de objetos do Python?

Desculpa estragar sua manhã/tarde/noite de estudos fazendo você se preocupar com dois tipos de objetos, sendo que acabou de aprender que tudo no Python é um objeto!

Porém, fica tranquilo que é bem simples, os tipos são:

Mutáveis e Imutavéis

Vou ilustrar pra diminuir a teoria! Inspirado na filosófica frase do nosso querido Linus:

Talk is cheap, show me the code!

Opaa, comportamento diferente!

Agora podemos ver que o comportamento da nossa Example() não é válido pra str que também é um tipo nativo do Python. Porém, nossa classe Example() é um bom exemplo de objeto mutável! Quando alteramos o original, isso se propaga para todos os seus names .

Okay, mas e em objetos como os dos pacotes da biblioteca padrão? Que tal uma brincadeira com o pacote datetime ?

❤️ Rick and Morty ❤️

Olha só que maneiro! Conseguimos alterar a maneira que o Python acessa a propriedade de um objeto comum, como o datetime, e ainda sim, podemos manter o comportamento original!

(OBS: Essa manipulação de objetos é também chamada de metaprogramação e é muito comum no JavaScript, onde também temos um comportamento parecido quando se trata de manipulação de objetos.(Logo logo solto um artigo com mais detalhes sobre isso em Js 😈) Porém não sei sé é considerado muito pythonista fazer isso hahaha!

E podemos ir um pouco mais além, modificando nossa classe Example para deixarmos ela com um comportamento como os dos objetos nativos, pois como tudo é um objeto, na verdade o que o objeto int tem por baixo dos panos, é algo assim:

Desculpa por estragar a magia dos objetos nativos 😅

Caramba! Então podemos ver que na verdade, quando pedimos pro ipython imprimir pra gente o valor do int object ele na verdade chama o método int.__str__ dele! Quando só chamamos ele, ele usa o método int.__repr__ para chamar uma representation daquele objeto!

Podemos notar melhor, alterando o código acima para:

Mostrando quais são os métodos que o Python realmente chama!

E que tal, só pra finalizar, mexermos com mais um comportamento estranho de um objeto imutável, o objectnativo que pessoalmente acho um dos mais legais de todas as linguagens, uma tuple (Uma tupla, se você nunca usou, imagine como um array que pode recer diversos tipos de dados diferentes 😺)?

Podemos, seguindo nossos exemplos, fazer algo assim:

😄

Entãaao, se fizermos isso:

example_tuple[0] = 42

Temos a atribuição para a primeira posição da tuple para 42? Se você já fez isso alguma vez, sabe que o resultado é esse:

* Insira o GIF da aguia aqui de novo hahahahaha *

Opa! Então quer dizer que uma tupla é um objeto imutável? Exatamente! Agora você sabe como a tuple se comporta por baixo dos panos! Porém, vale lembrar que uma tupla pode conter objetos mutáveis, mostrando que o estado de imutabilidade é por objeto, não interferindo em nada nas hierarquias!

Agora temos uma mini estrutura de trie

Essa função eu acho massa! O que estamos fazendo é basicamento uma estrutura de dados chama trie que consiste em um dicionário muito simples, porém muito poderoso, que “explode” strings, uma implentação animal dela é usada pra busca em texto quase 50x mais rápida que Regular Expressions! (Se quiser ver ela em Golang, eu fiz uma legal também :D).

Coisas legais a parte, podemos ver que o Python consegue “trocar” o binding de um name para outro, é o que fazemos com o name root na função add_to_tree que demonstra como podemos alterar o que o nome tree representa para os novos objetos que estamos colocando nele.

Outro ótimo exemplo é com listas! Podemos fazer algo assim:

Olha que legal, dentro da função, atribuimos um novo valor ao name input_list e então, dentro do escopo da função, alteramos o name input_list para valer aquela lista de 0 a 9 (10 itens), e alteramos o primeiro item para 10, alterando assim a lista de 0 a 9, porém, repare na primeira linha da função, que antes de fazer algo com o name input_list nós trocamos a primeira posição para 10 também, então ele realmente trocou no binding original!

Oopa, acho que fomos rápido demais, que tal analizarmos linha a linha o que acontece com a lista test_list e a lista input_list ?

Antes de chamarmos a função list_changer temos:

  • test_list = [5, 5, 5] e o name input_list ainda não existe! hahaha

Depois chamamos a função passando a test_list como paramêtro, então temos o name test_list apontando para um endereço que contém um list object que “vale” [5, 5, 5] .

Quando chamamos a função list_changer :

  • test_list = [5, 5, 5] em um endereço
  • input_list agora é um name que tem o binding pro mesmo endereço que o name test_list aponta!
  • Na primeira linha quando modificamos a posição zero da lista para 10 é na verdade o list object original, que o name test_list sempre está apontando.
  • Na linha seguinte, temos uma atribuição ao name input_list para um novo list_object que eu fiz, uma lista de 0 a 9 hahaha!

Então dai para a frente, dentro do escopo da função list_changer o name input_list deixa de referenciar o list object que o name test_list aponta, então percebemos um novo comportamento do nosso querido Python!

Agora que estamos mais familiarizados com names , bindings , e objects , chegamos ao ultimo conceito do modo de execução do Python!

Blocos e Escopos!

Continuando a analisar nossa função list_changer podemos entender que o corpo de uma função (Apesar de no Python não parecer muito explicíto hahaha) tem seu próprio escopo ou seja, só pelo simples comportamento do name dentro da função list_changer percebemos que um name só “vale” realmente dentro do seu escopo, que é criado por um block ou um bloco de código Python que pode ser executado sozinho. Os tipos mais comuns de blocos são um módulo, (ou module), uma classe (ou class) e o corpo das funções. Aqui vai uma ilustração legal para entendermos um pouco disso:

Olha só que legal! Definindo a função dentro de uma função, e se cada função cria seu escopo dentro do seu corpo, como a função print_formatted_calculation tem acesso aos names value e number_of_digits sendo que eles não foram passados como paramêtros? E pior que isso, como as duas tem acesso ao name GLOBAL_CONSTANT? Então chegamos a pergunta que define a conclusão dessa sessão de desmistificação da Matrix do Python, para finalizar nossa sessão na simulação,

COMO DIABOS O INTERPRETADOR ACHA O NAME ?

Simples, ele procura em tudo! Começando pelo bloco mais perto do acesso, ou seja, toda vez que você acessa um name ele se pergunta:

  • “Okay, aqui perto (dentro desse bloco), existe esse name ?” Se sim, ele já acessa aquele endereço, se não,
  • “Okay, tem outros blocos acima desse?”, Se sim, ele muda o bloco de pesquisa e volta pra primeira pergunta, se não,
  • Ele retorna aquele famoso erro:

Simples não? Porém, isso nos leva a uma observação importante, que a principio pode não parecer óbvia, isso pode ser perigoso.

Pense no escopo como uma lista do Python, (ou array em qualquer outra linguagem), quanto mais pra dentro você acessar um name que está fora do bloco que está agora, mais posições de memória o interpretador terá que olhar, portanto, isso diminui a performance do seu código!. Claro que em scripts simples, ou pequenas aplicações, você nunca vai sentir isso, mas agora, lembre que as vezes você acessa propriedades que são colocados ao criar uma sub classe de algum framework, por exemplo, um Handler do Tornado, qualquer propriedade está mais “distante” do que da sua função/classe/ código! Porém, a performance não é o pior dos problemas, pois, se tratando de uma lista, se em qualquer posição antes o mesmo name for redefinido, você não vai acessar o endereço de memória que esperava! Podendo deixar seu script/aplicação simples, com um comportamento imprevisivel! Imagine então aquela galera que curte esse ditado:

Kkkkkkk

Brincadeiras a parte, o Python, por saber que nem sempre pegaremos códigos limpos e maravilhosos, tem dois operadores que são pouquissimo usados, que admito, só descobri para compartilhar com vocês! Os operadores nonlocal e global

Revivendo a filosofia do nosso querido Linus,

Veja que interessante, que os operadores na verdade forçam o interpretador a trocar o binding do name que peço para o valor mais distante/valor no bloco acima, ignorando qualquer outra atribuição, e também, traz aquele valor e redefine ele pra todo mundo pra baixo! Ou seja, você realmente troca o binding daquele name , forçando qualquer acesso posterior a acessar o endereço de memória que você pediu. Forçando assim que você não atribua um novo endereço aquele name e sim, forçando aquele name a apontar para o endereço que quiser!

Use this power with responsibility!

Sumarizando, agora você deve estar apto a entender melhor como o Python interpreta seu código, e sabendo de tudo isso! Se lembre que qualquer alteração nesses comportamentos, por mais legais que sejam (😈) deve ser documentada e/ou exposta de alguma maneira para que sua equipe não sofra debulhando códigos ou perdendo os cabelos no Google. Lembre-se que seu código pode durar até mais tempo que você! Então deixe-o mais bonito que puder dentro do tempo necessário, e documente tudo! :D

Espero que tenham gostado de um pouco de curiosidades, e divirtam-se acima de tudo! Python é lindo para prototipagem rápida, porém também é muito bom pra codebases extensas.

Isso é tudo pessoal!

Como ir além?

Eu agrupei vários artigos que li durante meu aprendizado de Python a alguns anos, que vem desde o básico de um código com qualidade para durar muito tempo até tópicos de Data mining, e todas essas buzzwords de tópicos legais nesse repositório no meu GitHub, como eu sabia que isso provavlemente geraria duvidas também, se sinta a vontade pra abrir uma issue no repositório e continuamos conversando!

Muito obrigado galera! E bons estudos! :D

--

--

Igor Franca
Nerdzão/Nerdgirlz

Node.js Witch Doctor and at the free time playing with security stuff :D