Python: Imports e Bibliotecas
Tags: python, programming
Published: 2023-05-12 21:11:18

Se você já conhece um pouco sobre o mundo da programação, deve saber que, na maioria dos casos, um só arquivo não será o suficiente para escrever seu programa, exceto se você escrever 700 linhas em um único arquivo (não faça isso, por favor). A modularidade e reutilização de código são fundamentais em qualquer linguagem de programação. Em Python, podemos atingir esse objetivo importando pacotes, módulos e bibliotecas. A importação de módulos permite que os desenvolvedores acessem funções, variáveis e classes criadas em outros arquivos e os utilizem em seus próprios programas. Já a importação de bibliotecas oferece uma ampla variedade de recursos prontos para uso, como algoritmos de aprendizado de máquina, visualização de dados, manipulação de arquivos, entre outros.

## Imports e mais imports

### Importando conteúdos

Para fazer uso de bibliotecas e fatores externos ao arquivo atual de código, devemos importar esses conteúdos através da sintaxe

```python
import <nome_do_conteudo>
```

no começo do arquivo, onde `nome_do_conteudo` refere-se a qualquer unidade de código que pode ser exportada de um lugar para outro no programa. Dentre essas unidades, temos principalmente os módulos, os pacotes e as bibliotecas.

Na teoria, qualquer arquivo python já é um módulo, portanto, se você criar outro arquivo e precisar chamar uma função ou variável localizada nele, basta importá-lo utilizando a chamada `import <nome_do_arquivo>` sem a extensão “.py”. Tome o exemplo:

Estamos criando um módulo que realiza cálculos para o nosso programa. Para tal, crie o arquivo `calculo.py` e insira o seguinte conteúdo nele:

```python
# calculo.py

def somar(a, b):
    return a + b

def subtrair(a, b):
    return a - b
```

São apenas duas funções, uma soma e a outra subtrai. Agora, no arquivo que você irá executar, chame o módulo através do nome do arquivo e use suas funções. As funções se ligam ao nome do módulo importado por uma notação de ponto `.` onde se escreve `<nome_do_modulo>.funcao()`. Veja o código abaixo:

```python
import calculo

print(calculo.somar(1, 2))
```

Agora execute o programa e veja se o output equivale a esse:

```python
$> python3 <nome_do_arquivo>.py
3
```

Assim, você criou um módulo, importou em outro arquivo e chamou uma de suas funções. Com esse conceito, aprendemos uma nova formula de encapsulamento e modularização de código.

Você pode, ainda, importar uma função específica do módulo/pacote desejado utilizando uma sintaxe similar a anterior:

```python
from <modulo> import <nome>
```

Diferente de Javascript e outras linguagens, Python segue uma ordem de busca para achar módulos importados:

1. Procura nó diretório em que o script que importa está localizado;
2. Em caminhos localizados na variável de ambiente `PYTHONPATH`. Ela pode ser acessada através do módulo `sys` chamando `sys.path`;
3. Em um diretório de instalações da linguagem;

Caso sua biblioteca tenha um nome muito grande ou desconfortável de escrever o tempo todo, é possível dar um “apelido” para a importação através da sintaxe `as`. Do exemplo do módulo `calculo.py`, podemos apelidar o módulo a fim de escrever menos:

```python
import calculo as c

print(c.somar(1, 2))
```

Tome muito cuidado com os nomes escolhidos para apelidos, eles podem se tornar pouco intuitivos e difíceis de ler. Um exemplo disso seria você apelidar a biblioteca `string` de `macarrao`. Faz sentido? Óbvio que não.

### A função `dir(obj=None)`

Cada módulo criado tem sua própria assinatura, denotada por uma tabela privada de símbolos que dita cada objeto daquele módulo. Quando importamos um módulo `x` através de `import x`, estamos inserindo a tabela de símbolos de um módulo em outro.

