Git e GitHub


Introdução: Git e GitHub

No desenvolvimento de software duas situações ocorrem com frequência:

  • na medida em que se escreve código é comum que uma alteração crie problemas que tornam importante voltar para um estágio anterior, onde o problema não existe;
  • vários programadores podem trabalhar em um mesmo projeto e nem sempre é fácil juntar as alterações feitas.

Além disso é comum se fazer uma bifurcação do projeto em algum ponto para fazer experimentações ou gerar novo projeto. O controle de versões facilita o fluxo de trabalho de quem necessita acrescentar características, corrigir erros ou voltar para etapas anteriores de um projeto.


O Git é uma ferramenta de controle de versão (um sistema de versionamento) que mantém um histórico do projeto, permitindo o retorno à qualquer ponto e facilitando as junções de códigos desenvolvidos separadamente. Ela é uma ferramenta usada na linha de comando (embora existam aplicativos GUI que podem ser encontrados na página Git – GUI clients). Ele não é o único aplicativo para o controle de versões mas tem se tornado o mais usado deles. Git é considerado a segunda criação mais famosa e usada de Linus Torvalds, o criador do Linux. Git é um sistema de controle de versão distribuído (distributed version control system, DVCS).

Além dos clientes GUI disponíveis para o controle do Git e integração com o GitHub existem diversas IDEs que facilitam esse controle de versão, como o PyCharm e o VSCode. As sessões do Jupyter Notebook também podem ser controladas com o Git.

GitHub é uma plataforma na internet que usa o Git para hospedar código e projetos, e controle de versão. Existem outras alternativas, como GitLab e BitBucket.

Git e GitHub são coisas diferentes! Git é uma ferramenta de controle de versão de código aberto criada em 2005. GitHub é uma empresa fundada em 2008 que criou um site que usa o Git. Você pode usar o Git apenas em sua máquina, mas isso torna mais difícil o compartilhamento de código.

Conceitos e definições:

Alguns conceitos e definições são importantes para o uso do Git e do GitHub. Todos eles serão desenvolvidos no texto do artigo.

Projeto: é qualquer conjunto de código destinado à realização de uma tarefa. Ele pode ser um texto em processo de construção, um aplicativo com seu código ou um conjunto de páginas da web.
Diretório de trabalho é aquele em que seu projeto é desenvolvido. Seus arquivos não são automaticamente rastreados pelo Git, que precisa ser informado de quais arquivos deve acompanhar.
Área de preparação
(staging area)
é o conjunto de arquivos marcados para acompanhamento pelo Git. Nenhum arquivo no diretório de trabalho é automaticamente marcado para acompanhamento pelo Git. Arquivos são colocados na staging area com o commando git add <arquivo> e no repositório com git commit.
Repositório local é uma área na máquina local dedicado ao armazenamento do Git para o estado do projeto nos momentos decididos pelo desenvolvedor. Ele armazena as diversas versões e ramificações feitas por um ou mais desenvolvedores. Repositórios são, às vezes, chamados de “repos”.
Repositório do GitHub é um repositório remoto, hospedado no site do GitHub. Podemos sincronizar nosso repositório local com o do GitHub ou vice-versa, baixando para a máquina local o repositório remoto.
HEAD é o ponteiro que marca o estado do código ativado no Git. Esse estado consiste nos branch e commit ativos.

Nem todas as alterações no projeto precisam ser armazenadas. Por isso apenas são guardadas as situações quando se faz um commit, por decisão do desenvolvedor. Estritamente dizendo, commit não necessita guardar sempre os arquivos inteiros no repositório. Ele tenta ser o mais leve possível, armazenando apenas as modificações feitas desde o último commit. Por isso alternar entre um e outro estado é uma operação bastante rápida.

A estrutura do Git dispensa o uso de um servidor centralizado para armazenar todas as modificações. Ele permite que vários desenvolvedores alterem seus repositóritos locamente em suas máquinas e depois o façam o upload de seus trabalhos. Ele também facilita o trabalho de juntar as diversas modificações feitas.

Instalando o Git

Muitos sistemas operacionais instalam e mantém atualizado o Git. Você pode verificar se tem o Git instalado em seu sistema abrindo um terminal e digitando:

$ git --version
git version 2.35.1

A versão será exibida, ou uma mensagem de erro caso ele não esteja instalado. É possível atualizar a versão do Git usando o próprio Git:

$ git clone https://github.com/git/git

Se necessária a instalação visite o site do Git – downloads. Alguns exemplos de instalação:

 Debian/Ubuntu
# PPA para Ubuntu da última versão estável
$ add-apt-repository ppa:git-core/ppa
$ apt update
$ apt install git

# Debian/Ubuntu
$ apt-get install git

Fedora (até versão 21, usando yum)
$ yum install git
Fedora (versão 22 ou posterior, usando dnf)
$ dnf install git

Para Windows faça download e instale os executáveis apropriados.

Configurando o Git

Vamos também configurar o nome e email associado ao repositório local, usando git config:

$ git config --global user.email "seu_email@example.com"
$ git config --global user.name "Seu nome"
# nenhuma resposta é exibida

Essas informações são usadas pelo Git para marcar quem foi o autor de cada alteração registrada. Além do nome podemos usar outros comandos para alterar o comportamento e aparência do Git no console.

# os seguintes comandos instruem git usar cores para realce de sintaxe no console
$ git config --global color.ui true
$ git config --global color.status auto
$ git config --global color.branch auto

# para definir um editor
$ git config --global core.editor nome_do_editor

# para definir a ferramenta default de merge (junção de versões)
$ git config --global merge.tool vimdiff

# para listar todas as configurações alteradas do git
$ git config --list

As configurações de config --global color tornam mais legíveis as linhas do console. Por default Git usa o editor padrão definido nas configurações do sistema. Esse padrão pode ser alterado aqui, por exemplo fazendo git config --global core.editor vim

Quando se trabalha simultaneamente com o GitHub podemos fazer alterações em qualquer um dos ambientes e depois se sincronizar com o outro. Alterações locais não alteram o repositório do GitHub, até que sejam a ele enviadas.

Usando o Git localmente

Na máquina local usamos um diretório de trabalho para desenvolver nosso projeto. Digamos que queremos iniciar um projeto que contenha código Python para manipulação de texto. Denominaremos esse projeto de PyTexto e criaremos um diretório com esse mesmo nome. Depois, usando o terminal, navegamos até este diretório e inicializamos o Git.

# cria pasta, inicializa git
$ mkdir ~/Projetos/PyTexto
$ cd ~/Projetos/PyTexto
$ git init
  hint: Using 'master' as the name for the initial branch. This default branch name
  hint: is subject to change.  (...)

# para renomear o branch ou ramo podemos usar git branch -m novo_nome

# vamos renomear para "main" 
$ git branch -m main
# para visualizar a situação do git
$ git status
  On branch main
  No commits yet
  nothing to commit (create/copy files and use "git add" to track)
  
# outras informações sobre o repositório local podem ser vistas com
$ git log  

) O Git cria um diretório oculto .git dentro do diretório de trabalho, e inicializa um branch (ramificação) original, chamada master, que renomearemos para main. Por default, até o Git 2.35, a inicialização cria um branch (ou ramo) principal chamado branch master. Tem sido uma prática adotada por desenvolvedores renomear esse ramo para branch main ou branch trunk (ou o nome que você preferir). É possível configurar o Git para que o branch inicial de seus novos repositórios se chame branch main:

# altera o nome default da branch inicial
$ git config --global init.defaultBranch main


Para inserir ou editar arquivos usarei o editor de código Geany, disponível para Linux, macOS e Windows. Qualquer editor ou IDE podem ser usados.

É uma boa prática criar um arquivo (usando o seu IDE preferido ou qualquer editor de texto) README.md (ou README.txt) onde o desenvolvedor coloca instruções de uso e instalação, comentários sobre o projeto, versão, etc. A extenção .md indica que o arquivo contém texto formatado com a marcação markdown. Você pode ler sobre markdown aqui!

Depois criamos outro arquivo no nosso diretório de trabalho. Nesse caso inserimos um arquivo do python com o nome palindromo.py. Os conteúdos podem ser, por exemplo:

# arquivo README.md
# Projeto de teste e aprendizado do Git
Consiste em testes de funcionamento do Git

e o arquivo do python:

# arquivo palindromo.py
# palindromo (testa se a palavra é um palíndromo)
def palindromo(palavra):
    palavra = palavra.upper()
    p = ("" if palavra == palavra[::-1] else "não")
    return (f"{palavra} {p} é um palíndromo")

Em princípio nenhum dos arquivos nesse diretório são rastreados pelo Git até que o informemos que deles devem fazer parte do repositório. Podemos ver isso com git status:

$ git status
  On branch main
  No commits yet
  Untracked files:
  (use "git add ..." to include in what will be committed)
    README.md
    palindromo.py
  nothing added to commit but untracked files present (use "git add" to track)

Vemos que o Git reconhece que dois arquivos foram inseridos na pasta de trabalho, mas nenhum deles foi marcado para acompanhamento (colocado em stage). Isso é feito com o comando:

$ git add nome_do_arquivo
# no nosso caso
$ git add README.md
$ git add palindromo.py

# caso existam vários arquivos, e todos devem ser marcados, usamos
$ git add -A

Com isso os arquivos são marcados como estando na área de preparação (staging area), e são atualizados pelo Git até que um commit seja executado. Vamos verificar novamente o status do repositório:

$ git status
  On branch main
  No commits yet
  Changes to be committed:
     (use "git rm --cached ..." to unstage)
     new file:   README.md
     new file:   palindromo.py

Esse comando informa que o branch main está ativo, que README.md e palindromo.py estão em stage, prontos para o commit. Para enviar arquivos em stage para o repositório fazemos o commit. A lista de todos os commits feitos naquele branch pode ser vista com git log.

$ git commit - m "Commit 1 com arquivos README.md e palindromo.py"

# para ver a lista de todos os commits naquele branch
$ git log
  commit 48c3f1f8fae05f87bd79f58b2d4f0cca42d8f8d5 (HEAD -> main)
  Author: Guilherme Santos Silva <gssilva57.gmail.com>
  Date:   Tue Mar 15 14:39:42 2022 -0300

    Commit 1 com arquivos README.md e palindromo.py
:

Se o output de git log for maior que a área disponível do console ele termina com o sinal :. Isso é uma indicação de que você pode rolar a tela do console para ler todo o texto. Para sair desse modo digite q ou CTRL-z. Uma forma compacta de adicionar arquivos e fazer commit ao mesmo tempo consiste em usar a marca commit -am:

$ git commit -am "descrição do commit"

O comando commit faz com todos os arquivos adicionados à área de preparação sejam enviados para o repositório. A chave -m "mensagem" adiciona uma mensagem junto a esse commit, que pode ser visualizado mais tarde. Ela deve ser um texto curto e explicativo de quais alterações estão sendo gravadas.
git
Esse comando falha se não tiver sido feita a configuração de git config --global user.email e git config --global user.name. Caso contrário a alteração pode ser verificada com git status.

$ git status
  On branch main
  nothing to commit, working tree clean

Dessa forma criamos, alteramos e inserimos todas as alterações no repositório.

A lista de todos os commits feitos naquele branch pode ser vista com git log.

# para ver a lista de todos os commits naquele branch
$ git log
  commit 48c3f1f8fae05f87bd79f58b2d4f0cca42d8f8d5 (HEAD -> main)
  Author: Guilherme Santos Silva <gssilva57.gmail.com>
  Date:   Tue Mar 15 14:39:42 2022 -0300
    Commit 1 com arquivos README.md e palindromo.py

O output desse comando mostra um hash identificador e mostra que o branch ativo é HEAD -> main, o autor e data e o texto atribuído ao commit.

Após ter feito um commit podemos voltar para a sitação anterior usando git reset. Existem três modos de ser fazer um reset.

# desfaz o commit sem modificar nenhum dos arquivos comitados
$ git reset --soft <numero de hash>

# desfaz o commit sem modificar arquivos comitados, mas desfazendo os adds
$ git reset --mixed <numero de hash>

# desfaz o commit e apaga tudo o que foi modificado após o commit prévio
$ git reset --hard <numero de hash>

Fazendo o reset --soft retornamos o projeto para um estado logo anterior ao commit. Desta forma podemos fazer os consertos necessários e voltar a dar commit. reset --hard deve ser usado com cuidado pois significa a perda de todas as alterações feitas após o último commit (principalmente quando se trabalha em grupo). O <numero de hash> é o código exibido em git log (48c3f1f8fae05f87bd79f58b2d4f0cca42d8f8d5 no último caso). Apenas os primeiros 7 dígitos podem ser usados (48c3f1f). Após o reset o estado ativo (indicado por HEAD -> main) será aquele indicado por esse número.

