Entendendo o modo de execução do Python
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!
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()
!
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_everything
na 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:
Para ver o objeto na real, podemos usar a função nativa do Python dir()
para ajudar a vizualizar o objeto int
:
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!
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
?
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:
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:
E que tal, só pra finalizar, mexermos com mais um comportamento estranho de um objeto imutável, o object
nativo 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:
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!
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 oname
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çoinput_list
agora é umname
que tem obinding
pro mesmo endereço que oname
test_list
aponta!- Na primeira linha quando modificamos a posição zero da lista para
10
é na verdade olist object
original, que oname
test_list
sempre está apontando. - Na linha seguinte, temos uma atribuição ao
name
input_list
para um novolist_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:
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.
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