Para saber o que está na tabela de símbolos do módulo atual, podemos fazer uso da função `dir`, que retorna uma lista de todos os símbolos especificados em `obj`. Se não for passado nenhum valor como parâmetro, a função retornará uma lista contendo a tabela de símbolos de um módulo.

Toda instrução que possa ser passada como parâmetro tem uma tabela privada de símbolos, incluindo variáveis, funções, classes e afins. Logo, qualquer valor dentro dessa categoria pode ser passado como parâmetro para `dir`.

Por exemplo:

```python
x = 10
print(dir(x))
```

O código acima declara uma variável `x`, que recebe 10 e escreve no terminal sua tabela de símbolos. Como seria a tabela de símbolos de um inteiro?

```python
$> python3 <nome_do_arquivo>.py
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', 
'__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', 
'__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__',
 '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__',
 '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', 
'__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', 
'__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', 
'__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', 
'__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', 
'__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 
'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 
'numerator', 'real', 'to_bytes']
```

Maluquice? Um pouco. Na verdade, a tabela de símbolos de uma variável inteira é o conjunto de todas as funções e propriedades que fazem parte do módulo do tipo `int` específicado em tempo de projeto da linguagem, ou seja, essa é a lista que todo inteiro possui como base. Todos esses símbolos podem ser acessados através da notação de ponto `.`.

### Módulos como scripts

Bom, como sabemos, Python não requer uma função `main` para executar um programa. Isso porque `main` está impllícita no código e é gerada em tempo de execução dentro do arquivo invocado por `python3`. Logo, todo e qualquer arquivo python pode ser considerado um script executável. 

Por exemplo, eu tenho dois arquivos: `main.py`, o arquivo que criei para ser o executável, e `biblioteca.py`, um módulo a ser importado. Suponhamos que o conteúdo de `biblioteca.py` seja o seguinte:

```python
frase = "cheio de papinho gostoso"

def printa_lista(lista):
    for el in lista:
        print(el)
```

Podemos simplesmente fazer `python3 biblioteca.py` e executar o arquivo como se nada houvesse, mas isso não produziria nenhum retorno. O código cria objetos, mas não os executa. No caso, se quisermos um retormo, basta usar `print`  e invocar a função `printa(lista)`.

```python
frase = "cheio de papinho gostoso"

def printa_lista(lista):
    for el in lista:
        print(el)

print(frase)
printa_lista([1, 2, 3, 4])
```

E assim teremos o output:

```
$> python3 biblioteca.py
cheio de papinho gostoso
1
2
3
4
```

Porém, entretanto, todavia, isso também gera output quando esse arquivo é importado por `main`, já que todo e qualquer símbolo é passado de um arquivo para outro. Talvez esse não seja o comportamento ideal, a menos que você queira outputs rolando soltos em toda a sua estrutura de arquivos. A partir dessa necessidade, podemos definir uma condição especial para o arquivo caso ele seja o executável. Se é uma condição, obviamente devemos usar `if`.

Quando um arquivo `.py` é importado como módulo, o interpretador Python gera uma variável especial `__name__` que recebe o nome do módulo. No entanto, se o tal arquivo `.py` é executado como um script por si só (ou seja, ele é o escolhido da vez pela CLI), essa variável especial `__name__` recebe a string `"__main__"`. Dito isso, agora podemos diferenciar um módulo qualquer do arquivo que receberá a função main. Adaptando `biblioteca.py` às nossas necessidades:

```python
frase = "cheio de papinho gostoso"

def printa_lista(lista):
    for el in lista:
        print(el)

if (__name__ == "__main__"):
    print(frase)
    printa_lista([1, 2, 3, 4])
```

Logo, se esse arquivo for o script executavel a receber `main`, ele escreverá `frase` e executará a função. Se não, será tratado como módulo.

Ter o comportamento de módulos como script é extremamente útil especialmente para testes, pois cada módulo pode ser visto como uma unidade funcional por si só, o que facilita a implementação de testes unitários.

### Pacotes em Python