Retomando o projeto: Mais tarde, para retomar o trabalho no projeto local, basta voltar no mesmo diretório de trabalho (que contém a pasta oculta .git. Nesse diretório podemos continuar a edição dos arquivos existentes, inserindo novos arquivos, modificando ou apagando os existentes.

Agora editamos o arquivo README.md e inserimos o arquivo indesejado.py (com qualquer conteúdo) e o adicionamos à área de stage.

# volte para o diretório de trabalho    
$ cd ~/Projetos/PyTexto
# conteúdo do arquivos listado abaixo
$ geany README.md
# insere novo arquivo indesejado.py
$ geany indesejado.py

# para adicionar todos os arquivos de uma vez
$ git add -A

Use o seguinte conteúdo para README.md:

# arquivo README.md
# Projeto de teste e aprendizado do Git
Consiste em testes de funcionamento do Git
Inserimos um arquivo a ser removido

Se, mais tarde, verificamos que um arquivo qualquer <arquivo.ext> não deveria fazer parte do projeto podemos removê-lo de duas formas:

# rm --cached é usado para remover o arquivo de stage sem apagá-lo do diretório de trabalho
git rm --cached <arquivo.ext>

# rm -f para remover o arquivo de stage e forçar seu apagamento do diretório de trabalho
git rm -f <arquivo.ext>

# se o arquivo não está na pasta default
$ git rm --cached diretorio/<arquivo.ext>

# para remover todos os arquivos na pasta
$ git rm --cached diretorio/*

Para efeito de teste vamos remover o arquivo indesejado.py. Depois vamos restaurá-lo, criando outro arquivo com o mesmo nome, adicioná-lo e fazer um commit.

# removemos o arquivo
$ git rm -f indesejado.py

# gravamos novo arquivo, add e commit
$ echo "qualquer coisa" > indesejado.py
$ git add indesejado.py
$ git commit -m "3 commit, inserindo indesejado.py"

# para ver o estado do repositório commitado
$ git ls-tree -r main
  100644 blob 26bb6383445abf68be689e9724de464d1908bbcc   README.md
  100644 blob 6f16acbbc80d444145a09d897a93591cd806d3f8   indesejado.py
  100644 blob 1904a1ec5c3900cccbc80ae3b94e3debd34afb42   palindromo.py

# para remover do repertório (e do sistema de arquivos)   
$ git rm indesejado.py
  rm 'indesejado.py'

echo "qualquer coisa" > indesejado.py é uma linha de comando do bash que permite gravar um arquivo com o conteúdo da string.

Podemos navegar para qualquer commit, que terá o estado do projeto feito naquele momento. Vamos verificar isso. O comando cat do linux lista o conteúdo do arquivo. Use o comando equivalente para seu SO ou abra o arquivo com o editor. Para ver os commits feitos, juntamente com um código que os identificam, usamos git log ou git log --oneline (uma versão com output simplificado).

$ cat README.md
  # Projeto de teste e aprendizado do Git
  Consiste em testes de funcionamento do Git

# para listar todos os commits feitos
$ git log --oneline
48c3f1f (HEAD -> main) 3. insere indesejado.py
0073fc0 3. insere indesejado.py
6ce467f Commit 1 com arquivos README.md e palindromo.py

# podemos alternar entre diferentes commits. Vamos voltar para o 1º
$ git checkout 6ce467f
  Note: switching to '6ce467f'.
  HEAD is now at 6ce467f Commit 1 com arquivos README.md e palindromo.py

# nesse estado o arquivo README.md tem o seguinte conteúdo
$ cat README.md
  # Projeto de teste e aprendizado do Git
  Consiste em testes de funcionamento do Git  

Essa operação mostra como é importante inserir texto explicativos de cada situação quando fazemos o commit. O procedimento acima mostra que, voltando para um commit anterior o estado do projeto, incluindo todos os arquivos nele existentes, volta para o que foi gravado naquele commit.

Visualizando alterações

Uma funcionalidade importante do Git consiste na possibilidade de verificar quais alterações foram feitas em arquivos. Para ver isso vamos alterar o arquivo README.md de seguinte forma:

arquivo original arquivo modificado
# Projeto de teste e aprendizado do Git
Consiste em testes de funcionamento do Git
# Projeto de teste e aprendizado do Git

Linha inserida para testar o diff

Para ver as alterações desde o último estado gravado usamos git diff.

$ git diff
  diff --git a/README.md b/README.md
  index 26bb638..85818ef 100644
  --- a/README.md
  +++ b/README.md
  @@ -1,2 +1,3 @@
  # Projeto de teste e aprendizado do Git
  -Consiste em testes de funcionamento do Git
  +
  +Linha inserida para testar o diff

Os sinais significam: + linha inserida; linha removida.
Esse output indica que o arquivo README.md foi alterado: a 1ª linha foi mantida; a 2ª foi apagada (sinal ) e uma linha em branco inserida; uma 3ª linha foi inserida, (sinal +).
Se todas as linhas fossem removidas e uma nova linha inserida teríamos o output de diff:

$ git diff
-# Projeto de teste e aprendizado do Git
-Consiste em testes de funcionamento do Git
+# Novo linha inserida

Se mais de um arquivo foi modificado, todas as alterações aparecem em git diff. Para ver apenas o nome dos arquivos alterados, o que é útil quando houve muitas alterações, usamos a chave –name-only.

$ git diff --name-only
  README.md

Podemos também ver alterações de apenas um arquivo com git diff nome_arquivo.ext. Se, após examinar as modificações, desistimos de alterar um dos arquivos, voltamos atrás apenas na edição desse arquivo.

$ git checkout HEAD README.md

O README.md voltará para o estado anterior, usado na comparação por diff. HEAD é um atalho para o branch atual. Agora verificamos que git diff não exibirá nenhuma mensagem.

$ git checkout HEAD README.md
  Updated 1 path from 384f1f8
$ git diff

Conta do GitHub


GitHub é um serviço de hospedagem online para repositórios Git. Ele permite a sincronização entre repositórios na máquina local e no site remoto. Com esse repositório remoto você tem uma cópia atualizada de todas as suas versões do projeto.

Abra uma conta no GitHub. Dentro da página existem manuais e ajudas para o uso do site. Depois crie um novo repositório (veja figura), dando o nome desse repositório. Escolha o nível de acesso em Descrição -> acessibilidade (Público ou Privado). Quaisquer alterações feitas em um repositório ficam em suspenso até que uma ordem de commit seja emitida, o que faz com que o estado atual do projeto seja armazenado no repositório.

É sempre recomendado inserir um arquivo README junto com seu projeto. Ele serve para descrever o projeto ou adicionar instruções de instalação e seu conteúdo é exibido na primeira página do repositório. Assim como no Git local, toda alteração de arquivo de projeto só se torna parte do repositório após um commit (o que no GitHub é feito com um botão!)

Como veremos, sempre podemos sincronizar as alterações de um repositório local com o repositório do GitHub.

Clonagens

Quando nossas alterações locais estão terminadas podemos clonar nosso projeto para o GitHub. Isso significa que replicamos todo o repositório local para o GitHub. Para isso usamos git push:

O nome do repositório local é o nome de nosso projeto, PyTexto. Entre no GitHub e crie remotamente um repositório com o mesmo nome. (Isso é feito clicando no botão New na sua página inicial.) Quando isso é feito o GitHub fornece um endereço para https, no meu caso https://github.com/gssilva57/PyTexto.git. Agora os repos podem ser conectados:

# conecta repos
$ git remote add origin https://github.com/gssilva57/PyTexto.git

# abra o branch principal de seu repo
$ git branch -M main

# push repo local para o remoto
$ git push -u origin main


Dessa forma o repositório local para o projeto PyTexto estará espelhado no GitHub.

Se estamos na pasta de projeto onde um repositório Git foi definido, todas as alterações serão sincronizadas com o GitHub. Para ver essas alterações atualizamos o navegador aberto no GitHub e podemos ver nele, dentro desse projeto, os arquivos palindromo.py e README.md.

Se você fizer alterações remotas no GitHub de qualquer parte do projeto dê um commit no site. Em sua máquina local vá até o diretório do projeto e digite, na linha de comando:

# para fazer o download das alterações do github para o projeto local
$ git pull

Agora seu projeto local estará novament sincronizado com o remoto e exibirá as alterações feitas lá.

Sincronização com o GitHub

Clonar um repositório do GitHub significa fazer uma cópia do repositório para seu computador local com um download ( git pull). A clonagem faz uma cópia completa de todos os dados do repositório, incluindo todos os commits e branches que o GitHub possui naquele momento. Você pode clonar um projeto para o mesmo repositório junto com outros desenvolvedores para realizar a mesclagem (merge) e fazer correção de conflitos.

Repo Local com o GitHub

Se você tem conteúdo próprio no GitHub você pode reproduzir em sua máquina local o repositório remoto. Esse é um processo chamado clone. Para isso precisamos obter a URL da página do repositório do GitHub. Depois navegamos até o diretório onde queremos esse repositório duplicado e usamos:

$ git clone url_do_projeto_no_github
# o seguintes informações são pedidas
Username for 'https://github.com': seu_user_namer
Password for 'https://gssilva57@github.com':  token de acesso

Atualmente o GitHub não permite que essa operação seja feita apenas com a senha de acesso ao site. A melhor forma de autenticar o comando de linha é habilitando a autenticação em dois passos no GitHub e criando um token de acesso. Todas as informações para isso estão disponíveis no GitHub.

Clonando repositórios de terceiros


Você também pode clonar o repositório de outra pessoa que o tenha tornado público. Dessa forma você para contribuir com um projeto ou simplesmente usar o código de outro desenvolvedor (desde que ele assim o autorize). No GitHub existem livros, vídeos, áudios para treinamento de desenvolvedores, bancos de dados sobre temas variados, blocos de código para diversas finalidades, projetos de jogos, de aplicativos open-source abertos para a participação de devenvolvedores que desejam colaborar, e muito mais.

Você pode fazer uma busca no próprio GitHub ou procurar por sugestões com um mecanismo de busca. Uma lista de projetos interessantes pode ser vista no site Hackernoon.com, página: githubs top 100 most valuable repositories.

Clonando o Microsoft VSCode

Como um exemplo, vamos clonar o Microsoft VSCode. Na página citada acima, do Hackernoon.com encontramos a URL: https://github.com/Microsoft/vscode. Na página do projeto encontramos uma tabela com arquivos e pastas do projeto. Acima da tabela temos o botão code, como na figura, com as alternativas para se baixar todo o repositório. Uma delas consite em usar a própria url acima, como o comando git clone URL:

# criamos uma pasta para abrigar o repo
$ mkdir ~/Projetos/vscode
# navegamos até essa pasta
$ cd ~/Projetos/vscode

# executamos o comando git de clonagem
$ git clone https://github.com/Microsoft/vscode

Agora, se verificarmos o conteúdo da pasta, veremos que o projeto do vscode está lá. Nesse caso está incluído um arquivo README.md com instruções de uso do repositório e instalação do aplicativo.

O VSCode (ou Visual Studio Code) é um bom editor de código feito pela Microsoft para Windows, Linux e macOS. Ele pode ser usado com várias linguagens, inclusive o Python. VSCode é leve e inclui recursos para depuração, realce de sintaxe, conclusão de código inteligente, snippets, refatoração de código e Git incorporado. Usuários podem alterar tema, atalhos de teclado e instalar extensões que adicionam funcionalidades adicionais.

Gerenciando Conflitos

Quando mais de um desenvolvedor altera o projeto em locais diferentes as alterações são mescladas sem nenhuma mensagem de conflito. Um conflito de versões ocorre quando dois desenvolvedores alteram as mesmas linhas de código no mesmo arquivo. Nesse caso o Git lança uma mensagem de conflito de mesclagem e passa para o desenvolvedor a responsabilidade de resolver a situação.

Uma forma de visualizar a situação é a seguinte: em sua máquina local edite o arquivo palindromo.py alterando apenas a linha com o return:

# arquivo palindromo.py
...
    return ("Texto alterado no palíndromo")

Salve o arquivo e faça o commit. Agora no site do GitHub abra o mesmo arquivo e faça uma alteração diferente, na mesma linha:

...
return (f"A palavra {palavra} {p} é um palíndromo")

Faça o commit para que a alteração entre no repositório. Na prática essa seria uma alteração feita por um colega desenvolvedor. Temos agora alterações feitas localmente e no GitHub.

Agora, na máquina local, vamos fazer o push do arquivo modificado para o GitHub. Um mensagem de erro é exibida e o arquivo onde existe o conflito é listado.

# Um atalho (shortcut) para git add e git commit pode ser usado:
$ git pull

# em seu computador local, no terminal, uma mensagem de erro é emitida
  CONFLICT (content): Merge conflict in palindromo.py
  Automatic merge failed; fix conflicts and then commit the result

O arquivo palindromo.py em seu computador foi alterado para mostrar onde foi o conflito. Ele agora tem o seguinte conteúdo:

# arquivo palindromo.py
# palindromo (testa se a palavra é um palíndromo)
def palindromo(palavra):
    palavra= palavra.upper()
    p = ("" if palavra == palavra[::-1] else "não")
<<<<<<< HEAD
    return (f"{palavra} {p} é um palíndromo")
=======
    return ("Texto alterado no palíndromo")
>>>>>>> a986 ... (um código longo)

Como vemos as duas alterações foram exibidas, a primeira feita na máquina local, a segunda no GitHub. Para resolver o conflito apague as mensagens de erro, mantendo apenas a linha (ou linhas) considerada correta:

# exibindo só a linha alterada de palindromo.py
    return (f"{palavra} {p} é um palíndromo")

Em seguida refazemos as etapas add e commit, enviando arquivo palindromo.py atualizado para o GitHub:

# podemos usar atalho para git add e  git commit
$ git commit –am “commit resolvendo conflito em merge”

O arquivo correto, editado à mão pelo desenvolvedor, estará em ambos os repositórios, local e remoto.

Ramificações (ou branching )

É possível ramificar o código de um projeto com o Git de forma que o desenvolvedor possa trabalhar com um “ramo” sem alterar o código-fonte do projeto original. Isso é útil no caso de uma tentativa de variação do projeto que pode ser, mais tarde, definitivamente incorporada ou não. Se o novo ramo for bem sucedido ele pode ser mesclado com o original ou mesmo substituí-lo completamente. Um novo branch permite a alteração daquele estado sem alterar o estado original de onde foi ramificado. Quando um novo branch é criado ele é composto exatamente dos mesmos arquivos e commits de onde foi copiado.

Suponha, por exemplo, que o projeto esteja em uma fase que agrada ao seu cliente e você entrega a ele uma versão funcional. Pode ocorrer mas tarde que ele deseje acrescentar funcionalidades ao projeto. Nesse ponto será interessante criar novo branch ou ramificação. Uma ramificação pode ser criada à partir de qualquer estágio de commit.

Ramificando um projeto

À partir do branch main podemos criar uma ramificação com git checkout (ou git switch). Um novo branch é criado e ativado, com conteúdo idêntico ao do daquele usado como base. O comando git branch pode ser usado para verificar todas as ramificações criadas até agora.

# para ramificar o branch ativo
$ git checkout -b novo_branch
  Switched to a new branch 'novo_branch'

$ git branch
  main
* novo_branch

checkout
O parâmetro -b faz com que, além de criar um novo branch, ele também será selecionado. O prefixo * mostra que estamos agora com novo_branch ativado. O comando alternativo git switch para a troca de branchs é novo e está em fase experimental. Se estivermos com o branch main ativado podemos usar switch:

$ git branch
* main
  novo_branch
$ git switch novo_branch
  Switched to branch 'novo_branch'

Para alterar o nome de um branch ative aquele cujo nome você quer mudar. Depois use git branch -m nome_novo caso queira trocar o nome do branch.

$ git checkout nome_antigo
$ git branch -m nome_novo
$ git status
  On branch nome_novo

Também é possível renomear qualquer branch se você estiver com o branch raiz ativado (no nosso caso branch main) sem ativá-lo antes.

# vá para o branch raiz
$ git checkout main

# use parâmetro -m para renomear qualquer branch
git branch -m nome_antigo nome_novo

Verificando o significado das ramificações

Na mesma pasta de trabalho, edite o arquivo README.md.

# Projeto de teste e aprendizado do Git
Consiste em testes de funcionamento do Git
## Esse linha foi inserida no novo_branch

Use git add e git commit para inserir a nova versão no repositório.

$ git add palindromo.py
$ git commit -m "versão destruída do código"

O código deve ser atualizado, para esse branch. Depois podemos voltar para o branch main:

$ git checkout main
Switched to branch 'main'

# verifique a situação dos branches
$ git branch
* main
  novo_branch

# verifique o conteúdo de README.md
$ cat README.md  
  # Projeto de teste e aprendizado do Git
  Consiste em testes de funcionamento do Git

O branch main está novamente selecionado. Abrindo novamente o arquivo README.md vemos que ele está agora em sua versão anterior, antes que a última linha tenha sido inserida.

Caso as alterações feitas no branch ‘novo_branch’ sejam aprovadas e você queira incorporá-las no projeto principal, use git merge:

# estando branch main ativo faça
$ git merge novo_branch

Resumo de comandos

Podemos ver uma lista de comandos do Git usando:
$ git --help
Esses são os comandos do Git mais usados:

Para iniciar nova área de trabalho
clone clona um repositório para novo directório
init cria repositório vazio ou reinicializa um existente
Para atuar sobre modificações atuais
add adiciona um arquivo na área de preparação (staging)
mv move ou renomeia um arquivo, diretória ou symlink
restore restaura árvore de arquivos de trabalho
rm remove arquivos da árvore de trabalho
Para examinar histórico e estado
bisect use busca binária para encontrar o commit que introduziu um erro
diff exibe alterações entre commits e na árvore de trabalho
grep imprime linhas que satisfazem um padrão
log exibe logs de commits
show exibe diversos tipos de objetos
status exibe o status da árvore de trabalho
Para acrescentar, marcar e alterar seu histórico
branch lista, cria ou apaga ramificações (branches)
commit grava alterações no repositório
merge faz a junção de dois ou mais históricos de desenvolvimento
rebase reaplica commits sobre outra base
reset reset HEAD atual para um estado especificado
switch alterna entre ramos (branches)
tag cria, lista, apaga ou verifica um objeto tag assinado com GPG
Atividades de colaboração
fetch download objetos e referências de outro repositório
pull download de outro repositório e integra com branch local
push atualiza referências remotas com objetos associados

Bibliografia

Livros

  • Ahmad, Jawwad & Belanger, Chris: Advanced Git, The Raywenderlich Tutorail Team, 2021.
  • Chacon, Scott e Straub, Ben: Pro Git, Second Edition, Apress, disponível em git-scm.
  • Santacroce, Ferdinando: Git Essentials, Packt, Mumbai, 2017.
  • Umali, Rick: Learn Git in a Month of Lunches, Manning, Nova Iorque, 2015.

Sites

todos visualizados em março de 2022.

Flask, parte 2


Templates do Jinja2


Um aplicativo web (ou outro qualquer) deve ser escrito de forma clara, para facilitar sua expansão e manutenção. Uma das formas usadas pelo Flask para implementar esse estratégia é a de colocar código python e html separados. Os templates, como vimos, são modelos ou estruturas básicas que podem ser preenchidas dinamicamente, de acordo com as requisições. Esse é o chamado de modelo de separação entre lógica de negócio e lógica de exibição (business and presentation logic). Templates são tratados por um dos módulos que compõem o Flask: o módulo Jinja2.

Um exemplo básico de template para a exibição de um artigo poderia ser o seguinte:

# template.html
<h1>{{ titulo }} </h1>
<p>{{ autor }}, {{ data }} </p>
<p>{{ texto_do_artigo }} </p>
<p>{{ pe_de_pagina }} </p>

Os campos {{ variavel }} são chamados de localizadores (placeholders) para os valores que serão passados pelas funções view. Em muitos casos as informações usadas para popular essas variáveis são lidas em um banco de dados.

Já vimos o exemplo:

@app.route(“/frutas/<nome_da_fruta>”)
def frutas(nome_da_fruta):
return render_template(“frutas.html”, nome_da_fruta = nome_da_fruta)

onde /frutas/<nome_da_fruta>, fornece o valor da varíavel passada para o parâmetro da função (em vermelho). Dentro do corpo da função a variável de mesmo nome (em verde) recebe esse valor. Esses nomes não precisam ser os mesmo, embora esse seja uma prática comum entre programadores do python.

O método render_template() é parte do Jinja2 para integrar templates e lógica do aplicativo.

Filtros


Variáveis e objetos do python podem ser integrados nos templates de algumas formas. Por meio do módulo Jinja temos diversos filtros para manipular campos em templates. Já vimos como inserir uma variável em um template. Um exemplo de filtro é title(), que torna a string no formado de título, com a primeira letra de cada palavra em maísculo.

# suponha que temos a variável titulo = "a casa da mãe joana"
# essa string pode ser exibida dentro de uma tag <h1>
<h1> {{ titulo }} </h1>
↳ a casa da mãe joana

# para maísculas na primeira letra de cada palavra
<h1> {{ titulo | title() }} </h1>
↳ A Casa Da Mãe Joana

Uma descrição dos filtros para texto está na tabela abaixo.

Filtro Descrição
capitalize Converte 1º caracter em maiúsculo, os demais em minúsculo
lower Converte todos os caracteres minúsculo
upper Converte todos os caracteres maiúsculo
title Converte 1º caracter de cada palavra em maiúsculo
trim Remove espaços em branco no início e no fim
safe Renderiza o valor sem aplicar escape (inclui tags)
striptags Remove todas as tags HTML do valor

safe: O filtro safe informa ao Flask que a tag html pode ser renderizada com segurança. Mais exemplos abaixo.
striptags: remove as tags <tag> e </tag> e retorna o texto puro.

Exemplos de Filtros em Strings

Suponha que temos uma variável de nome titulo. Nos templates ela pode ser exibida diretamente, como uma string, ou passando por algum dos vários filtros. Nos quadros seguintes os comentários são iniciados por # enquanto outputs são identificados pelo sinal .

# suponha que a variável titulo2 não está definida
# default fornece um valor default (se titulo2 não está definido).
<h1> {{titulo2 | default ("Título Não Encontrado")}} </h1>
↳ Título Não Encontrado

# torna maiúscula a primeira letra
<h1> {{"mercado" | capitalize()}} </h1>
↳ Mercado

# em linha anterior ao uso podemos definir um valor
# capitalize torna maiúscula a 1ª letra de cada palavra
{% set titulo2 = "um título para a página" %}
<h1> {{ titulo2 | capitalize()}} </h1>
↳ Um título para a página

# title() torna maiúscula a 1ª letra de cada palavra
<h1> {{titulo2 | title()}} </h1>
↳ Um Título Para A Página

# substituir um trecho em uma string
{{ "Bom dia galera!" | replace("Bom dia", "Boa noite") }}
↳ Boa noite galera!

# inverter a ordem dos elementos
{{ "Olá galera!" | reverse() }}
↳ !arelag álO

Conversores

Por default os valores passados em uma url e capturados como valores do python são strings. Alguns conversores podem transformar essas strings em caminhos (que usam barras / ), inteiros ou decimais.

@app.route("/usuario/<int:id>")
def exibir_id(id):
    # esta função recebe id como um inteiro e o exibe
    return f"O id digitado é {id}"

@app.route("/path/")
def exibir_caminho(caminho):
    # recebe e retorna o caminho passado
    return f"Caminho {caminho}"

Os seguintes conversores estão disponívies:

string (default) qualquer string sem barras / ou \
int converte em inteiros positivos
float converte em números decimais
path strings contendo barras de caminho
uuid strings UUID†

† Uma string UUID (Universally Unique IDentifier), também chamada de GUID (Globally Unique IDentifier) é um número de 128-bits usado na troca de informações em computação.

Valores numéricos podem ser convertidos entre inteiros e decimais, e um valor default ser fornecido.

# números inteiros podem ser convertidos em decimais, ou decimais em inteiros
{{ 10 | float() }}
↳ 10.0 ou 0.0      # 0.0 se a conversão não for possível
{{ 10.0 | int() }}
↳ 10

# um valor default, em caso de erro
{{ "qualquer" | float (default = "Erro: texto não pode ser convertido em decimal") }}
↳ Erro: texto não pode ser convertido em decimal

Manipulação de Listas

Diversas operações são disponíveis em listas.

# join: junta elementos de uma lista
{{ [1, 2, 3] | join() }}
↳ 123
{{ ["Um", "Dois", "Tres"] | join() }}
↳ UmDoisTres

# inserindo um separador
{{ [1, 2, 3] | join ("|") }}
↳ 1|2|3
{{ ["Um", "Dois", "Tres"] | join("-") }}
↳ Um-Dois-Tres

# o filtro list() retorna uma lista
{{ "Guilherme" | list()}}
↳ ["G","u","i","l","h","e","r","m","e"]

# random() seleciona um item aleatorio da lista
{{ ["Mercúrio", "Venus", "Terra"] | random() }}
↳ Venus

{% set pe_pagina = ["citacao 1", "citacao 2", "citacao 3", "citacao 4", "citacao 5"] %}
{{ pe_pagina | random() }}
↳ citacao 4

# replace (visto acima para strings) também pode ser usado em listas
{% set lista = ["Nada", "a", "dizer"] %}
{{ lista | replace ("Nada", "Tudo") }}
↳ ["Tudo", "a", "dizer"]

# o filtro reverse() também pode inverter uma lista
# mas seu resultado é um objeto iterador
{% set lista = ["unidade", "dezena", "centena"] %}
{{ list | reverse() }}
↳ <list_reverseiterator object at 0x7fc0b6262518>

# para usar o objeto lista sem usar iterações temos que usar o método list()
{{ list | reverse() | list() }}
↳ ["centena", "dezena", "unidade"]

O filtro random() pode ser útil para exibir um artigo aleatório do site na homepage, para escolher uma imagem ou um pé de página, etc.

Outros exemplos de manipulação de listas incluem o uso de first(), last(), uso de índices e de laços para percorrer toda a lista.

# first() é 1º elemento da lista, last() é o último elemento
{% set nomes = ["João", "Pedro", "da", "Silva"]  %}
<p> Nome: {{ nomes | first() }} </p>
<p> Segundo Nome: {{ nomes [1] }} </p>
<p> Sobrenome: {{ nomes | last() }} </p>
↳ Nome: João
↳ Segundo Nome: Pedro
↳ Sobrenome: Silva

# o tamanho de uma lista é retornado com {{ lista | length }}
# laços for são usados para percorrer os elementos
{% set comentarios = ["Comenta 1", "Comenta 2", "Comenta 3", "Comenta 4"]%}
<p>Temos ({{comentarios | length}}): comentários</p>
{% for comentario in comentarios %}
    <p> {{ comentario }} </p>
{% endfor%}
# resulta em
↳ Temos 4: comentários
↳ Comenta 1
↳ Comenta 2
↳ Comenta 3
↳ Comenta 4

O filtro safe

O filtro safe serve para passar para o interpretador do Flask a informação de que as tags html devem ser renderizadas. Sem ele a string "<texto>" é exibida literalmente, inclusive com os delimitadores "<>".

Os códigos &lt; (<) e &gt; (>) são entidades html, descritas nesse site.

Por motivo de segurança o Jinja2 remove as tags html. Por exemplo: uma variável com valor "<li> TEXTO </li>" será renderizada como "&lt;li&gt; TEXTO &lt;/li&gt;" por extenso e sem provocar a renderização do navegador. Com o filtro safe o TEXTO é exibido como um ítem de lista.

# exibição literal de uma string
{{ "<b>Texto a exibir!</b>" }}
↳ <b>Texto a exibir!</b>

# para forçar a renderização da tag <b> (negrito)
{{ "<b>Texto a exibir!</b>" | safe }}
↳ Texto a exibir!

# define uma lista
{% set lista = ["<li>Um elefante</li>", "<li>Dois elefantes</li>", "<li>Três elefantes</li>"] %}
<ul>
{% for item in list %}
    {{ item | safe }}
{% endfor %}
</ul>
# será renderizado como
↳
⏺ Um elefante
⏺ Dois elefantes
⏺ Três elefantes

# alternativamente
{% set lista = ["Um elefante", "Dois elefantes", "Três elefantes"] %}
<ul>
{% for item in list %}
    <li> {{ item }} </li>
{% endfor %}
</ul>
# será renderizado da mesma forma.
# Nesse caso não existem tags na lista e safe é desnecessário.

Observação importante: Qualquer input digitado por usuários deve passar pelo filtro safe para evitar que alguma instrução danosa seja processada pelo navegador.

Laços e bifurcações


Vimos que um template recebe variáveis do python e pode processá-las com código. Por exemplo, modificamos o template frutas.html da seguinte forma:

# frutas.html
<body>
    {% if nome_da_fruta == None %}
        <p>Você não escolheu uma fruta!</p>
    {% elif nome_da_fruta == "laranja" %}
        <p>Você escolheu laranja, a melhor fruta!</p>
    {% else %}
        <p>Você escolheu a fruta: {{ nome_da_fruta }}</p>
    {% endif %}
</body>

No código de meu_site.py modificamos a função frutas para que por default ela receba None (caso nada seja escrito após o nome do diretório /frutas/:

# meu_site.py (apenas trecho)
@app.route("/frutas/")
@app.route("/frutas/<nome_da_fruta>")
def frutas(nome_da_fruta=None):
    return render_template("frutas.html", nome_da_fruta=nome_da_fruta)

Agora temos as respostas:

url resposta no navegador
http://127.0.0.1:5000/frutas/ Você não escolheu uma fruta!
http://127.0.0.1:5000/frutas/laranja Você escolheu laranja, a melhor fruta!
http://127.0.0.1:5000/frutas/goiaba Você escolheu a fruta: goiaba

No template, os trechos entre chaves não são parte do html e sim do Python, gerenciado pelo Flask. Dessa forma podemos integrar as páginas da web com as inúmeras bibliotecas do Python.

Quatro tipos de marcações estão disponíveis para para inserção do código nos templates.

sintaxe usadas para
{% ... %} linhas de instruções
{{ ... }} expressões
{# ... #} comentários
# ... ## instruções inline

Variáveis

Variáveis a serem usadas nos templates podem ser de qualquer tipo. Por exemplo:

{% set nomes = ["João", "Pedro", "da", "Silva"]  %}
<p>O segundo nome da lista: {{ nomes[1] }}.</p>
↳ O segundo nome da lista: Pedro.

{% set id = 3 %}
<p>Quem? {{ nomes[id] }}!</p>
↳  Quem: Silva!

# uso de um dicionário
{% set dicionario = {"Nome":"Paul"; "Sobrenome":"Dirac"; "Profissão":"Físico"}  %}
<p>Estes são os dados do: {{ dicionario["Nome"] }}.</p>
{% for chave, valor in dicionario.items() %}
    <p>{{ chave }} : {{ valor }}</p>
{% end for %}
↳ Estes são os dados do: Paul.
↳ Nome: Paul
↳ Sobrenome: Dirac
↳ Profissão :Físico

# no caso geral, se um objeto é passado para o template e tem um método, podemos usar:
<p>Obtendo um valor com um método de objeto disponível: {{ objeto.metodo() }}.</p>

Incluindo trechos com include

Se uma parte do template é repetida várias vezes ela pode ser colocada à parte, em arquivo separado, e incluída na template principal. Por ex., se temos um pé de página que aparece em diversas de nossas páginas ele pode ser gravado à parte.

# arquivo pe_de_pagina.html
<div class="pe_pagina">
<p>Esté é o meu pé de página</p>
</div>

Esse código assume que existe a definição de uma classe css chamada pe_pagina.
Em todos os arquivos que devem exibir o pé de página inserimos:

# todo o texto da página
# pé de página
{% include 'pe_de_pagina.html' %}

Macros

Macros são formas de implementar o princípio DRY (Don’t Repeat Yourself ), uma prática de desenvolvimento de software que visa reduzir a repetição do linhas semelhantes ou com mesma função no código. Ele torna o código mais legível e de fácil manutenção, uma vez que alterações devem ser feitas em um único bloco. Templates, macros, inclusão de conteúdo externo (como em include()) e heranças de modelos são todos instrumentos utilizados para isso.

Outra forma de gerar código reutilizável é através da criação de macros, um recurso similar às funções usuais do Python. Macros podem ser gravadas em arquivos separados e importadas dentro de todos os templates que fazem uso delas, facilitando a modularização do código.

Uma macro pode executar tarefas simples como simplesmente montar as linhas da lista. Suponha que temos uma lista de linhas e queremos montar uma lista não ordenada em html:

 {% macro montar_lista(linha) %}
     <li>{{ linha }}</li>
 {% endmacro %}
 <ul>
 {% for linha in linhas %}
     {{ montar_lista(linha) }}
 {% endfor %}
 </ul>

Outra possibilidade importante para a modularização consiste em gravar um arquivo macros.html que contém a macro vermelho(texto, marcas). Ele retorna linhas de uma lista coloridas de vermelho se texto == marcas, de azul caso contrário.

# arquivo macros.html
{% macro vermelho(texto, marcar) %}
{% if texto == marcar %}
    <li style="color:red;">{{ texto }}</li>
{% else %}
    <li style="color:blue;">{{ texto }}</li>
{% endif %}
{% endmacro %}

O arquivo mostrar_frutas.html importa a arquivo anterior, com a sua macro, e faz uso dela para exibir a lista ordenada.

# arquivo mostrar_frutas.html
{% from "macros.html" import vermelho %}
{% set frutas = ["Abacate", "Abacaxi", "Laranja", "Uva"]  %}
{% set selecionado = "Abacaxi"  %}
<ol>
 {% for fruta in frutas %}
     {{ vermelho(fruta, selecionado) }}
 {% endfor %}
</ol>

O resultado no navegador é o seguinte:

Assim como é válido no Python, podemos fazer a importação de forma alternativa (mostrando só linhas diferentes):

# arquivo mostrar_frutas.html
{% import "macros.html" as macros %}
{% for fruta in frutas %}
    {{ macros.vermelho(fruta, selecionado) }}
{% endfor %}

Herança de Templates

Similar à herança de classes no modelo POO do python, podemos criar um template base e derivar dele outros templates que herdam a sua estrutura. Os templates base definem blocos que podem ser sobrescritos nos templates filhos. Um template base pode ter a seguinte estrutura:

# arquivo base.html    
<html>
<head>
 {% block head %}
 <title> Artigo {% block title %}{% endblock %} </title>
 {% endblock %}
</head>
<body>
 {% block body %}
 {% endblock %}
 {% block final %}
 <p>Site construído com Python-Flask!</p>
 {% endblock %} 
</body>
</html>

A instrução {% block nome_do_bloco %}{% endblock %} pode ser substituída por conteúdo nos templates filhos (ou derivados). Para herdar desse template usamos extends e redefinimos os blocos da base:

# arquivo derivado.html    
{% extends "base.html" %}
{% block title %}Nome do Artigo{% endblock %}
{% block head %}
<style>
# estilos css ficam aqui
</style>
{% endblock %}
{% block body %}
<h1>Nome do Artigo</h1>
<p>Texto do artigo</p>
{% endblock %}
{% block final %}
 {{ super() }}
{% endblock %}

O bloco final, {% block final %}{{ super() }}{% endblock %}, usa super() para simplesmente importar o conteúdo do arquivo base. Se a base e o derivado contém texto o conteúdo da base é sobreposto.

Solicitações e Respostas do Servidor (Request, Response)

† O que é uma thread?

Ao receber uma solicitação de um cliente o Flask responde passando para as funções de visualização (view functions) os objetos que serão usados para a construção da página web. Um exemplo é o objeto request, que contém a solicitação HTTP enviada pelo cliente. Temos que nos lembrar que o aplicativo pode receber um grande volume de solicitações em múltiplas threads. Para evitar que todas as funções de visualização recebam essas informações como parâmetros, o que pode tornar o código complexo e gerar conflitos o Flask usa contextos para que esses objetos fiquem temporariamente acessíveis dentro desses contextos.

Para os exemplos que se seguem vamos trabalhar usando o python no terminal. Para isso ativamos o ambiente virtual e o próprio python:

$ cd ~/caminho_para_o_projeto
$ source ./bin/activate
# o prompt muda para indicar ativação de venv
$ (venv) $ python
Python 3.9.10 (main, Jan 17 2022, 00:00:00)

Graças aos contextos, funções de visualização como a seguinte podem ser escritas:

from flask import request
@app.route('/')
def index():
    navegador = request.headers.get('User-Agent')
    return f'<p>Verificamos que seu navegador é o {navegador} </p>'

Que retorna no navegador (no meu caso):

Contextos: Note que request não foi passado explicitamente para index(), agindo como se fosse uma variável global. Isso é obtido pelo Flask com os contextos ou ambientes reservados. Dois ambientes são usados: o contexto de aplicativo e o contexto de requisição.

As seguintes variáveis existem nesses contextos:

nome da variável Contexto Descrição
current_app Aplicativo A instância do aplicativo ativo.
g Aplicativo Objeto usado pelo aplicativo para armazenamento de dados durante uma requisição. Resetada a cada requisição.
request Requisição Objeto que encapsula o conteúdo da requisição HTTP enviada pelo cliente.
session Requisição Representa a sessão do usuário, um dicionário que o aplicativo usa para armazenar valores entre requisições.

Esses contextos só estão disponíveis quando o aplicativo recebe uma requisição (por meio de uma url digitada no navegador). O Flask ativa o contexto de aplicativo disponibilizando current_app e g, e o contexto de requisição disponibilizando request e session, para a thread, e em seguida os remove. Um exemplo desse comportamento pode ser visto no código, executado dentro do python:

from meu_site import app
from flask import current_app
current_app.name                     # um mensagem de erro é exibida
                                     # pois o contexto não está ativo
RuntimeError: working outside of application context

app_contexto = app.app_context()
app_contexto.push()                 # o flask ativa o contexto
current_app.name                    # o nome do app é impresso
'meu_site'
app_contexto.pop()

Flask usa os métodos objeto.push() e objeto.pop() para iniciar e terminar um contexto. A variável current_app.name só existe enquanto o contexto está ativado.

Preparando uma resposta

Para responder a uma solicitação o Flask armazena um mapa que associa URLs (e suas partes) à função resposta que deve ser executada. Esse mapa está armazendo em app.url_map.

from meu_site import app
app.url_map
Map([<Rule '/contatos/' (HEAD, OPTIONS, GET) -> contatos>,
 <Rule '/frutas/' (HEAD, OPTIONS, GET) -> frutas>,
 <Rule '/' (HEAD, OPTIONS, GET) -> index>,
 <Rule '/frutas/<nome_da_fruta>' (HEAD, OPTIONS, GET) -> frutas>,
 <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>])

A lista mostra o mapeamento das funções view que criamos e mais um, denominado /static, acrescentado automaticamente para acessar arquivos estáticos como arquivos de estilo (cascading style sheets, *.css) e imagens. Os elementos (HEAD, OPTIONS, GET) são passados dentro da URL. Os dois primeiros são gerenciados internamento pelo Flask.

Objeto request

O objeto request, armazenado na variável request, contém toda a informação passada na requisição pela URL. Os atributos e métodos mais comuns desse objeto são listados a seguir.

Atributo/Método Descrição
form Dicionário com todos os campos de form submetidos na requisição.
args Dicionário com todos os argumentos passados na string de pesquisa da URL.
values Dicionário com valores combinados de form e args.
cookies Dicionário com todos os cookies incluídos na requisição.
headers Dicionário com todos os cabeçalhos HTTP incluídos na requisição.
files Dicionário com todos os arquivos de upload incluídos na requisição.
get_data() Retorna dados em buffer na requisição.
get_json() Retorna dicionário com dados JSON incluído na requisição.
blueprint Nome do blueprint que está processando a requisição.
endpoint Nome do endpoint processando a requisição. Flask usa a função view como nome do endpoint para um caminho.
method Método da requisição HTTP, (GET ou POST).
scheme Esquema da URL (http or https).
is_secure() Retorna True se a requisição veio de conexão segura (HTTPS).
host O host definido na requisição, incluindo o número da porta se fornecido pelo cliente.
path Parte da URL que define o caminho.
query_string Parte da URL que define a string de pesquisa (query), como um valor binary (raw).
full_path Parte da URL que define caminho e pesquisa (query).
url Requisição completa da URL fornecida pelo cliente.
base_url O mesmo que url, sem a parte de pesquisa (query).
remote_addr Endereço de IP do cliente.
environ Dicionário com o ambiente de WSGI da requisição.
(†) Blueprints serão vistos mais tarde.

Hooks de Solicitação (request hooks)

Com o Flask podemos registrar funções que devem ser chamadas antes ou depois de uma solicitação. Essas funções podem ser usadas para executar tarefas úteis, tais como autenticar um usuário, abrir e fechar a conexão com um banco de dados, etc. Quatro ganchos (hooks) são disponibilizados:

before_first_request Registra função para execução antes da primeira requisição. Útil para tarefas de inicialização do servidor.
before_request Registra função a ser executada antes de cada requisição.
after_request Registra função a ser executada após cada requisição, caso não ocorram exceções não tratadas.
teardown_request Registra função a ser executada após cada requisição, mesmo que ocorram exceções não tratadas.

Um exemplo de uso desses hooks seria o de usar before_request para coletar dados que serão usados ao longo do ciclo de vida do aplicativo para um usuário e os armazenar na variável g para uso posterior.

Como vimos, uma requisição resulta em uma resposta por meio de uma das funções view enviada ao cliente. Ela pode ser uma página simples de html construída com auxílio dos templates ou algo mais complexo. Junto com a resposta, de acordo com o protocolo HTTP, é enviado um código de status (status code) indicando o sucesso da solicitação. Por default o Flask retorna o status_code = 200 para solicitação bem sucedida. Podemos usar uma função view para retornar outro código.

@app.route('/')
def index():
    return 'Ocorreu um erro!', 400
42 é a resposta do Guia do Mochileiro das Galaxias!

A função acima retorna uma tupla com uma string e um inteiro. É possível e útil fazer com que essas funções retornem um objeto response no lugar da tupla.

from flask import make_response
@app.route('/')
def index():
   response = make_response('Resposta Final sobre o Universo!')
   response.set_cookie('resposta', '42') return response

Dessa forma response passa a conter um cookie (que é gerenciado pelo navegador que o recebe). A tabela seguinte mostra métodos e atributos mais usados no objeto response.

Métodos e atributos do objeto response

Atributo/Método Descrição
status_code Código numérico de status do HTTP
headers Objeto tipo dicionário com todos os cabeçalhos a serem eviados em response
set_cookie() Acrescenta um cookie no objeto response
delete_cookie() Remove um cookie
content_length Comprimento do corpo da response
content_type Tipo de midia do corpo da response
set_data() Define o corpo da response como string ou bytes
get_data() Retorna o corpo da response


Dois tipos de resposta especiais para casos que ocorrem com frequência são fornecidos como funções auxiliares. Uma delas é uma forma de lidar com erros, eviando um código por meio do método abort(). No exemplo abaixo usamos essa função para enviar uma mensagem 404 (página não encontrada) caso a id de um usuário não seja encontrada com load_user(id).

from flask import abort
@app.route('/usuario/<id>')
def usuario(id):
    usuario = load_user(id)
    if not usuario:
        abort(404)
    return f'<div class="usuario">Bem vindo {usuario.name}</div>'

Esse exemplo supõe que exista um template para usuario e que ele carrega instruções css para a classe usuario.

Se load_user(id) retornar None é executada abort(404) que retorna a mensagem de erro. Uma exceção é levantada e a função usuario() é abandonada antes de atingir a instrução return.

O outro tipo de resposta é o redirect que não retorna uma página mas sim uma nova URL redirecionando o navegador. redirect retorna status_code = 302 e a nova URL (dada no código).

from flask import redirect
@app.route('/')
def index():
    return redirect('https://phylos.com/programacao')

Bibliografia

Veja a bibliografia na Parte 1.


Flask, Parte 3 está em preparação!

Flask, parte 1


Web Frameworks

Para a construção de páginas e aplicativos web é essencial algum conhecimento de html e css, não cobertos nesse artigo.

Um aplicativo web web application ou simplesmente web app) é um aplicativo executado através de um servidor web, diferente de um aplicativo executado na máquina local, e geralmente rodados e visualizados por meio de um browser ou navegador. Eles podem ser um conjunto de páginas de texto dinâmicas contendo, por exemplo, pesquisas em uma biblioteca, um gerenciador de arquivos na nuvem, um gerenciador de contas bancárias ou emails, um servidor de músicas ou filmes, etc. Com frequência esses aplicativos estão conectados a um banco de dados, podendo fazer neles consultas e modificações.

Um framework para a web é um conjunto de softwares destinados a oferecer suporte ao desenvolvimento de aplicativos na Web. Eles buscam automatizar a construção dos web apps e seu gerenciamento durante o funcionamento, após sua publicação na web. Alguns frameworks web incluem bibliotecas para acesso a banco de dados, templates para a construção dinâmica das páginas e auxílio à reutilização de código. Eles podem facilitar o desenvolvimento de sites dinâmicos ou, em alguns casos, de sites estáticos.

Framework Flask

O Flask é um micro-framework em Python usado para desenvolvimento e gerenciamento de web apps. Ele é considerado micro porque possui poucas dependências para seu funcionamento e pode ser usado com uma estrutura inicial bem básica, voltada para aplicações simples. Apenas duas bibliotecas são instaladas junto com o Flask. Ele não contém, por ex., um gerenciador de banco de dados ou um servidor de email. No entanto esses serviços podem ser acrescentados para ampliar as funcionalidades do aplicativo.

Em particular, o Flask não inclui uma camada de abstração com banco de dados. Em vez disso é possível instalar extensões, escolhendo o banco de dados específico que se quer usar. Essas extensões também podem auxiliar a validação de formulário, manipulação de upload, tecnologias de autenticação aberta, entre outras.

Flask foi desenvolvido por Armin Ronacher e lançado em 2010. Algumas vantagens citadas em seu uso são: (a) projetos escrito com a Flask são simples (comparados àqueles gerados por frameworks maiores, como o Django) e tendem a ser mais rápidos. (b) Ele é ágil e modular: o desenvolvedor se concentra apenas nos aspectos utilizados de seu aplicativo, podendo ampliar sob demanda. (c) Os projetos são pequenos mas robustos. (d) Existe uma vasta comunidade de desenvolvedores contribuindo com seu desenvolvimento, apresentando bibliotecas que ampliam sua funcionalidade.

Criando um aplicativo básico

Nessa primeira parte vamos criar um aplicativo bem básico mas funcional. Depois entraremos em outros detalhes do Flask.

Para criar um projeto usando o Flask (ou, na verdade, outro projeto qualquer) é aconselhável criar antes um ambiente virtual para que os pacotes instalados não conflituem com outros em projetos diferentes. A criação e manutenção de ambientes virtuais está descrita na página Ambientes Virtuais, PIP e Conda. Alguns IDEs, como o Pycharm realizam automaticamente esse processo. No meu caso ele ficará abrigado em ~/Projetos/flask/venv. Para simplificar denotarei esse ambiente simplesmente pelo nome flask, que é também o nome da pasta que abriga esse projeto.

Nesse ambiente instalamos o Flask com pip install flask. Uma estrutura básica de ambiente já estará montada após esse passo. Em seguida criamos um arquivo do python, de nome meu_site.py.

# meu_site.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def homepage():
    return "Esta é a minha homepage"

if __name__ == "__main__":
    app.run()

# é exibido no console do python
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Esse arquivo não deve se chamar flask.py para evitar conflito de nomes.

A variável __name__, passada para o construtor do Flask contém o nome do módulo principal e é usada para determinar a localização do aplicativo no app. Outros diretórios e arquivos, como pastas de templates, arquivo de estilo e imagens, serão localizadas à partir dessa raiz.

Aplicativos do Flask incluem um servidor de desenvolvimento que pode ser iniciado com o comando run. Esse comando busca pelo nome do aplicativo na variável de ambiente FLASK_APP. Se queremos rodar o aplicativo meu_site.py executamos na linha de comando:

# Linux ou macOS
(venv) $ export FLASK_APP=meu_site.py
(venv) $ flask run
 * Serving Flask app "hello"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

# Microsoft Windows
(venv) $ set FLASK_APP=meu_site.py
(venv) $ flask run
 * Serving Flask app "hello"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 
 # alternativamente se pode iniciar o servidor com
 app.run()

No Pycharm, ou outros IDES, você pode executar diretamente esse código.

Da biblioteca flask importamos apenas (por enquanto) a classe Flask. Uma instância da classe é criada com app = Flask(__name__) onde a variável __name__ contém o nome do projeto. A linha @app.route("/") é um decorador que informa que a função seguinte será rodada na raiz / do site. Quando esse arquivo .py é executado dentro de uma IDE ou usando python meu_site.py, na linha de comando, é exibido no console várias mensagens, entre elas a url http://127.0.0.1:5000/, que pode ser clicada ou copiada para a linha de endereço do navegador. Isso resulta na exibição, dentro do navegador, da página:


Clientes e Servidores: O navegador age como o cliente que envia ao servidor uma solicitação, através de uma URL digitada na barra de endereços. O servidor da web transforma essa solicitação em ações a serem realizadas do lado do servidor e retorna uma página com conteúdo de texto e multimídia, renderizados pelo navegador. O Flask fica do lado do servidor, construindo a resposta. Entre outras coisas ele possui um mapeamento entre as URLs e as funções route() que serão executadas no código *.py.

O endereço e a porta 127.0.0.1:5000 são padrões para o Flask. app.run() cria um servidor que atende à requisição HTTP do navegador, exibindo a página html. Qualquer texto retornado pela função homepage() é renderizado no formato html e exibido no navegador. Por exemplo, se fizermos as alterações, colocando o texto entre tags h1:

@app.route("/")
def homepage():
    return "<h1>Esta é a minha homepage</h1>"

if __name__ == "__main__":
    app.run(debug=True)

o texto agora é renderizado como um título de nível 1:

o mesmo texto será exibido mas agora com formatação de título, a tag h1. Todas as demais tags podem ser utilizadas. O parâmetro debug=True faz com que alterações no código sejam imediatamente repassadas para as requisições ao servidor, sem a necessidade de rodar todo o projeto novamente. Com isso basta recarregar a página do navegador para que alterações sejam exibidas, clicando no ícone de atualização ou pressionando F5. No mode debug os módulos dois módulos chamados reloader e debugger estão ativados por default. Com o debugger ativado as mensagens de erro são direcionadas para a página exibida. O mode debug nunca deve ser ativado em um servidor em produção pois isso fragiliza a segurança do site.

Também podemos ativar o módulo no código que executa o aplicativo:

(venv) $ export FLASK_APP=meu_site.py
(venv) $ export FLASK_DEBUG=1
(venv) $ flask run

O decorador @app.route("/") registra a função homepage() junto com a página raiz do site. Outras páginas vão executar outras funções. Por exemplo, uma página de contatos pode ser inserida por meio da inserção de nova função no código. Nesse caso criaremos a função contatos().

# meu_site.py

from flask import Flask
app = Flask(__name__)

@app.route("/")
def homepage():
    return "<h1>Esta é a minha homepage</h1>"

@app.route("/contatos")
def contatos():
    txt = (
    "<h1>Página de Contatos</h1>"
    "<ul>"
    "<li>Contato 1</li>"
    "<li>Contato 2</li>"
    "</ul>"
    )
    return txt

if __name__ == "__main__":
    app.run(debug=True)

Usamos acima a concatenação de string com parênteses: (str1 str2 ... strn).
Agora, além da homepage temos a página de contatos em 127.0.0.1:5000/contatos, com a seguinte aparência.

A funções contatos() e homegage() são chamadas funções de visualização (view functions).

Html e templates: Notamos agora que o código em meu_site.py contém sintaxe misturada de Python e html e pode ficar bem complexo em uma página real exibida na web. Para evitar isso o Flask permite a criação de templates. Fazemos isso da seguinte forma: no diretório raiz onde está o projeto (o mesmo onde foi gravado meu_site.py) criamos o diretório template (o nome default do Flask). Dentro dele colocamos nossos templates. Por exemplo, criamos os arquivos homepage.html,

# homepage.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Homepage: teste Flask</title>
</head>
<body>
    <h1>Este é o título da Homepage</h1>
    <p>Com os devidos parágrafos...</p>
</body>
</html>

e contatos.html:

# contatos.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Contatos</title>
</head>
<body>
    <h1>Página de Contatos</h1>
    <ul>
    <li>Contato 1</li>
    <li>Contato 2</li>
    </ul>
    </body>
</html>

Vários IDEs podem auxiliar na criação desses arquivos html, fornecendo um esqueleto básico a ser preenchido pelo programador.

Além disso modificamos nosso código python para usar a renderização dos templates, importando render_template.

# meu_site.py
from flask import Flask, render_template
app = Flask(__name__)

@app.route("/")
def homepage():
    return render_template("homepage.html")

@app.route("/contatos")
def contatos():
    return render_template("contatos.html")

if __name__ == "__main__":
    app.run(debug=True)

Quando esse código é executado temos a referência ao link que, se aberto, mostra as páginas criadas: digitando 127.0.0.1:5000 abrimos nossa homepage:

Mas, se digitarmos 127.0.0.1:5000/contatos a outra página é exibida:

Uma página pode receber parâmetros do código em python. Por exemplo, digamos que queremos exibir uma página para cada produto existente em uma loja virtual que vende frutas. Nesse caso acrescentamos no código de meu_site.py:

@app.route("/frutas/<nome_da_fruta>")
def frutas(nome_da_fruta):
    return render_template("frutas.html")

Para receber esse parâmetro temos que gravar a página frutas.html na pasta templates, com um conteúdo que receba essa variável.

# frutas.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Frutas disponíveis</title>
</head>
<body>
    <h1>Frutas</h1>
    <p>Você escolheu a fruta: {{nome_da_fruta}}</p>
</body>
</html>

Se for digitado no campo de endereços do navegador, ou passado por meio de um link na tag <a href="http://127.0.0.1:5000/frutas/laranja">Laranja</a> a parte do endereço <nome_da_fruta> = laranja é passado como valor de parâmetro na função frutas("laranja") que é disponibilizado dentro do código html como {{nome_da_fruta}}.

Resumindo: @app.route("/frutas/<nome_da_fruta>") envia uma string na variável nome_da_fruta para a função frutas que, por sua vez repassa ao código html. Dentro do html a variável fica disponível como {{nome_da_fruta}} (dentro de uma dupla chave).

Por exemplo, se digitamos na barra de endereços do navegador http://127.0.0.1:5000/frutas/laranja teremos a exibição de

Essa técnica pode ser usada, por ex., para criar páginas para diversos usuários usando um único template usuario.html.

@app.route('/usuario/<nome>')
def usuario(nome):
    return render_template("usuario.html")
# ou
@app.route('/usuario/<int:id>')
    return render_template("usuario.html")

A parte do código <int:id> é um filtro que transforma a entrada digitada em inteiro, quando possível e será melhor explicada adiante.

Formatando com CSS

O texto dentro de uma página html (HyperText Markup Language) pode ser formatado de algumas formas diferentes, usando css (Cascading Style Sheets). Quando se trata do uso de um framework a forma preferida consiste em apontar no cabeçalho para um arquivo externo css. No Flask isso é feito da seguinte forma: um arquivo css é gravado na pasta static da pasta do projeto. Digamos que gravamos o arquivo /static/styles.css com um conteúdo mínimo, apenas para demonstração, tornando vermelhas as letras do título e azuis as letras dos parágrafos:

# arquivo /static/styles.css
h1 { color:red; }
p { color:blue; }

No cabeçalho das páginas html, dentro da tag <head> colocamos um link para o arquivo de estilos:

# homepage.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="static/styles.css">
    <title>Homepage: teste Flask</title>
</head>

Agora, ao acessar a homepage veremos:


Com todas essas alterações o projeto tem agora a estrutura de pastas mostrada. Na figura à esquerda todas as pastas, inclusive aquelas criadas pelo gerenciador de ambientes virtuais são mostradas. No meu caso elas foram criadas automaticamente pelo IDE Pycharm, mas podem ser criadas pelo programador sem dificuldade. Na figura à direita são mostradas apenas as pastas criadas pela programador diretamente. Um projeto com esse formato roda perfeitamente, apesar de não contar com as vantagens do ambiente virtual (veja artigo).

Outras estruturas de código podem ser inseridas nos templates, como veremos.

Opções de comando de linha

Quando se roda o flask diretamente no terminal podemos ver uma mensagem de ajuda com (venv) $ flask --help, verificar os caminhos definidos no app ou entrar em uma shell interativa.

# Exibir ajuda do flask    
(venv) $ flask --help
  Usage: flask [OPTIONS] COMMAND [ARGS]...
  
    A general utility script for Flask applications.
  
    Provides commands from Flask, extensions, and the application. Loads the
    application defined in the FLASK_APP environment variable, or from a wsgi.py
    file. Setting the FLASK_ENV environment variable to 'development' will
    enable debug mode.
  
      $ export FLASK_APP=hello.py
      $ export FLASK_ENV=development
      $ flask run
  
  Options:
    --version  Show the flask version
    --help     Show this message and exit.
  
  Commands:
    routes  Show the routes for the app.
    run     Run a development server.
    shell   Run a shell in the app context.
  
# exibe os caminhos ativos no aplicativo
  (venv) $ flask routes
  Endpoint  Methods  Rule
  --------  -------  -----------------------
  contatos  GET      /contatos/
  frutas    GET      /frutas/
  frutas    GET      /frutas/
  homepage  GET      /
  static    GET      /static/
  
# entra em uma shell interativa    
  (venv) $ flask shell

A shell do flask inicia uma sessão python no contexto do atual aplicativo onde podemos executar testes ou tarefas de manutenção. O comando flask run admite vários parâmetros:

(venv) $ flask run --help
  Usage: flask run [OPTIONS]
  
    Run a local development server.
  
    This server is for development purposes only. It does not provide the
    stability, security, or performance of production WSGI servers.
  
    The reloader and debugger are enabled by default if FLASK_ENV=development or
    FLASK_DEBUG=1.
  
  Options:
    -h, --host TEXT                 The interface to bind to.
    -p, --port INTEGER              The port to bind to.
    --cert PATH                     Specify a certificate file to use HTTPS.
    --key FILE                      The key file to use when specifying a
                                    certificate.
    --reload / --no-reload          Enable or disable the reloader. By default
                                    the reloader is active if debug is enabled.
    --debugger / --no-debugger      Enable or disable the debugger. By default
                                    the debugger is active if debug is enabled.
    --eager-loading / --lazy-loading
                                    Enable or disable eager loading. By default
                                    eager loading is enabled if the reloader is
                                    disabled.
    --with-threads / --without-threads
                                    Enable or disable multithreading.
    --extra-files PATH              Extra files that trigger a reload on change.
                                    Multiple paths are separated by ':'.
    --help                          Show this message and exit.  

O argumento --host informa ao servido qual é o ambiente web que pode acessar nosso servidor de desenvolvimento. Por default o servidor de desenvovimento do Flask só aceita chamadas do computador local, em localhost. Mas é possível configurá-lo para receber chamadas da rede local ou de ambientes mais amplos. Por exemplo, como o código

(venv) $ flask run --host 0.0.0.0
 * Serving Flask app "hello"
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

todos os computadores conectados pelo mesmo ip terão acesso ao aplicativo.

Implantação de um aplicativo Flask

O processo de implantação (ou deploy) de um aplicativo consiste nas etapas necessários para colocá-lo acessível para seus usuários. No caso de um aplicativo web a implantação significa estabelecer um servidor ou usar servidores já disponíveis, que os usuários possam acessar, e colocar seu aplicativo como um de seus serviços.

O desenvolvimento do aplicativo se dá em um ambiente de desenvolvimento onde podem existir condições próprias para o debug e nem todas as medidas de segurança estão implementadas. Depois ele passa para a etapa de uso, no ambiente de produção. Uma conta no Heroku pode ser criada, e um site com poucos acessos pode ser mantido sem custos. Se o site for escalonado e crescer a conta deve ser atualizada e paga. A lista abaixo contém links para o Heroku e outros provedores.

Bibliografia

Livros sobre Flask

  • Aggarwal, Shalabh: Flask Framework Cookbook, 2.Ed., Packt, Birmingham-Mumbai, 2019.
  • Ashley, David: Foundation Dynamic Web Pages with Python Create Dynamic Web Pages with Django and Flask, Apress, 2020.
  • Gaspar, D.;StoufferHaider, J.: Mastering Flask Web Development, 2.Ed., Packt, Birmingham-Mumbai, 2018.
  • Grinberg, Miguel: The Flask Mega-Tutorial, Edição do autor, 2020.
  • Grinberg, Miguel: Flask Web Development, Developing Web Applications with Python, O’Reilly, Sebastopol, 2018.
  • Haider, Rehan: Web API Development With Python, CloudBytes, 2020.
  • Maia, Italo: Building Web Applications with Flask, 2.Ed., Packt, Birmingham-Mumbai, 2015.
  • Relan, Kunal: Building REST APIs with Flask, 2.Ed., Apress, 2019.

Referências na Web

Sobre HTML e CSS

todos acessados em fevereiro de 2022.


Ambientes Virtuais, PIP e Conda


Ambiente virtual

Um ambiente virtual é uma área isolada de seu computador onde pacotes específicos são instalados para o uso, sem o problema de conflitarem com outras versões instaladas. Com isso cada projeto pode ter suas próprias dependências, diferentes das possíveis dependências em outros projetos. Até mesmo versões diferentes do python podem ser usadas. Um projeto do Python pode usar diversos pacotes e módulos, sendo que alguns deles podem não estar na biblioteca padrão. Vários ambientes podem ser criados e gerenciados separadamente, sem limite na quantidade, pois são apenas diretórios contendo scripts. Esses ambientes podem ser criados usando as ferramentas no comando de linha venv, virtualenv ou pyenv. Nos concentraremos aqui na ferramenta venv.

Criando ambientes virtuais

Na construção de um aplicativo uma versão específica de uma biblioteca, ou até do próprio Python, pode ser necessária. Para isso a linguagem oferece a possibilidade de se criar ambientes virtuais: um ambiente independente armazenado em uma árvore de diretórios própria contendo a instalação do Python e pacotes em versão específica.

O módulo venv é usado para criar e gerenciar ambientes virtuais. Ele seleciona e organiza a versão do Python e dos módulos usados no projeto.

Para criar um ambiente virtual você deve decidir em que diretório ele deve ser abrigado. Depois execute o módulo venv como um script, no prompt de comando, informando o caminho do diretório. Uma boa prática é criar uma pasta oculta .venv na sua pasta raiz ou pasta de projetos para abrigar todos os seus ambientes virtuais. No exemplo criaremos a pasta ~/Projetos/.venv/aprendendo:

$ python3 -m venv ~/Projetos/.venv/aprendendo
Figura 1: estrutura de arquivos com venv.

Dentro da pasta aprendendo é criada uma estrutura de pastas contendo uma cópia do interpretador e alguns arquivos de configuração, mostrada na figura 1.

Essas pastas tem o conteúdo:

  • bin: arquivos que interagem com o ambiente virtual
  • include: cabeçalhos C que compilam os pacotes Python
  • lib: uma cópia da versão do Python junto com uma pasta site-packages onde cada dependência está instalada

Parte desses arquivos são links simbólicos (ou symlinks) que apontam para as corretas versões das ferramentas do python.

Na pasta bin ficam os scripts de ativação usados para definir o ambiente para uso. Depois de criado o ambiente pode ser ativado por meio do comando activate.

No Windows activate.bat é um arquivo de lote que indica a posição do executável que aciona o ambiente. No Linux source é um comando interno da shell que lê e executa o arquivo indicado.
# No windows:
$ ~\Projetos\.venv\aprendendo\Scripts\activate.bat

# No Linux (bash shell)
$ source ~/Projetos/.venv/aprendendo/bin/activate

Ao ser carregado um novo ambiente o prompt de comando muda para indicar qual ambiente está em uso. Nesse prompt carregamos o Python (no meu caso 3.8.8 Anaconda), importamos a biblioteca sys e verificamos os caminhos em uso.

(aprendendo) $ python
Python 3.8.8 (default, Apr 13 2021, 19:58:26) 
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
   ['', '/home/usr/.anaconda3/lib/python38.zip', '/home/usr/.anaconda3/lib/python3.8', '/home/usr/.anaconda3/lib/python3.8/lib-dynload', '/home/usr/Projetos/.venv/aprendendo/lib/python3.8/site-packages']
>>> sys.prefix
   '/home/usr/Projetos/.venv/aprendendo'

Esse ambiente está isolado do meio externo. Por exemplo, no meu caso o pandas foi instalado junto com o Anaconda. No entanto ele não pode ser apontado por um código rodando no ambiente (aprendendo).

>>> import pandas
    Traceback (most recent call last):
      File "", line 1, in 
    ModuleNotFoundError: No module named 'pandas'

Para sair do ambiente reservado usamos deactivate no terminal.

(aprendendo) (base) [guilherme@gui ~]$ deactivate
(base) [guilherme@gui ~]$ python
Python 3.8.8 (default, Apr 13 2021, 19:58:26)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas

Como o pandas está instalado globalmente, nenhuma mensagem de erro é gerada e o módulo fica disponível para uso.

A ativação de um ambiente significa a especificação do local onde estão os executáveis e bibliotecas importadas. Para reativar o ambiente nos “deslocamos” até a pasta onde ele está instalado e executamos activate.

$ cd /home/usuario/Projetos/.venv/aprendendo
$ source bin/activate
# o prompt é alterado
(aprendendo) $ python
>>> import sys
>>> sys.prefix
    '/home/guilherme/Projetos/.venv/aprendendo'
Observação Importante: Para instalar env com versão específica do Python

Pode ocorrer que existam mais de uma versão do Python instalada em seu computador. Para criar um ambiente com outra versão devemos executar o script do venv na versão que desejamos para o ambiente virtual. Por exemplo, para um ambiente com python 3.11 (supondo que ele esteja instalado nesse computador) devemos executar:

$ python3.11 -m venv ~/Projetos/.venv/VsCode
$ source ~/Projetos/.venv/VsCode/bin/activate
$ python
>>> Python 3.11.0a7 (main, Apr  7 2022, 00:00:00) [GCC 11.2.1 20220127 (Red Hat 11.2.1-9)] on linux
>>> Type "help", "copyright", "credits" or "license" for more information.

Nesse caso criamos um ambiente virtual no diretório ~/Projetos/.venv/VsCode.

Uma discussão um pouco mais detalhada sobre a instalação de ambientes virtuais e uso do pip para sistemas onde mais de uma versão do python estão instaladas pode ser lida em: Python, pip e venv.

Gerenciando ambientes virtuais com pip

Uma vez dentro do novo ambiente você pode instalar pacotes usando pip que, por default, encontra e instala pacotes do Python Package Index. PIP é o gerenciador padrão de pacotes (ou bibliotecas) do Python, que acessa um reservatório de pacotes publicados no Python Package Index, ou PyPI. Em versões mais recentes ele vem instalado por default. Um pacote é um conjunto de arquivos que executam uma ou várias funções. Eles podem ser importados em um aplicativo para extender a funcionalidade do Python padrão. PIP funciona por meio de comandos de linha, digitados no prompt do sistema operacional.

A sintaxe de venv é descrita abaixo. Aqui usamos a notação [parâmetros opcionais], o pipe | para indicar opções (um ou | outro). Apenas ENV_DIR é obrigatório e posicional.

venv [-h] [–system-site-packages] [–symlinks | –copies] [–clear] [–upgrade]
[–without-pip] [–prompt PROMPT] [–upgrade-deps] ENV_DIR [ENV_DIR …]
Cria o ambiente virtual em um ou mais diretórios especificados.
ENV_DIR Diretório onde criar o ambiente virtual,
-h, –help exibe (o presente) texto de ajuda,
–system-site-packages Dá acesso ao ambiente virtual para a pasta site-packages do sistema.
–symlinks Tenta usar symlinks no lugar de cópias, quando os symlinks não são default na plataforma.
–copies Tenta usar cópias no lugar de symlinks, mesmo que symlinks sejam o default na plataforma.
–clear Apaga o conteúdo do diretório de ambiente, se existe, antes da criação do ambiente.
–upgrade Atualiza o diretório de ambiente, caso o python do ambiente tenha sido atualizado.
–without-pip Interrompe a instalação ou atualização via pip nesse ambiente. (Por default o pip é acionado).
–prompt PROMPT Estabelece um prefixo alternativa para o prompt desse ambiente.
–upgrade-deps Atualiza as dependências do pip setuptools para o última versão disponível em PyPI.

Depois que o ambiente é criado você deve ativá-lo com o script source pasta/do/ambiente/bin/activate.

Instalação de PIP

Você pode verificar a presença do pip (ou conferir a versão) com pip --version, no prompt de comando.

$ pip --version
  pip 22.0.3 from /home/usuario/Projetos/.venv/aprendendo/lib/python3.8/site-packages/pip (python 3.8)    

O output do comando mostra a versão do pip e do python sendo usados no ambiente virtual instalado na pasta /home/usuario/Projetos/.venv/aprendendo.

Alternativamente, é possivel encontrar onde está instalado o pip:

# no Windows    
C:\> where pip3  
# no Linux
$ which pip
~/.anaconda3/bin/pip
$ which pip3
~/.anaconda3/bin/pip3

Caso o pip não esteja instalado, isso pode ser feito de duas formas: ensurepip e get-pip.py.

No prompt do terminal de seu sistema (que representaremos por $, comentários por #) digite:

$ python -m ensurepip
# ou 
$ python -m ensurepip --upgrade

A chave -m garante que pip seja executado como um módulo. Na segunda forma se garante que apenas versões mais novas que atual (se presente) seja instalada. Nenhuma ação será executa se já existe a instalação, ou se está em sua versão mais atual, no segundo caso. pip será instalado globalmente ou no ambiente virtual, se esse estiver ativo.

Outra alternativa é baixar o script get-pip.py e executá-lo com a primeiro linha no código. A segunda linha é uma forma de atualizá-lo.

# instalar pip
$ python get-pip.py
# fazer atualização de pip
$ python -m pip install --upgrade pip

Instalando módulos com o PIP

O uso geral de pip é o seguinte:

# no linux/Mac    
$ python -m pip <argumentos>
# ou
$ pip <argumentos>
# no windows    
$ py -m pip <argumentos>

Para instalar um módulo com o PIP executamos pip com o argumento install e o nome do módulo. Vários módulos podem ser instalados com uma única linha.

$ python -m pip install 
# ou    
$ pip install 
# por exemplo, para instalar o Flask
$ pip install Flask
# vários módulos
$ python -m pip install   ... 

Para controlar qual é a versão a ser instalado usamos:

python -m pip install Modulo             # instalar a última versão
python -m pip install Modulo ==1.0.4     # instalar versão especificada
python -m pip install 'Modulo >=1.0.4'   # especificar a versão mínima

É possível passar para pip uma lista de requisitos para a exata reprodução de um ambiente.

# para gerar o arquivo relativa a um ambiente:
$ python -m pip freeze > requirements.txt
# para reproduzir a instalação:
$ python -m pip install -r requirements.txt    

O arquivo requirements.txt contém uma lista dos argumentos do pip install. Essa lista pode ser gerada com freeze.

pip pode ser usado para instalar pacotes de outros repositórios. Por exemplo, se você deseja instalar o pacote rptree disponível em TestPyPI package index, ou no GitHub.

# no TestPyPI    
python -m pip install -i https://test.pypi.org/simple/ rptree
# no GitHub
python -m pip install git+https://github.com/realpython/rptree

Observação: pode ocorrer que em seu computador o python 3 esteja instalado com o nome python3.

<h2=”idm”>Listando e desinstalando módulos

Módulos instalados podem ser vistos com o argumento list. A lista obtida reflete a instalação do Flask e suas dependências. Uma lista com pacotes desatualizados é obtida com a chave list –outdated.

$ python -m pip list
  Package      Version
  ------------ -------
  click        8.0.3
  Flask        2.0.2
  itsdangerous 2.0.1
  Jinja2       3.0.3
  MarkupSafe   2.0.1
  pip          22.0.3
  setuptools   49.2.1
  Werkzeug     2.0.2    

# listar apenas pacotes desatualizados
$ python -m pip list --outdated
  Package    Version Latest Type
  ---------- ------- ------ -----
  setuptools 49.2.1  60.8.1 wheel

Pode ocorrer que você deseje usar outro pacote e queira remover o antigo de seu computador. A desinstalação é feita com uninstall. pip desinstala pacotes com versões desatualizadas antes de fazer uma atualização para versão mais nova.

Um cuidado deve ser tomado: Quando um pacote é instalado é possível que ele possua dependências que são instaladas juntas com ele. Se você tem muitos pacotes instalados é possível que mais de um use a mesma dependência. Por isso é importante verificar se ele é dependência de outro ou se possui dependências. Para isso usamos python -m pip show <modulo>.

$ python -m pip show Flask
Name: Flask
Version: 2.0.2
Summary: A simple framework for building complex web applications.
Home-page: https://palletsprojects.com/p/flask
Author: Armin Ronacher
Author-email: armin.ronacher@active-4.com
License: BSD-3-Clause
Location: /home/guilherme/.anaconda3/lib/python3.8/site-packages
Requires: itsdangerous, Jinja2, click, Werkzeug
Required-by: 

Vemos no output que Flask possui as dependências (Requires: )itsdangerous, Jinja2, click e Werkzeug. Por outro lado ele não é exigido por nenhum outro modulo (Required-by:) portanto pode ser seguramente desinstalado. Para isso usamos uninstall.

$ python -m pip uninstall Flask

O mesmo procedimento deve ser usado com as dependências, caso você queira apagá-las.

Busca por pacotes

pip pode fazer buscas por um pacote com o comando:

python -m pip search "query"

A pesquisa retorna uma lista de pacotes com uma breve descrição de cada um.
Importante: pip search deixou de funcionar em dezembro de 2020. Um substituto para esse comando é poetry (que deve ser instalado em seu sistema). Por exemplo, uma busca por pacotes com “pandas” no nome: (o output está truncado).

$ poetry search "pandas"
  pandas (1.4.0)
     Powerful data structures for data analysis, time series, and statistics
  pandas3 (0.0.1)
     Boto3 extension to help facilitate data science workflows with S3 and Pandas
  pandas-alchemy (0.0.2)
     SQL based, pandas compatible DataFrame & Series


Poetry é similar ao npm do JavaScript, gerenciando pacotes e auxiliando na criação de distribuições de aplicativos e bibliotecas, além da inserção no PyPI. Outro pacote é o Pipenv que gerencia pacotes e controla ambientes virtuais. Real Python: Pipenv Guide.

Estrutura de um projeto Python

O Python é bastante flexível na questão de estrutura de pastas para um projeto. No entanto algumas sugestões foram dadas para um desenho ótimo para um projeto. Aqui eu sigo as sugestões de Lucas Tonin.
Vamos denominar nosso projeto de meu_projeto. Se estamos usando, como é recomendado, um ambiente virtual podemos criá-lo com python3 -m venv ~/Projetos/.venv/meu_projeto, que já estabelece uma estrutura mínima de pastas. Vamos por hora ignorar as pastas relativas ao ambiente virtual.

Para definir uma terminologia chamamos de projeto python tudo aquilo que estará no diretório base, que em nosso caso é ~/Projetos/.venv/meu_projeto. Todos os arquivos relacionados ao desenvolvimento, teste e arquivos auxiliares ficam nesse diretório. Chamamos de pacote (package) ao conteúdo de um subdiretório dentro do projeto com o mesmo nome. O pacote contém o código-fonte do aplicativo. Ele isola o código fonte de todos os outros arquivos. Depois de pronto a instalaçao do projeto inclui apenas os arquivos contidos nesse diretório, ignorando código fonte e testes.

Arquivo __init__.py: O pacote deve necessariamente conter pelo menos um arquivo como o nome __init__.py. A presença desse informa ao python que esse diretório é um pacote. __init__.py é executado automaticamente quando esse pacote é carregado e deve conter as inicializações para o aplicativo. Duas coisas importantes podem ser aí incluídas: (a) uma variável ROOT_DIR com o caminho absoluto do atual pacote, onde estiver no momento; (b) as configurações de logger, quando existir.

from os.path import dirname, abspath
ROOT_DIR = dirname(abspath(__file__))
# inicialização de logs

Um arquivo de documentação, README.md: geralmente esses arquivos são escritos em MARKDOWN. Ele deve conter uma descrição de seu projeto para outros usuários de seu código, ou para você mesmo no caso de retomar após um tempo esse trabalho, além de instruções de instalações. Um exmplo simples:

# Meu Projeto
Um aplicativo simples para a importação de arquivos *csv* e exportação para banco de dados SQL.
## Instalação
Para instalar execute `pip install /caminho/meu_projeto`

Usando setup.py: o arquivo setup.py contém informações sobre configurações do pacate a ser instalado. No mínimo ele deve conter:

import setuptools
setuptools.setup(name='meu_projeto', packages=['meu_projeto'])

que informa o nome do projeto e do pacote. O parâmetro packages informa ao pip que apenas esse diretório será instalado.

Para projetos maiores e mais complexos, que envolvem muitas dependências, é útil acrescentar um arquivo requirements.txt Este arquivo lista os pacotes necessários para o projeto, que não fazem parte da biblioteca padrão. Com ele o pip pode baixar e instalar automaticamente todas as dependências, se não o encontrar já instalado. Por exemplo, se o projeto depende do numpy (um pacote para computação científica) a arquivo deverá conter pelo menos a linha

numpy==1.18.2

O arquivo setup.py deve ser modificado para que essa informação seja usada.

# setp.py
import setuptools
with open('requirements.txt', 'r') as f:
    install_requires = f.read().splitlines()

setuptools.setup(name='meu_projeto',
                 packages=['meu_projeto'],
                 install_requires=install_requires)


Um arquivo LICENCE, que descreve a licença sob a qual você está distribuindo seu projeto, pode ficar no diretório base, onde pode ser facilmente encontrado. Finalmente, uma pasta separa para os testes usados para testar a correção do código.

O projeto fica portanto com a seguinte estrutura mostrada na figura.

Usando Jupyter Notebook em ambiente virtual

Anaconda é uma plataforma de distribuição de Python e R muito usada para a ciência de dados e aprendizado de máquina. Ele simplifica a instalação de pacotes como pandas, NumPy, SciPy, e pode ser usada com diversas outras linguagens. Conda é o gerenciador padrão de pacotes do Anaconda, multiplataforma e agnóstico à linguagem e que pode ser usado para instalar pacote de terceiros. O Anaconda Navigator, instalado junto com o Anaconda, é uma interface gráfica que permite o gerenciamento de pacotes coda, com busca, instalação, atualização e desinstalação, execução dos aplicativos incluidos na Anaconda Cloud ou outro repositório Anaconda local. Todo esse sitema está disponível para Windows, macOS e Linux.

O Anaconda Cloud é um serviço de nuvem que abriga pacotes, notebooks e ambientes Python para variadas situações e casos, incluindo pacotes conda e PyPI públicos e privados. Ele permite o uploud de pacotes de usuário e notebooks, sem a necessidade de um login ou conta na nuvem.

Jupyter Notebook é uma ambiente interativo de interface do usuário da Web onde se pode rodar código nas linguagens instaladas, criar documentos de notebook contendo texto (em Markdown), imagens e vídeos. Esses documentos podem ser partilhados e publicados na nuvem, onde podem ser alterados e executados por outros usuários.

O Jupyter Notebook faz um gerenciamento de ambiente próprio. Mas tambem podemos criar um ambiente virtual específico para ele. Isso é bastante útil pois esse ambiente é usualmente dedicado à computação científica e aplicações com grandes volumes de dados, exigindo bibliotecas específicas. Para isso criamos um ambiente virtual, que chamaremos de jupyter. Depois ativamos o ambiente e instalamos o Jupyter Notebook dentro desse ambiente.

# criamos o ambiente virtual    
$ python3 -m venv ~/Projetos/.venv/jupyter_venv
# ativamos esse ambiente
$ cd /home/usuario/Projetos/.venv/jupyter_venv
$ source bin/activate 
# instala o Jupyter Notebook no ambiente
(jupyter) $ ipython kernel install --user --name=jupyter_venv

Para usar o ambiente virtual abra o jupyter e selecione o kernel no menu kernel | change kernel. A opção para o ambiente jupyter_venv deve estar disponível, como mostra a figura, como uma das opções de kernel a usar.

Para desinstalar o ambiente fazemos:

$ jupyter-kernelspec uninstall jupyter_venv

Gerenciador conda


Para quem está trabalhando com a distribuição Python Anaconda (site) é mais interessante usar o gerenciador conda, que além de gerenciar pacotes e suas dependências controle também ambioentes virtuais. Ele pode ser usado com Python, R, Ruby, Lua, Scala, Java, JavaScript, C/ C++, FORTRAN e outras.

Com o conda você pode pesquisar por pacotes, instalar os pacotes desejados ou construir um pacote do usuário com build (conda-build deve ser instalado).

A versão de conda pode ser verificada, e atualizada se necessário. Essa atualização pode incluir atualização de outros pacotes e remoção de pacotes não usados.

# verificar versão    
$ conda --version
  conda 4.10.3
# informações mais detalhadas podem ser obtidas
$ conda info
    active environment : base
    active env location : /home/guilherme/.anaconda3
# -----(outpup truncado)-----

# atualizar versão
$ conda update conda
# se existir versão mais recente
  Proceed ([y]/n)? y

A pesquisa e instalação de pacotes é feita com search e install. A construção (build) de pacotes é feitas com build.

# pesquisar   
$ conda search scipy
# instalação
$ conda install scipy
# o novo pacote deve estar na lista
$ conda list

# construir um pacote
$ conda build meu_projeto

Versões podem ser especificadas, inclusive com o uso de operadores lógicos.

conda install numpy=1.11                  #(instala versão especificada)
conda install numpy==1.11                 #(idem)
conda install "numpy>1.11"                #(versão superior a 1.11)
conda install "numpy=1.11.1|1.11.3"       #(versão 1.11.1 ou 1.11.3)
conda install "numpy>=1.8,<2"          #(versão maior ou igual a 1.8 mas inferior a 2)

A barra | é o operador OR: “pacote=1.1|1.3” significa 1.1 ou 1.3.
A vírgula , é o operador AND: “pacote=1.1,1.3” significa ambos 1.1 e 1.3.
O igual = é o operador fuzzy: “pacote=1.11” pode ser “1.11”, “1.11.1”, , “1.11.8”, etc.
O duplo igual == é o operador exato: “pacote==1.11” pode ser “1.11”, “1.11.0”, , “1.11.0.0”, etc.

Gerenciamento de ambientes com conda

Um novo ambiente, que chamaremos de cientifico pode ser criado, e simultanemanete instalado nele o pacote pandas. Caso um versão do Python diferente da versão default do Anaconda instalado usamos a declaração conda create --name nome_ambiente python=n, onde n é a versão desejada.

# criar um ambiente com um pacote
$ conda create --name cientifico pandas
  Proceed ([y]/n)? y
  
# ativar o ambiente
$ conda activate cientifico

# para verificar a versão do python em uso
$ python --version
  Python 3.8.8

# para criar ambiente coom outra versão do python
conda create --name cientifico python=3.9

Uma lista de todos os ambientes disponíveis pode ser vista com info --envs. As pastas listadas dependem do local onde os ambientes foram criados.

$ conda info --envs
  conda environments:
      base           /home/username/Anaconda3
      cientifico   * /home/username/Anaconda3/envs/cientifico

O ambiente ativo aparece com um asterisco *.

Um canal conda é um local na rede onde pacotes estão armazenados. Por default Conda busca em uma lista de canais e baixa arquivos de Repo Anaconda, onde alguns pacotes podem ser prorietários. Outros repositórios podem ser apontados com o conda, por exemplo o Conda Forge, uma organização parte do GitHub que contém um grande número de pacotes gratuitos. O parâmetro --override-channels é usado para que os canais default (gravados em .condarc) sejam ignorados.

# para apontar para o conda-forge
$ conda install scipy --channel conda-forge

# múltilpos repositórios podem ser apontados
$ conda install scipy --channel conda-forge --channel bioconda
# argumentos que aparecem na frente são pesquisados primeiro

# para pesquisar em repositório local, ignorando os defaults
$ conda search scipy --channel file:/caminho/local-channel --override-channels

Para instalar um pacote presente no conda-forge também é possível fazer:

$ conda config --add channels conda-forge
$ conda config --set channel_priority strict
$ conda install "nome-do-pacote"

Muito mais é possível com o Conda: consulte as instruções em Conda Docs.

Argumentos positionais
comando descrição
clean Remove pacotes e caches não utilizados.
compare Compara pacotes entre ambientes conda.
config Modifica os valores de configuração em .condarc. (feito após a configuração do comando git).
Grava arquivo .condarc do usuário
create Cria um ambiente conda a partir de uma lista de pacotes especificados.
help Exibe uma lista de comandos conda disponíveis e suas ajudas.
info Exibe informações sobre a instalação atual do conda.
init Inicializa o conda para interação do shell. [Experimental]
install Instala uma lista de pacotes em um ambiente conda especificado.
list Lista pacotes vinculados em um ambiente conda.
package Utilitário de pacote conda de baixo nível. (EXPERIMENTAL)
remove Remove uma lista de pacotes de um ambiente conda especificado.
uninst Alias para remove.
run Execute executável em um ambiente conda. [Experimental]
search Pesquisa pacotes e exibe informações associadas.
update Atualiza pacotes conda para a versão compatível mais recente.
upgrade Alias para update.
Argumentos opcionais
comando descrição
-h, –help Mostra ajuda,
-V, –version Mostra versão.

Bibliografia

Uma discussão sobre a instalação de ambientes virtuais e uso do pip para sistemas onde mais de uma versão do python está instalada pode ser lida em: Python, pip e venv.

Gráficos com Bokeh

O que é Bokeh


Bokeh é uma biblioteca de visualização de dados interativa em Python que existe desde 2013. Ela pode ser usada para a plotagem de gráficos em diversos níveis de sofisticação, representando conjuntos simples ou complexos de dados. A biblioteca pode ser usada por usuários com pouca experiência em programação ou programadores experientes com acesso aos seus comandos mais intrincados. Os gráficos do Bokeh podem ser interativos e embutidos em páginas da web.

Algumas definições básicas na terminologia de Bokeh são necessárias:

Application um aplicativo Bokeh é um documento renderizado e executado no navegador.
Glyphs glifos são os blocos de construção do Bokeh como linhas, círculos, retângulos e outras formas,
Server o servidor Bokeh é usado para compartilhar e publicar gráficos e aplicativos interativos para um público de sua escolha
Widgets os widgets do Bokeh são controles tais como menus suspensos, controles deslizantes e outras ferramentas de interface gráfica com o usuário que permitem interatividade

Instalação

Para instalar o Bokeh, se você tem Anaconda ou Miniconda, basta usar o comando: conda install bokeh.
Usando pip a biblioteca pode ser instalada com: pip install bokeh.

Comandos básicos

Dois tipos de saídas podem ser obtidas: o gráfico enviado para um arquivo output_file('arquivo.html') ou embutidos no Jupyter Notebook, output_notebook(). Bokeh possui uma interface similar à do matplotlib, que é denominada bokeh.plotting. A classe principal dessa interface é Figure que contém os métodos para a inclusão de glyphs em um gráfico.

» # importar as classes necessárias
» from bokeh.io import output_notebook, show
» from bokeh.plotting import figure
» output_notebook()
Figura 1
» # dados a plotar » x = [0,1, 0,3] » y = [0,10,90,10] » # instanciar um objeto figure » fig = figure(plot_width=450, plot_height=300) » # desenhar uma linha ligando os pontos dados » fig.line(x,y) » # exibir a figura 1 » show(fig)

A variável fig contém um objeto da classe com largura e altura especificadas, e instrução relativas às ferramentas a serem apresentadas, do lado direito no caso. O comando fig.line(x,y) usa o glyph line (linha) para ligar os pontos dados nas duas listas.

Glyphs

Glyphs são todos os elementos gráficos como linhas, círculos e cruzes marcadores de pontos, etc. Diferentes glyphs podem ter parâmetros ajustáveis diferentes. No exemplo aplicamos uma cor de fundo à figura, largura e altura. fig.circle() recebe os parâmetros posição (x,y), tamanho, que no caso é variável, cada círculo com raio size=y, largura de linha (as circunferências) line_width=5, e cor color=['red', 'blue','green','yellow']. Cada um dos discos tem uma cor diferente.

» # define dados    
» x = [1,2,3,4]
» y = [10,40,90,160]
Figura 2
» # instancia figura com cor de fundo e dimensões dadas » fig = figure(background_fill_color='#aabbff', » plot_width=450, plot_height=300) » fig.circle(x,y, size=y, line_width=5, color=['red', 'blue','green','yellow'], alpha=.5) » # exibir figura 2 » show(fig)

Os seguintes glyphs estão disponíveis:

asterisk() cross() diamond() diamond_cross()
circle() circle_x() circle_cross() triangle()
inverted_triangle() square() square_x() square_cross() x()

Alguns exemplos de uso de glyphs line, circle, cross, asterisk, x estão abaixo. As ordenadas y foram calculadas para formarem uma sequência de parábolas empilhadas, exceto pela reta horizontal amarela larga de fundo.

» # define valores da abscissa. Ordenadas serão calculadas    
» x = np.arange(10)

» plot = figure(plot_width=650, plot_height=300)
» plot.line(x, 100, color='yellow', line_width=140, alpha=.2,)
» plot.circle(x, x**2, size = 20, color='red', alpha=.5, line_width=7)
» plot.cross(x, x**2+50, size = 20, color='blue', alpha=.8, line_width=7)
» plot.asterisk(x, x**2+100, size = 40, color='green', alpha=.8, line_width=7)
» plot.x(x, x**2+150, size = 40, color='black', alpha=.8, line_width=7)
# figura 3
» show(plot)
Figura 3

As propriedades de cada glyph podem ser calculadas e dependentes em qualquer fonte de dados. Na caso abaixo usamos a própria ordenada x para calcular alguns desses parâmetros. A propriedade color=['yellow','blue']*5 garante que os 10 ‘diamantes’ plotados alternem entre as cores amarelo e azul.

» x = np.arange(10)    
» plot = figure(plot_width=650, plot_height=300)
» plot.circle_cross(x, x, size = 5+x, color='#ffaaff', alpha=1, line_width=7+x)
» plot.circle_dot(x, x, size = 30-2*x, color='#66aaff', alpha=.5, line_width=2)
» plot.inverted_triangle(x, x+5, size = 30-2*x, color='red', alpha=.9, line_width=2)
» plot.diamond(x, x+5, size = 30-2*x, color=['yellow','blue']*5, alpha=.8, line_width=2)
» show(plot)
» # figura 4 é plotada

» # outro plot com tamnho e cor variáveis
» x = np.arange(10)
» plot = figure(plot_width=600, plot_height=300)

» for k in range(100):
»     plot.circle(k, (k-50)**2, size = k*2, color=(255*k/100, 200, 255),
»                 fill_color=(2.5*k, 100, 255-2.5*k), alpha=.4, line_width=2)
» show(plot)
» # figura 5 é plotada

Gráficos de Barras (Bar Plots )

Para gráficos de barras a sintaxe é um pouco diferente. As coordenadas x são o ponto central da barra vertical, top é a altura. A largura width= 1 significa nenhum espaçamento entre barras. As cores podem ser uma só ou uma lista, de mesmo tamanho que o número de barras. Para as barras horizontais o comprimento das barras é dado por right e a largura da barra é height.

» x = [8,9,10]
» y = [1,4,2]

» # barras verticais
» plot = figure(plot_width=600, plot_height=300)
» # plot.vbar para traçar barras verticais
» plot.vbar(x,top = y, color = ['blue','red','green'], width= .8, alpha=.5)
» show(plot)     # exibe gráfico 6

» # barras horizontais
» plot = figure(plot_width=600, plot_height=300)
» plot.hbar(x, right = y, color = ['#77aaff','#aa77ff','#ff77aa'], height= .9, alpha=.5)
» show(plot)     # exibe gráfico 7


O desenho da regiões ou patches é feito com plot.patches. As regiões são descritas por meios das coordenadas de suas arestas, dois pares de listas para cada figura. As propriedades fill_color, line_color, line_width, alpha receberam listas de 3 elementos, um para cada figura. Se um valor único for passado ele será válido para todas as figuras.

» # regiões a colorir
» x_coords = [[1,1,3,], [2,2,2.5], [1.5,1.5,4,4]]
» y_coords = [[2,6,4], [3,6,7], [3,6,7,2]]

» plot = figure(plot_width=600, plot_height=300)
» plot.patches(x_coords, y_coords, fill_color = ['#77aaff','#aa77ff','#ff77aa'],
               line_color ='black', alpha=.4)
» show(plot)      # figura 8
Figura 8

Gráficos de Dispersão (Scatter Plots )

Gráficos de dispersão podem ser feitos com qualquer um dos glyphs. No exemplo abaixo a mesma plotagem é feita com círculos e com cruzes de tamanhos diversos, para efeito estético.

» from bokeh.models import Range1d
» plot = figure(plot_width=400, plot_height=250,
»               x_axis_label = 'Coordenada x (abcissa)',
»               y_axis_label = 'Ordenada y', title='Gráfico de dispersão')
» plot.x_range = Range1d(0, 5)
» plot.y_range = Range1d(0, 8)
» fcor = ['red','green','blue','brown','violet']
» x = np.array([1,2,3,4,4])
» y = np.array([5,6,2,2,4])
» plot.circle(x,y, size =x*15, color = '#aa55ff', fill_color=fcor, fill_alpha=.3)
» plot.diamond(x,y, size = x*15, color = 'red', alpha=.5,
»              fill_alpha=.4, fill_color=fcor[::-1])

» show(plot)    # figura 9
Figura 9

Observe que as coordenadas x, y poderiam ser listas. Como são arrays (do numpy) as operações para o cálculo do tamanho são permitidas. As faixas de coordenadas e ordenadas plotadas são controladas por x_range, y_range e estabelecidas por meio da função Range1d(m, n) (importada de bokeh.models). Os parâmetros color e alpha se referem ao traçado do glyph, enquanto fill_color e fill_alpha ao seu preenchimento. Relembrando, fcor[::-1] retorna a lista em ordem reversa.

Dataframes e ColumnDataSource

Usamos, até aqui, listas e arrays como fonte de nossos dados e serem plotados. Também podemos usar dataframes como fontes e o processo não é muito diferente. Se um dataframe tem uma coluna x e outra y plotamos o gráfico x × y simplesmente passando as series como parâmetros para x e y: plot.line(x = df['x'], y = df['y']).

Para montar um gráfico um pouco mais elaborado vamos usar os dados já descritos na seção sobre matplotlib. São dados sobre o número de nascimentos em países do mundo de 1950 até 2020, e a estimativa à partir de 2021. Importamos o arquivo .csv para o dataframe dfBrasil e selecionamos apenas as linhas relativas ao Brasil, até o ano de 2020. Esse dataframe é usado para plotar o gráfico de linhas. Outro dataframe, dfDecada, contendo apenas linhas com anos múltiplos de 10, é usado para plotar círculos. O raio do círculo é proporcional ao número de nascimentos.

» import pandas as pd
» dfNasc = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecionamos apenas linhas sobre o Brasil, até 2020
» dfBrasil = dfNasc[(dfNasc['Entity']=='Brazil') & (dfNasc['Year'] < 2021)]
» dfBrasil = dfBrasil.rename(columns={'Year':'ano', dfBrasil.columns[3]:'nasc'})
» # mantemos apenas colunas 'ano', 'nasc'
» dfBrasil = dfBrasil[['ano', 'nasc']]
» dfBrasil.head(2)
↳          ano         nasc
  4050    1950    2439820.0
  4051    1951    2467186.0

» # criamos outro df, apenas com anos multiplos de 10
» dfDecada = dfBrasil[dfBrasil['ano']%10==0]

» cor = ['salmon','gold','teal','plum','powderblue','coral','wheat','azure']
» plot = figure(plot_width=400, plot_height=250,
»               x_axis_label = 'Ano',
»               y_axis_label = 'Nascimentos (milhões)',
»               title='Número de Nascimentos no Brasil')
» plot.line(x = dfBrasil['ano'], y = dfBrasil['nasc']/1e6, color='black')
» plot.circle(x = dfDecada['ano'], y = dfDecada['nasc']/1e6,
»             size=dfDecada['nasc']/1e5, fill_color = cor,
»             fill_alpha=.5)
» show(plot)    # figura 10
Figura 10

Uma forma útil de fazer a conexão com os dados é o objeto ColumnDataSource. Ela é especialmente útil quando se usa a mesma fonte para diversas plotagens e para vários widgets. ColumnDataSource cria um dicionário onde as chaves podem ter nomes definidos pelo usuário e as valores correspondentes são os dados contidos em colunas do dataframe (ou outra fonte).

Vamos retornar aos dados relativos aos nascimentos nos países do mundo. Dessa vez vamos manter apenas dados sobre o Brasil e a Indonésia (escolhido porque é um país que tem população próxima à brasileira), apenas nos anos de 1950 até 2020. Nessa tabela os países recebem os códigos Code='BRA' e 'IDN', respectivamente.

» dfNasc = pd.read_csv('./dados/number-of-births-per-year.csv')
» # selecionamos as linhas sobre o Brasil e a Indonésia, até 2020
» dfBI = dfNasc[((dfNasc['Code']=='BRA') | (dfNasc['Code']=='IDN')) & (dfNasc['Year'] < 2021)]
» dfBI = dfBI.rename(columns={'Year':'ano', dfBI.columns[3]:'nasc'})

Desses dados criamos um dataframe apenas com dados brasileiros, outro com dados sobre a Indonésia. Para mesclar esses dataframes alteramos as colunas ‘nasc’ respectivamente para ‘BRA’ e ‘IDN’.

» dfB = dfBI[['ano','nasc']][dfBI['Code']=='BRA'].rename(columns={'nasc':'BRA'})
» dfB.head(3)
↳          ano          BRA
  4050    1950    2439820.0
  4051    1951    2467186.0
  4052    1952    2523577.0

» dfI = dfBI[['ano','nasc']][dfBI['Code']=='IDN'].rename(columns={'nasc':'IDN'})
» dfI.head(3)
↳          ano          IDN
  14700    1950    2867664.0
  14701    1951    2939269.0
  14702    1952    3078414.0
Para ler mais sobre a operação do pandas realizada, similar a um INNER JOIN do sql, consulte o artigo Pandas e SQL Comparados, nesse site.

Ambos os dataframes têm 71 linhas. Usamos pandas.merge() para juntar esses dataframes pelo campo ‘ano’, um processo similar ao INNER JOIN do sql. Depois criamos três novas colunas: (1) campo dif, com a diferença por ano entre os números brasileiros e indonésios, (2), difM, a média entre os dois e (3) raio, descrito no comentário † abaixo.

» dfBI = pd.merge(dfB, dfI, on='ano')
» dfBI.head(3)
↳       ano          BRA          IDN
  0    1950    2439820.0    2867664.0
  1    1951    2467186.0    2939269.0
  2    1952    2523577.0    3078414.0

» dfBI['dif'] = dfBI['IDN'] - dfBI['BRA']
» dfBI['difM'] = (dfBI['IDN'] + dfBI['BRA'])*.5
» dfBI['raio'] = dfBI['dif']/33000                   # veja comentário †

» # o dataframe fica assim:
» dfBI
↳          ano           BRA           IDN          dif          difM         raio
    0     1950     2439820.0     2867664.0     427844.0     2653742.0     12.964970
    1     1951     2467186.0     2939269.0     472083.0     2703227.5     14.305545
    2     1952     2523577.0     3078414.0     554837.0     2800995.5     16.813242

() A terceira coluna adicional, raio, é a diferença vezes um fator para que os discos em plot.circle() preencham o espaço entre os nascimentos nos dois países, centrados na média. Essa plotagem aqui tem apenas efeito visual e para demonstrar os parâmetros do plot.

» from bokeh.models import Range1d
» from bokeh.plotting import ColumnDataSource
    
» # cria o objeto ColumnDataSource
» data = ColumnDataSource(dfBI)
» plot = figure(width=900, height=250, x_axis_label = 'Ano', y_axis_label = 'Nascimentos e diferenças',
»               background_fill_color='#cfefff', border_fill_color='#ddeeff',
»               title='Nascimentos no Brasil e Indonésia')

» plot.x_range = Range1d(1950, 2035)
» plot.y_range = Range1d(0, 5.5E6)

» plot.line(x = 'ano', y = 'BRA', source = data, color = 'red', legend_label = "Brasil")
» plot.line(x = 'ano', y = 'IDN', source = data, color = 'green', legend_label = "Indonésia")
» plot.x(x = 'ano', y = 'dif', source = data, color = 'blue', legend_label = "diferença")
» plot.asterisk(x = 'ano', y = 'difM', source = data, color = 'black', legend_label = "média")
» plot.circle(x = 'ano', y = 'difM', source = data, fill_color = 'whitesmoke', alpha=.2, size = 'raio')

» show(plot)    # figura 11
Figura 11

Nesse gráfico introduzimos as legendas para cada plot. O campo difM foi plotado duas vezes, uma com um asterisco, outro com círculos com tamanhos determinados pelo campo raio. As faixas de plotagem, ranges, foram determinados para incluir gráfico e legendas. Cor de fundo para o gráfico e bordas são definidas com background_fill_color e border_fill_color.

Para o próximo gráfico baixamos para a subpasta dados do atual projeto o arquivo owid-covid-data.csv, publicado por Our World in Data com dados diários sobre a vacinação mundial contra o covid, entre 01/01/2020 e 26/09/2021. Deste aproveitamos apenas algumas colunas para plotar gráficos para efeito de demonstração do bokeh.

» # importamos os dados para um dataframe
» dfVacina = pd.read_csv('./dados/owid-covid-data.csv')

» # o dataframe tem 64 colunas e 119454 linhas
» dfVacina.shape      # (119454, 64)

» # podemos ver os nomes das colunas com
» dfVacina.columns    # nomes omitidos aqui

» # usamos apenas as colunas no dicionário
» colunas = {'date':'data',
»            'iso_code':'code',
»            'total_cases':'total',
»            'gdp_per_capita':'pib',
»            'human_development_index':'idh',
»            'life_expectancy':'expVida',
»            'total_deaths_per_million':'mortes',
»            'people_vaccinated_per_hundred':'vacinados'           
»           }
» # renomeamos as colunas
» dfVacina = dfVacina.rename(columns=colunas)

» # uma lista dos novos nomes:
» lst = list(colunas.values())
» # geramos novo df apenas com essas colunas
» df = dfVacina[lst]
» # eliminamos os linhas com NaN
» df = df.fillna(method='bfill')      # veja comentário ‡

» # as três primeiras linhas são
» df.head(3)
↳              dia    code   total         pib      idh   expVida    mortes   vacinados
   0    2020-02-24     AFG     5.0    1803.987    0.511     64.83     0.025         0.0
   1    2020-02-25     AFG     5.0    1803.987    0.511     64.83     0.025         0.0
   2    2020-02-26     AFG     5.0    1803.987    0.511     64.83     0.025         0.0

» # finalmente montamos um dataframe contendo apenas o último dia registrado
» dfUltimo = dfU[dfU['dia']=='2021-09-26']

() O método df.fillna(method='bfill') preenche valores nulos com o valor encontrado na mesma coluna, em linha posterior. (Leia aqui sobre tratamento de dados ausentes).

Lembramos que code identifica o país, total é o número total de casos de infecção por covid, mortes é o número total de mortes, por milhão e vacinados é o número de pessoas vacinadas, por 100 mil.

Podemos, em alguns casos, desejar incluir no gráfico um valor calculado a partir de um ou mais campos da tabela. Por ex., considerando que o campo idh varia entre 0,4 até 0,95, podemos usar esse campo, multiplicado por um fator, como informação do tamanho dos círculos plotados. Para fazer isso poderíamos incluir uma coluna extra com esse valor, como já foi feito em exemplos anteriores. Mas quando usamos o ColumnDataSource temos uma forma mais direta de fazer o mesmo. Podemos passar valores calculados no dicionário de valores que alimenta o ColumnDataSource.

» from bokeh.plotting import ColumnDataSource
» data = ColumnDataSource(data = {
»                        'idh' : dfUltimo['idh'],
»                        'expVida' : dfUltimo['expVida'],
»                        'tamanho': dfUltimo['idh']*20,
»                        'grande': dfUltimo['idh']*40,
»                        'alfa': dfUltimo['idh']*.08})
» plot = figure(width=600, height=300, x_axis_label = 'IDH',
»               y_axis_label = 'Exp. Vida', outline_line_color='black',
»               background_fill_color='#F5F1E3', title='IDH x Expectativa de Vida')

» plot.circle(x = 'idh', y = 'expVida', source = data, color='blue', alpha=.6,
»             fill_color = 'white', fill_alpha=1,  size = 'tamanho')
» plot.circle(x = 'idh', y = 'expVida', source = data, color='black', alpha= .1,
»            fill_color = 'red', fill_alpha='alfa', size = 'grande')

» show(plot)     # figura 12
Figura 12

Os campos do dataframe foram passados como valores em um dicionário cujas chaves são usadas como nome de campos nas plotagens. Os campos 'tamanho': dfUltimo['idh']*20 e 'grande': dfUltimo['idh']*40 são calculados para servir como informação para o tamanho (size ) dos círculos. O segundo círculo plotado tem apenas efeito estético, com um tamanho maior que o primeiro. O campo calculado alfa (uma fração do idh) é usado para regular a transparência dos discos vermelhos maiores.

O uso de ColumnDataSource permite que mais de um dataframe forneça dados para o gráfico. No entanto todas as series envolvidas devem ter o mesmo tamanho. Para ver isso vamos separar os dados sobre o Brasil e os EUA em duas tabelas separadas.

» # separa os dados relativos ao Brasil e os EUA
» dfBU = df[(df['code']=='BRA') | (df['code']=='USA')].copy()    # comentário §

» # para usar as datas no eixo x transformamos o campo 'dia' de string em datetime
» dfBU.loc[:,'dia'] = pd.to_datetime(df.loc[:,'dia'], format='%Y/%m/%d')    

» # com essa transformação a coluna passa a conter um datetime (timestamp). Por ex.:
» dfBU.loc[15250][0]
Timestamp('2020-02-26 00:00:00')

» # criamos dataframes para os dois países                      # comentário ‡
» dfUS = dfBU[(dfBU['code']=='USA') & (dfBU['dia'] &ge '2020-02-26')]
» dfBR = dfBU[dfBU['code']=='BRA']

(§) O uso de df2 = df1.copy() realiza uma cópia e não apenas pega um slice de df1. Esse procedimento evita mensagens de erro na linha seguinte, quando um campo do dataframe será alterado.

() No dataframe original existe um número maior de valores para os EUA. O corte na data especificada faz com que dfUS e dfBR tenham o mesmo tamanho.

Podemos agora plotar gráficos do número de mortes por COVID no Brasil e EUA, no mesma figura.

» cds = ColumnDataSource(data = {
»                        'dataBRA' : dfBR['dia'],
»                        'dataUSA' : dfUS['dia'],
»                        'mortesBRA' : dfBR['mortes'],
»                        'mortesUSA' : dfUS['mortes']
»                        })

» plot = figure(width=600, height=300,
»               x_axis_type = 'datetime', x_axis_label = 'data', y_axis_label = 'mortes',
»               background_fill_color='#fafaff', title='Mortes no Brasil e EUA')

» plot.circle(x = 'dataBRA', y = 'mortesBRA', source = cds, color='green' ,alpha=.2,
»             fill_color = 'yellow', fill_alpha=.3, size = 15, legend_label='EUA')


» plot.circle(x = 'dataBRA', y = 'mortesUSA', source = cds, color='blue' ,alpha=.2,
»             fill_color = 'red', fill_alpha=.3, size = 15, legend_label='EUA')

» plot.legend.location = 'top_left'

» show(plot)    # figura 13
Figura 13

Introduzimos nesse gráfico o uso de x_axis_type = 'datetime' para informar que o eixo x receberá dados de uma series temporal. plot.legend.location = 'top_left' informa a posição para as legendas.

Layouts

Layouts permitem a organização de gráficos em linhas e colunas múltiplas. Neles é possível vincular escalas de eixos entre gráficos diferentes.

Para explorar os layouts vamos usar o dataframe já montado df, que contém os campos dia, code, total, pib, idh, expVida, mortes, vacinados, descritos acima. Com ele construiremos 4 gráficos e os exibiremos em linhas, colunas e matrizes. A tabela inclui dados dos países ao longo de vários anos e, portanto, não há uma interpretação muito clara de seu significado. O objetivo é apenas o aprendizado da técnica.

» # transformando a coluna dia para um datetime
» df.loc[:,'dia'] = pd.to_datetime(df.loc[:,'dia'], format='%Y/%m/%d')

» #  a fonte de todos os gráficos é a mesma, nesse caso
» from bokeh.plotting import ColumnDataSource
» cds = ColumnDataSource(data = df)

» # gráfico 1
» plot1 = figure(width=300, height=200, x_axis_type = 'datetime',
»                x_axis_label = 'Data', y_axis_label = 'Mortes',
»                background_fill_color='#fafaff', title='Mortes no Mundo')

» plot1.dot(x = 'dia', y = 'mortes', source = cds, color='rosybrown' ,alpha=.5)

» # gráfico 2
» plot2 = figure(width=300, height=200,
»                x_axis_label = 'Expectativa de vida', y_axis_label = 'mortes',
»                background_fill_color='#fafffa', title='Expectativa de Vida x PIB')

» plot2.dot(x = 'expVida', y = 'pib', source = cds, color='red' ,alpha=.1)

» # gráfico 3
» plot3 = figure(width=300, height=200,
»                x_axis_type = 'datetime', x_axis_label = 'data', y_axis_label = 'mortes',
»                background_fill_color='#ffefff', title='PIB x Mortes')

» plot3.dot(x = 'pib', y = 'mortes', source = cds, color='blue' ,alpha=.05)

» # gráfico 4
» plot4 = figure(width=300, height=200, x_axis_label = 'PIB', y_axis_label = 'IDH',
»                background_fill_color='#9f9fff', title='PIB x IDH no mundo')
» plot4.dot(x = 'pib', y = 'idh', source = cds, color='yellow')

No código acima construimos quatro gráficos. Abaixo exploramos as possibilidades de layouts em linha, em coluna e em matriz.

» from bokeh.layouts import row, column
» # agrupar 2 gráficos em uma linha
» linha_layout = row(plot1,plot2)
» show(linha_layout)

» coluna_layout = column(plot3,plot4)
» show(coluna_layout)


» matriz_layout = column(row(plot1,plot2), row(plot3,plot4))
» show(matriz_layout)

Uma solução também interessante consiste em apresentar todos os gráficos no mesmo espaço, usando as classes Tabs e Panel. No código abaixo criamos 3 painéis e passamos nos argumentos os gráficos já construídos. Cada painel pode conter linhas e colunas, vistas anteriormente e passados no argumento child, além de um título que será usado nas guias ou tabs. Os painéis são inseridos em um objeto Tabs e exibidos.

» # importamos as classes necessárias
» from bokeh.models.widgets import Tabs, Panel
» # criamos 3 paineis
» tab1 = Panel(child = plot1, title = 'Mortes')
» tab2 = Panel(child = row(plot2,plot3), title = 'Exp Vida, PIBxMortes')
» tab3 = Panel(child = plot4, title = 'PIB x IDH')
» # insere os paineis no objeto Tabs
» objeto_tabs = Tabs(tabs = [tab1, tab2, tab3])
» # exibe o objeto
» show(objeto_tabs)

Ao clicar em uma guia o painés correspondente é exibido. Na figura estão mostrados a 1ª guia (figura 17) e a 3ª (figura 18).

Um layout de rede (grid layout) pode reunir gráficos em uma matriz, gerando resultado similar ao mostrado na figura 16. Para isso podemos usar o seguinte código.

» from bokeh.layouts import gridplot
» # cria uma rede ou grid
» grid_layout = gridplot([plot1, plot2], [plot3, plot4])
» show(grid_layout)
» # uma figura como a figura 16 é plotada.

Ao montar o grid_layout um espaço em branco pode ser inserido com None no lugar da variável do gráfico.

Algumas vezes é importante que dois ou mais gráficos tenham a mesma escala em um ou ambos os eixos. Para isso usamos o código como o seguinte.

» # criamos plots com a mesma escala (aqui no eixo do x)
» plot2.x_range = plot1.x_range
» # criamos um layout  (aqui em linha)
linha_layout = row(plot2, plot1)
show(linha_layout)

Anotações e Widgets

Para os próximos exemplos vamos usar o aqquivo population.csv, baixado do site Our World in Data, na página sobre população mundial.

O arquivo ./dados/population.csv foi baixado no link acima.

import pandas as pd
» # Importar dados para um dataframe
» df = pd.read_csv('./dados/population.csv')    

» # as colunas têm os nomes
» df.head(0)
↳ Entity   Code   Year   Total population (Gapminder, HYDE & UN)

» # 4 colunas e 53307 linhas
» df.shape # (53307, 4)

» # renomeamos as colunas
» colunas = {'Entity':'pais',
»            'Code':'codigo',
»            'Year':'ano',
»            'Total population (Gapminder, HYDE & UN)':'populacao'}
» df = df.rename(columns=colunas)

» # as colunas agora têm os nomes
» df.head(0)
↳ pais   codigo   ano   populacao

Já vimos como colocar títulos e legendas nas gráficos. No exemplo abaixo o título e posição são ajustados como uma propriedade de plot, diferente do parâmetro usado antes. Além disso podemos marcar regiões do gráficos com cores diferentes e incluir texto explicativo para realçar algum aspecto dos dados. Para isso usamos as classes Label e LabelSet.

Para alimentar esse gráfico vamos criar 3 ColumnDataSouces diferentes: para população e ano geramos cdsUSA para os EUA, cdsBRA para o Brasil, ambos após 1750. cdsLabel é usado para inserir anotações sobre os anos de independência e abolição da escravidão para os dois países.

» cdsUSA = ColumnDataSource(data = {
»     'ano' : df[(df['codigo']=='USA')  & (df['ano'] >= 1750)]['ano'],
»     'pop' : (df[(df['codigo']=='USA')  & (df['ano'] >= 1750)]['populacao'])/1e6,
» })
» cdsBRA = ColumnDataSource(data = {
»     'ano' : df[(df['codigo']=='BRA')  & (df['ano'] >= 1750)]['ano'],
»     'pop' : (df[(df['codigo']=='BRA')  & (df['ano'] >= 1750)]['populacao'])/1e6,
» })

» cdsLabel = ColumnDataSource(data=
»      dict(x=[1776, 1800, 1882, 1888],  y=[50, 100, 200, 260],
»           nota=['Indep. EUA (1776)', 'Abol. EUA (1857)',
»           'Indep. BR (1882)', 'Abol. BR (1888)']))

Agora estamos prontos para plotar esses dados. As únicas importações novas são das classes Label, LabelSet. Os dois gráficos de barra abaixo recebem os campos ano e pop, cada um relativo a um dos países.

» from bokeh.io import output_file, show, output_notebook
» from bokeh.plotting import figure
» from bokeh.plotting import ColumnDataSource
» from bokeh.models import Label, LabelSet

» output_notebook()

» grafico = figure(plot_width=600, plot_height=300, x_axis_label = 'ano',
                   y_axis_label = 'População (em milhões)')
» grafico.title.text = 'População do Brasil e do EUA de 1800 até o presente'
» grafico.title_location = 'above'

» grafico.vbar(x = 'ano', top = 'pop', source=cdsUSA,
               color = 'red', width= .1, legend_label = 'EUA')
» grafico.vbar(x = 'ano', top = 'pop', source=cdsBRA,
               color = 'green', width= 1, legend_label = 'Brasil')

» labels = LabelSet(x='x', y='y', text='nota', x_offset=0,
                    y_offset=0, source=cdsLabel, render_mode='canvas')

» texto = Label(x=1750, y=150, render_mode='css',
»               text='Independência e Abolição', text_color='blue',
»               border_line_color='#a0a0f0', border_line_alpha=1.0,
»               background_fill_color='linen', background_fill_alpha=1.0)

» grafico.add_layout(labels)
» grafico.add_layout(texto)
» grafico.legend.location = 'top_left'
» show(grafico)

Os objetos Label, LabelSet são criados com seus respectivos atributos e depois inseridos no grafico.

Usando mapas de cor

Para atribuir cores para uma categoria de dados, separando visualmente a informação para cada categoria, podemos atribuir uma cor a cada uma delas usando CategoricalColorMapper. Nele associamos a uma lista de fatores (factors ou dados categóricos) com uma lista de cores (em palette).

No exemplo inicializamos a variável mapaDeCor como um CategoricalColorMapper atribuindo os parâmetros factors e palette aos nomes das categorias e uma lista de cores. A associação é feita através do parâmetro transform no scatter plot. Novamente dois plots são traçados para efeito estético.

» from bokeh.io import output_notebook, show
» from bokeh.plotting import figure, CategoricalColorMapper
» from bokeh.models import ColumnDataSource, Range1d
» output_notebook()

» cor = ['salmon','gold','firebrick','plum','powderblue','teal','wheat','red']
» nome = ['Otto', 'Ana', 'Joana', 'Jorge', 'Marco', 'Agildo','Lu','Zana']
» dicio= dict(nome=nome,
»             altura=[1.70, 1.65, 1.48, 1.88, 1.58, 1.62, 1.83, 1.91],
»             peso=[97, 65, 89, 76, 67, 74,65, 94]
»            )
» mapaDeCor = CategoricalColorMapper(factors=nome, palette=cor)

» cds = ColumnDataSource(data=dicio)

» p = figure(title='Alunos: distribuição peso x altura',
»            x_range=Range1d(60, 110), y_range=Range1d(1.2, 2.2),
»            plot_width=400, plot_height=250)

» p.scatter(x='peso', y='altura', size=20, source=cds,
»           color=dict(field='nome', transform=mapaDeCor), alpha=.2)
» p.scatter(x='peso', y='altura', size=10, source=cds,
»           color=dict(field='nome', transform=mapaDeCor))
» p.xaxis[0].axis_label = 'Peso (kgs)'
» p.yaxis[0].axis_label = 'Altura (metros)'

» labels = LabelSet(x='peso', y='altura', text='nome',
                    x_offset=0, y_offset=8, source=cds)

» p.add_layout(labels)
» show(p)

Bibliografia

  • Jolly, Kevin: Hands-On Data Visualization with Bokeh, Interactive web plotting for Python using Bokeh, 2018 Packt Publishing, Mumbay.
  • Site Bokeh: Documentation, acessado em agosto de 2021.
  • Site Bokeh: First Steps, acessado em agosto de 2021.
  • Site Our World in Data, contendo grande variedade de tabelas com dados sobre vários temas, do mundo.
  • Rodés-Guirao, Lucas: COVID-19 Dataset by Our World in Data no Github. Acessado em outubro de 2021.

Python: Iteradores, Itertools e Funções Geradoras


Iteradores

Nessa seção para imprimir vários resultados eu uso quase sempre print(x, end=' ') para que uma nova linha não seja lançada após cada impressão. O objetivo é diminuir o espaço de página usada e facilitar a leitura.

Já vimos que objetos que são sequências e coleções podem ser lidos iterativamente com o uso do operador for.

» lista = ['Aa', 'Bb', 'Cc', 'Dd', 'Ee', 'Ff']
» for t in lista:
»     print(t, end=' < ')
↳ Aa < Bb < Cc < Dd < Ee < Ff <

Laços for são claros e concisos. Por trás desse resultado simples, a instrução for chama a função iter() na sequência (ou coleção) que retorna um objeto iterador. Dentro do iterador existe o método __next__() que acessa os elementos no contêiner um de cada vez. Ao final da iteração, quando se extinguem os elementos, __next__() levanta uma exceção StopIteration que é reconhecida pelo laço como o fim da iteração. O método __next__() pode ser chamado através da função interna next(), como mostra o exemplo:

» iterador = iter('OMS')
» print(next(iterador))
» print(next(iterador))
» print(next(iterador))
» print(next(iterador))
↳ O
↳ M
↳ S
↳ StopIteration:

No código acima usamos iter(sequencia) que retorna um iterável da sequência ‘OMS’. StopIteration é uma mensagem de erro ao final da iteração, com o iterador esgotado. Ela aparece aqui resumida.

Podemos construir uma classe iterável implementado nela os métodos __iter__() e __next__(). No caso abaixo a classe simplesmente retorna a sequência invertida, do último elemento para o primeiro

» class Inverter:
»     def __init__(self, data):
»         self.data = data
»         self.index = len(data)

»     def __iter__(self):
»         return self

»     def __next__(self):
»         if self.index == 0:
»             raise StopIteration
»         self.index -= 1
»         return self.data[self.index]

# instanciamos um objeto Inverter usando uma lista como argumento
» inv = Inverter(['Aa', 'Bb', 'Cc', 'Dd', 'Ee', 'Ff'])

» for t in inv:
»     print(t, end=' < ')
↳ Ff < Ee < Dd < Cc < Bb < Aa <

# seq é Inverter usando uma string como argumento
» seq = Inverter('Joazeiro')
» for t in seq:
»     print(t, end=' < ')
↳ o < r < i < e < z < a < o < J <     

Sequências podem ser transformados em iteradores com as funções:

iter(objeto, sentinel) retorna o objeto (uma sequência) como iterável,
interrompe a iteração quando o valor retornado for igual à sentinel (opcional),
reversed(objeto) retorna a sequência como iterável, em ordem inversa.

que, como vimos, são úteis quando usadas com loops for, while. Por exemplo, com uma lista (ou uma tupla):

» x = ["apple", "banana", "cherry"]
» print(next(x))
↳ TypeError: 'list' object is not an iterator

» # transformado a lista em um iterador
» x = iter(["apple", "banana", "cherry"])
» print(next(x), next(x), next(x))
↳ apple banana cherry


Um objeto zip() é um iterador que junta duas sequências e retorna tuplas com elementos das sequências em seus argumentos.

» z = zip(['a', 'b', 'c', 'd'], [1, 2, 3, 4])
» print(next(z))
» print(next(z))
↳ ('a', 1)
↳ ('b', 2)

Da mesma forma map retorna um iterável.

» m = map(lambda x, y: x**y, [8, 2, 9], [5, 3, 7])
» print(next(m))
» print(next(m))
» print(next(m))
↳ 32768
↳ 8
↳ 4782969

# outro exemplo
» list(map(len, ['Abacate', 'Uva', 'Jacoticaba']))
↳ [7, 3, 10] 

Observe que um objeto pode ser um iterável (como uma lista) mas não ser um iterador. No entanto ele pode ser transformado um um iterador.

» numeros = [1, 2, 3, 4, 5]
» next(numeros)
↳ TypeError: 'list' object is not an iterator

» i = iter(numeros)
» next(i)
↳ 1
» next(i)
↳ 2
» # o estado do iterador é armazenado entre iterações
» for x in i:
»     print(x, end=' ')
↳ 3 4 5

Muitas das funções do Python retornam iteradores ao invés de listas ou tuplas. Por exemplo enumerate, reversede open(file)retornam iteradores.

» alunos = ['Pedro', 'Maria', 'Marco']
» nAluno = enumerate(alunos)        # enumerate
» next(nAluno)
↳ (0, 'Pedro')

» for t in nAluno:
»     print(t)
↳ (1, 'Maria')
↳ (2, 'Marco')

» invertido = reversed(alunos)       # reversed
» next(invertido)
↳ 'Marco'

» f = open('./dados/linhas.txt')     # arquivo
» next(f)
↳ 'Esta é a linha 1\n'

Por outro lado muitas funções internas (e das bibliotecas) aceitam iteráveis (e iteradores) como parâmetros. Abaixo exemplos do uso de zip, dict recebendo iteradores.

» numeros = [10, 20, 30]
» quadrados = (n**2 for n in numeros)
» quadrados    # é um objeto generator (que é um iterador)
↳ <generator object <genexpr> at 0x7f67280c1120>

» z = zip(numeros, quadrados)        # zip recebe um iterador como argumento
» print(next(z), next(z), next(z))
↳ (10, 100) (20, 400) (30, 900)

» # Um dicionário pode receber um iterável como argumento
» alunos = ['Pedro', 'Maria', 'Marco']
» nAluno = enumerate(alunos)         # um iterador
» d = dict(nAluno)
» d
↳ {0: 'Pedro', 1: 'Maria', 2: 'Marco'}

Módulo itertools

Outros módulos foram tratados em A Biblioteca Padrão dessas notas.

Um módulo da biblioteca padrão interessante para manipulação de iteradores é o itertools, que contém diversos métodos para manipulação de iteráveis e funções que retornam iteráveis. Elas são voltadas para a velocidade de execução e uso otimizado de memória.

Existem dois tipos de interadores: iteradores infinitos continuam a rodar indefinidamente se nenhuma condição de parada for imposta, enquanto os iteradores finitos são criados com um número determinado de ciclos determinados. O módulo Itertools possui funções de cada tipo:

iteradores infinitos count, repeat, cycle,
iteradores finitos chain, compress, tee, dropwhile, takewhile.

Iteradores finitos

Função chain(): A função chain reune listas transformando-as em um único iterável.

» import itertools as it
» abc = ['Aa', 'Bb', 'Cc']
» vxz = ['Vv', 'Xx', 'Zz']
» for txt in it.chain(abc, vxz):
»     print(txt, end=' ')
↳ Aa Bb Cc Vv Xx Zz 

» # o objeto retornado por chain é um iterável: 
» letras = it.chain(abc, vxz)
» for i in range(3):
»     print(next(letras), end=' ')
» print(next(letras), next(letras), next(letras))
↳ Aa Bb Cc Vv Xx Zz

Os argumentos de chain(iteravel,...) podem ser de qualquer tipo, desde que iteráveis. Por exemplo, podemos juntar uma lista, um dicionário e um it.count():

» lista = ['Aa', 'Bb', 'Cc']
» dicio = {'G': 'g', 'H': 'h'}
» conta = it.count()
» for k in it.chain(lista, dicio.items(), conta):
»     print(k, end=' ')
↳ Aa Bb Cc ('G', 'g') ('H', 'h') 0 1 2 3 4 5 6 7 8 9 10 ...

O 4º e 5º loop retornam as tuplas do dicionário. Em seguida os inteiros são retornados. (A exibição foi interrompida manualmente!)

Função compressed(iteravel, seletor): compressed recebe um iterável e um seletor (uma lista de booleanos) e retorna um iterável filtrado pelo seletor, contendo apenas elementos onde o seletor é True.

» aluno = ['Paulo', 'Maria', 'Ricardo']
» aprovado = [False, True, True]
» for n in it.compress(aluno, aprovado):
»     print(n, end=' ')
↳ Maria Ricardo 

Os argumentos de compressed podem ser iteráveis com infinitos elementos. Por exemplo, abaixo construímos o iterador com elementos True ou False dependendo de ser o número par ou impar.

» par = (x%2==0 for x in it.count())
» for pares in it.compress(it.count(), par):
»     print(pares, end=' ')
↳ 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 ...

Função tee(iteravel): recebe um iterável e retorna dois objetos clonados do original. O iterável original fica esgotado depois da operação. Também é possível passar um parâmetro para gerar n clones, em itertools.tee(iteravel, n).

» elementos = (x**x for x in range(6) if x**x%2==0)
» lista1, lista2 = it.tee(elementos)
» print(lista1)                      # as listas são objetos itertools._tee
↳ <itertools._tee object at 0x7fc032da5ac0>

» print(list(lista1), list(lista2))
↳ ['1', '2', '3'] ['1', '2', '3']
» print(list(elementos))             # a lista original fica esgotada
↳ []

# um parâmetro pode ser passado para gerar n clones
» vitaminas = ['A', 'B12']
» vitas = it.tee(vitaminas, 4)
» for i in vitas:
»     print(list(i), end=' ')
↳ ['A', 'B12'] ['A', 'B12'] ['A', 'B12'] ['A', 'B12'] 

Função dropwhile(função, iteravel): recebe um iterável e uma função seletora (que avalia cada elemento do iterável como um booleano) e retorna os elementos do iterável à partir do primeiro retorno False da função. Em outras palavras ela descarta os elementos avaliados como True no início da lista (ou iterável). Podemos obter a lista à partir do último valor avaliado como False com reversed(lista).

» def par(x):                       # retorna True se x é par
»     return x % 2 == 0
» numeros = [0, 2, 4, 8, 16, 17, 32, 64, 67, 128]

» # o primeiro False ocorre em 17
» for t in it.dropwhile(par, numeros):
»     print(t, end=' ')
↳ 17 32 64 67 128

» # o mesmo pode ser conseguido com uma função lambda
» print(list(it.dropwhile(lambda t: t%2==0 , numeros)))
↳ [17, 32, 64, 67, 128]

» # lista anterior ao último valor avaliado como False (em ordem reversa)
» print(list(it.dropwhile(lambda t: t%2==0 , reversed(numeros))))
↳ [67, 64, 32, 17, 16, 8, 4, 2, 0]

Função takewhile(função, iteravel): retorna todos os termos iniciais de um iterável até que um de seus elementos avalie como False pela função. Os demais elementos, mesmo que avaliando como True, são descartados.

» anos = [10,30,50, 70, 90, 110, 1, 2, 3]
» take = it.takewhile(lambda x: x < 100, anos)
» for a in take:
»     print(a, end= ' ')
↳ 10 30 50 70 90 

Função itertools.zip_longest(iter1, iter2): diferente da função interna zip(), zip_longest(iter1, iter2) retorna um iterador com o comprimento da maior entre iter1, iter2, substituindo valores ausentes por None.

» # usando zip (built-in)    
» x = [0, 1, 2, 3, 4, 5]
» y = ['A', 'B', 'C']
» list(zip(x, y))
↳ [(0, 'A'), (1, 'B'), (2, 'C')]

» # usando itertools.zip_longest
» list(it.zip_longest(x, y))
↳ [(0, 'A'), (1, 'B'), (2, 'C'), (3, None), (4, None), (5, None)]

Iteradores infinitos

Função count(): a funçãocount() produz uma sequência de comprimento indefinido de inteiros, apropriada para contagem.

» import itertools as it
» t = 0
» for x in it.count():
»     if x > 10: break
»     print(x, end=' ')
↳ 0 1 2 3 4 5 6 7 8 9 10    

Se uma condição de parada do loop não for inserida ele gera uma quantidade indefinida de números. A função pode receber os parâmetroscount(inicio, passo), para indicar o início da contagem é o incremento.

» for i in it.count(10, 2):
»     if i > 24:
»         break
»     else:
»         print(i, end=' ')
↳ 10 12 14 16 18 20 22 24

Um iterador não retorna elementos indexados, o que significa que não podemos tomar apenas uma fatia (ou slice). No entanto podemos usar a função itertools.isslice().

Função isslice(iterador, n): Retorna os primeiros n elementos do iterador.

» quadrados = (n**2 for n in it.count())
» list(it.islice(quadrados, 10))
↳ [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Outra abordagem possível é usar itertools.takewhile (ou itertools.dropwhile) para fazer filtragens:

» algunsQuadrados = (n**2 for n in it.count(25, 5))
» list(it.takewhile(lambda x: x<2000, algunsQuadrados))
↳ [625, 900, 1225, 1600]

Função repeat(objeto, n): retorna o objeto repetido n vezes, em um iterável. Se n for omitido (o que é o mesmo que passar n=None) o ciclo se repete indefinidamente.
Função cycle(objeto): é o mesmo que repeat() retornando o objeto um número indefinido de vezes.

»  função repeat
» for i in it.repeat([1,2,3], times = 3):
»     print(i, end = ' ')
↳ [1, 2, 3] [1, 2, 3] [1, 2, 3]

» função cycle
» l = ['Far', 'West', 'Wing']
» i = 0
» for t in it.cycle(l):
»     if i < 5:
»         print(t, end=' ')
»     else:
»         break
»     i += 1
↳ Far West Wing Far West 

Funções Geradoras

Como sabemos, uma função do Python é executada de forma sequencial e sem interrupções, até que uma instrução return seja encontrada. Nada depois disso é executado. (Lembrando: uma função sem return retorna None.)

» def exibeLinhas():
»     print('linha 1')
»     print('linha 2')
»     print('linha 3')

» exibeLinhas()
↳  linha 1
↳  linha 2
↳  linha 3

Instrução yield: É possível pausar uma função no meio de sua execução, retomando depois no ponto de pausa. Com isso é possível criar funções que agem como um iterador. Uma função contendo pelo menos uma instrução yield, é chamada uma função geradora. Geradores são uma generalização de iteradores que produzem seus dados apenas sob demanda, sendo por isso chamados de lazy (prequiçosos).

» def funcaoGeradora():
»     print('Linha 1 é retornada')
»     yield 1
»     print('Linha 2 é retornada')
»     yield 2
»     print('Linha 3 é retornada')
»     yield 3

» gera = funcaoGeradora()
» print(gera)
↳ <generator object funcaoGeradora at 0x7f69643c8dd0>
» print(next(gera))
↳ Linha 1 é retornada
↳ 1
» print(next(gera))
↳ Linha 2 é retornada
↳ 2
» print(next(gera))
↳ Linha 3 é retornada
↳ 3
» print(next(gera)) # um erro é lançado "StopIteration"

Esse tipo de função retorna um objeto gerador que é, em termos de comportamento, um iterador. A iteração sobre esse objeto é feita com a função interna next(). Quando o iterador está esgotado o uso de next() retorna uma exceção.

yield pode ser usado para retornar qualquer valor no iterador, inclusive None, se um valor for omitido. yield substitui a instrução return que, se usada, interrompe o ciclo do iterador. Uma função geradora pode ser transformada, com todos os seus valores, em uma lista, com list(geradora).

» def geradora():
»     for i in range(10):
»         yield i

» g = geradora()
» while True:
»     try:
»         print(next(g), end=' ')
»     except:
»         break
↳ 0 1 2 3 4 5 6 7 8 9

» g = geradora() # para repopular o iterador
» for i in g:
»     print(i, end=' ')
↳ 0 1 2 3 4 5 6 7 8 9

» g = geradora()
» lista = list(g)
» print(lista)
↳ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

A função geradora só pode ser percorrida uma vez, ficando esgotada no final. Como mostrado no último bloco de código, da mesma forma que uma função geradora pode ser percorrida com next(), o iterador por ela gerado pode ser percorrido em um loop for.

Relembrando: Uma classe pode retornar um objeto iterável por meio dos métodos especiais __iter__() e __next__(). O código next(g) é equivalente à chamar o método interno do objeto iterável, g.__next__().

» # class iteradora
» class Quadrados:
»     def __init__(self, quantos):
»         self.quantos = quantos
»         self.atual = 0​

»     def __iter__(self):
»         return self

»     def __next__(self):
»         quad = self.atual ** 2
»         self.atual += 1
»         if self.atual > self.quantos:
»             raise StopIteration
»         return quad

» q = Quadrados(11)
» for s in q:
»     print(s, end=', ')
↳ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 

As funções geradoras podem conseguir o mesmo resultado de forma mais compacta. As operações de iteração, de obter o elemento seguinte e a parada por exceção são automaticamente fornecidas. Elas retornam um objeto gerador, que é um iterável.

» def quadrados(quantos):
»     for n in range(quantos):
»         yield n**2

» q = quadrados(11)
» for s in q:
»     print(s, end=', ')        
↳ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100,

Uma função geradora pode guardar o estado em que terminou a última chamada até ser chamada novamente. Por exemplo, podemos contruir uma geradora para exibir os elementos da sequência de Fibonacci, indefinidamente.

» # Ex.: geradora da sequência Fibonacci
» def fibonacci():
»     a, b = 1, 1
»     while True:
»         yield a
»         a, b = b, a + b
        
» # inicializamos um objeto da sequência
» f = fibonacci()
» # as 10 primeiras iterações resultam em
» for i in range(10):
»     print(next(f), end=' ')
↳ 1 1 2 3 5 8 13 21 34 55 

» # as 10 iterações seguintes
» for i in range(10):
»     print(next(f), end=' ')
↳ 89 144 233 377 610 987 1597 2584 4181 6765 

Podemos também calcular quantos números primos se desejar, desde que o computador tenha capacidade de processamento e memória para isso.

» def numerosPrimos():
»     num = 2
»     yield num
»     while True:
»         num += 1
»         primo = True
»         for i in range(2, int(num/2)):
»             if(num % i) == 0: # achou um divisor
»                 primo = False # logo não é primo
»                 break
»         if primo:
»             yield num

» for p in numerosPrimos():
»     print(p, end=' ')
»     if p > 100:
»         break
↳ 2 3 4 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101                 

Os dois últimos exemplos ilustram o fato de que a função geradora armazena seu estado entre as chamadas e os retornos com yield. Como já vimos o loop for cuida das chamadas a next() e o encerramento do laço, quando existir.

Método send(): Outra característica importante dos geradores é a possibilidade de interagir com o código do iterador. Um valor pode ser passado para yield através do método send(). Em outras palavras, além de retornar um valor calculado dentro da função geradora yield recebe o valor passado por send(), que funciona como next() mas passa um valor para yield.

» # definimos uma função geradora com send
» def usandoSend():
»     while True:
»         recebido = yield
»         print('Recebido =', recebido)

» f = usandoSend()    # inicializamos um objeto gerador
» next(f)             # a primeira interação deve ser feita para a entrada no iterador

» # passando integer 0 para yield
» f.send(0)
↳ Recebido = 0
» # passando string para yield
» f.send('palavra')
↳ Recebido = palavra
» # uma iteração send envia None para yield
» next(f)
↳ Recebido = None

Com essa funcionalidade o código que chama o iterador pode modificar o comportamento da função e, portanto, dos valores retornados. No exemplo abaixo o gerador retorna inteiros, de 1 em 1, exceto se um incremento extra for enviado por meio de send().

» # definimos uma função geradora com send
» def usandoSend():
»     n = 0
»     while True:
»         n +=1
»         incremento = yield n
»         if recebido:
»             n += incremento

» # inicializamos o gerador
» f = usandoSend()    # inicializamos um objeto gerador
» # os 4 primeiros outputs são
» print(next(f), next(f), next(f), next(f))
↳ 1 2 3 4
» # incremento 4 primeiros outputs são
» p = f.send(10)
» print(p)
↳ 15
» # os 4 próximos outputs são
» print(next(f), next(f), next(f), next(f))
↳ 16 17 18 19

O exemplo seguinte usa send() e será usado na próxima seção para ilustrar o uso de throw().

» # definimos um gerador
» def gerador_letras(texto):
»     local = 0
»     while True:
»         mandou = yield texto[local]
»         if mandou:
»             local = mandou
»         else:
»             local += 1

» # inicializamos um gerador            
» let = gerador_letras('Apocalipse Zumbi')
» next(let)
↳ 'A'
» # enviamos via send
» let.send(11)
↳ 'Z'
» next(let)
↳ 'u'

Método throw(): throw() permite que se lance uma exceção no ponto onde o gerador foi interrompido, na última execução. Ele ativa a exceção e retorna o valor seguinte (como faria yield), ou uma StopIteration se o gerador estiver esgotado. A exceção deve ser tratada dentro da função geradora, caso contrária será repassada para o código que chamou a função. A função abaixo permite que se veja em que ponto está a execução do gerador, usando throw().

» # definindo gerador com tratamento para exceção
» def gerador_letras(texto):
»     local = 0
»     while True:
»         try:
»             mandou = yield texto[local]
»         except Exception:
»             print(f'Exceção atingida na posição {local}')
»         if mandou:
»             local = mandou
»         else:
»             local += 1

» let = gerador_letras('Sei que nada será como antes')
» print(next(let), next(let))
↳ S e

» # pula a posição para 4
» let.send(4)
↳ 'q'

» # levanta exceção (que, no caso, mostra posição atual no gerador)
» let.throw(Exception)
↳ Exceção atingida na posição 4
↳ 'q'

» print(next(let), next(let))
↳ u e
  • throw(exceção): permite que se envie para o iterador qualquer tipo de exceção,
  • close(): fecha o iterador e levanta a exceção GeneratorExit.
» # fechando o iterador
» f.close()
» f.send('palavra')   # uma exceção é lançada
↳ StopIteration

yield from: permite que um gerador chame outros, de forma sequencial. No exemplo abaixo o gerador_principal esgota primeiro o gerador1, depois o gerador2, em sequência.

» # geradores de geradores
» def gerador1():
»     yield 'Linha 1 do gerador 1'
»     yield 'Linha 2 do gerador 1'​

» def gerador2():
»     yield 'Linha 1 do gerador 2'
»     yield 'Linha 2 do gerador 2'

» def gerador_principal():
»     yield from gerador1()
»     yield from gerador2()

» delegando = gerador_principal()
» print(next(delegando))
» print(next(delegando))
» print(next(delegando))
» print(next(delegando))
↳ Linha 1 do gerador 1
↳ Linha 2 do gerador 1
↳ Linha 1 do gerador 2
↳ Linha 2 do gerador 2

» # o mesmo resultado seria obtido com
» delegando = gerador_principal()
» for i in delegando:
»     print(i)    # linhas de output omitidas

Observação: Alguns objetos built-in são geradores e têm internamente implementados os métodos yield, next, iter, StopIteration . São eles:
range, dict.items, zip, map e File Objects.

A conjectura de Collatz

O exemplo de função geradora a seguir ilustra um problema interessante na matemática, denominado conjectura de Collatz. Collatz se perguntou se uma operação aritmética simples repetida sobre números inteiros positivos produziria sempre uma sequência terminada em 1. A sequência geralmente considerada é a seguinte: iniciando com um inteiro n cada termo da sequência é gerado da seguinte forma. Se o número é par o seguinte é n/2; se o número é impar o seguinte é 3n + 1. Para todos os inteiros testados a conjectura é verificada. Por exemplo, começando com n=7 temos: {7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1}. No entanto até hoje não foi possível provar o caso geral e o problema permanece em aberto.

Para gerar a sequência vamos construir um iterador que recebe um parâmetro n, o número inicial da sequência. Observe que a sequência seria infinita sem um corte pois 1 ⟶ 4 ⟶ 2 ⟶ 1, um loop infinito. Portanto a interrompemos manualmente quando ela atinge 1. O iterador é usado pela função collatz(n) que retorna a sequência inteira e quantas iterações foram feitas (protanto quantos elementos existem na sequência).

» # sequência de Collatz
» def iterador(numero):
»     while True:
»         yield numero
»         numero = int(numero/2 if numero%2==0 else 3 * numero + 1) 

» def collatz(n):
»     itt = iterador(n)
»     quantos = 0

»     while True:
»         quantos += 1
»         val = next(itt)
»         print(val, end=' ')
»         if val == 1:
»             print(f'\nFinal alcançado em {quantos} iterações!')
»             break

» collatz(7)
↳ 7 22 11 34 17 52 26 13 40 20 10 5 16 8 4 2 1 
↳ Final alcançado em 17 iterações!
Sequência de Collatz para n = 7

Geradores e gerenciamento de memória


Geradores são uma forma prática de interagir com grande volume de dados sem esgotar a memória do computador. Eles permitem que um grande volume de dados possam ser acessados em blocos, por partes. Cada bloco pode ser retornado sob demanda, enquanto o gerador armazena seu estado entre cada chamada.

Bibliografia

Um Laço no Tempo



“Tempo e espaço são modos pelos quais pensamos e não condições nas quais vivemos.
A distinção entre passado, presente e futuro é só uma ilusão, ainda que muito persistente.”

Albert Einstein

Existe no universo uma linha de eventos incomum, no sentido de extraordinária ou rara. Em algum lugar no tempo e do espaço alguém conseguiu emitir partículas que atingiam um detector microsegundos antes de sua emissão. Muitos não acreditaram, como ainda não acreditam, na possibilidade desses saltos temporais, e argumentaram que simplesmente “não se vê todo dia a chegada de viajantes do futuro”.

Por motivos compreensíveis o nome dessa cientista foi esquecido, mas nós a chamaremos de Paula, por convenção e facilidade de relato. Servirá esse nome a uma homenagem a Paul Dirac, o grande físico inglês que revelou a existência da anti-matéria e a possibilidade de partículas viajando do futuro para o passado. Paula foi ridicularizada pelos colegas, é claro, mas ainda assim continuou a aperfeiçoar seu experimento. Depois de inúmeras tentativas, acumulados muitos fracassos, ela conseguiu transferir objetos maiores a cada tentativa, por intervalos de tempo cada vez mais longos. Incapaz de detectar esses objetos nas regiões intermediárias entre a emissão e a detecção ela elaborou a hipótese de que eles estavam viajando por regiões interstícias do espaço e do tempo, fora ou além das quatro dimensões que conhecemos bem. Pelo menos foi esse o nome e o conceito que ela usou.

Pouco tempo se passou, de acordo com sua própria contagem, até que ela conseguiu transferir microorganismos, depois pequenos animais. É claro que alguém com tamanha curiosidade e engenho, como poderíamos esperar, não demoraria muito até preparar todo um aparato para mandar a si mesma para o passado. Em data ignorada Paula se lançou em uma viagem arriscada e chegou ao mundo na mesma linha de eventos onde já estava, mas 40 anos antes da partida. Sem mencionar sua origem ela encontrou e fez amizade com os pais que, como logo percebeu, se conheciam mas se odiavam. Ela foi tomada pela ansiedade: talvez não devesse interferir com o fluxo dos eventos mas a cada dia, quando se aproximava a data provável de sua concepção e nenhum dos dois dava qualquer sinal de interesse pelo outro, mais crescia a sua angústia.

Procurando remediar a situação, que nem ela mesma sabia se era indevida, Paula tentou aproximar os jovens enaltecendo, um para o outro, as figuras daqueles que teriam que gerá-la. Ela foi delicada e cuidadosa a princípio, perdendo aos poucos o tato e usando termos impróprios para jovens daquele tempo. De forma inesperada, totalmente fora de seu controle, ela provocou maior antipatia e rejeição entre os dois. Foram dias, horas e minutos amargos, enquanto aumentava sua perceção de que não havia tempo para reconstruir uma situação favorável. Naquele momento, mesmo que os pais se relacionassem e tivessem um filho, este filho não seria a Paula. Isso significa que, na narrativa dessa linha do tempo, Paula nunca nasceu. Ali ocorreu o evento raro, magnífico na teoria mas horrível para uma pessoa. O mundo e sua história se bifurcaram. Desesperada ela tentou retomar sua vida na mesma linha de mundo de onde havia saído. Desapontada descobriu que não conseguiria, naquela época, equipamentos com tecnologia adequada. Sua tentativa foi um fracasso e Paula desapareceu em algum lugar e algum momento.

Portanto existe na história um laço que contém um único indivíduo que nasceu, cresceu e deixou de existir após voltar até os momentos anteriores à sua própria concepção. O primeiro universo, onde deveriam existir pessoas que conheceram e sentiam saudades de Paula, existiu durante o laço, o intervalo entre seu nascimento e o fim. Um novo universo, que ela apenas visitou tangencialmente e com brevidade, surgiu em consequência.

Não é improvável que o mesmo tipo de coisa tenha ocorrido novamente, algumas ou muitas vezes. Também não é difícil especular que visitantes do futuro destruam o seu passado, por um ou outro motivo. Talvez seja esse mesmo o motivo para não recebermos com frequência “visitantes chegando do futuro”.

Há quem diga que, em outra linha de mundo, Paula não teria viajado para o passado. De fato, eles defendem, ela se enviou para o futuro. Nesse caso não haveria quem relatasse hoje o ocorrido e, sendo isso verdade, esse conto pode ser seguramente ignorado.

História da Matemática


História da Matemática

Como construir um Astrolábio: Jakob Köbel, 1524. (wikipedia)

Os Ativistas


“Aqueles que consideramos os grandes homens e mulheres só são grandes porque nós estamos de joelhos. Precisamos nos levantar!”.

Elysée Loustallot, jornalista francês, 1761 – 1790.

Eu e mais dois jovens colegas ativistas estávamos descendo os últimos degraus do prédio onde participamos das Conferências da ONU sobre o Clima quando fomos abordados por uma desconhecida. Ela fez três afirmações: “Não represento perigo para vocês”, “precisamos salvar uma Terra” e “vocês são capazes de executar essa tarefa”. Depois perguntou: “concordam em me ajudar?” Nos entreolhamos surpresos e, sem tempo para raciocinar, concordamos.

Senti um formigamento pelo corpo e ligeira náusea. Depois me vi em outro local, difícil de ser descrito. Primeiro vi vários instrumentos de controle, algo como o cockpit de um avião. Depois, olhando para fora, confirmei que estava no ar, em local muito afastado da superfície. A desconhecida se apresentou como Ana e acrescentou: “não sou daqui e nem de agora. Olhem pela janela!”

A Terra abaixo de nós se acelerava até que não podíamos mais ver continentes, nuvens nem oceanos. As cores se misturaram até formar um azul bem claro, uniforme e sem textura. Entendi que estávamos em alta velocidade, sem compreender como isso seria possível: “porque não sinto nosso veículo em velocidade ou sendo acelerado?”, perguntei. Ana riu: “cancelamento de inércia. Um efeito diferente será distinguível em breve. Prestem atenção!”

A Terra agora era um bola lisa, com bordas muito bem definidas. “Olhem!” Primeira a borda da esfera perdeu a nitidez. Em seguida várias outras esferas menores despontaram ao seu lado. Passados alguns segundos as esferas bifurcadas se dividiram também, formando uma flor complexa. “Estamos vendo muitas Terras”, disse Ana, “bifurcações de eventos à partir de um evento qualquer em uma delas. Vocês conhecem o conceito de muitos mundos, da mecânica quântica?”. Discutimos rapidamente o assunto. Conhecíamos aquela ideia sem saber que ela poderia ter uma aplicação prática.

Ana percebeu nossa desorientação. “Cada evento tem muitas maneiras de progredir no tempo. Chamamos de trajetórias essas maneiras. Aquelas de maior probabilidade formam as Terras que vocês estão vendo. As Terras improvavéis se dissolvem no espaço-tempo. A aplicação prática de tal abstração é a seguinte: observando eventos destruidores que se propagam em muitas versões ou trajetórias podemos saber se um evento tem potencial danoso para seres sencientes ou não.”

“Por exemplo, observem aquela evolução…” , ela apontou para uma das esferas entre o centro e as bordas. À princípio eu não consegui resolver um esfera única em meio a tantas outras. Aos poucos, discutindo com os colegas, comecei a ver uma esfera escurecida, no meio de outras mais brilhantes. Ana nos informou: “A cor escura decorre da abundância de CO2. Essa Terra está em processo de extinção da vida abrigada. Vocês estão vendo a morte lenta de um planeta.”

Um dos meus colegas perguntou: “se são tantas Terras, por que deveríamos nos precupar com a morte de uma delas?”. “Acaso você sabe em qual delas você vive?”, perguntou a mulher. “Mas essa discussão é irrelevante. Olhem por mais algum tempo.” Foi isso o que fizemos até perceber que a cor escura do planeta moribundo se espalhava para esferas adjacentes. Ana voltou a falar, com voz tensa: “efeitos dos eventos se espalham. A humanidade, em todas as trajetórias de mundos, está tornando inviável a vida humana. Esse não é um evento singular e muitas estrelas com vida senciente são destruídas nesse processo, quando suas civilizações atingem eras tecnológicas. A imagem tipo flor que vocês estão vendo … nós a chamamos de Orbis. Nosso objetivo é salvar o maior número possível de Orbis.”

Ana fez um gesto nos impedindo de fazer perguntas. “Vocês se lembram dos limites de poluição acordados pelos chefes de nações na reunião da ONU?” Concordamos com a cabeça. “Tais metas serão cumpridas?” Sabíamos que não. “Vocêm devem agir!” Apontando para nós três ela continuou. “Vocês serão dirigentes políticos ou científicos em seus respectivos países. Comecem agora a estudar formas de produção da energia limpa.” Um dos colegas perguntou: “sabemos como gerar energia limpa. Mas também sabemos que as formas alternativas de geração de energia são caras. A comunidade econômica mundial não tolera perdas.”

Ana parecia estar se preparando para se despedir de nós e não interrompeu nenhum de seus movimentos por causa da pergunta. Ela só disse: “quando chegar a sua vêz de agir, o que será em breve, a economia já estará combalida o bastante. Não poderão se opor a políticos e cientistas que apresentarem projetos sólidos e factíveis. E vocês não estarão sozinhos … boa sorte!”

Senti vertigem novamente, menos assustado agora que sabia o que estava ocontecendo. Nos encontramos no alto da escadaria, onde estávamos mais ou menos três minutos antes de nossa partida. Nos abraçamos: “temos que falar sobre isso!” e fomos para um bar na esquina mais próxima, para confirmar nossas impressões e combinar nossas atitudes futuras.


Desafios Matemáticos




Um problema de lógica razoavelmente difícil!

Vejo alguém de olhos azuis!

O Raio do Círculo

Figura 1

Segue um exemplo de um tipo de questão recorrente em testes de admissão em empresas de tecnologia. Outras soluções, além da aqui apresentada, são encontradas em sites, como por exemplo no canal do Youtube Universo Narrado.

Dois arcos perpendiculares seccionam um círculo, como mostrado na figura 1. Qual é o raio do círculo?

A Distância mais Curta

Figura 3

Dados dois pontos A e B que estão do mesmo lado de uma reta r e não são pontos desta reta, qual é o caminho mais curto ligando A e B e que toca a reta r, (figura 3)?

Gauss e a soma dos 100 primeiros inteiros

Conta-se que Gauss teria encontrado a soma dos 100 primeiros inteiros em 30 segundos, na escola primária. Seu professor, aborrecido com a algazarra que faziam as crianças, teria mandado que todos calculassem esta soma e Gauss apresentou a resposta rapidamente. Esta é, na verdade, uma operação que pode ser feita de cabeça se você tiver a criatividade de Gauss …

Rolagem de discos

Figura 6

Esta questão apareceu no SAT americano (um teste usado para admissões nas universidades, aplicado no mundo todo) em 1982. Apenas 3 alunos entre os 300 mil que fizeram o teste acertaram. Até os examinadores que prepararam o problema erraram a solução e a questão teve que ser retirada da pontuação. No entanto é possível resolver essa questão com conhecimentos do nosso ensino médio.

O raio do disco A (vermelho, na figura 6) é de 1/3 do raio do disco B (cinza). O disco A desliza sem escorregar sobre o disco B até dar uma volta completa em torno do disco B e retornar para a sua posição original.

Quantas voltas o disco A terá dado em torno de si mesmo?

Leia também

Desafios Lógicos