Além dos módulos e das bibliotecas nativas como opções de modularizar funcionalidades, também temos os pacotes. Um pacote é uma coleção de módulos que compartilham um mesmo diretório e, geralmente, um mesmo objetivo. Isso permite a organização e modularização de código de forma mais clara e estruturada.

Suponha que você esteja desenvolvendo uma aplicação grande que possui inúmeros módulos. A medida que o número de módulos cresce, a dificuldade de manutenção também cresce junto. Imagina ter os módulos de autenticação, pagamento e interação com banco de dados juntos em uma só estrutura linear? Complicado, hein!

Para tal, temos os pacotes. Para criar um pacote em Python, é necessário criar uma pasta que conterá todos os módulos relacionados dentro dessa pasta. Os arquivos dentro de uma pasta são comumente associados entre si por contexto ou funcionalidade. Por exemplo, uma pasta com o nome “usuário” terá funcionalidades que remetem aos casos de uso do usuario no sistema.

Pacotes podem ser vistos como uma versão de estrutura hierárquica dos módulos, já que também usam notação de ponto, mas se organizam por pastas. Da mesma forma que módulos evitam colisões entre variáveis com a modularização, pacotes evitam as colisões entre nomes de módulos, pois estão um nível acima na hierarquia de organização. Tome como exemplo a estrutura abaixo:

![Uma imagem representando a estrutura de arquivos em Python, onde temos um arquivo main.py e um diretório de nome pacote, contendo dois arquivos arquivo1.py e arquivo2.py, respectivamente](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mw2rjnffriq0i0xle6s0.png)

Temos um diretório `pacote` com dois módulos `arquivo1.py` e `arquivo2.py` com os seguintes conteudos, respectivamente:

```python
# arquivo1.py

def fala_oi():
    print("oi do primeiro módulo")

frase_do_dia = "água mole em pedra dura, tanto bate até que fura"
```

```python
# arquivo2.py

def fala_tchau():
    print("tchau do segundo módulo")
```

Dadas essas informações, se o diretório `pacote` estiver em uma localização (caminho no sistema de arquivos) que não entre em conflito com outros ou com a variável de ambiente `PYTHONPATH`, então você pode referenciar os dois módulos através da notação de ponto. Observe a importação abaixo em `main.py`:

```python
import pacote.arquivo1 as a1
import pacote.arquivo2 as a2

a1.fala_oi()
a2.fala_tchau()
```

## Bibliotecas

A importação de bibliotecas é uma das características mais poderosas do Python. Ao importar bibliotecas, podemos expandir a funcionalidade do Python para além das bibliotecas nativas e pacotes do projeto e assim tornar o código mais modular e reutilizável.

### Bibliotecas nativas

As bibliotecas nativas são aquelas que já vêm pré-instaladas com a instalação padrão da linguagem e do interpretador. Essas bibliotecas incluem módulos e pacotes que fornecem funcionalidades básicas para a linguagem, como operações matemáticas, manipulação de strings, entrada e saída de arquivos, entre outras.

Além disso, as bibliotecas nativas do Python são de código aberto e são mantidas pela comunidade de desenvolvedores, o que permite que qualquer pessoa possa contribuir com melhorias e correções de bugs. Tá aí a dica caso queira entender como as coisas funcionam.

Algumas das bibliotecas nativas mais comuns do Python incluem:

- math: oferece funções matemáticas básicas, como cálculo de raiz quadrada, exponenciação e trigonometria.
- string: fornece funcionalidades para manipulação de strings, como a formatação de strings e busca de caracteres específicos.
- os: permite a interação com o sistema operacional, como a criação de arquivos e diretórios e a manipulação de variáveis de ambiente.
- datetime: oferece funcionalidades para trabalhar com datas e horários.
- random: gera valores aleatórios, como números inteiros e floats.

Essas bibliotecas são essenciais para o desenvolvimento de programas em Python e, por isso, é importante ter um conhecimento básico sobre elas.

Veja o exemplo abaixo utilizando a biblioteca `os`:

```python
import os

print(os.getcwd())
```

No código, importamos a biblioteca `os` e utilizamos a função `getcwd()`, uma abreviação para “**get** **c**urrent **w**orking **d**irectory”, que retorna o diretório onde o script está rodando. No meu caso, o output é:

```
$> python3 <nome_do_arquivo>.py
/home/iugstav/dev/py/
```

A importação de bibliotecas é idêntica à importação de módulos, com os mesmos comportamentos de apelidos e afins.

### Bibliotecas externas

A instalação de bibliotecas externas é uma etapa importante para qualquer projeto em Python que necessite de recursos adicionais para executar tarefas específicas. Embora a linguagem tenha um conjunto abrangente de bibliotecas nativas, muitas vezes é necessário utilizar recursos de terceiros para atender às necessidades de um projeto.

Existem diversas bibliotecas em Python, tanto as que são nativas da linguagem quanto as que foram criadas por desenvolvedores da comunidade. Algumas das bibliotecas mais populares em Python incluem:

- NumPy: usada para manipulação de matrizes e operações matemáticas;
- Pandas: utilizada para análise e manipulação de dados;
- Matplotlib: usada para criação de gráficos e visualizações;

Como já foi dito algumas vezes nessa série de materiais, Python tem bastante foco em machine learning, computação científica e ciência de dados. De fato, seu ecossistema tem forte influência nisso, pois existem inúmeras bibliotecas focadas nessas áreas, inclusive as três citadas acima. Isso se dá pela portabilidade que outras linguagens têm no ecossistema da linguagem. Existem inúmeras APIs que portam código em C, FORTRAN, Rust e outras para um código compreensível para bibliotecas em Python.

Para utilizar bibliotecas externas em Python, é necessário instalá-las em seu sistema. Existem diversas maneiras de realizar a instalação de bibliotecas, sendo as mais comuns:

1. Utilizando o gerenciador de pacotes **pip**: o pip é um gerenciador de pacotes do Python que permite buscar, instalar, atualizar e desinstalar bibliotecas de forma simples e rápida. Para instalar uma biblioteca usando o pip, basta executar o seguinte comando no terminal: `pip install <nome_da_biblioteca>`.
2. Utilizando o **Anaconda**: o Anaconda é uma plataforma que inclui o Python e um conjunto de bibliotecas científicas populares. Ele também possui um gerenciador de pacotes próprio chamado conda, que permite instalar bibliotecas de forma semelhante ao pip.
3. Instalando manualmente: em alguns casos, a biblioteca pode não estar disponível no repositório do pip ou conda, ou pode ser necessário instalá-la de forma manual. Nesses casos, é necessário baixar os arquivos da biblioteca diretamente do site do desenvolvedor e seguir as instruções de instalação disponibilizadas.

Independentemente do método escolhido, é importante lembrar que algumas bibliotecas podem ter dependências em outras bibliotecas. Portanto, é recomendável verificar as dependências antes da instalação, para garantir que todas as bibliotecas necessárias estejam instaladas corretamente.

Aqui, usaremos pip e a bibliotema NumPy para manipular matrizes. Para instalar a NumPy, digite o comando no seu terminal:

```python
pip install numpy
```

De acordo com a própria documentação da biblioteca, NumPy (**Num**erical **Py**thon) é uma biblioteca open source usada em quase todas as partes da ciência e engenharia que conta com matrizes multidimensionais e listas próprias. Usaremos ela no próximo tópico sobre matrizes.

Vimos que a modularização de código é fundamental para tornar nossos programas mais organizados e reutilizáveis, e que a utilização de pacotes pode facilitar muito esse processo. Além disso, exploramos como é possível integrar outras linguagens à Python, o que pode ser útil em áreas onde a performance é crítica.

Ao compreender e dominar o uso de bibliotecas em Python, podemos aproveitar ao máximo todo o poder e flexibilidade que a linguagem oferece, além de economizar tempo e esforço em nossos projetos.