Jornada Python - André Guilhon
Jornada Python - André Guilhon
Sobre a obra:
Sobre nós:
eLivros .love
Converted by convertEPub
André Guilhon Juliana Guamá
Antonio Muniz Karine Cordeiro
Cláudio Gomes Rodrigo Isensee
Eduardo Gaspar Tatiana Escovedo
Jornada Python
Uma jornada imersiva na
aplicabilidade de uma das
mais poderosas
linguagens de
programação do mundo
Rio de Janeiro
2022
Copyright© 2022 por Brasport Livros e Multimídia Ltda.
Todos os direitos reservados. Nenhuma parte deste livro poderá ser
reproduzida, sob qualquer meio, especialmente em fotocópia (xerox),
sem a permissão, por escrito, da Editora.
Editor: Sergio Martins de Oliveira
Gerente de Produção Editorial: Marina dos Anjos Martins de Oliveira
Editoração Eletrônica: Abreu’s System
Capa: Trama Criações
Técnica e muita atenção foram empregadas na produção deste livro.
Porém, erros de digitação e/ou impressão podem ocorrer. Qualquer
dúvida, inclusive de conceito, solicitamos enviar mensagem para
[email protected], para que nossa equipe, juntamente com o
autor, possa esclarecer. A Brasport e o(s) autor(es) não assumem
qualquer responsabilidade por eventuais danos ou perdas a pessoas ou
bens, originados do uso deste livro.
BRASPORT Livros e Multimídia Ltda.
Rua Washington Luís, 9, sobreloja – Centro
20230-900 Rio de Janeiro-RJ
Tels. Fax: (21)2568.1415/3497.2162
e-mails: [email protected]
[email protected]
[email protected]
www.brasport.com.br
Jornada Colaborativa
Referências bibliográficas
Dedicatória e agradecimentos
try:
#faz algo
except:
#trata exceção
import this
Python 2 Python 3
A versão 3.0 foi lançada em 2008 com o objetivo de corrigir vários
problemas de design e facilitar mais ainda o aprendizado para os que
estão iniciando, segundo o próprio Guido van Rossum. Como pode ser
notado nos exemplos a seguir, a principal mudança foi em alguns tipos
básicos.
Python 2 Python 3
u "José toma café com açúcar." "José toma café com açúcar."
Python 2 Python 3
Tipos básicos – Inteiros e longos
No Python 2, um valor com uma magnitude maior se tornava inteiro
longo, então se acrescentava “L” no final do inteiro. Já no Python 3, isso
acabou: todos são considerados inteiros.
Tabela 2.4. Comparação entre versões – Tipos básicos inteiros e longos.
Fonte: a autora.
x = 10000000000000000L x = 10000000000000000
long(x) int(x)
Python 2 Python 3
if x <> y: if x != y:
pass pass
Python 2 Python 3
Tipagem dinâmica
Nas linguagens de tipo de variável forte, o compilador não realiza
transformações inesperadas de tipos em operações com elementos de
tipos diferentes. Nesse sentido, Python é uma linguagem de tipagem
forte porque, ao declarar a variável de determinado tipo, essa condição
prevalecerá para gerar o resultado esperado nas operações (PYTHON
WIKI, s.d.). Python somará 1 + 0.1 gerando o resultado esperado 1.1.
Mas será impossível, por exemplo, realizar:
>>> print(pesquisa)
Resulta em:
>>> lista_variaveis_texto = [
... 'um',
... 'dois',
... 'tres',
... 'quatro',
... 'cinco',
... 'seis',
... 'sete',
... ]
>>> for numero, texto in enumerate(lista_variaveis_texto):
... vars()[texto] = numero + 1
>>> print(um, dois, tres, quatro, cinco, seis, sete, sep=' < ')
A linguagem de alto nível serve como uma “cola” para unir módulos e componentes,
criando aplicativos especializados rapidamente. Em certo sentido, a linguagem se
torna uma ‘estrutura de script’, permitindo a criação acelerada de protótipos de novos
aplicativos. O desenvolvimento de módulos de extensão para a linguagem
corresponde tanto quanto possível a uma especialização tardia do código (SANNER,
1999).
Multifuncionalidade
Uma espécie de ímpeto que aparece em muitos pythonistas é a vontade
de escrever um sistema operacional ou então programar robótica
diretamente. A linguagem de alto nível é limitada para esse tipo de
função, portanto seria necessária a utilização de adaptadores para outra
linguagem capaz de manipular a alocação de memória ou acessar o
hardware. Entretanto, a linguagem possui muitos adeptos, e até
mesmo essa limitação tende a ser transposta com o tempo
(MICROPYTHON, s.d.).
Python resolve 99% de todas as outras necessidades computacionais
do programador. A linguagem pode ser executada nos sistemas UNIX,
DOS/Windows e Mac, contando com uma variedade de módulos que
abstraem problemas de diversas áreas de atuação (RASHED; AHSAN,
2012). Embora não esteja no escopo deste livro, com o módulo Tkinter
é possível criar programas para execução nos referidos sistemas
operacionais. Outras possibilidades frequentemente utilizadas são os
websites funcionais com Django ou Flask, APIs com Django REST
framework, jogos com pygame (também fora do escopo) e mais. Além
disso, possui funcionalidades científicas em ciência de dados,
inteligência artificial, aprendizado de máquina, análise avançada,
matemática pura e aplicada, representações gráficas, computação em
paralelo para grandes volumes de dados e talvez até problemas a serem
descobertos na computação quântica.
Desempenho
Seguindo o raciocínio da abstração e versatilidade, para atingir o
desprendimento alcançado pela linguagem passamos por uma sutil
troca pelo desempenho. Por exemplo, a tipagem dinâmica requer a
verificação de tipos, ao contrário da tipagem estática, onde o tipo já
está declarado. Nesse caso, somente ler é mais rápido do que ler e ve-
rificar. Uma linguagem tão dinâmica é mais difícil de ser otimizada, o
que justifica o ganho de desempenho em relação a outras linguagens,
as quais comprometem a flexibilidade em troca da performance
(SHAW, 2018).
Afirmar apaixonadamente que Python tem alto desempenho requer
contextualização, não pela falta de paixão, mas pelo rigor científico.
Quando a performance é um ponto crucial da execução, podemos
aceitar a utilização de outras linguagens onde esse requisito seja uma
de suas vantagens. Quando a aplicação puder ser relativamente inferior
em desempenho de execução do que linguagens que manipulam níveis
computacionais mais baixos, então Python ofereceria a melhor solução
em projeto e desenvolvimento pela flexibilidade, compreensão em
equipes e testes.
Portanto, ao falarmos de desempenho estritamente voltado para
velocidade de execução, há outras linguagens mais indicadas. Mas, se,
por exemplo, o desafio for processar uma grande quantidade de dados
recolhidos de um website que organiza as informações em tabelas
HTML, converter esse material em dataframes, aplicar um modelo
preditivo não supervisionado com testes dinâmicos de parametrização,
gerar resultados periódicos a serem enviados para um banco em SQL e
consequentemente a uma REST API, disponibilizar gráficos periódicos
em uma página da internet, a página e a API com controle funcional de
acessos, tudo isso em poucos meses de desenvolvimento, neste caso,
Python tem melhor desempenho!
Produtividade
A tipagem dinâmica nos oferece um enorme ganho na produtividade,
pois a declaração das variáveis é um processo extremamente simples.
Essa simplicidade permeia a linguagem Python, de maneira que as
atividades de escrita e leitura tornam-se acessíveis a diferentes
programadores. Mais ainda, em certos casos, projetar e programar
caminham simultaneamente, na medida em que o processo de escrita
do código é tão aproximado de um pseudocódigo ou de um comentário
explicativo.
Sendo uma linguagem de alto nível, não nos preocupamos com muitos
detalhes que atrapalham no processo da resolução de problemas. O
pythonista conta com todo o ócio criativo de apenas escrever as
instruções e focar no resultado. Nota-se que entender como funcionam
os módulos é importante para não assumir cegamente os resultados
esperados como corretos. A filosofia de reutilização do código que
permeia a criação da linguagem, para ser colocada em produção,
precisa de testes, o que é uma tarefa relativamente fácil. Há inclusive
métodos de codificação orientados aos testes, o que basicamente
significa codificar em resposta a testes previamente definidos.
Python tem uma produtividade de desenvolvimento multifuncional,
onde uma mesma linguagem alcança um espectro amplo de problemas
e aplicações. Python apresenta ótimo desempenho em diversos
contextos, ressaltando a balança entre flexibilidade e performance.
Todos esses fatores fazem da programação em Python uma atividade
extremamente prazerosa.
Comunidade
A comunidade Python é reconhecidamente aberta a iniciantes. Essa
característica advém da própria intenção por trás da criação da
linguagem. Tendo sido fortemente influenciada pela linguagem ABC,
cujo objetivo em parte era ensinar programação no seu formato mais
básico, Python veste essa característica facilitadora na sua arquitetura
(VAN ROSSUM, 1996). Reforçando essa ideia, a comunidade está
voltada para colaborar com a resolução de problemas e o aprendizado.
A ideia está estampada na página oficial: “nossa comunidade pode
ajudar no suporte aos iniciantes, experts, e colabora com a base aberta
e incremental de conhecimento”1. Nas palavras do criador:
Seu sucesso é o produto de uma comunidade, começando pelos pioneiros que a
adotaram quando publiquei Python pela primeira vez na rede, e que espalharam a
notícia em seu próprio ambiente. Me enviaram elogios, críticas, solicitações,
contribuições e revelações pessoais por e-mail. Estavam dispostos a discutir todos os
aspectos da linguagem na lista de discussão que logo criei, além de me educar, ou me
levar na direção correta, quando a minha intuição inicial falhou (VAN ROSSUM,
1996).
1 <https://www.python.org/community/>.
4. Biblioteca padrão e
documentação oficial
Paulo R. Z. Pinto
Definições
Neste capítulo iremos abordar as principais características da biblioteca
padrão Python e os principais componentes que acompanham uma
instalação básica de Python 3.8. Porém, consideramos importante
iniciarmos pela definição e diferenciação de alguns dos termos
utilizados para designar esses componentes:
✓ Módulo é um objeto que funciona como uma unidade
organizacional básica de código Python, geralmente composto por
um único arquivo do tipo “.py”. Um módulo define um namespace
contendo objetos Python arbitrários (instâncias, classes, funções,
etc.).
✓ Pacote (package) é a forma de definir namespaces (estruturado
em uma pasta, ou arquivo tipo “.zip”) contendo um conjunto de
módulos, permitindo que módulos de mesmo nome possam
coexistir, sem interferência. Para um componente ser considerado
um pacote, deve existir uma pasta contendo um arquivo chamado
“__init__.py” (mesmo que vazio).
✓ Biblioteca (library) é um conceito mais genérico, que pode
designar um projeto externo, composto por um único módulo ou
um pacote ou um conjunto de pacotes. As bibliotecas são projetos
de terceiros que estendem as funções da linguagem e que são
instaladas copiando os arquivos necessários para pastas
específicas de um ambiente Python. Essa instalação em geral é
feita por ferramentas de gestão de ambientes, como o pip, o conda
ou um instalador próprio.
✓ Biblioteca padrão (standard library, também abreviada para stdlib)
é o conjunto de pacotes e módulos que acompanham uma
instalação padrão de Python. Geralmente são considerados como
“parte da linguagem” (por exemplo: math, time, string, sys, etc.).
Não é necessário instalar nada além do Python para ter as
funcionalidades da biblioteca padrão, que podem ser importadas
em seus scripts sempre que necessário.
Ao fazer a instalação do Python 3.8, também são disponibilizados tipos
de dados, algumas instruções e funções que serão vistos na Parte III
deste livro (Fundamentos de Programação). Tendo em vista a grande
quantidade de conteúdo, não será possível detalhar e exemplificar o
funcionamento de todos os componentes disponibilizados na biblioteca
padrão do Python. Recomendamos a leitura da documentação oficial
no site2.
Para listar os módulos e pacotes que compõem a biblioteca padrão do
Python, abra o terminal do seu sistema operacional e execute:
$ pip list
package Version
----------- -------
pip 20.2.2
setuptools 49.6.0
2 <https://www.python.org/>.
5. PEP – Python Enhancement
Proposal
Sérgio Berlotto Jr.
Como vimos até aqui, até mesmo as PEPs são orientadas por PEPs, e
isso acontece com tudo no Python. Então, como curiosidade, vamos ver
em seguida algumas PEPs interessantes:
✓ PEP-0: indexação de todas as PEPs.
✓ PEP-20: descreve o Zen do Python (Zen of Python). O Zen do
Python é uma lista de curtos pensamentos e ideais que resumem
como o Python funciona.
✓ PEP-3000: descreve processos e ideias sobre a criação do Python
na versão 3.
✓ PEP-257: descreve as convenções para docstrings.
✓ PEP-8: descreve os padrões de formatação de código mais
indicados para utilizar com Python.
A PEP-8 é a PEP com a qual você provavelmente mais terá contato. Ela
é muito importante pois descreve justamente os padrões de codificação
para programas e scripts feitos em Python. Python tem uma
característica muito particular, que é a indentação e o escopo definidos
por meio de espaços e não por chaves. Seguir a PEP-8 para criar os seus
códigos em Python é muito importante e facilita o entendimento entre
times de desenvolvedores. A PEP-8 é tão importante e o assunto é tão
comentado que temos várias ferramentas que nos auxiliam na
formatação do código, como flake, pep8, bandit, entre outros. Todas as
ferramentas seguem as indicações da PEP-8.
Vimos então que as PEPs são instrumentos essenciais de manutenção e
definição dos processos e features da linguagem Python. É muito
importante conhecer as principais PEPs para se tornar um bom
pythonista. E fique atento também às suas constantes atualizações.
PARTE II.
PREPARANDO O AMBIENTE
6. Instalando
Marcus Paiva
Naiara Cerqueira
Windows
Antes de começar a instalação no seu computador, vamos verificar se o
Python 3 já está instalado. Para isso, abra a tela de pesquisa do
Windows (clique no ícone de lupa ou pressione as teclas Win+S) e
digite “python”. Se o Python estiver instalado, então um aplicativo
(app) aparecerá com o nome “Python 3.x”, onde x é a versão.
Caso não apareça um app, recomenda-se instalar o Python via
instalador. Para tanto, baixe o executável no site oficial3 e fique atento
para fazer o download da versão correta para o seu computador, 64 bits
ou 32 bits.
Logo no início da instalação, você deve selecionar a opção ‘Add Python
X.X to PATH’. O restante da instalação é bastante simples, sendo
necessário, basicamente, confirmar e avançar em cada etapa. Esse
processo configurará na máquina o interpretador, as bibliotecas padrão
da linguagem e a documentação.
Caso prefira instalar via terminal, abra o prompt de comando e digite:
$ msiexec /i Python<version>.msi
Linux
A instalação do Python no Linux pode funcionar de forma diferente
para as distribuições Debian/Ubuntu e para as distribuições Red Hat
Enterprise (RHEL)/Fedora/CentOS. Para o Debian/Ubuntu, vamos
primeiro começar verificando se o Python 3 já está instalado. Para isso,
abra o terminal e digite o seguinte código:
$ which Python
Ou
$ which Python3
ou
$ python3 --version
Com isso o Python 3 estará instalado no seu Linux, pronto para que
você crie o seu primeiro programa.
MacOS
Antes de começar a instalação, vamos verificar a existência do Python
no sistema. Abra o terminal e digite o seguinte comando:
$ which python
ou
$ which python3
$ xcode-select --install
Instalando o brew:
$ brew doctor
Confira a versão:
$ python3 --version
3 <https://www.python.org/downloads/windows/>.
4 <https://www.python.org/downloads/windows/>.
7. Primeiro programa
Adamys Monnerat
Tatiana Escovedo
Guilherme Rozenblat
primeiroNumero = 1; segundoNumero = 2;
Modo script
Os scripts são “roteiros” seguidos por sistemas computacionais e
carregam informações que são processadas e transformadas por um
programa principal. Caso você necessite de um programa que realize a
soma de notas escolares e exiba o resultado, um computador, por meio
de um script, vai realizar (interpretar) uma sequência de passos para
executar a soma e apresentar o resultado na tela. Os scripts também
podem ser utilizados para modificar uma fonte que será exibida em
algum site ou programa, realizar o processamento de condicionais, de
repetições e muitas outras finalidades.
Mesmo que você ainda não seja um programador, você pode perceber
exemplos de execução de scripts no dia a dia como um usuário da
internet. Por exemplo: ao clicar em algum botão do seu navegador, a
barra com opções é apresentada. Aparentemente é uma ação simples.
Entretanto, para que isso aconteça, um script foi ativado no momento
em que o usuário clicou com o botão do mouse e desencadeou a
exibição do item.
Interpretador
O interpretador é um programa de computador que executa instruções
escritas em uma linguagem de programação, e Python é uma
linguagem interpretada. A finalidade de um interpretador é utilizar de
mecanismos para a execução do programa, executando o código criado
pelo usuário (comunicação textual) de forma direta ou traduzindo para
alguma representação intermediária.
Para que isso ocorra, certos tipos de tradutores transformam um
código-fonte em uma linguagem simplificada, denominada código
intermediário, que pode ser diretamente “executada” por um programa
chamado interpretador. Nós podemos imaginar o código intermediário
como uma linguagem de máquina de um computador abstrato
projetado para executar o código-fonte.
Interpretadores são, em geral, menores que compiladores, facilitando a
implementação de construções complexas em linguagens de
programação. Entretanto, o tempo de execução de um programa
interpretado é geralmente maior que o tempo de execução deste
mesmo programa compilado, pois o interpretador deve analisar cada
declaração no programa a cada vez que é executado e depois executar a
ação desejada, enquanto o código compilado apenas executa a ação
dentro de um contexto fixo, anteriormente determinado pela
compilação. Esse tempo no processo de análise é conhecido como
“overhead interpretativa”.
CLI
A CLI significa interface de linha de comando (Command Line Interface).
Resumidamente, é uma interface que suporta passagem de parâmetros
via linha de comando em terminais. É um programa que aceita
parâmetros para executar comandos e iniciar um novo sistema de I/O
(input e output) no terminal sem finalizar o programa. O exemplo a
seguir ilustra o CLI5:
import click
@click.command()
@click.option("--count", default=3, help="Number of greetings.")
@click.option("--name", prompt="Your name", help="The person to
greet.")
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT
times."""
for _ in range(count):
click.echo(f"Hello, {name}!")
if __name__ == '__main__':
hello()
Jupyter Notebook
O Jupyter Notebook6 é um projeto open source que surgiu a partir do
IPython em 2014 como um ambiente computacional web e interativo
baseado na estrutura servidor-cliente. IPython (Interactive Python7) é
um projeto destinado a proporcionar arquitetura para computação
interativa usada pelo projeto Jupyter Notebook.
O Jupyter Notebook permite criar e manipular documentos de
notebook contendo uma lista sequencial de células com código, texto
(usando markdown), fórmulas matemáticas e estatísticas, gráficos ou
imagens. O Jupyter Notebook pode ser utilizado para seleção e
transformação de dados, modelagem estatística, simulação de valores
e resultados, visualização de dados, aprendizado de máquina, entre
outros. Podemos ainda utilizar diversas linguagens como o Python, R,
Julia, Ruby, Scala e Haskell, além de integração com big data e
ferramentas como Apache Spark e TensorFlow.
O Jupyter Notebook possui três componentes:
Notebook web application: aplicativo web interativo para escrever
✓ e executar códigos de maneira interativa e criar documentos do
notebook, com funcionalidades como destaque de sintaxe
automático, anexar resultados dos cálculos ao código que os
gerou, autocompletar o código com Tab, indentação, equações
matemáticas com sintaxe LaTeX, texto descritivos, visualização e
apresentação dos dados.
✓ Notebook documents: documentos (extensão “.ipynb”) com a
representação de todo o conteúdo visível no notebook web
application, incluindo entradas e saídas dos cálculos, texto
narrativo que acompanha o código, equações, imagens e
representações de objetos de saída.
✓ Kernels: a arquitetura do kernel permite que o código seja
executado em várias linguagens de programação diferentes,
comunicando a saída para o notebook web application e o
navegador. Cada notebook document possui seu próprio kernel.
Spyder
O Spyder12 (Scientific Python Development Environment) é um ambiente
de desenvolvimento robusto com edição avançada, testes interativos,
recursos de depuração, exploração e visualização de dados. O Spyder é
instalado como parte das distribuições Anaconda que também já traz
pacotes científicos como NumPy, SciPy, Pandas, IPython, QtConsole,
Matplotlib e SymPy. O Spyder é uma IDE de código aberto leve e
extensível com um bom sistema de plugins e API.
O Spyder foi projetado por cientistas, engenheiros e analistas de dados,
fornecendo um ambiente científico poderoso para o Python. Ele
salienta os erros, apresenta autocompletar de código, destaca funções
e variáveis do código com cores personalizáveis, funciona no editor
multilíngue, autoindentação inteligente com base na estrutura do
código, editor de atalhos de teclado personalizável, possui um
explorador de variáveis que mostra o valor armazenado dentro de cada
uma dispensando comandos de print na tela, disponibiliza recurso para
visualizar instantaneamente qualquer documento de objeto e modificar
seus próprios documentos e permite navegação por células, funções,
classes, blocos, etc.
Por meio do recurso Profiler, avalia o impacto no desempenho das
funções no arquivo de código para identificar gargalos e potencializar a
otimização. O recurso Static Code Analysis identifica erros de estilo e
sintaxe, práticas inadequadas de desenvolvimento e outros problemas
no código. O Project Explorer é um recurso integrado com
versionamento GIT. O recurso permite salvar e restaurar com facilidade
propriedades, sessões e configurações para trabalhar em várias
aplicações de desenvolvimento. O recurso ‘Ajuda’ permite pesquisar
uma vasta documentação ou código-fonte para objetos Python (como
classes, funções, módulos e bibliotecas), sendo acionado via atalho ou
automaticamente ao digitar um parênteses esquerdo “(” após o nome
da função.
PyCharm
O PyCharm13 é uma IDE com um amplo conjunto de ferramentas
integradas para criar um ambiente de desenvolvimento em Python para
ciência de dados com uma interface simples, personalizável e completa,
facilitando aqueles que estão dando os primeiros passos com Python.
Essa IDE é desenvolvida pela companhia JetBrains e possui as versões
Community, Edu (projetos de código aberto e gratuitas sob a licença
Apache) e a versão Professional (edição comercial com mais recursos
como frameworks web, desenvolvimento remoto e distribuído,
ferramentas de banco de dados e integração com IPython Notebook).
Além das funcionalidades tradicionais de console e depurador, a IDE
facilita a aplicação de um código limpo e de fácil manutenção, controle
de qualidade com verificação do PEP-8, aumento da produtividade,
assistência aos desenvolvedores em tarefas rotineiras, utilização de
várias versões do Python com o virtualenv, expansão de recursos com
plugins e suporte a pacotes científicos que também estão no Anaconda,
como NumPy, Matplotlib e SciPy.
O PyCharm possui um depurador com interface gráfica onde o
desenvolvedor pode acompanhar, desativar ou silenciar os breakpoints,
além de ver e editar os valores de variáveis e parâmetros. A IDE ainda
possui refatoração de código automatizada de forma rápida e segura,
inspeção e análise de código, autocompletar inteligente com destaque
de erros e correção de sintaxe, recursos avançados de navegação no
projeto, personalização de teclas de atalho e diferentes temas de cores,
suporte a diversos sistemas de controle de versão e acesso a manuais
de bibliotecas.
6 <https://jupyter.org/>.
7 <https://ipython.org/>.
8 <https://www.anaconda.com/>.
9 <https://colab.research.google.com/>.
10 <https://databricks.com/>.
11 <https://docs.aws.amazon.com/dlami/latest/devguide/tutorial-jupyter.html>.
12 <https://www.spyder-ide.org/>.
13 <https://www.jetbrains.com/pycharm/>.
14 <https://code.visualstudio.com/docs/languages/python>.
9. Criação de ambiente virtual
Sérgio Berlotto Jr.
Alexandra Raibolt
Juliana Guamá
Vamos agora falar um pouco de cada uma delas para que você possa
entender como funcionam os ambientes virtuais e qual o propósito de
cada uma dessas ferramentas.
Você pode ver essa lista até como uma certa ordem de evolução. De
forma geral, a criação de um ambiente virtual funciona com a estrutura:
$ comando_pacote [caminho_p_raiz_projeto/nome_ambiente_virtual]
[versao_Python]
venv15
Esta ferramenta já vem disponível com o Python em seu core desde a
sua versão 3.6. Ou seja, não é necessário instalar nada para já conseguir
gerenciar alguns ambientes virtuais.
Vamos iniciar criando o nosso primeiro ambiente virtual, chamado
projeto1, para instalar todas as bibliotecas de que necessitamos.
O nome do ambiente virtual pode ser qualquer um, vai depender de
você conseguir escolher um bom nome que represente o que ele
contém ou qual sua finalidade. A PEP 405, que informa as boas práticas
sobre uso de ambientes virtuais, não possui sugestões de
nomenclatura, porém usualmente escolhe-se o nome do projeto ou os
nomes que indiquem que a pasta possui as configurações de ambiente
virtual como “venv”, “pyenv”, “environment”. Portanto, criar um
ambiente chamado projeto1 já o faz pensar que esse ambiente contém
as libs de que o projeto1 necessita, seja ele qual for.
Execute o seguinte comando no terminal do sistema operacional:
Toda vez que o símbolo “$” aparecer, entenda que é um comando a ser
executado em terminal, independentemente do sistema operacional.
Este comando vai criar uma pasta no diretório corrente, chamada de
projeto1, e vai colocar dentro dela toda a estrutura e configurações
necessárias para que um ambiente virtual possa ali estar.
Você não necessita decorar o que existe dentro dessa pasta, e
geralmente nem vai precisar alterar nada, mas é bom ter uma ideia de
alguns itens.
O que teremos dentro do diretório do ambiente virtual criado é:
✓ Pasta bin (em Linux) ou Scripts (em Windows): contém os
executáveis do Python, pip e outros necessários ou que forem
acrescentados.
✓ Pasta lib/PythonX.Y/site-packages/ (em Linux) ou Lib/site-
packages/ (em Windows): é o diretório onde os arquivos das
bibliotecas estarão. Este diretório será utilizado automaticamente
pelo ambiente Python quando você estiver com o ambiente virtual
ativo.
✓ projeto1/pyvenv.cfg: o arquivo de configuração do ambiente.
Ainda no comando de criação do ambiente virtual, temos como
informar um caminho no nome do ambiente, para que ele não seja
criado na pasta corrente, veja:
Ou em plataforma Windows:
Isso fará com que a estrutura do nosso ambiente virtual seja criada na
pasta .venvs dentro da nossa pasta pessoal.
Além disso, podemos também criar um ambiente virtual informando
com qual executável Python queremos trabalhar; assim, podemos criar
ambientes virtuais que executam versões diferentes de Python que
estejam já instaladas em nossa máquina. Isso é interessante, pois
muitas vezes, em distribuições Linux, já temos o Python 3 e o 2
instalados ou, em plataforma Windows, podemos ter instalado mais de
uma versão também. Portanto:
$ source projeto1_py2/bin/activate
(projeto1_py2) $ python -V
Python 2.7.17
Agora que você já tem seu ambiente virtual criado, é necessário fazer
uma ação para ativá-lo, ou seja, você deve habilitar o uso desse
ambiente para que possa usufruir das bibliotecas nele instaladas ou
instalar novas.
Para ativar o ambiente, você precisa executar o seguinte comando no
terminal do Linux:
$ source projeto1/bin/activate
$ projeto1\Scripts\activate
Com isso, você chama o script que faz a ativação do ambiente virtual e
configura as variáveis de ambiente necessárias para que, a partir desse
momento, você utilize o ambiente virtual e não mais o ambiente do
sistema operacional original. Mas como sabemos se estamos com um
ambiente virtual ativo? Temos duas formas de identificar.
Visualmente, depois de ativar o ambiente conforme explicação
anterior, será exibido o nome do ambiente ativo entre parênteses ao
lado esquerdo do seu terminal, desse jeito:
(projeto1_py2) $ _
Outra forma, que é bastante útil quando precisamos criar scripts que
por algum motivo necessitam estar em um ambiente virtual, é por meio
da variável de ambiente VIRTUAL_ENV, que contém o path do
ambiente virtual que está ativo. Esta variável de ambiente é criada e
populada quando um ambiente virtual é ativado. Você não precisa fazer
nada, basta utilizá-la quando necessário, estando com seu ambiente
virtual ativo.
Agora que você já sabe como criar ambientes virtuais, a instalação dos
pacotes segue o padrão do Python. No momento em que você executar
um pip install, como será mostrado no próximo capítulo, estando com
um ambiente virtual ativo, o pacote será instalado no diretório site-
packages do ambiente, e não no site-packages do Python instalado no
sistema.
Caso você queira trocar de ambiente, você precisa desativar esse
ambiente virtual, para que você possa ativar outro, ou então sair dele
para executar seus programas em seu ambiente original da instalação
padrão.
Para desativar o ambiente virtual, seja em Linux ou Windows, execute o
comando deactivate:
$ deactivate
Este comando faz com que seu ambiente deixe de estar ativo. As
variáveis de ambiente que foram alteradas para que o ambiente virtual
entrasse em funcionamento voltam ao normal do seu sistema e você,
assim, passa a utilizar novamente a instalação padrão do Python no
sistema operacional.
Em ambiente Windows:
$ virtualenv projeto1
$ source projeto1/bin/activate
(projeto1) $ python -V
Python 3.8.2
(projeto1) $ deactivate
virtualenvwrapper16
Agora que entendemos como os ambientes virtuais funcionam,
podemos trabalhar com uma ferramenta que facilita ainda mais no uso
deles, apesar de possuir uma configuração inicial mais complexa. Assim,
a primeira coisa que é preciso entender é que talvez você precise de um
pouco mais de experiência com ambientes virtuais para utilizá-la.
Você pode ter notado que é necessário informar uma pasta, seja ela no
mesmo diretório ou em um diretório diferente, para que possamos criar
o ambiente virtual.
Sendo assim, quando falamos em organização do nosso ambiente em
geral e do ambiente dos nossos projetos, vimos que a organização
ficará a nosso cargo quando utilizamos o virtualenv ou o venv, pois nós
é que necessitamos definir onde os ambientes virtuais serão criados,
organizando tanto nomes quanto localização. Isso não é
necessariamente ruim, mas pode nos levar a um pequeno caos se
fizermos de forma diferente em cada projeto, criando um ambiente em
um lugar e outro ambiente em outro.
É aí que o virtualenvwrapper entra. Como a própria documentação dele
fala, o virtualenvwrapper é um conjunto de extensões sobre as
ferramentas de virtualenv. Essas extensões incluem wrappers (do inglês
“invólucro”, “empacotador”) que auxiliam no gerenciamento de vários
ambientes virtuais para projetos diferentes.
Vamos entender um pouco então. O trabalho que teremos que fazer é
o mesmo, criar e ativar ambientes, instalar dependências e desativar
ambientes. A diferença é “como” o virtualenvwrapper e suas extensões
nos ajudam a fazer isso melhor.
Veja quais são as vantagens de utilizar o virtualenvwrapper:
1. Organiza todos os ambientes virtuais em um local apenas.
2. Disponibiliza ferramentas como criar, copiar e excluir, úteis
quando existem múltiplos ambientes virtuais.
3. Com um comando apenas conseguimos trocar de ambiente
virtual.
4. Facilidade de tab completion para os comandos de gestão dos
ambientes virtuais, inclusive com o nome dos ambientes.
5. Permite a configuração de hooks para todas as operações feitas.
6. Um sistema de plugins que permite que criemos mais extensões.
$ export WORKON_HOME=~/.virtualenvs
$ mkdir -p $WORKON_HOME
$ source /usr/local/bin/virtualenvwrapper.sh
Para desativar:
$ deactivate
$ lssitepackages
$ lsvirtualenv
Entre tantas possibilidades, estes são alguns dos comandos que nos
auxiliam muito por meio do virtualenvwrapper.
pipenv17
Mas não para por aí. Depois de saber como gerenciar seus ambientes
virtuais com o venv, virtualenv e virtualenvwrapper, você pode
trabalhar com uma ferramenta que faz “todo o trabalho sujo” por você.
Se você achou que o virtualenvwrapper já era uma grande ferramenta
(o que não deixa de ser verdade), é hora de conhecer o pipenv.
O pipenv é uma ferramenta que traz para o Python tudo o que tem de
melhor no mundo dos empacotadores das outras linguagens e é uma
ferramenta destinada a desenvolvedores mais experientes.
Ele automaticamente cria e gerencia ambientes virtuais e, além disso,
administra também a instalação das bibliotecas utilizadas no projeto
por meio de um único arquivo chamado pipfile.
Como o pipenv é uma união de ferramentas de gerenciamento, o
trabalho com ele é um pouco diferenciado. Vamos ver do que ele é
capaz.
A instalação do pipenv ocorre da maneira padrão:
$ set PATH=%PATH%;`<caminho_completo_aqui>`
Um detalhe aqui que não temos nas outras ferramentas é que, como o
pipenv trabalha com o gerenciamento tanto de ambientes virtuais
como de dependências, foi criado o arquivo pipfile, que é usado para
rastrear as dependências do projeto. Sendo assim, não precisamos mais
nos preocupar com o famoso arquivo requirements.txt, que
precisamos sempre criar manualmente.
Agora você quer testar seu ambiente? Então execute o seguinte
comando:
$ pipenv shell
conda18
Conda é um gerenciador de ambientes virtuais e de bibliotecas que vem
instalado junto com o Anaconda, que é um projeto que agrega várias
bibliotecas e ambientes utilizados em análise de dados, como Python e
R, várias bibliotecas dessas linguagens, entre outras coisas.
O conda tem o funcionamento muito parecido com o venv, mas já vem
com baterias incluídas. Vejamos alguns comandos.
Para criar um ambiente virtual:
$ conda deactivate
Para instalar uma biblioteca:
15 <https://docs.python.org/3/library/venv.html>.
16 <https://virtualenvwrapper.readthedocs.io/en/latest/>.
17 <https://pipenv.pypa.io/en/latest/>.
18 <https://docs.conda.io/en/latest/>.
10. Gerenciadores de pacotes
Jefferson da S. Nascimento
Cláudio Henrique Franco Gomes
$ pip list
$ python -m pip list
$ conda list
19 <https://pip.pypa.io/>.
20 <https://pypi.org/>
21 <https://anaconda.org>.
22 <https://anaconda.org/anaconda/repo>.
23 <https://www.anaconda.com/>.
24 <https://docs.conda.io/en/latest/miniconda.html>.
11. Linters
Cláudio Henrique Franco Gomes
exemplo_de_docstring()
Este é um docstring.
PyCharm
A IDE PyCharm também oferece a possibilidade de instalação de
plugins. Para instalar um linter, basta ir em File > Settings > Plugins e
buscar por Pylint (Figura 11.3).
Figura 11.3. Plugin Pylint para PyCharm.
Fonte: o autor.
Spyder
No Spyder, a habilitação de um linter é mais fácil ainda: basta ir em
Tools > Preferences ou pressionar “Control + Alt + Shift + P”, opção
Editor, aba Code Introspection/Analysis e marcar as opções desejadas
(Figura 11.5). A análise do código pode ser realizada tanto a cada
período de tempo quanto somente ao salvar o arquivo. Os erros são
indicados na borda lateral do editor (Figura 11.6). Veja mais detalhes na
documentação do Spyder Static Code Analysis32.
Figura 11.5. Opções de configuração de linting do Spyder.
Fonte: o autor.
Jupyter Lab
No menu do Jupyter Lab, em Settings, escolha Enable Extension
Manager. Agora entre no Extension Manager, na barra lateral esquerda
no ícone que representa uma peça de quebra-cabeças. No campo de
busca, procure por “jupyterlab-flake8” e mande instalar a extensão
(Figura 11.7). Pode ser necessário fazer um novo build da IDE. Basta
aceitar e aguardar. Após instalada, a extensão passa a funcionar para
todo notebook no Jupyter Lab (Figura 11.8).
26 <https://pypi.org/project/pyflakes/>.
27 <https://www.pylint.org/>.
28 <https://flake8.pycqa.org/en/latest/>.
29 <https://pypi.org/project/mccabe/>.
30 <https://pypi.org/project/autopep8/0.8/>.
31 <https://github.com/google/yapf>.
32 <https://docs.spyder-ide.org/current/panes/pylint.html>.
33 <https://github.com/PyCQA>.
34 <https://meta.pycqa.org/en/latest/>.
PARTE III.
FUNDAMENTOS DE
PROGRAMAÇÃO
12. Tipos de dados e variáveis
Davi Luis de Oliveira
>>> valor = 1
>>> type(valor)
<class `int`>
Expressões
Expressões são as construções mais básicas da programação,
consistindo em associações entre variáveis, valores, operadores e
chamadas de função que produzem algum valor em tempo de
execução. Um exemplo simples de expressão pode ser a soma de dois
números inteiros:
>>> 1 + 2
3
>>> a = b = c = 1
Avaliação de expressões
Como regra geral, as expressões são normalmente avaliadas de acordo
com a ordem de precedência dos operadores envolvidos, assim como
na matemática. Por exemplo, na expressão do trecho de código a
seguir, a multiplicação acontece antes da soma:
>>> 3 + 2 * 5
13
>>> 1 + 2 + 3
6
>>> 2 * 3 + 1
7
>>> 2 * (3 + 1)
8
Avaliação em curto-circuito
Um tipo muito comum de expressão chama-se condicional e recebe
esse nome devido ao fato de seu resultado ser utilizado como condição
para a execução de algum trecho de código por meio de estruturas de
controle. Isso é possível porque todo valor em Python pode ser avaliado
em “contexto booleano”. Em outras palavras, pode ser considerado
verdadeiro (True) ou falso (False).
Os valores avaliados como falso em Python são: False, 0 (zero), None,
strings vazias e conjuntos vazios. Todos os outros valores são
considerados verdadeiros.
Esses valores (e, consequentemente, as expressões) podem ser
combinados por meio dos operadores lógicos and e or, de forma a
permitir condições mais gerais e completas. Ambos os operadores são
avaliados em curto-circuito, o que significa que seu resultado é
retornado assim que seja possível deduzi-lo, mesmo que a expressão
ainda não tenha sido avaliada por completo.
Para entender melhor, vamos a uma rápida revisão sobre a lógica
booleana. A operação and (e) produz um valor verdadeiro se, e somente
se, ambos os operandos forem verdadeiros. Caso contrário, o valor é
falso. Assim, no exemplo a seguir, a primeira linha é avaliada como True
(verdadeiro), enquanto a segunda é avaliada como False (falso).
Comentários
Os comentários não resultam em qualquer valor, não são executados
como instruções e são completamente ignorados pelo interpretador
durante a execução. Isso deve deixar algumas pessoas se perguntando:
“então para que servem os comentários?”. A verdade é que os
comentários são muito úteis e desempenham um papel importante na
programação.
A familiaridade com a sintaxe de uma linguagem não é garantia de que
será possível entender imediatamente qualquer código escrito nela.
Isso porque, na maioria das vezes, a chave para o funcionamento de um
algoritmo envolve algum conhecimento que vai além da linguagem em
si. Coisas como avançados cálculos matemáticos, estruturas de dados
complexas, arranjos técnicos muito específicos ou regras de negócio
complicadas podem ter sido empregados na sua construção, podendo
dificultar o trabalho de outros desenvolvedores que possam ter contato
com o código.
Para solucionar essa problemática, as linguagens de programação
possuem formas de escrever comentários, isto é, de adicionar
anotações ou informações para ajudar o entendimento por uma outra
pessoa ou até pelo próprio desenvolvedor que criou o código, quando
este precisar voltar a ter contato com um código que escreveu há muito
tempo.
Em Python, os comentários são declarados colocando o símbolo #
(cerquilha, jogo da velha, hashtag, sharp, como preferir chamar)
precedendo o texto, normalmente no início da linha. Tudo o que vier
depois desse símbolo até o final da linha é ignorado pelo interpretador,
mesmo que seja uma expressão válida.
Tipo Armazena
str textos
>>> id(user1)
139841703011744
>>> id(user2)
139841703011744
35Filósofo grego do século V a.C. Notório por defender, entre outras coisas, que o movimento,
as mudanças e transformações físicas, não existem, sendo apenas ilusões provocadas pelos
nossos sentidos.
15. Strings
Rafael Gonsalves Cruvinel
Escrevendo strings
Existem algumas formas de escrever strings em Python:
>>> start = 10
>>> "abcdefghijklmnopqrstuvwxyz"[start:]
'klmnopqrstuvwxyz'
>>> end = 10
>>> "abcdefghijklmnopqrstuvwxyz"[:end]
'abcdefghij'
>>> start = 5
>>> end = 10
>>> "abcdefghijklmnopqrstuvwxyz"[start:end]
'fghij'
>>> step = 2
>>> "abcdefghijklmnopqrstuvwxyz"[::step]
'acegikmoqsuwy'
>>> "010203040506070809"[1::2]
'123456789'
>>> "abcdefghijklmnopqrstuvwxyz"[10]
`k`
Formatando strings
Às vezes, precisamos inserir um número ou uma variável em uma string,
no meio de um texto. Para isso, existem alguns caminhos válidos,
conforme veremos a seguir.
Formatação ‘%-format’
No lugar de substituição utilize %s para substituir o valor de uma
string, %f o valor de um float e %d o valor de um inteiro.
Grupo de strings
Como comentado anteriormente, a codificação utilizada para Python 3
é UTF-8, chamado de Unicode. O padrão Unicode é compatível com o
padrão ASCII (American Standard Code for Information Interchange),
uma decodificação muito utilizada, contendo 128 caracteres.
A biblioteca padrão para a tratativa de textos é a string. Ao importá-la,
teremos acesso a funções relacionadas com o padrão ASCII, por isso a
necessidade de comentá-las. Temos algumas coleções de textos
interessantes para utilizarmos nos nossos projetos.
Somando strings
O operador soma auxilia a concatenar strings.
Multiplicação de strings
Para repetir uma mensagem muitas vezes, basta utilizar o operador de
produto, conforme o código a seguir:
>>> 10 * "ha"
hahahahahahahahahaha
Funções built-in36
O tipo str em Python possui várias funções ‘built-in’, isto é, funções que
já vêm prontas para uso e podem ser acessadas por tipos str. Vejamos
como funcionam algumas dessas funções a seguir. Para entender
melhor o que são funções e como acessá-las veja o Capítulo 21 –
Funções.
36 <https://docs.python.org/3/library/stdtypes.html>.
16. Coleções
Rodrigo Alves Mendonça
Tipos de coleções
Dentre os tipos de coleções, temos as sequências, os mapeamentos e
os conjuntos. As sequências são dados encadeados, nem sempre do
mesmo tipo. Entre suas implementações temos: list, tuple e range. Já
no mapeamento, a coleção implementa o conceito de chave e valor, e
sua implementação mais usada é o dict. Os conjuntos não armazenam
em sua estrutura dados repetidos e não guardam posição destes; suas
implementações mais usuais são o set e frozenset. O tipo string e
bytearray também são considerados sequências, porém não trataremos
deles neste capítulo. O Capítulo 15 – Strings trata exclusivamente do
tipo string.
Iteração
O Python suporta o conceito de iteração, ou seja, repetição. Esse
conceito é implementado usando basicamente dois métodos que
suportam esse padrão: container.__iter() e container.__next__().
Enquanto o __iter__() retorna o objeto em si, o método __next__()
retorna o próximo item do contêiner. Se não existir item a ser
retornado, ele lança uma exceção StopIteration. Ambos são necessários
para permitir que tanto os contêineres quanto os iteradores possam
usar as diretivas for e in.
Agora que já falamos de forma genérica sobre coleções, vamos falar de
alguns tipos específicos mais utilizados.
Lista (list)
As listas são sequências mutáveis, normalmente usadas para armazenar
coleções de itens. Você pode criar uma lista de diversas formas, porém
as mais comuns são:
✓ Usando um par de colchetes para criar a lista vazia: [ ] ou usando o
construtor list().
✓ Usando colchetes, separando itens com vírgulas: [‘a’, ‘b’] ou
list([‘a’, ‘b’]).
✓ Usando uma compreensão de lista: [x for x in iterável] (mais sobre
isso no Capítulo 20 – Comprehension).
O uso do construtor list() pode deixar seu código mais legível, porém
você deve ficar atento, pois ele recebe como parâmetro um iterável.
Ex.:
Tupla (tuple)
Tuplas são sequências imutáveis. As maneiras mais comuns de criar
tuplas são:
✓ Usando um par de parênteses para indicar a tupla vazia: () ou
usando o construtor tuple(). O construtor tuple() recebe um
iterável opcional, sendo necessário tomar cuidado ao usá-lo para
instanciar tuplas contendo strings, como vimos no caso das listas.
✓ Usando uma vírgula à direita. Ex.: 1, ou (1,).
✓ Separando itens por vírgulas. Ex.: 1, ‘b’, ‘c’ ou (‘a’, ‘b’, 2). Observe
que é a vírgula que cria uma tupla, não os parênteses. Os
parênteses são obrigatórios apenas no caso vazio ou para evitar
ambiguidade sintática. Por exemplo, funcao(a, b, c) é uma
chamada de função com três argumentos, enquanto funcao((a, b,
c)) é uma chamada de função com uma tupla como argumento
único.
Dicionário (dict)
O dicionário é um objeto do tipo mapeamento, é mutável e,
diferentemente do que vimos até agora, cada elemento é um par de
chave e valor, ou seja, ele mapeia valores de chaves para objetos. Ex.:
Views de dicionários
Os métodos items(), keys() e values() retornam uma nova view; esse é
um objeto de exibição que fornece uma visualização dinâmica das
entradas do dicionário. Isso significa que, quando o dicionário muda, a
visualização reflete essas alterações. Elas podem ser iteradas para gerar
seus respectivos dados e oferecer suporte a testes de associação. Ex.:
37 <https://docs.python.org/3/library/stdtypes.html#>.
17. Operadores e condicionais
Daniele A. Longato da Silva
Operadores de comparação
Igual a == Igualdade
Diferente de != Diferença
>>> 15 == 15
True
>>> 15 == "15"
False
>>> entrada = 10
>>> if type(entrada) is int:
... print(entrada, 'é um inteiro.')
10 é um inteiro.
entrada = 10.5
if type(entrada) is int:
print(entrada, 'é um inteiro.')
elif type(entrada) is str:
print(entrada, 'é uma string.')
elif type(entrada) is bool:
print(entrada, 'é um booleano.')
else:
print('A entrada é um', type(entrada))
O que acabou de acontecer foi que a instrução print() foi executada três
vezes seguidas, informando o variável x a cada repetição. Ou seja, o x
percorreu o intervalo especificado de três posições (0,1,2). Esse
intervalo foi especificado pela função range com o parâmetro 3. É
possível realizar o mesmo procedimento com a função while, conforme
o exemplo a seguir:
>>> x = 0
>>> while x < 3:
... print('Passou pelo número', x)
... x += 1
Passou pelo número 0
Passou pelo número 1
Passou pelo número 2
Desmistificando o for
O divertido do Python é que ele é bem flexível. Vamos verificar o que
podemos fazer com essas instruções. Podemos trabalhar com
intervalos de lista de texto, como no exemplo a seguir:
Isso mesmo, não acontece nada. Apenas fica garantido que o fluxo siga
da forma original.
As mesmas funções poderiam ser utilizadas no while em vez de for. Um
ótimo exercício seria executar o mesmo procedimento descrito em for,
mas utilizando o while.
Looping inline
Loop com apenas uma linha existe? Sim, existe. Veja um exemplo:
Loop infinito.
Loop infinito.
Loop infinito.
Definição e exemplos
Exceções são erros detectados durante a execução que podem ou não
ser gerados pelo próprio desenvolvedor, com finalidade de controlar o
fluxo ou o encerramento do código. Elas são definidas como classes
(veremos mais detalhes no Capítulo 26 – Classes, objetos, métodos,
atributos) que herdam as propriedades (Capítulo 28 – Herança,
Polimorfismo e Classes Abstratas) da classe BaseException.
Durante o ciclo de desenvolvimento, enfrentamos diversas exceções
quando tentamos executar um código, seja por erro lógico ou por erro
de sintaxe. Essas exceções podem ser internas do Python ou
customizadas de alguma biblioteca importada. Vejamos alguns
exemplos de exceções internas clássicas:
✓ KeyboardInterrupt: durante a execução, é verificado regularmente
se o usuário pressiona as teclas para ativar essa exceção
(normalmente Ctrl + C ou Delete). Esta exceção é bem útil no caso
de o código estar em um loop infinito ou para abortar a execução
do código:
Capturando exceções
Vimos alguns exemplos e algumas formas de ativá-las internamente. A
captura das exceções tem como objetivos que o processo não se
encerre de maneira abrupta e que se tenha alguma tratativa em relação
ao erro/exceção. Para capturá-las, utilizamos blocos de try, except e,
opcionalmente, finally. A parte do código que deve ser colocada dentro
de cada bloco é definida por:
✓ try: bloco onde será avaliada e levantada a exceção a ser
capturada.
✓ except: bloco onde será tratado o erro.
✓ finally: bloco que será executado, independentemente de ocorrer
ou não a exceção. Alguns exemplos práticos para a utilização do
finally são:
• salvar mudanças no banco de dados.
• fechar arquivos (Capítulo 22 – Manipulação de arquivos).
• fechar navegadores (Capítulo 54 – Web scrapping).
def contador_infinito():
from time import sleep
i = 0
while True:
try:
print(i)
i += 1
sleep(i)
except KeyboardInterrupt:
continuar = input(
; 'Você apertou Ctrl-C. ' \
'Aperte S para continuar. ' \
'Aperte N para parar. '
)
if continuar == 'N':
break
KeyError:
>>> try:
... x = 1/0
... except ZeroDivisionError as error:
... print(error)
division by zero
def capturar_excecao(chave_divisao):
dicionario = {0: 'zero'}
try:
print(dicionario[chave_divisao])
print(1/chave_divisao)
except Exception as error:
print(f'Capturamos "{error.__repr__()}"')
>>> capturar_excecao(1)
Capturamos "KeyError(1)"
>>> capturar_exception(0)
zero
Capturamos "ZeroDivisionError('division by zero'
def capturar_excecao_em_cadeia(chave_divisao):
dicionario = {0: 'zero', 'um': 1}
try:
print(dicionario[chave_divisao])
print(1/chave_divisao)
except ZeroDivisionError:
print('Exceção de divisão por zero!')
except KeyError:
print('Chave não encontrada no dicionário!')
except Exception:
print('Exceção desconhecida!')
>>> capturar_excecao_em_cadeia(0)
zero
Exceção de divisão por zero!
>>> capturar_excecao_em_cadeia(1)
Chave não encontrada no dicionário!
>>> capturar_excecao_em_cadeia('um')
1
Exceção desconhecida!
Levantando exceções
Aprendemos a capturar as exceções levantadas por erros que ocorrem
durante a execução, dependentes dos parâmetros ou ações. Mas
podemos levantar nossas próprias exceções no momento que
quisermos com o comando raise.
>>> raise
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: No active exception to reraise
Utilizando raise...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ValueError: Levantei uma exceção!
Customizando exceções
Chegamos na parte de criar nossas próprias exceções! Para isso,
necessitamos do conceito de herança (Capítulo 28 – Herança,
Polimorfismo e Classes Abstratas). Não é necessário se aprofundar no
momento, mas é importante saber que estamos utilizando herança
para construir nossas classes customizadas.
O primeiro passo para criar uma exceção customizada é definir a sua
classe (Capítulo 26 – Classes, objetos, métodos e atributos),
herdando da classe Exception:
>>> calcular('+', 1, 2)
Fim de execução
3
>>> calcular('k', 1, 2)
Operador "k" desconhecido!
Lista de operadores válidos: +, -, *, /
Fim de execução
>>> calcular('/', 1, 0)
Ops! Você tentou fazer uma divisão por zero!
Fim de execução
>>> calcular('-', 8, 'x')
Erro durante a operação: unsupported operand type(s) for -: 'int'
and 'str'
Fim de execução
>>> try:
... raise ValueError
... except Exception:
... print(
... 'Ocorreu uma exceção! '\
... 'Vamos tratá-la dividindo um número por zero!'
... )
... x = 1/0
Ocorreu uma exceção! Vamos tratá-la dividindo um número por zero!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ValueError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 5, in <module>
ZeroDivisionError: division by zero
A dúvida que pode surgir após a leitura das definições dessas classes é:
por que não tratar todas as exceções capturando da classe
BaseException em vez da classe E xception? Será que é somente para
economizar na quantidade de letras para digitar?
Podemos dizer que esse é um ótimo fator a ser levado em
consideração, mas a resposta está na hierarquia das exceções. Nela,
podemos observar que quatro classes derivam imediatamente da
BaseException: SystemExit, KeyboardInterrupt, GeneratorExit e
Exception, ou seja, exceções importantes de escapes estão relacionadas
diretamente à BaseException. Para esclarecer melhor o problema de
utilizar a classe BaseException para captura de qualquer exceção, vamos
analisar o seguinte exemplo:
Onde:
✓ <expressão de saída> irá gerar cada item da lista resultante.
✓ <variável> recebe os valores da lista de entrada e tem escopo
local.
✓ <lista> é a lista de entrada, que será iterada pelo loop.
✓ if <condição> é a parte opcional, que executa um filtro nos itens
da lista de entrada.
A expressão anterior pode ser codificada assim em Python:
>>> numeros = list(range(1,10))
>>> S = [ 2 * numero for numero in numeros if numero^2 > 3 ]
[8, 10, 12, 14, 16, 18]
Na linha 1, inicializamos uma lista com números que vão de 1 a 10, que
é o retorno do método range. Na linha 2, a variável S receberá uma lista
de numero * 2 para os números existentes na lista numeros que,
elevados ao quadrado, forem maior que 3.
As list comprehensions foram definidas na PEP 202 (WARSAW, 2000) e
elas têm algumas variações possíveis para sua execução. As mais
simples são:
Mas da mesma forma que o if pode estar no lado direito, fazendo filtro,
ele pode estar do lado esquerdo decidindo qual expressão será
executada, assim como já executamos em alguns casos no Python, por
exemplo:
Queremos montar uma lista com cada vendedor e seu total de vendas,
então podemos utilizar o método zip e iterar sobre o resultado obtido.
Porém, o resultado retornado é um zip object:
>>> lista_palavras = [
... ["casa", "cama", "banho"],
... ["longe", "perto"],
... ["joao", "maria", "jorge"]
... ]
>>> [ palavra for sublista in lista_palavras for palavra in
sublista]
['casa', 'cama', 'banho', 'longe', 'perto', 'joao', 'maria',
'jorge']
O que aconteceu aqui? Para cada sublista existente dentro da lista
lista_palavras, nós iteramos em cada palavra dentro dela e retornamos
por meio da variável palavra, tornando tudo uma lista só.
Após a PEP 202, surgiu a PEP 274 (WARSAW, 2001) que trouxe os dict
comprehensions e os set comprehensions, que, basicamente, têm as
mesmas características das list comprehensions, porém:
✓ set comprehensions retornam um objeto set e não list;
✓ dict comprehensions retornam um objeto dict e não list e
necessitam ter chave como valor de formato de retorno;
✓ ambas utilizam chaves (“{” e “}”) para delimitar os loops, e não
colchetes (“[” e “]”).
Para executar uma função, basta chamá-la por seu nome, passando os
parâmetros necessários:
>>> def imprimir():
... print("Uma mensagem")
>>> imprimir()
Uma mensagem
Escopo de variáveis
O escopo de uma variável é o contexto onde a variável existe durante a
execução de um código. Podemos definir o contexto como sendo de
três tipos: global, local e parâmetros. O contexto global indica que a
variável pode ser enxergada ao longo de todo o código-fonte. O
contexto local indica que a variável existe somente dentro de uma
função ou método de classe. Já o contexto de parâmetros são variáveis
locais utilizadas para passarem dados a funções ou métodos de classe.
No exemplo seguir, a variável mensagem pode ser acessada de dentro
da função. Esse é o contexto global.
>>> imprimir_nivel1()
Variável nível 3
Variável nível 3
Variável nível 1
>>> print(mensagem)
Variável global
Parâmetros arbitrários
Caso não se saiba quantos argumentos devem ser definidos ou caso se
deseje deixar a quantidade de argumentos em aberto, pode-se
especificar que a função espera receber uma lista ou um dicionário
deles.
No caso de uma lista, a declaração da função seria semelhante ao que
já fizemos anteriormente. A diferença está no fato de que já se espera
que os parâmetros estejam desempacotados ao chamar a função, ou
seja, não chamamos a função passando um objeto lista, mas sim uma
sequência de números separados por vírgulas ou uma lista
desempacotada:
Lambda
Python também permite definir uma função sem nome por meio da
palavra-chave lambda. A sintaxe para definir uma função assim é:
Funções aninhadas
Em Python é possível declarar funções dentro de outras funções. Se
dentro de uma função muito complexa blocos de código se repetirem
muitas vezes, pode ser bom criar funções desses blocos, tornando o
código-fonte mais limpo e mais fácil de manter.
Decorators
Um decorator é uma forma de modificar o comportamento de uma
função. Em decorators, funções são recebidas como parâmetros e então
executadas dentro de funções internas, chamadas empacotadoras.
Essas funções empacotadoras, por sua vez, recebem os parâmetros da
função modificada. Para utilizar um decorator, basta colocar o nome do
decorator precedido por um símbolo de arroba (@) antes da função a
ser decorada.
>>> help(soma)
Help on function soma in module __main__:
soma(a:float, b:float) -> float
Retorna a soma de dois números.
Caractere Significado
w Abre para escrita apagando seu conteúdo. Cria o arquivo caso não exista.
b Modo binário.
t Modo texto.
Caso só seja informado o nome do arquivo, por padrão, ele será aberto
para leitura em modo texto (“rt”). Para criar um arquivo podemos
utilizar o modo ‘x’, ‘a’ ou ‘w+’:
>>> f.close()
option = 2
contacts = []
while (option != 3):
print("\nSeja bem vindo ao programa de cadastro de contatos!")
print("""
1 - Novo contato
2 - Listar contatos
3 - Sair
""")
option = int(input(":"))
if option == 1:
contacts.append(input("Informe o contato: "))
print("Contato criado!")
elif option == 2:
print("Listando contatos")
for contact in contacts:
print(contact)
elif option != 3:
print("Opção inválida!")
option = 2
while (option != 3):
print("\nSeja bem vindo ao programa de cadastro de contatos!")
print("""
1 - Novo contato
2 - Listar contatos
3 - Sair
""")
option = int(input(":"))
if option == 1:
name = input("Informe o contato: ")
with open("contacts.txt", "a") as f:
f.write(f"\n{name}")
print("Contato criado!")
elif option == 2:
print("Listando contatos")
with open("contacts.txt", "r") as f:
for contact in f.readlines():
print(contact, end="")
elif option != 3:
print("Opção inválida!")
option = 2
while (option != 3):
print("\nSeja bem vindo ao programa de cadastro de contatos!")
print("""
1 - Novo contato
2 - Listar contatos
3 - Sair
""")
option = int(input(":"))
if option == 1:
name = input("Informe o contato: ")
with open("contacts.txt", "a") as f:
f.write(f"\n{name}")
print("Contato criado!")
elif option == 2:
print("Listando contatos")
try:
with open("contacts.txt", "r") as f:
for contact in f.readlines():
print(contact, end="")
except FileNotFoundError:
print("Lista de contatos vazia.")
elif option != 3:
print("Opção inválida!")
38 <https://docs.python.org/3/library/functions.html#open>.
23. Conexão com banco de dados
Cláudio Henrique Franco Gomes
39 <https://github.com/dropbox/PyHive>.
40 <http://hive.apache.org/>.
24. Documentando o código
Juliana Guamá
Roger Sampaio
Docstrings
Segundo a PEP 257 (GOODGER; VAN ROSSUM, 2001), Docstring é
uma string em formato literal, usada para documentar módulos,
funções, classes e métodos. Essa string pode ter uma ou várias linhas,
porém nada é atribuído dentro. Docstring é referenciada através do
atributo especial __doc__ do objeto que está sendo documentado.
Diferentemente dos comentários, Docstrings descrevem o que a função
faz e não como.
A PEP ainda sugere que a Docstring que explica um módulo deve estar
presente no __init__.py. Existem quatro PEPs que descrevem as
Docstrings, e o roadmap para elas pode ser encontrado na PEP 256.
Algumas das convenções de boas práticas introduzidas na PEP 257 são:
✓ Comentário de uma linha se o conteúdo for pequeno.
✓ Usar múltiplas linhas se o conteúdo ultrapassar 72 caracteres
(VAN ROSSUM; WARSAW; COGHLAN, 2001).
✓ Não deve existir linha em branco entre a linha de definição da
função/método/classe e a Docstring.
✓ O comentário deve iniciar e terminar em três aspas duplas.
✓ Toda a documentação de um mesmo projeto deve seguir um
mesmo estilo.
✓ A documentação usa verbos no imperativo afirmativo na terceira
pessoa do singular. Essa indicação facilita o entendimento da
função porque a leitura fica “a função faz”, “ela faz”.
✓ A documentação deve trazer informação complementar, ou seja,
não se descreve o que já foi descrito no nome da função.
✓ Para evitar extensa documentação, sugere-se uso de type hinting
(será visto mais à frente neste capítulo).
Exemplos:
#má prática
def soma(a, b):
"""Retorna a soma do a com b""""
return a + b
#boa prática
def soma(a, b):
"""Retorna soma para `int` e `float`; concatena tipo `str`""""
return a + b
Pode-se acessar a Docstring pelo atributo _doc_ da função
documentada:
>>> print(soma.__doc__)
Retorna soma para `int` e `float`; concatena tipo `str`
Pydoc
Para acessar documentação de pacotes de Python pode-se usar Pydoc,
que já vem instalado por padrão nas versões do interpretador da
linguagem Python. Para usar, basta rodar como script pelo prompt de
comando (Windows) ou terminal (Linux/MacOS):
Type hinting
Introduzida na atualização de Python 3.541, type hinting serve como
indicador do tipo de variável que se espera para a função.
GoogleDoc
Guia de estilo criado pelos desenvolvedores do Google e usado para
documentar os códigos Python desenvolvidos pela empresa42.
Args:
a (Any): int, float ou str
b (Any): int, float ou str
"""
if type(a) == type(b):
return a+b
return None
NumpyDoc
O estilo segue o formato NumPy e possui documentação43
referenciando como usar o estilo. Seu código é aberto no GitHub44 para
receber contribuições da comunidade.
from typing import Any
def soma(a: Any, b: Any) -> Any:
"""Se type(a) == type(b) realiza a soma para tipos numéricos,
concatena para `str`
Parameters
----------
a : Any
int, float ou str
b : Any
int, float ou str
"""
if type(a) == type(b):
return a+b
return None
41 <https://docs.python.org/3/library/typing.html>.
42 <https://google.github.io/styleguide/pyguide.html#Comments>.
43 <https://numpydoc.readthedocs.io/en/latest/format.html>.
44 <https://github.com/numpy/numpydoc>.
PARTE IV.
ORIENTAÇÃO A OBJETOS
25. Introdução à orientação a
objetos e seus quatro pilares
Tatiana Escovedo
Viviane Laporti
Definição
Como vimos no capítulo anterior, uma classe representa a estrutura
que serve como modelo para a criação de um objeto: a classe é o
projeto de um objeto e fornece a descrição do comportamento
(operações) comum e do estado (dados) dos objetos. Classes estão
para objetos assim como formas estão para bolos: podemos preparar
vários bolos a partir de uma mesma forma, assim como podemos criar
vários objetos a partir de uma mesma classe. A forma definirá algumas
propriedades comuns (por exemplo, sabor) e comportamentos (por
exemplo, ser comestível) dos bolos, mas cada bolo poderá ter valores
diferentes para a propriedade sabor.
Assim, as classes são estruturas de dados que unem variáveis
(propriedades) e funções (comportamentos) dentro de um mesmo
contexto. Em Python, tudo deriva de uma classe, inclusive tipos básicos
como tipos numéricos e textos. Veja o que acontece quando
executamos o comando help(int):
>>> help(int)
Help on class int in module builtins:
class int(object)
| int(x=0) -> integer
| int(x, base=10) -> integer
|
| Convert a number or string to an integer, or return 0 if no
arguments
| are given. If x is a number, return x.__int__(). For floating
point
| numbers, this truncates towards zero.
|
| If x is not a number or if base is given, then x must be a
string,
| bytes, or bytearray instance representing an integer literal in
the
| given base. The literal can be preceded by '+' or '-' and be
surrounded
Note que o tipo int deriva da classe int, que, por sua vez, herda da
classe object atributos e métodos comuns a todas as classes. Vimos no
capítulo anterior que as classes podem formar hierarquias, e uma
subclasse pode herdar as características de sua superclasse, mas
falaremos mais sobre herança no Capítulo 28 – Herança,
polimorfismo e classes abstratas.
Em Python, classes podem ser definidas em qualquer lugar, inclusive
dentro de funções. Usualmente, classes são definidas mais próximas do
início do programa ou em módulos próprios, o que costuma deixar o
código que vai fazer uso dessas classes mais limpo e fácil de manter.
Sintaxe
A sintaxe para definir uma classe é:
class NomeDaClasse:
DECLARAÇÕES e COMANDOS
Objetos e instâncias
A classe PersonagemDeRPG do exemplo anterior descreve como deve
ser a estrutura de personagens de RPG no nosso jogo. Quando esta
classe é usada para criar um personagem específico, dizemos que é
instanciado um objeto. Vale a pena ressaltar que uma classe pode ser
escrita só para reunir dentro de si funções utilitárias, não sendo
necessariamente usada para criar objeto (muito embora lembre um
canivete suíço!). Quando este objeto é atribuído a uma variável, diz-se
que essa variável referencia este objeto. Isto é, a variável possuirá as
mesmas características da classe.
No exemplo a seguir, definimos uma classe Pessoa que possui dois
métodos: __init__, responsável por inicializar o atributo nome durante
a criação da instância, e __str__, que é chamado sempre que queremos
converter Pessoa para string. Quando passamos uma instância de um
objeto para o comando print, este vai buscar pelo método __str__ para
saber como representar o objeto em questão na saída padrão. As
variáveis pessoa_1 e pessoa_2 referenciam instâncias (objetos) da
classe Pessoa. Ambas possuem o atributo nome nos métodos __init__
e __str__, mas o atributo nome poderá ter valores distintos em cada
uma dessas instâncias.
Métodos
Dentro de classes é possível definir métodos, que representam as ações
que os objetos criados a partir desta classe poderão realizar. O método
de uma classe recebe pelo menos um parâmetro (a instância da classe),
que é o termo que permite ao método saber a qual instância se referem
aqueles atributos. Essa instância é passada pelo parâmetro self. Veja
novamente o exemplo anterior e note como os métodos __init__ e
__str__ foram escritos com o parâmetro self.
O parâmetro self não é uma palavra reservada em Python. Assim, se o
desenvolvedor quiser, ele pode nomear o primeiro parâmetro dos
métodos de suas classes como bem entender, muito embora por
convenção isso não seja recomendado. O exemplo anterior poderia ser
escrito conforme segue, sem nenhum prejuízo do resultado:
Atributos
Existem três tipos de variáveis que podem ser declaradas dentro de
uma classe: atributos de classes, atributos de instâncias e variáveis de
métodos. Vejamos as diferenças entre cada um.
Atributos de classes são ligados à classe, e todas as instâncias da classe
compartilham o mesmo atributo. Já atributos de instância são
específicos para cada instância, podendo ter valores diferentes de uma
instância para outra. No exemplo a seguir, criamos a classe Pessoa com
o atributo de classe num_pessoas, utilizado para contar quantas
instâncias da classe Pessoa foram criadas. No método __init__ criamos
um atributo de instância nome para guardar o nome de cada indivíduo
que for criado e incrementamos em uma unidade o atributo de classe
num_pessoas.
>>> pessoas[0].__dict__
{'nome': 'João'}
>>> pessoas[0].__class__.__dict__
mappingproxy({'__module__': '__main__', 'num_pessoas': 4,
'__init__': <function Pessoa.__init__ at 0x0000028473098CA0>,
'__dict__': <attribute '__dict__' of 'Pessoa' objects>,
'__weakref__': <attribute '__weakref__' of 'Pessoa' objects>,
'__doc__': None})
>>> pessoas[2].__dict__
{'nome': 'George'}
>>> pessoas[2].__class__.__dict__
mappingproxy({'__module__': '__main__', 'num_pessoas': 4,
'__init__': <function Pessoa.__init__ at 0x0000028473098CA0>,
'__dict__': <attribute '__dict__' of 'Pessoa' objects>,
'__weakref__': <attribute '__weakref__' of 'Pessoa' objects>,
'__doc__': None})
Note que para todas as instâncias da classe Pessoa, cada uma delas
terá um valor distinto de nome, mas o valor de num_pessoas será o
mesmo em todas, uma vez que atributos de classe são compartilhados
entre todas as instâncias.
Também poderíamos acessar o atributo __dict__ da classe
diretamente, a qual, por si só, é uma instância de uma metaclasse (esse
assunto será abordado no Capítulo 40 – Metaprogramação).
>>> Pessoa.__dict__
mappingproxy({'__module__': '__main__', 'num_pessoas': 4,
'__init__': <function Pessoa.__init__ at 0x0000028473098CA0>,
'__dict__': <attribute '__dict__' of 'Pessoa' objects>,
'__weakref__': <attribute '__weakref__' of 'Pessoa' objects>,
'__doc__': None})
Como não definimos o atributo numero como public (sem __), quando
tentamos executar o comando conta1234.numero, o valor 1234, que é
o número da Conta conta1234, é exibido sem erros. Porém, veja o que
acontece quando tentamos fazer o mesmo para o atributo __saldo, que
foi definido como non-public:
>>> (Conta.__saldo)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'Conta' has no attribute '__saldo'
>>> conta1234._Conta__saldo
750.84
>>> dir(conta1234)
['_Conta__saldo', '__class__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'numero']
Nenhuma mensagem de erro foi exibida! Será que foi possível alterar o
valor do saldo diretamente? Para ter certeza, vamos novamente
acessar __saldo fazendo:
>>> conta1234._Conta__saldo
750.84
>>> dir(conta1234)
['_Conta__saldo', '__class__', '__delattr__', '__dict__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__saldo',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'__weakref__', 'numero']
45 <https://www.python.org/dev/peps/pep-0008/#id49>.
28. Herança, polimorfismo e
classes abstratas
Tatiana Escovedo
Viviane Laporti
Herança
Em orientação a objetos, a herança é um processo pelo qual um objeto
adquire as propriedades e os comportamentos de outro objeto. Para
compreender melhor o processo de herança, podemos fazer uma
analogia com o mundo físico, pensando em pessoas e papéis que elas
assumem.
Se pensarmos em herança genética, percebemos os filhos como
herdeiros de características físicas como cor dos olhos, traços, estatura,
tom de pele e até mesmo aspectos comportamentais, características de
personalidade que herdaram de seus pais e antepassados. Nessa
analogia, as características físicas seriam os atributos, enquanto os
aspectos comportamentais seriam descritos pelos métodos.
Passando ao mundo virtual, pense em um sistema de uma universidade
que tem como usuários professores, alunos, coordenadores de curso e
outros profissionais associados à organização. Se compararmos todos
esses papéis, percebemos que todos possuem algumas características
em comum, como login, senha, nome, CPF, endereço, telefone etc.
Imagine que, neste sistema, cada trecho de código que manipula e
armazena esses valores implementa separadamente a manipulação
desses dados. Esse tipo de implementação traria consigo o risco de cada
CPF, por exemplo, aceitar a entrada de maneira diferente: em um lugar
seria possível utilizar apenas números e em outro seria necessário
utilizar também “.” e “-”. Correríamos o risco de ter no sistema diversos
formatos diferentes para uma mesma informação, e regras fora do
padrão.
Neste caso, poderíamos concentrar todos os dados pessoais comuns
para todos os papéis do sistema em uma única classe, representada na
Figura 28.1 como Pessoa:
Polimorfismo
Com base no primeiro exemplo da seção anterior, podemos afirmar que
todo Aluno é uma Pessoa (e todo Professor também), pois Aluno é
uma extensão de Pessoa e herda suas propriedades e métodos. Assim,
podemos nos referir a um Aluno (ou um Professor) como sendo uma
Pessoa. Se alguém procurar uma Pessoa da universidade, servirá tanto
um Aluno quanto um Professor, pois ambos são Pessoas. A herança
funciona dessa forma.
Já polimorfismo é a capacidade de um objeto poder ser referenciado de
várias formas (o que não significa que o objeto pode se transformar em
outro tipo!). Ou seja, se tivermos um método que espera receber um
objeto do tipo Pessoa, ele pode receber no lugar um objeto do tipo
Aluno ou do tipo Professor. Veja o exemplo a seguir:
>>> entrada.permite_entrada(quadrado1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in permite_entrada
AttributeError: 'Quadrado' object has no attribute 'consulta_nome'
Classes abstratas
Vamos recordar o primeiro exemplo deste capítulo, onde definimos a
superclasse Pessoa e as subclasses Aluno e Professor:
Entretanto, será que faz sentido termos no nosso sistema uma classe
Pessoa? Afinal, os usuários do sistema serão de tipos específicos, como
Aluno, Professor, Coordenador… mas nunca tão genéricos como
Pessoa. Porém, se removermos a classe Pessoa do nosso programa,
perderemos duas importantes vantagens: o reúso de código entre as
subclasses de Pessoa e a flexibilidade de termos um argumento
polimórfico no método permite_entrada. Além disso, teríamos que
escrever muito código repetido, o que não é uma boa prática. Para
resolver esse impasse, podemos tornar Pessoa uma classe abstrata.
Em orientação a objetos, uma classe abstrata não pode ser instanciada
(o que resolverá nosso problema de evitar que sejam criados objetos do
tipo Pessoa). Em Python, uma classe abstrata deve conter pelo menos
um método abstrato, e podemos criar uma classe abstrata herdando da
superclasse para classes abstratas ABC (Abstract Base Classes),
pertencente ao módulo abc. Vamos então tornar a classe Pessoa
abstrata no exemplo a seguir:
Métodos estáticos
O decorator staticmethod possibilita a criação de um método estático
em uma classe. Esse método não precisa que a classe seja instanciada
para ser utilizado. Métodos estáticos não recebem nem a instância nem
a classe como parâmetro, ou seja, eles não têm qualquer conhecimento
sobre a classe que os encapsula. Além disso, métodos estáticos podem
ser usados sem instanciar. Eles só lidam com os parâmetros, como as
funções tradicionais. Por exemplo, poderíamos criar uma classe com
funções utilitárias e todas estáticas, como mostra o código a seguir:
Métodos de classe
O decorator classmethod possibilita a criação de um método de classe.
O método de classe, assim como os métodos estáticos, não precisa que
a classe seja instanciada para ser invocado. Mas, diferentemente dos
métodos estáticos, ele recebe a classe como primeiro parâmetro.
Um método de classe é um recurso muito útil para criar funções que
retornam instâncias de classes. Por exemplo, suponha uma classe que
ao ser instanciada recebe a temperatura em graus Celsius. Poderíamos
criar um método de classe para receber o valor da temperatura em
graus Fahrenheit e retornar uma instância dessa classe com o valor da
temperatura em Celsius. Veja o exemplo a seguir:
Propriedades
Quando criamos uma classe com atributos e métodos, sempre
pensamos em escrever métodos para obter e informar o valor de cada
atributo (os conhecidos métodos getters e setters). O Python oferece
uma maneira mais elegante de escrever tais métodos, por meio do uso
do decorator property e do decorator setter.
Por exemplo, suponha que tenhamos uma classe que espera receber
um número de CPF, mas não armazena nada além dos números, isto é,
pontos e traços são descartados. Porém, na hora de exibir o CPF, os
pontos e traços devem ser utilizados para tornar a formatação mais
agradável. O código a seguir ilustra esse exemplo:
Generator Pattern
Se precisássemos de algo mais complexo, como uma paginação em um
conjunto de dados muito grande, poderíamos utilizar o design pattern
generator, isto é, poderíamos criar uma classe com os métodos __iter__
e __next__ necessários para iterar. A classe ListarElementos a seguir
faz exatamente o mesmo que a função listar_elementos do exemplo
anterior.
Expressão Generator
Escrever uma função generator, conforme vimos anteriormente, é bem
mais simples que escrever uma classe seguindo o design pattern
generator. No entanto, muitas vezes precisamos de algo ainda mais
simples do que essas duas opções. Para esses casos, podemos utilizar
um recurso um pouco mais “pythônico”: as expressões generators.
Uma expressão generator é basicamente uma list comprehension, só
que, em vez de colchetes, usamos parênteses. Por exemplo, a classe
ListarElementos poderia ser escrita simplesmente como:
0
1
4
9
16
25
36
49
64
81
>>> print(func_map)
<map object at 0x7f098df6fbe0>
>>> print(list(func_map))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Descriptor.__set__
>>> print(usuario.nome)
Descriptor.__get__
Pedro Paulo
>>> del usuario.nome
Descriptor.__delete__
Classe property
Outra forma de criar descriptors é por meio da classe property, que
recebe como parâmetros em sua inicialização os métodos get, set,
delete e a docstring da instância. Para maiores detalhes, veja o help
dessa classe:
>>> help(property)
Help on class property in module builtins:
class property(object)
| property(fget=None, fset=None, fdel=None, doc=None)
|
| Property attribute.
|
| fget
| function to be used for getting an attribute value
| fset
| function to be used for setting an attribute value
| fdel
| function to be used for del'ing an attribute
| doc
| docstring
...
Decorators
Conforme vimos no Capítulo 30, decorators são uma forma de
modificar o comportamento de uma função. Podemos criar descriptors
por meio de decorators. Note que o código fica mais limpo e elegante
que as duas formas apresentadas anteriormente:
Prós e contras
Descriptors podem ser utilizados para acionar certas ações quando os
atributos da classe são acessados. Ao mesmo tempo em que facilitam o
encapsulamento de dados e possibilitam a reutilização de código,
dificultam a leitura e a verificação de erros no código, por criarem mais
uma camada de abstração. Eles também facilitam a criação de objetos
em tempo de execução. Enfim, descriptors oferecem ao desenvolvedor
uma série de ferramentas que podem ajudar a esconder do usuário a
complexidade do código, mas é necessário ter em mente o custo que
vem com elas.
34. Métodos mágicos
Sérgio Berlotto Jr.
>>> dir(vitamina)
['__add__', '__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', 'fruta']
46 <https://docs.python.org/pt-br/3/reference/datamodel.html>.
35. Modelos arquiteturais
Davi Luis de Oliveira
Karine Cordeiro
Arquitetura em camadas
A arquitetura em camadas surgiu nos anos 1980 (há quem diga que foi
inspirada na arquitetura de computadores) com o objetivo de
modularizar o desenvolvimento dos sistemas, dividindo
responsabilidades e gerindo as dependências de maneira que as
camadas superiores se comunicassem com as camadas inferiores,
mantendo dependência somente com a camada imediatamente
inferior. São elas: camada de apresentação, controle, negócio e acesso a
dados. A Figura 35.1 ilustra a arquitetura em camadas.
SOA
A sigla vem do termo em inglês Service Oriented Architecture e foi
introduzida pela primeira vez pelos pesquisadores Roy Schulte e Yefim
Natis do Gartner Group em 1996. Inicialmente eles apresentaram o
modelo arquitetural com base nas experiências de seus clientes que
tinham aplicações cliente-servidor.
A arquitetura orientada a serviços (SOA) é um paradigma e disciplina de design que
ajuda a TI a atender às demandas de negócios... reduz a redundância e aumenta a
usabilidade, a capacidade de manutenção e o valor. Isso produz sistemas modulares
interoperáveis que são mais fáceis de usar e manter (GARTNER GLOSSARY, s.d.).
MVC
O MVC (Model, View, Controller) é um padrão arquitetural bastante
utilizado em diversos projetos, além de ser o padrão em alguns
frameworks por possibilitar uma divisão de responsabilidades por cada
parte do projeto, assim isolando as regras de negócios da interface com
o usuário. Para isso, o MVC é dividido basicamente em model, view e
controller.
✓ Model: é a representação das informações ou dados que você
deseja exibir para o usuário. Logo, é nessa camada que existem
recursos para comunicação, armazenamento e até mesmo
validação dessas informações em um banco de dados.
Geralmente, cada model é representado por uma classe no
sistema.
✓ View: consiste em uma camada de apresentação do usuário, que
poderá acessar funcionalidades definidas por meio das regras de
negócio do controller. Logo, o código responsável pelo front-end
(por exemplo, em HTML, CSS e JavaScript) será utilizado nessa
camada.
✓ Controller: esta camada possui as regras de controle do seu
projeto, atuando como intermediador das requisições entre o
model e a view. Então, toda requisição feita pelo usuário passa
pelo controller, que executa as regras de controle e, caso
necessário, faz a comunicação com um model para manipulação
dos dados e, em seguida, renderiza o conteúdo solicitado.
MVT
Este modelo arquitetural é exclusivo do framework Django e é bastante
similar à arquitetura MVC. Na MVT (Model, View, Template), a
requisição do usuário é feita pela interface do projeto que chamamos
de template. Esta faz as solicitações para a view, que é responsável
pelas definições de regras de negócio, e utiliza o model para realizar o
acesso aos dados. Por fim, a view renderiza o template com as
informações solicitadas.
✓ Model: esta camada possui a mesma funcionalidade do model da
arquitetura MVC e consiste em uma abstração da solicitação de
operações do banco de dados do sistema. A única diferença é que
no MVT a solicitação do model ocorre pela view.
✓ View: é nesta camada que são implementadas as regras de
negócios da aplicação, além de ser intermediadora entre o model e
o template.
✓ Template: funciona como uma camada de apresentação e lida
completamente com a parte da interface do usuário. Todos os
arquivos do projeto relacionados à interface do app são
encontrados nesta camada.
Arquitetura de microsserviços
James Lewis e Martin Fowler (LEWIS; FOWLER, 2014) definem
arquitetura de microsserviços como “uma abordagem para o
desenvolvimento de um único aplicativo como um conjunto de
pequenos serviços, cada um executando seu próprio processo e se
comunicando com mecanismos leves, geralmente uma API de recurso
HTTP”. Ainda utilizando a abordagem dos dois, as aplicações nessa
arquitetura são construídas e implementadas de forma independente
entre si, em máquinas de implantação totalmente automatizadas com
um mínimo de gerenciamento centralizado, podendo ser escritos em
diferentes linguagens de programação e usar diferentes tecnologias de
armazenamento de dados.
Essa arquitetura consiste em vários projetos que se comunicam entre si,
portanto foge do conceito de um sistema monolítico onde toda
aplicação se encontra em um único projeto. Outro aspecto importante
a ser ressaltado é que, além da divisão de vários projetos que se
comunicam entre si, é preciso utilizar várias estratégias para
configuração e gerenciamento do ambiente, tanto local como remoto.
Quando se adota uma arquitetura de microsserviços, ganham-se
algumas vantagens, como: independência no desenvolvimento de cada
projeto, já que cada equipe trabalha focando no seu projeto específico;
flexibilidade, devido à inserção de novos projetos no ambiente do
sistema; liberdade na escolha da tecnologia, uma vez que cada
microsserviço pode ser feito com conjuntos de tecnologias diferentes;
forte modularização, o que reduz a complexidade e possibilita o reúso
de componentes; e implantações independentes, reduzindo impactos
de uma implementação gerar falhas em outra.
Também é importante ressaltar que a utilização de microsserviços tem
várias desvantagens, como: complexidade adicional ao sistema, uma
vez que sistemas distribuídos são mais propensos a lentidão e falhas de
comunicação; aumento da complexidade na criação de testes que
analisam as interações entre serviços; cada serviço se torna responsável
por manter a consistência eventual dos dados (um dos principais
paradigmas em desenvolvimento de sistemas distribuídos); aumento da
complexidade do ambiente operacional e eventual necessidade de uma
equipe de operações mais madura.
A definição da arquitetura de um sistema deve levar em conta, além da
maturidade, o nível de conhecimento e a curva de aprendizado da
equipe, os fatores relacionados ao ciclo de vida da aplicação a ser
desenvolvida e o que é importante, dado o cenário geral da aplicação.
Não existe bala de prata. Para ter sucesso na escolha da arquitetura de
um sistema é necessário que o desenvolvedor, arquiteto ou time tenha
clara visão do que é importante e, após definida a abordagem, gastar
esforços para manter os elementos arquiteturais em boas condições.
PARTE V.
TÓPICOS AVANÇADOS DE
PROGRAMAÇÃO
36. SOLID
Eduardo Bizarria Gaspar
Helcio Gomes
Rodrigo Isensee
Sérgio Berlotto Jr.
Tatiana Escovedo
William Villela de Carvalho
Princípio aberto/fechado
A letra “O” do SOLID corresponde ao Open/Closed Principle (OCP).
Este princípio nos diz que uma classe deve estar aberta para extensão,
porém fechada para modificações.
Quando novos comportamentos e recursos precisam ser adicionados
no software, devemos estender e não alterar o código-fonte original.
Por exemplo, organizar o seu código com uma superclasse genérica e
abstrata o suficiente permitirá que o programa seja estendido
acrescentando novas subclasses sem precisar alterar as classes
existentes. Veja um exemplo48:
Para se criar uma classe para uma impressora padrão, utilizar a classe
ImpressoraFazTudo como base viola o princípio ISP, pois a classe
cliente vai herdar métodos que não utilizará (Escaneia e EnviaFax).
Utilizando o princípio ISP, vamos dividir a classe ImpressoraFazTudo
em classes menores, para que as classes clientes implementem
somente o que precisam:
Dessa forma, quando um novo tipo de animal for incluído, não haverá a
necessidade de alterar a classe Dono, que agora conhece apenas a
superclasse Animal, e não as suas subclasses.
Nomenclatura
Segundo o Zen do Python, “explícito é melhor que implícito”, ou seja,
quando escrevemos nosso código Python, é muito importante deixar
muito bem claro o que queremos desenvolver naquele bloco de código.
Para isso, é preciso saber nomear variáveis, funções, classes, pacotes,
etc. Escolher nomes que façam sentido poupa tempo na interpretação e
manutenção de seu código, de forma que você seja capaz de descobrir
naturalmente do que se trata.
>>> O = 2
>>> # Isso pode parecer que você está tentando atribuir 2 a zero
Classe Comece cada palavra com uma letra maiúscula. Model, MyClass
Não separe palavras com sublinhados.
Layout de código
Segundo o Zen do Python, “belo é melhor que feio”, ou seja, como você
formata seu código tem papel fundamental na forma como ele é
entendido.
✓ Indentação: pelo padrão da linguagem Python, devemos usar
quatro espaços (ou um Tab) por nível de indentação; caso seu
código seja antigo, você pode manter como oito espaços (ou dois
Tabs). A linguagem Python reconhece automaticamente o nível de
indentação predominante e segue esse padrão.
✓ Tabulações ou espaços: nunca misture tabulações e espaços. A
forma mais popular de indentar um código Python é somente com
espaços; a segunda forma é somente com tabulações. Nunca
devemos misturar os dois tipos de indentação: quando temos essa
situação, devemos convertê-la para usar somente espaços. Para
novos projetos, dê preferência para uso somente de espaços.
Muitos editores já trazem opções para facilitar sua utilização.
✓ Comprimento máximo de linhas: ainda nos dias atuais, temos
muitos monitores limitados a linhas de 80 colunas. Além disso,
muitas pessoas usam a limitação de janelas a 80 caracteres, que
permite ter várias janelas abertas, lado a lado. As quebras de linha
nessas características ficam muito ruins; portanto, limite todas as
linhas de seu programa a um máximo de 79 caracteres.
Para longos blocos de texto (docstrings ou comentários), devemos
limitar seu comprimento em 72 colunas. A melhor maneira de
continuar linhas longas é usando a continuação implícita, entre
parênteses, colchetes e chaves. Se necessário, você pode adicionar um
par extra de parênteses em uma expressão, mas, em alguns casos, uma
barra invertida fica melhor. Por exemplo:
49 <https://google.github.io/styleguide/pyguide.html>.
38. Programação funcional
Marcell Guilherme C. da Silva
O script anterior produz [‘18 anos.’, ‘22 anos.’, ‘42 anos.’] como
resultado da variável idades_formatadas, e continuaremos
reproduzindo esse comportamento nos próximos exemplos.
Contrapondo-se à programação imperativa, há a programação
declarativa. Em vez de digitarmos uma sequência de passos, daremos
um “predicado”, ou seja, uma descrição de como cada dado será
transformado. A leitura fica algo como: “cada item desta coleção será
outro transformado”. Repare que a semântica agora não é mais de
ordem (imperativa), mas de descrição (indicativa). Exemplos de
expressões declarativas são as comprehensions (já detalhadas no
Capítulo 20 – Comprehension):
Caso você esteja confortável com o idioma, fica mais normal de ler uma
frase no indicativo: “números ímpares recebem esses números sem o 2
e 4”. Isso poderia ser feito com uma função de predicado também:
>>> _(numeros).last().value()
Diferentemente da única opção nativa pop, o last é seguro e não irá
interferir na lista original de números: ela continua com todos os quatro
elementos. Isso deixa seu comportamento bem mais previsível e fácil
de testar, e é mais legível do que numeros[-1] para a mesma
finalidade.
Outras funções úteis que inexistem na biblioteca padrão do Python
também estão disponíveis, como uma busca por predicado. Dada a
lista:
>>> meus_usuarios = [
... dict(nome='John Doe', idade=15),
... dict(nome='Jane Doe', idade=25),
... dict(nome='Lorem Ipsum', idade=99),
... ]
>>> print(recuperar_adultos(meus_usuarios))
Adultos da lista: Jane Doe, Lorem Ipsum
Concorrência e paralelismo
Para realizarmos tarefas assíncronas, as técnicas mais comuns são
concorrência e paralelismo. Essas técnicas nos permitem executar
tarefas simultâneas. O paralelismo as executa de forma paralela, por
meio de diversos núcleos (core) de processamento, enquanto a
concorrência ocorre quando essas tarefas utilizam os mesmos recursos
de forma concorrente, ou seja, as tarefas são executadas parcialmente
e são trocadas de contexto até todas serem finalizadas. É muito comum
uma combinação de paralelismo e concorrência nos sistemas atuais.
Mas como trocamos de contexto durante a execução de atividades
concorrentes?
Antes de detalharmos como ocorre essa gestão de execuções paralelas
e concorrentes, precisamos relembrar alguns conceitos. O primeiro
deles: o que é uma thread?
De modo geral, uma aplicação possui processos que executam suas
tarefas por meio de threads. Ou seja, uma thread é a menor unidade de
execução de instruções dentro de uma aplicação.
Em Python, podemos executar tarefas assíncronas por meio do módulo
threading. Uma das formas de se fazer isso é estendendo a classe
Thread deste módulo e implementando o método run(), pois esse
método será chamado quando essa thread for iniciada. Exemplo:
import time
import queue
from multiprocessing import Process, Queue, current_process
def processar_fila_de_pedidos(novos_pedidos, pedidos_processados):
'''
A função espera receber dois objetos do tipo Queue
'''
while True:
try:
'''
pega um pedido da fila, a função get_nowait() levantará
uma queue.Empty exception se a fila estiver vazia
'''
pedido = novos_pedidos.get_nowait()
except queue.Empty:
break
else:
# Processa o pedido e adiciona a fila de pedidos
processados
print(pedido[1])
pedidos_processados.put(
f'Pedido número: {pedido[0]}, ' + \
'foi processado por {current_process().name}'
)
time.sleep(.5)
return True
def processar_pedidos():
quantidade_de_pedidos = 10
quantidade_de_processos = 4
novos_pedidos = Queue()
pedidos_processados = Queue()
processos = []
for i in range(quantidade_de_pedidos):
# para nosso exemplo, um pedido é uma tupla com id,
descricao
pedido = (i, f"Pedido de compra número: {i}")
novos_pedidos.put(pedido)
# criando os processos
for w in range(quantidade_de_processos):
'''
O parametro TARGET recebe a funcão que será executada e o
parametro ARGS os paramentros da função passada no
parametro
anterior.
'''
p = Process(
target=processar_fila_de_pedidos,
args=(novos_pedidos, pedidos_processados)
)
processos.append(p)
p.start()
# finalizando os processos
for p in processos:
p.join()
# imprime os resultados
while not pedidos_processados.empty():
print(pedidos_processados.get())
return True
if __name__ == '__main__':
processar_pedidos()
O módulo AsyncIO
O módulo AsyncIO nos traz conceitos um pouco diferentes do que
vimos até o momento. Ele introduz três novos termos: event loop,
coroutines e futures. Em linhas gerais, ele nos permite realizar códigos
assíncronos usando uma única thread.
Antes de continuar, vale a pena ressaltar que, como o IPython, o
interpretador interativo utilizado nas IDEs Spyder e Jupyter, usa o
AsyncIO, recomendamos executar os códigos aqui descritos direto no
terminal.
Corrotinas (coroutines)
Podemos usufruir da concorrência em nossos programas
implementando as corrotinas. Uma corrotina é uma rotina (função,
método etc.) que concorda (ou permite) que sua execução seja adiada
para que outra rotina seja executada e depois retorne a execução para
si. Portanto, ela coopera com outras rotinas.
Executando rotina_muito_lenta...
Executando rotina_rapida
.
..
...
....
.....
......
.......
........
.........
..........
rotina_muito_lenta finalizada!
Conclusão
Neste capítulo foi possível vermos alguns conceitos e exemplos de
códigos para a criação de programas que possam utilizar a programação
assíncrona. Essa é uma poderosa técnica de programação que possui
diversas abordagens, portanto sugerimos que execute os exemplos
deste capítulo e explore as demais alternativas existentes na linguagem
para que possa usufruir desses recursos para resolver os problemas
computacionais que deseja. Para mais informações, consulte a
documentação do Python, utilizada como referência bibliográfica deste
capítulo:
✓ threading – Thread-based parallelism51
✓ multiprocessing – Process-based parallelism52
✓ AsyncIO – Asynchronous I/O53
✓ Coroutines and tasks54
51 <https://docs.python.org/3/library/threading.html>.
52 <https://docs.python.org/3/library/multiprocessing.html>.
53 <https://docs.python.org/3/library/asyncio.html>.
54 <https://docs.python.org/3/library/asyncio-task.html>.
40. Metaprogramação
Cláudio Henrique Franco Gomes
>>> type(int)
<class 'type'>
>>> type(object)
<class 'type'>
>>> type(str)
<class 'type'>
>>> class Simples():pass
>>> type(Simples)
<class 'type'>
O problema disso é que teríamos que decorar cada função cujo tempo
gostaríamos de obter. Por exemplo: se criássemos uma classe Filho,
descendente da classe Pai, ela não herdaria o decorador da classe Pai.
Veja no exemplo a seguir que somente quando chamamos a classe
__init__ da classe Pai (que está decorada) é que saberemos quanto
tempo levou a sua execução:
>>> # ruim
>>> def ltTParaMl(x):
... return x * 1000
>>> # bom
>>> def transforma_litro_para_mililitro(valor_em_lt):
... return valor_em_lt * 1000
>>> # ruim
>>> if soma > 1250: # O que significa este número?
... faca_algo()
>>> # bom
>>> MINIMO_NECESSARIO_PARA_DESCONTO = 1250
>>> if soma > MINIMO_NECESSARIO_PARA_DESCONTO:
... faca_algo()
>>> # ruim
>>> if variavel in (1,15,99,120):
... raise Exception('Não aceito')
>>> # bom
>>> planos_nao_permitidos = (1,15,99,120)
>>> if variavel in planos_nao_permitidos:
... raise Exception('Não aceito')
>>> #ruim
>>> def lista_usuarios():
... if request not in ('POST','PUT','DELETE'):
... return get_users_list()
... else:
... return "Method not allowed"
>>> #bom
>>> def lista_usuarios():
... if request.method == 'GET':
... return get_users_list()
... return "Method not allowed"
>>> # ruim
>>> nao_deve_processar = True
>>> if not nao_deve_processar:
... # faz algo aqui
>>> # bom
>>> deve_processar = True
>>> # ou
>>> deve_processar = False
>>> if deve_processar:
... # faz algo aqui
>>> # ou
>>> if not deve_processar:
... # faz algo aqui
>>> # ruim
>>> def contem_letra(letra, nome):
... if letra in nome:
... return True
... else:
... return False
>>> # bom
>>> def contem_letra(letra, nome):
... return letra in nome
>>> # ou
>>> # ruim
>>> if conta.numero in get_lista_de_contas_bloqueadas():
... conta.bloqueada = True
>>> # bom
>>> conta.bloqueada = (conta.numero in
get_lista_de_contas_bloqueadas())
>>> # ruim
>>> def metodo_com_muitos_parametros(
... nome, idade, data_nascimento, salario, funcao
... ):
... # faz algo aqui...
>>> # bom
>>> def metodo_com_muitos_parametros(dados_do_usuario):
... nome = dados_do_usuario.get('nome', None)
... # faz algo aqui...
>>> # ruim
>>> # verifica se a venda entra na regra do desconto promocional
>>> if venda.valor > 120 and venda.itens > 12:
... venda.desconto = 12
>>> # bom
>>> if venda.permite_desconto_promocional():
... venda.aplica_desconto_promocional()
Também não devemos utilizar comentários óbvios, que só atrapalham a
leitura:
A regra do escoteiro
Quem conhece os escoteiros e seus ensinamentos sabe que uma das
premissas difundidas no movimento é: sempre que sair de um lugar
onde está acampando, deixe-o melhor do que o encontrou. Trazendo
esse ensinamento para nosso código: sempre antes de entregar seu
código deixe-o melhor do que encontrou, e aqui entra o refactoring.
Refactoring significa reestruturação, ou seja, refazer, remontar, parte de
um código que necessita melhorias. Claro que estaremos “refatorando”
para deixá-lo melhor, para que siga as dicas do clean code, certo?
Lembre-se sempre da regra do escoteiro!
Mas é óbvio também que não temos como sair que nem loucos
refatorando tudo que encontramos. Este é um trabalho que pode ser
feito pouco a pouco, no dia a dia. Mas busque sempre fazê-lo quando
possível.
Teste sempre
Todo código que desenvolvemos precisa ser testado de forma
automatizada. Os testes devem atingir nosso código de forma que
garantam sua funcionalidade conforme o esperado. Sempre que
fizermos refactoring ou alterações em nosso código precisamos ter
certeza de que não quebramos nada da lógica do nosso produto.
Existe uma técnica de desenvolvimento chamada TDD (Test Driven
Development ou desenvolvimento guiado por testes) que diz que
primeiro escrevemos o teste do código, deixando-o quebrado, ou seja,
com o teste falhando, e depois implementamos o código que faça com
que o teste tenha sucesso. Em seguida, “refatoramos” esse código para
que ele tenha um padrão elevado de qualidade e legibilidade. Essa
forma de pensar ajuda muito quando queremos elevar o nível do nosso
código, garantindo seu funcionamento.
SOLID
Outra dica para quem quer fazer um código limpo é sobre a utilização
dos princípios SOLID, os quais vimos no Capítulo 36 deste livro. Eles
andam juntos e devemos sempre procurar utilizá-los.
Padrões de projeto
Padrões de projeto (ou design patterns) são soluções para problemas já
conhecidos que encontramos no desenvolvimento de códigos
orientados a objetos. Esses padrões nos permitem escrever códigos
melhores. Não precisamos querer reinventar a roda toda vez que
codificamos, então não fique preocupado nem perca tempo em “criar
uma solução para chamar de sua”. Não estamos abordando esses
padrões aqui neste livro, mas busque aprender mais sobre o assunto e
como aplicá-los em Python. Esta é uma dica valiosa para quem quer
escrever códigos limpos e de qualidade.
Conclusão
Códigos mal escritos geram prejuízos enormes de manutenção. As
pessoas levam muito mais tempo tentando entender os códigos do que
de fato implementando as correções ou melhorias. Programadores que
fazem códigos que seguem as boas práticas do clean code contribuem
efetivamente para um desenvolvimento ágil e inteligente do seu
produto. Para saber mais, recomendamos o seguinte livro de referência,
utilizado na elaboração deste capítulo: “Código Limpo: habilidades
práticas de agile software” (MARTIN, 2009).
PARTE VI.
TESTES
42. Introdução a testes
Marcell Guilherme C. da Silva
55 <https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement>.
43. TDD – Test Driven
Development
Cláudio Henrique Franco Gomes
56 <https://github.com/jornada-colaborativa/livro-python>.
45. Testes unitários e testes
automatizados
Saulo Filho Perceval
Lourena Ohara
Testes unitários
Testes unitários podem ser considerados os testes mais básicos de uma
aplicação, e geralmente a responsabilidade de elaborá-los é do próprio
desenvolvedor que produziu o software a ser testado. Como o nome
sugere, o teste unitário, também chamado de teste modular, se
encarrega de testar uma unidade do software, ou seja, o menor
componente do código que pode ser acionado em separado do resto do
código (KOLODIY, 2015). No caso de um software escrito em um
paradigma estruturado, esse componente seria uma função; já no
paradigma orientado a objetos, seria o método de uma classe. Para
assegurar que os módulos estão trabalhando em conjunto de forma
adequada, são empregados os chamados testes de integração
(KOLODIY, 2015).
Os testes unitários devem ser curtos, rápidos, especificando bem o que
se pretende testar e com claras condições para falhar ou passar
(HELPER, 2012). Além de garantirem que o componente testado se
comporta da maneira desejada, os testes unitários também são úteis
para facilitar o processo de “debugar” o programa, ou seja, encontrar a
parte específica do programa que está gerando um bug ou erro e para
assegurar que uma mudança na lógica interna de um módulo ou a
adição de um novo módulo não gere um comportamento inesperado
no código (KOLODIY, 2015). O processo de criar testes unitários muitas
vezes também pode auxiliar o desenvolvedor a identificar que seu
programa pode ser modularizado de forma mais eficiente.
# Arquivo: calculator.py
class Calculator:
def pow(self, a, b):
# modulo a ser testado
if b < 0:
raise ValueError("exponent must be postive")
if b == 0:
return 1
result = a
for i in range(1, b):
result = result * a
return result
import unittest
from calculator import Calculator
class CalculatorTest(unittest.TestCase):
def test_pow_positive_exponent(self):
# teste unitario do modulo pow
# Arrange
calculator = Calculator()
# Action
result = calculator.pow(2, 3)
# Assert
self.assertEqual(result, 8)
if __name__ == '__main__':
unittest.main()
Cobertura de código
Cobertura de código são métricas utilizadas para indicar o quanto de
um código é executado durante a realização dos testes unitários deste
código (GURU99, s.d.). Uma boa cobertura de código nem sempre
significa qualidade nos testes unitários, muito menos a ausência de
bugs no código. Em um exemplo extremo, caso um desenvolvedor crie
testes que contemplem todos os possíveis cenários de todos os
módulos de seu código, mas não verifique o resultado das execuções
nas etapas de Assert dos testes, seu código será 100% coberto por
testes, mas sem que estes testes tenham qualquer eficiência em indicar
bugs no código.
Existem diversas maneiras de avaliar a cobertura de código, mas a mais
popular e frequente nos frameworks de teste é a chamada Statement
Coverage. Statement é basicamente uma instrução ou comando do
código, que pode ser, por exemplo, uma atribuição de variável,
definição de função ou classe, instanciamento de objeto, avaliação de
expressão, retorno de função, exceção, etc. Geralmente, em Python,
um statement equivale a uma linha de código.
Tomando como exemplo o código da classe Calculator apresentada
anteriormente, temos dez statements no total. O teste
test_pow_positive_exponent cobre oito desses statements,
abrangendo então 80% do código testado. Os statements marcados
com “#” são aqueles cobertos pelo teste, os demais não estão
cobertos.
# Arquivo: calculator.py
class Calculator: # ← Coberto pelo teste
def pow(self, a, b): # ← Coberto pelo teste
# modulo a ser testado
if b < 0: # ← Coberto pelo teste
raise ValueError
if b == 0: # ← Coberto pelo teste
return 1
result = a #
for i in range(1, b): # ← Coberto pelo teste
result = result * a # ← Coberto pelo teste
return result # ← Coberto pelo teste
import unittest
from calculator import Calculator
class CalculatorTest(unittest.TestCase):
# testes unitarios do modulo pow
def test_pow_positive_exponent(self):
# Arrange
calculator = Calculator()
# Action
result = calculator.pow(2, 3)
# Assert
self.assertEqual(result, 8)
def test_pow_negative_exponent(self):
# Arrange
calculator = Calculator()
# Action/Assert
with self.assertRaises(ValueError):
calculator.pow(2, -2)
def test_pow_zero_exponent(self):
# Arrange
calculator = Calculator()
# Action
result = calculator.pow(2, 0)
# Assert
self.assertEqual(result, 1)
if __name__ == '__main__':
unittest.main()
Agora uma observação. No teste test_pow_negative_exponent a
estrutura difere um pouco dos demais testes, pois é levantada uma
exceção no código testado (neste caso). No entanto, o teste ainda
segue o padrão AAA, já que o context manager self.assertRaises tem a
função de validar se a exceção esperada foi levantada durante o código
executado.
Com esses dois novos testes, atinge-se então 100% de cobertura do
código. O statement marcado com “#” é coberto pelo
test_pow_negative_exponent e o marcado com “##” é coberto pelo
test_pow_zero_exponent.
# Arquivo: calculator.py
class Calculator:
def pow(self, a, b):
# modulo a ser testado
if b < 0:
raise ValueError # ← Coberto pelo
test_pow_negative_exponent
if b == 0:
return 1 ## ← Coberto pelo test_pow_zero_exponent
result = a
for i in range(1, b):
result = result * a
return result
# Arquivo: reverse_string.py
def reverse_string(string):
# modulo a ser testado
reversed_string = ""
for i in range(len(string), 0, -1):
reversed_string += string[i-1]
return reversed_string
import unittest
from reverse_string import reverse_string
class TestReversestring(unittest.TestCase):
def test_reverse_string_not_empty_string(self):
# Arrange
string = "onibus"
# Action
result = reverse_string(string)
# Assert
self.assertEqual(result, "subino")
if __name__ == '__main__':
unittest.main()
# Arquivo: library_book.py
class Book:
def __init__(self, title):
self.title = title
self.is_lent = False
class Library:
def __init__(self):
self.lent_books = []
def lend_book(self, book):
# modulo a ser testado
if not book.is_lent:
book.is_lent = True
self.lent_books.append(book)
import unittest
from library_book import Library, Book
class TestLibrary(unittest.TestCase):
def test_lend_book_not_lent(self):
# Arrange
new_book = Book("Dom Casmurro")
new_library = Library()
# Action
new_library.lend_book(new_book)
# Assert
self.assertTrue(new_book.is_lent)
self.assertEqual(new_library.lent_books, [new_book])
if __name__ == '__main__':
unittest.main()
class LibraryCommunication:
def __init__(self, communication_service):
self.communication_service = communication_service
def send_return_warning(self, email_address):
# modulo a ser testado
text = "Your loan period ends tomorrow. Please " \
"remember to return or renew your books"
self.communication_service.send_email(email_address, text)
import unittest
from unittest.mock import Mock
class TestLibraryCommunication(unittest.TestCase):
def test_send_return_warning(self):
# Arrange
mock_email_service = Mock()
new_library_communication = LibraryCommunication(
mock_email_service
)
# Action
new_library_communication.send_return_warning("[email protected]")
# Assert
mock_email_service.send_email.assert_called_once_with(
"[email protected]",
"Your loan period ends tomorrow. Please "
"remember to return or renew your books"
)
# o metodo assert_called_once_with() verifica se o metodo
do
# objeto mock foi chamado apenas uma vez e como a os
argumentos
# passados. Do contrario levanta uma AssertionError
if __name__ == '__main__':
unittest.main()
Testes automatizados
Quando falamos em realizar testes, estamos também procurando e
promovendo velocidade e segurança no nosso desenvolvimento. A
automatização de testes é uma atividade que, além de promover essas
duas etapas, também diminui consideravelmente o trabalho manual e a
redução significativa de custos.
Os testes automatizados são realizados com a utilização de programas
e frameworks criados com essa finalidade. Com o apoio de uma
linguagem de programação, os cenários são descritos, definidos de
acordo com a necessidade do projeto, executados e analisados. Essa
execução pode ser feita em um certo tempo diversas vezes,
demonstrando o ganho de tempo que a automatização promove na
ação e análise dos testes.
GET
Devemos compor um arquivo no nosso projeto que fará a requisição do
tipo GET. Vamos criar um novo arquivo e nomeá-lo de get.py. Ao longo
da sessão, novos arquivos serão criados fazendo referência às
verbalizações de requisições. Utilizaremos a biblioteca Python request.
É necessário que seja importada dentro do nosso get.py. Esta é a
primeira linha do nosso arquivo.
As linhas a seguir fazem referência aos cabeçalhos da nossa requisição.
São os dados de autenticação e suas configurações para que possamos
enviá-la a fim de obter uma resposta. Configuramos as informações de
Accept e User-Agent. A próxima linha permite que o código realize uma
busca na URL informada na linha anterior. Aqui, determinamos uma
variável para visualizar posteriormente a nossa busca.
Por fim, realizamos um print da variável determinada. Uma vez que ela
faz a busca do corpo da requisição, esperamos que o terminal traga
exatamente essas informações. Veja a seguir como ficará a nossa
requisição get no projeto:
import requests
headers = {
'Accept': '*/*',
'User-Agent': 'request'
}
url = "https://heroes.free.beeceptor.com/heroes"
resp = requests.get(url, headers=headers)
print(resp.text)
POST
A requisição POST permite que sejam criadas novas informações. No
nosso exemplo, criaremos mais um super-herói para a API. A
configuração da requisição é similar ao GET anterior, porém cabem
algumas explicitações das modificações necessárias. A URL é
modificada, uma vez que o endpoint pertence a uma nova requisição.
Para o POST, utilizaremos “https://heroes.free.beeceptor.com/hero”.
Adicionaremos o objeto data, que trará as informações a serem
adicionadas. É muito importante sempre verificar a documentação da
API para conseguir inserir os dados no formato correto. No nosso
exemplo, o objeto deve ser enviado da seguinte maneira:
data = {
"id": 5,
"name": "Daredevil",
"age": 33,
"strength": "super-sense",
"weakness": "sound-frequency"
}
Por fim, a variável de resposta deve ser modificada para POST no lugar
de GET. Temos então a nossa requisição POST:
import requests
headers = {
'Accept': '*/*',
'User-Agent': 'request'
}
url = "https://heroes.free.beeceptor.com/hero"
data = {
"id": 5,
"name": "Daredevil",
"age": 33,
"strength": "super-sense",
"weakness": "sound-frequency"
}
resp = requests.post(url, headers=headers, data=data)
print(resp.text)
import requests
headers = {
'Accept': '*/*',
'User-Agent': 'request'
}
url = "https://heroes.free.beeceptor.com/hero/4"
resp = requests.get(url, headers=headers)
print(resp.text)
PUT
A requisição PUT sobrescreve ou atualiza informações já existentes na
API. É possível alterar informações como nome, idade ou fraquezas de
um herói. Novamente, é ideal acompanhar a documentação da API
para que os dados sejam fornecidos com o formato correto. Problemas
dessa natureza ou uma conexão errada com a URL podem causar falhas
de requisição, sendo exibidos códigos de erro padrão das APIs (400 ou
500, por exemplo).
Como feito no POST, devemos informar o objeto data contendo as
informações atualizadas para modificação. Além disso, a nossa URL
deve ser modificada de acordo com a determinação. Nesse caso, ela é
finalizada com o ID do objeto a ser modificado. Atualizando a nossa
variável de resposta com o comando PUT, temos a nossa requisição a
seguir:
import requests
headers = {
'Accept': '*/*',
'User-Agent': 'request'
}
url = "https://heroes.free.beeceptor.com/hero/4"
data = {
"id": 4,
"name": "Captain Marvel",
"age": 27,
"strength": "phisical-strength",
"weakness": "diseases"
}
resp = requests.put(url, headers=headers, data=data)
print(resp.text)
DELETE
É a requisição que vai permitir que sejam deletadas informações dentro
da API. Na URL, deve ser informado qual objeto será apagado através
do ID. Modificando também os dados da variável de resposta de acordo
com o verbo utilizado, nosso teste será feito da seguinte maneira:
import requests
headers = {
'Accept': '*/*',
'User-Agent': 'request'
}
url = "https://heroes.free.beeceptor.com/hero/1"
resp = requests.delete(url, headers=headers)
print(resp.text)
57 <https://docs.python.org/3/library/unittest.html>.
58 <https://docs.python.org/3/library/unittest.mock.html>.
59 <https://www.selenium.dev/>.
60 <https://beeceptor.com/>.
46. Mocks
Cláudio Henrique Franco Gomes
Suponha que você tenha escrito uma biblioteca de código com diversas
interfaces a sistemas externos, como bancos de dados e sites, e que
você não tenha acesso administrativo a essas interfaces, isto é, você
não possa controlar o comportamento delas, apenas acessar. Suponha
também que todo o código-fonte referente à conexão a essas
interfaces já tenha sido testado sem os possíveis erros e que o
“caminho feliz” esteja funcionando corretamente. Suponha que é hora
de inserir as condições de contorno para as situações de exceção, para
quando as interfaces estão lentas ou fora do ar. Sem acesso
administrativo às interfaces, como simular essas situações?
O módulo mock da biblioteca unittest do Python61 possibilita a
substituição e imitação de objetos em um ambiente de teste,
simulando diversas situações em condições controladas.
Por exemplo, no código a seguir, instanciamos um objeto Mock e
dizemos que seu valor de retorno é o número inteiro 3. Não importa
quais parâmetros são passados para a instância, ela sempre vai retornar
o mesmo número inteiro 3.
>>> atributos = {
... 'metodo.return_value':'valor arbitrário',
... 'metodo_com_erro.side_effect':IndexError
... }
>>> mock.configure_mock(**atributos)
>>> mock.metodo()
'valor arbitrário'
>>> mock.metodo_com_erro()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Python\lib\unittest\mock.py", line 1081, in __call__
return self._mock_call(*args, **kwargs)
File "C:\Python\lib\unittest\mock.py", line 1085, in _mock_call
return self._execute_mock_call(*args, **kwargs)
File "C:\Python\lib\unittest\mock.py", line 1140, in
_execute_mock_call
raise effect
IndexError
Decorator patch
Suponha que você tenha o seguinte trecho de código que verifica se um
site está on-line e, caso esteja, imprime o texto do site. Se você for
testar esse código, cada conexão ao site estará dependente da
velocidade do tráfego da rede. Para uma solicitação pontual, pode ser
um tempo irrisório, mas se ocorrem muitas solicitações ao longo do
programa, você pode ter um impacto não desejado na duração dos
testes.
# Arquivo: jornada.py
import requests
def imprime_site(url, metodo='get'):
response = requests.__dict__[metodo](url)
if response.status_code == 200:
print(response.text)
import unittest
from unittest.mock import patch
from jornada import imprime_site
@patch('jornada.requests.post')
@patch('jornada.requests.get')
def testa_imprime_site(mock_get, mock_post):
imprime_site('http://httpbin.org/get')
mock_get.assert_called()
imprime_site('http://httpbin.org/post', 'post')
mock_post.assert_called()
if __name__ == '__main__':
unittest.main()
import requests
def esta_online(url, metodo):
return requests.__dict__[metodo](url).status_code == 200
def imprime_site(url, metodo='get'):
if esta_online(url, metodo):
print(requests.__dict__[metodo](url).text)
import unittest
from unittest.mock import patch
from jornada import imprime_site
@patch('jornada.esta_online', return_value=False)
@patch('jornada.requests.post')
@patch('jornada.requests.get')
def testa_imprime_site(mock_get, mock_post, mock_online):
imprime_site('http://httpbin.org/get')
mock_get.assert_not_called()
imprime_site('http://httpbin.org/post', 'post')
mock_post.assert_not_called()
mock_online.assert_called()
if __name__ == '__main__':
unittest.main()
import requests
from pprint import pprint
def esta_online(url, metodo):
return requests.__dict__[metodo](url).status_code == 200
def imprime_site(url, metodo='get'):
try:
if esta_online(url, metodo):
print(requests.__dict__[metodo](url).text)
except Exception:
pprint("Exceção detectada.")
import unittest
from unittest.mock import patch
from jornada import imprime_site
@patch('jornada.pprint')
@patch('jornada.esta_online', side_effect=Exception('Erro'))
@patch('jornada.requests.post')
@patch('jornada.requests.get')
def testa_imprime_site(mock_get, mock_post, mock_online,
mock_pprint):
imprime_site('http://httpbin.org/get')
mock_get.assert_not_called()
imprime_site('http://httpbin.org/post', 'post')
mock_post.assert_not_called()
mock_online.assert_called()
mock_pprint.assert_called()
if __name__ == '__main__':
unittest.main()
import requests
class Jornada():
def __init__(self):
pass
def esta_online(self, url, metodo):
return requests.__dict__[metodo](url).status_code == 200
class Jornada():
def __init__(self, nome):
self.__nome = nome
@property
def nome(self):
return self.__nome
@nome.setter
def nome(self, nome):
self.__nome = nome
import unittest
from unittest.mock import patch
from unittest.mock import PropertyMock
from jornada import Jornada
@patch('jornada.Jornada.nome', new_callable=PropertyMock)
def testa_jornada(mock_property):
jornada = Jornada('Jupiter')
print(jornada.nome)
mock_property.assert_called()
jornada.nome = 'Zeus'
mock_property.assert_called()
print(jornada.nome)
mock_property.assert_called()
if __name__ == '__main__':
unittest.main()
import numpy as np
class Cubo():
def __init__(self):
self.__valor = np.random.randint(1, 6)
@property
def valor(self):
return self.__valor
def rolar(self):
self.__valor = np.random.randint(1, 6)
import unittest
from unittest.mock import patch
import jornada
@patch('jornada.Cubo')
def testa_jornada(mock_cubo):
class Dado():
def __init__(self):
pass
@property
def valor(self):
return 7
def rolar(self):
raise ValueError
mock_cubo.return_value = Dado()
cubo = jornada.Cubo()
assert cubo.valor == 7
try:
cubo.rolar()
except:
print("Exceção detectada.")
if __name__ == '__main__':
unittest.main()
61 <https://docs.python.org/3/library/unittest.mock-examples.html>.
PARTE VII.
DESENVOLVIMENTO DE
APLICAÇÕES WEB
47. Fundamentos da web
Everton de Castro
Tatiana Escovedo
O protocolo HTTP
Um protocolo é uma convenção para a execução de um procedimento
ou processo. Por exemplo, na língua portuguesa e nas mais comuns que
conhecemos, escrevemos em uma página seguindo um fluxo de cima
para baixo, da esquerda para direita, e quem lê a página deve seguir o
mesmo fluxo para a compreensão da mensagem. Na comunicação
entre sistemas web, protocolos são utilizados para a comunicação entre
os agentes, estabelecendo padrões universais que tanto o agente que
solicita quanto o agente que responde à mensagem conhecem.
O protocolo HTTP (Hyper Text Transfer Protocol, ou Protocolo de
Transferência de Hipertexto) é a base para a comunicação ou
transferência de dados entre sistemas web. Na sua primeira versão, o
HTTP/0.9 era um protocolo simples, criado para transferir informações
pouco sofisticadas. Esse protocolo evoluiu em versões posteriores,
sendo capaz de entregar informações em formatos mais elaborados. O
HTTP utiliza outro protocolo, o TCP (Transmission Control Protocol, ou
Protocolo de Controle de Transmissão), um padrão muito utilizado para
transferência de dados na internet. A principal função do HTTP é
viabilizar a comunicação entre dois agentes na web.
Modelo cliente-servidor
O modelo cliente-servidor representa bem a estrutura da web e como
os agentes se comunicam. Um servidor, normalmente, é um
computador robusto que tem em sua estrutura a capacidade de
disponibilizar recursos e serviços para um ou mais clientes. Esses
clientes podem ser navegadores (browsers) ou até mesmo outros
servidores. A comunicação neste modelo acontece por meio de uma
rede de computadores, seja ela local ou global como a internet.
Neste modelo, o cliente faz uma tentativa de conexão HTTP com o
servidor, que responde ao cliente se pode ou não estabelecer a
conexão. Depois de estabelecida uma conexão entre os dois agentes, a
troca de informações pode ocorrer em um formato bidirecional. O
cliente pode solicitar informações do servidor, por exemplo, quando um
usuário digita no navegador uma URL (Uniform Resource Locator) – que
é o endereço do servidor e caminho do recurso a ser acessado, por
exemplo, http://www.google.com –, ou enviar informações para o
servidor quando o usuário preenche um formulário de cadastro e envia
o conteúdo. A Figura 47.1 ilustra o modelo cliente-servidor:
Figura 47.1. Modelo cliente-servidor.
Fonte: Castro, 2018.
HTML
HTML (HyperText Markup Language ou Linguagem de Marcação de
Hipertexto) é a linguagem base para a web e possibilita a criação de
documentos estruturados que os navegadores são capazes de
interpretar e exibir como página após receber uma resposta de um
servidor com o conteúdo nesse formato.
Para as marcações a linguagem utiliza um conjunto de tags em um
fluxo de abrir e fechar. Do ponto de vista do navegador e de motores de
busca, as tags são identificadas como elementos, onde cada um tem
um objetivo semântico com uma formatação padrão, por exemplo:
<!DOCTYPE html>
<html>
<head>
<title>Fundamentos da web</title>
</head>
<body>
<h1>O que é HTML?</h1>
<p>HTML é uma linguagem de marcação...</p>
<a href="https://pt.wikipedia.org/wiki/HTML">Veja mais</a>
</body>
</html>
Até aqui vimos as tags mais básicas para exibição de texto, mas existem
tags para exibição de imagens, vídeo, listas etc. Outras tags muito
utilizadas em sistemas web são caixas de texto e formulários.
Formulários são utilizados para enviar dados para o servidor, por
exemplo, um cadastro de usuário, e devido à sua maior complexidade
não será detalhado neste capítulo.
O HTML evoluiu muito desde sua primeira versão, ganhando novas tags
à medida que os navegadores se tornavam mais poderosos,
possibilitando a criação de funcionalidades mais avançadas quando
comparadas às funcionalidades básicas dos primeiros navegadores que
utilizavam apenas as tags básicas de exibição de texto. Atualmente, os
profissionais que utilizam HTML para criação de páginas para sites e
sistema web possuem muitos recursos e ferramentas em suas mãos,
tendo apenas a criatividade como limite. Contudo, aspectos como
semântica e performance devem ser observados para não impactar
negativamente o potencial daquilo que criam.
<!DOCTYPE html>
<html>
<head>
<title>Fundamentos da web</title>
<style>
h1{
color:blue;
font-size: 40px;
}
</style>
</head>
<body>
<h1>O que é HTML?</h1>
<p>HTML é uma linguagem de marcação...</p>
<a href="https://pt.wikipedia.org/wiki/HTML">Veja mais</a>
</body>
</html>
h1{
color: blue;
font-size: 40px;
}
Outro recurso que adiciona mais funcionalidades a uma página web são
os scripts. O JavaScript é uma das linguagens de script mais populares,
que os navegadores interpretam para aplicar comportamentos em
páginas web. JavaScript é uma linguagem multiparadigma e dinâmica
utilizada para programar comportamentos atrelados a eventos no
navegador. Contudo, ela funciona em outros ambientes além do
navegador.
Como o HTML é estático, o JavaScript foi criado para adicionar
dinamismo em páginas web. Por exemplo, quando utilizamos uma
aplicação que possui uma barra superior retrátil, normalmente é um
código JavaScript que aplica o comportamento de aparecer e
desaparecer atrelado a um clique de botão. O mesmo se aplica para
validação de formulários, efeitos de esmaecer, etc. Veja a seguir um
exemplo de JavaScript:
<!DOCTYPE html>
<html>
<head>
<title>Fundamentos da web</title>
<link rel="stylesheet" href="estilo.css"></link>
<script>
function hideLink() {
var element = document.getElementById("link");
element.style.display = "none";
}
</script>
</head>
<body>
<h1>O que é HTML?</h1>
<p>HTML é uma linguagem de marcação...</p>
<button onclick="hideLink()">Esconder Link</button>
<a id="link" href="https://pt.wikipedia.org/wiki/HTML">Veja
mais</a>
</body>
</html>
[
{
"nome": "Marcus",
"email" : "[email protected]",
"id":1
},
{
"nome" : "Naiara",
"email" : "[email protected]",
"id" : 2
}
]
YAML
Criado em 2001 com inspiração no XML, Python, C, entre outras,
significa YAML Ain’t Markup Language – em português, YAML não é uma
linguagem de marcação. Diferentemente das anteriores, cada marcação
deve ser tabulada – a característica, como se percebe, vem do Python.
usuarios:
usuario:
nome: 'Marcus'
email: '[email protected]'
id: 1
usuario:
nome: Naiara
email: [email protected]'
id: 2
O que é GraphQL?
O GraphQL é uma sintaxe que descreve como solicitar dados e é
geralmente utilizada para carregar dados específicos de um servidor
para um cliente. Ele permite que o cliente especifique exatamente de
quais dados ele precisa, o que facilita a agregação de dados de várias
fontes. Usa um sistema de tipos para descrever dados.
Como ele permite que os clientes definam quais dados esperados e seu
formato, e essa mesma estrutura é retornada do servidor, impede o
retorno de quantidades excessivas de dados, mas isso tem implicações
na eficácia do armazenamento em cache da web dos resultados da
consulta.
Assim, o GraphQL, por padrão, fornece a menor solicitação possível, ao
contrário do REST, que envia tudo de uma vez, a solicitação mais
completa. Por esse motivo, o GraphQL pode ser mais útil em casos de
uso específicos em que um tipo de dado necessário é bem definido e
um pacote de dados baixo é preferido.
Além disso, com o GraphQL, o usuário pode fazer uma única solicitação
para buscar as informações necessárias, em vez de construir várias
solicitações REST para buscá-las.
Uma metáfora para explicar: o modelo REST é como pedir pizza, buscar
as compras no supermercado e as roupas limpas na lavanderia. Três
serviços, três ligações. GraphQL é como ter um assistente pessoal: uma
vez que você fornece o endereço dos três locais, você pode
simplesmente pedir o que você quer (me dê minhas roupas limpas, uma
pizza grande e uma dúzia de ovos) e aguardar o retorno. Em outras
palavras, o GraphQL estabelece uma linguagem padrão para conversar
com esse assistente pessoal mágico.
Na prática, uma API GraphQL é organizada em torno de três
componentes principais: o esquema, as consultas e os resolvedores.
A seguir, um breve exemplo de esquema que mostrará para os clientes
o que pode ser utilizado para consumir da sua aplicação:
type query{
pessoas : [Pessoa]
}
type Pessoa{
id : ID
nome : string
emails : [Email]
}
type Email{
email : string
}
Utilizando o esquema anterior, a seguir será realizada uma solicitação
de cadastros, onde será retornado apenas o nome do cadastro:
query{
pessoas{
nome
emails
}
}
{
"pessoas": [{
"nome": "Marcus",
"emails": ["[email protected]"]
}, {
"nome": "Naiara",
"emails": ["[email protected]", "[email protected]"]
}]
}
O que é WSGI?
Originalmente apresentada na PEP 333 e com uma nova revisão na PEP
3333, WSGI é uma interface padrão proposta entre servidores da web e
aplicativos ou estruturas da web Python para promover a portabilidade
de aplicativos da web em uma variedade de servidores da web (EBY,
2010).
A interface WSGI possui dois lados: o lado “servidor” ou gateway e o
lado “aplicação” ou framework. O lado do servidor chama um objeto
callable fornecido pelo lado do aplicativo. Os dois lados são
independentes, o que dá flexibilidade ao desenvolvedor para escolher
tanto o framework de desenvolvimento quanto o servidor web.
Mas atenção: WSGI não é um framework, e sim uma ferramenta para
simplificação da interface entre os diferentes frameworks de
desenvolvimento Python e os muitos servidores web disponíveis. Os
desenvolvedores ganham tanto em flexibilidade, à medida que podem
escolher o melhor framework para cada aplicação específica, quanto em
escalabilidade, visto que o responsável por atender às milhares de
solicitações de conteúdo dinâmico de uma só vez é o WSGI, não os
frameworks.
Um ponto muito importante para esse tipo de implementação é a
segregação de responsabilidades, que deve ser muito bem feita para
garantir a correta e eficiente escalabilidade do tráfego web.
A imagem a seguir ilustra o fluxo de uma requisição utilizando WSGI:
62 <https://developers.facebook.com/>.
63 <https://cloud.google.com/maps-platform/maps?hl=pt>.
64 <https://docs.python.org/3/library/json.html>.
65 <https://docs.python.org/3/library/xml.etree.elementtree.html>.
49. Conceito de frameworks
Tatiana Escovedo
Estrutura do Django
Para conseguirmos criar nossas aplicações utilizando o Django, é
necessário entender um pouco de seus componentes mais comuns. Para
explicar melhor esses componentes, vamos criar uma aplicação básica que
implementa um CRUD, que é uma sigla para o conceito de create, read,
update e delete. Ou seja, vamos criar uma aplicação que possibilita a
criação de dados em um banco de dados, a leitura desses dados (listagem),
a atualização e a exclusão dos dados. Assim, todos os exemplos estarão
relacionados, possibilitando um entendimento maior dos conceitos. A
partir de agora você irá acompanhar de forma prática a criação do projeto.
Início do projeto
Para começar, precisamos ter o Python instalado no computador,
conforme você acompanhou no Capítulo 6 – Instalando. Após a
instalação, aconselhamos a criação de um ambiente virtual com o
comando python -m venv nomedoseuambientevirtual, que deverá ser
dado via linha de comando (via terminal, por exemplo, o CMD do
Windows). Apesar desse procedimento não ser mandatório, ele será muito
útil para um aprendizado completo da linguagem. Se você optar pela
criação do ambiente virtual, todo o seu projeto deverá ser desenvolvido
dentro da pasta do ambiente virtual (uma pasta
nomedoseuambientevirtual será criada automaticamente) e será
necessário manter o seu ambiente virtual ativado durante o
desenvolvimento do seu projeto (para ativar o ambiente virtual, você
precisa executar – via linha de comando – o arquivo activate localizado
dentro da pasta nomedoseuambientevirtual\Scripts\). Consulte o
Capítulo 9 – Criação de ambiente virtual para mais detalhes. A partir de
agora não mencionaremos mais a necessidade de ativar o ambiente virtual
durante o desenvolvimento do seu projeto. Após isso, vamos instalar o
Django no seu ambiente de trabalho. Você pode utilizar o gerenciador de
pacotes da sua preferência (ver Capítulo 10 – Gerenciadores de pacotes).
No nosso caso, vamos optar pelo comando pip: pip install django==3.1.5
(versão do Django quando este livro foi escrito).
Após a instalação, agora precisamos criar um projeto com o comando
django-admin startproject produto. Com a execução do comando, será
criada, no diretório em que você executou o comando, uma nova pasta
chamada produto. Lembrando que a palavra produto é o nome do projeto
que escolhemos; você pode dar o nome que desejar. Dentro da pasta
produto você terá o arquivo manage.py e uma outra pasta chamada
produto. Sim, outra pasta com o mesmo nome. Dentro dessa outra pasta,
você terá os seguintes arquivos: __init__.py, asgi.py, settings.py, urls.py
e wsig.py. Essa é a estrutura padrão de uma aplicação web Django.
Todas as configurações do Django ficam no arquivo settings.py. Ele é o
coração do seu sistema e você estará sempre em contato com esse
arquivo durante o desenvolvimento do seu sistema. Sempre que você
precisar efetuar alterações relativas à configuração, é esse arquivo que
você deverá alterar. Para iniciar as configurações, podemos efetuar a
alteração de fuso horário e de idioma. Procure, dentro do arquivo, a linha
que contém TIME_ZONE e modifique para TIME_ZONE =
‘America/Sao_Paulo’. Altere também a linha LANGUAGE_CODE para
LANGUAGE_CODE = ‘pt-BR’.
Outro arquivo de extrema importância para o seu projeto é urls.py,
também chamado de root URLconf. É por intermédio desse arquivo que
você monta a estrutura de URLs do sistema. Ou seja, esse arquivo associa
uma determinada URL a um código Python, chamado de Views. Quando
uma requisição chega na aplicação, o Django se encarrega de procurar nas
configurações de URLs qual View ele deverá invocar para processar a
requisição. Imagine, por exemplo, que digitamos a URL www.meusite.co
m.br/meus_produtos no browser. O Django vai quebrar a URL em partes
e vai utilizar a parte /meus_produtos para poder procurar, no arquivo
urls.py, qual a View adequada para invocar.
Os arquivos asgi.py e wsgi.py são necessários para que a aplicação seja
executada em ambiente de produção, sendo o asgi.py para web servers
assíncronos e o wsgi.py para web servers síncronos. Devido ao curto
espaço que temos para falar sobre Django, não entraremos em detalhes
sobre esses arquivos, pois executaremos a nossa aplicação em ambiente
local.
Em relação ao arquivo __init__.py, ele serve para indicar que o conjunto
de arquivos em um diretório é um pacote Python. Porém, a partir do
Python 3, esse arquivo deixou de ser necessário (porém não inútil). De
qualquer forma, não precisaremos nos preocupar com ele, pois não irá
interferir diretamente na nossa aplicação.
O último arquivo que compõe a estrutura padrão de uma aplicação Django
é o manage.py. Esse arquivo será muito importante para nós, pois nos
auxiliará na execução dos diversos comandos necessários para o processo
de desenvolvimento de uma aplicação web. Aproveitando que falamos
nele, vamos acessar a pasta produto e criar o nosso primeiro App, com o
comando python manage.py startapp meus_produtos. Com esse
comando o Django vai criar uma nova pasta chamada meus_produtos
(você pode dar o nome que desejar) com o mínimo necessário de módulos,
são eles: __init__.py, admin.py, apps.py, models.py, tests.py e views.py.
E dentro dessa pasta também será criado o diretório migrations.
Para finalizarmos as informações e os procedimentos relativos à estrutura
de um App dentro do Django, precisamos agora adicionar o nosso App
‘meus_produtos’ nas configurações do Django, ou seja, no arquivo
settings.py. Para isso, abra esse arquivo, procure por INSTALLED_APPS e
adicione o nome do App entre aspas simples ao final da relação de Apps
que já estão previamente configurados pelo Django.
Nós definimos uma classe chamada Produto (você pode dar o nome que
desejar). Essa classe também é um objeto Python. Se você ainda não
conhece bem os conceitos de orientação a objeto, leia o Capítulo 25 deste
livro. Dentro da classe nós definimos as propriedades que já citamos, ou
seja, nome, descrição e preço. Para fazer isso, é necessário definir um tipo
para cada campo. Por exemplo, o campo de nome e descrição são do tipo
texto, enquanto o campo de preço pode ser do tipo decimal. Além dos
campos mencionados, também implementamos a função __str__. Essa
implementação não é obrigatória, mas é aconselhável, para que o nosso
painel de administração do Django (que veremos a seguir) fique mais
amigável ao nosso entendimento.
Migrations
Para que o Django sincronize as informações do arquivo models.py com o
nosso banco de dados, primeiramente precisamos fazer com que o Django
saiba que fizemos essas alterações. Para isso precisamos construir uma
migração do banco de dados para a adição da tabela de Produtos
utilizando o comando python manage.py makemigrations
meus_produtos. A título de conhecimento, esse comando criará o arquivo
0001_initial.py dentro da pasta meus_produtos/migrations. Pronto? A
nossa base de dados recebeu as informações do arquivo models.py? Não,
o que fizemos foi apenas criar o arquivo de migração. Esse arquivo contém
tudo que o Django precisa saber para atualizar a base de dados. Para
concluir o processo de migração, devemos executar o comando python
manage.py migrate. Se tudo correr bem, algumas informações aparecerão
na tela e a linha de comando logo voltará ao modo de digitação.
Após essa configuração, vamos criar um super usuário para que você possa
acessar o painel de administração do Django. Utilize o comando python
manage.py createsuperuser e cadastre seu usuário e senha. Não é
necessário cadastrar um e-mail, mas se você esquecer sua senha a
recuperação é feita somente por esse e-mail. Se a sua senha for muito
curta, comum ou similar ao seu nome de usuário, o Django solicitará uma
confirmação; basta digitar a letra ‘y’. Como estamos em um ambiente de
aprendizado, você poderá colocar uma senha simples. Mas é claro que em
ambiente de produção essa senha precisará ser bem mais robusta. Agora
que você já tem a sua senha de administrador, você já pode acessar o
módulo de administração do Django. Para isso, precisamos executar o
nosso servidor e, após alguns segundos, abrir o navegador e digitar: http://
127.0.0.1:8000/admin. Entre com o seu usuário e senha. Após a
autenticação, clique em ‘+Adicionar’ ao lado de ‘Produtos’. Agora você já
poderá cadastrar alguns produtos no sistema. Depois de adicionar alguns
produtos, a nossa tela ficou assim.
TEMPLATES = [
{
'BACKEND': 'Django.template.backends.Django.DjangoTemplates',
'DIRS': [TEMPLATE_DIR],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'Django.template.context_processors.debug',
'Django.template.context_processors.request',
'Django.contrib.auth.context_processors.auth',
; 'Django.contrib.messages.context_processors.messages',
],
},
},
]
<html>
<head>
<title>Meus Produtos</title>
</head>
<body>
<h1>Meus Produtos</h1>
{% for i in produto %}
<div style= "width: 500px; border: solid 2px black; margin-
bottom: 10px; padding-left: 8px;">
<p>
; {{ i.id }} - <b>Nome:</b> {{ i.nome }}
; <b>Preço:</b> R$ {{ i.preco }}
</p>
<p>
; <b>Descrição:</b> {{ i.descricao }}
</p>
</div>
{% endfor %}
</body>
</html>
Vamos agora fazer uma pequena alteração no arquivo urls.py para que a
nossa listagem de produtos apareça na página principal do site. A linha a
seguir deverá ser alterada:
path('produtos/', lista_produtos),
Vamos deixar o primeiro argumento do path como uma string vazia. Após
isso, salve e acesse a URL http://127.0.0.1:8000/ no navegador com o
servidor em execução.
path('', lista_produtos),
def cadastra_produtos(request):
if request.method == "POST":
formulario = cadastroForm(request.POST)
if formulario.is_valid():
objeto = formulario.save()
objeto.save()
formulario = cadastroForm()
else:
formulario = cadastroForm(request.POST)
return render(request, 'cadastra_produtos.html',{'formulario':
formulario})
Essa View será a principal responsável pela inserção dos dados no banco.
Repare que temos duas situações diferentes que podem ocorrer. A
primeira é quando acessamos a página pela primeira vez e precisamos de
um formulário totalmente em branco. Já a segunda coisa que pode ocorrer
é quando voltamos para a View com todos os dados do formulário
preenchidos que o usuário do site acabou de digitar. Se o acesso à página
for feito pela primeira vez, a nossa condicional irá direto para o else, que
vai gerar uma instância vazia da classe cadastroForm (variável
formulario). Isso quer dizer que um formulário vazio será impresso na tela.
Após algum usuário digitar informações no formulário e clicar em enviar
(você verá o layout do formulário mais à frente), a nossa condicional
entrará logo no primeiro if, pois o procedimento de enviar é um request do
tipo POST. Nesse caso, a primeira linha após o if vai instanciar um objeto
formulario contendo as informações que foram digitadas pelo usuário. A
partir daí, no segundo if, o objeto formulario é conferido se está correto
(se, por exemplo, atende a exigências como campo requerido etc.) e
posteriormente salvo no banco de dados. Ainda no arquivo views.py, não
podemos esquecer de fazer a importação do nosso formulário
cadastroForm conforme segue:
path('cadastra_produtos/', cadastra_produtos),
Ainda nesse mesmo arquivo precisamos importar a nossa nova View. Para
isso precisamos alterar a linha a seguir acrescentando cadastra_produtos
ao final:
<html>
<head>
<title>Cadastrar Produtos</title>
</head>
<body>
<h1>Cadastrar Produtos</h1>
<form method="POST">
{% csrf_token %}
{{ formulario.as_p }}
<button type="submit">Salvar</button>
</form>
</body>
</html>
<b>Descrição:</b> {{ i.descricao }}
</p>
<p>
<a href="{% url 'editar' i.id %}">Editar</a>
</p>
</div>
<p>
<a href="{% url 'editar' i.id %}">Editar</a>
</p>
<p>
<a href="{% url 'excluir' i.id %}">Excluir</a>
</p>
</div>
Agora vamos acessar o arquivo views.py e criar a nossa nova View. Basta
acrescentar o código a seguir ao final do arquivo.
<h1>Meus Produtos</h1>
<p>
<a href=" /cadastra_produtos/">Adicionar Novo Produto</a>
</p>
{% for i in produto %}
66 <https://docs.djangoproject.com/pt-br>.
51. Flask
Marcos Alexandre Castro
Sérgio Berlotto Jr.
import requests
url = 'http://localhost:5000/?nome=seu_nome'
r = requests.get(url)
print(r.status_code)
>>> 200
diretório do projeto
├── app.py # código fonte da aplicação
├── static # diretório para os arquivos estáticos
└── templates # páginas a serem renderizadas pelo Flask
└── index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Olá mundo!!!</title>
</head>
<body>
<h1>Olá mundo!!!</h1>
</body>
</html>
Agora vamos alterar o arquivo app.py para renderizar esta página. Além
disso, está explícito que a view responderá apenas a requisições do tipo
GET. Por fim, adicionamos dois parâmetros à função run: o primeiro é
debug=True, que permite ao desenvolvedor escrever código enquanto
a aplicação está rodando, o que elimina o trabalho de reiniciar o serviço
toda vez que o código é alterado; e o segundo parâmetro determina a
porta utilizada pela aplicação (explícito é melhor que implícito – veja o
zen of Python (PETERS, 2004).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Olá {{ nome }}!!!</title>
</head>
<body>
<h1>Olá {{ nome }}!!!</h1>
</body>
</html>
@app.route('/<nome>')
def index(nome=None):
if request.method == "POST":
nome = request.POST['nome']
elif request.args.get('nome'):
nome = request.args.get('nome')
elif nome:
nome = nome
else:
nome = 'mundo'
return render_template('index.html', nome=nome)
# Bloco principal de código (executado apenas quando o arquivo é
chamado)
if __name__ == '__main__':
# inclusão de 2 parâmetros
app.run(port=5000, debug=True)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Olá mundo </title>
</head>
<body>
<h1>Olá mundo </h1>
<hr>
{% autoescape false %}
{{ table }}
{% endautoescape %}
</body>
</html>
67 <https://jinja.palletsprojects.com/en/2.11.x/>.
68 <https://stackoverflow.com/>
52. Executando tarefas
assíncronas
Guilherme Arthur de Carvalho
69 <https://github.com/jornada-colaborativa/livro-python>
PARTE VIII.
ANÁLISE DE DADOS
53. Introdução ao processo de ETL
Guilherme de Almeida Gasque
O que é ETL?
A sigla ETL é uma abreviação para Extraction, Transform and Load
(Extração, Transformação e Carga). Trata-se de um conceito bastante
difundido na área de dados e com presença constante na realidade dos
engenheiros de dados. Este capítulo abordará de forma sucinta os
macroprocessos que constituem a etapa de ETL.
A extração, de maneira geral, é composta pela fonte de dados, a
estrutura da informação que será obtida e seu formato. Já a
transformação consiste na manipulação de dados, como filtragem das
observações ou junção de informações, comumente conhecidos como
joins, além da higienização, garantido assim a confiabilidade da
informação extraída. Por fim, temos a carga, processo responsável por
disponibilizar a informação extraída e transformada em um local para
que possa ser acessada e usada para consultas, análises e construção de
dashboards, dentre outros.
Antes de pensar em qualquer processo de ETL, é preciso definir a
arquitetura do projeto, tal como dimensionamento de máquinas,
seleção de serviços, definição de pipeline e tecnologias adotadas. É
importante, antes de iniciar qualquer processo de dados que envolvam
ETL, o entendimento profundo da informação e da entrega de valor que
deve ser feita, além do conhecimento das fontes de informações, de
onde os dados serão extraídos, do ambiente de processamento, onde
serão transformados, e do “cliente”, onde o dado será carregado e
persistido.
Extração
Antes de entender como a informação será extraída, é necessário
compreender a estrutura da informação para selecionar a biblioteca
apropriada para que o dado, após extraído, possa ser manipulado e
transformado. Os dados são comumente organizados em três
categorias: dados estruturados, dados semiestruturados e dados não
estruturados.
Os dados estruturados possuem duas dimensões: linhas e colunas. De
maneira técnica, são denominados como variáveis e observações e a
dimensão de uma tabela é sempre interpretada como linha versus
coluna, ou variáveis por observações. Também conhecido como tabelas
ou matrizes, é o tipo de estrutura utilizada em bancos de dados SQL ou
em data warehouses. Uma função bastante utilizada para a visualização
e a quantidade de variáveis por observações é shape, que retorna uma
tupla representando a dimensionalidade do Dataframe, conforme
demonstrado a seguir.
>>> iso3166.shape
(245, 4)
Transformação
A transformação de dados abrange o processo de tratamento e
preparação da informação. Para possibilitar tal processo, é necessário
entender quais são os conjuntos de dados, seus formatos e como
combiná-los.
São exemplos de conjunto de dados:
✓ Datasets: consistem em um conjunto de dados relacionados a
alguma especificidade, como consumo histórico de alimentos nos
continentes do planeta, informações demográficas sobre a
população de um país, uma planilha de gastos etc. São dados
tabulares, onde cada coluna representa uma variável e cada linha
corresponde a um registro (observação) do conjunto de dados em
questão.
✓ Dataframes: o conceito de Dataframe é bem similar ao de Dataset,
sendo também um conjunto de dados representados por variáveis
e observações em formato tabular. O que difere um Dataframe de
um Dataset é a existência de metadados, mais bem definidos para
o Dataframe, pois há um contexto para as variáveis. Por exemplo,
para um Dataframe de uma tabela de cadastros de usuários, a
variável nome deve conter apenas strings, já a variável idade deve
conter apenas dados de tipo inteiro.
De maneira prática, essa identidade dada a cada observação é
conhecida como schema. Para exibição do schema de um Dataframe,
pode-se utilizar a função info(), mostrada na próxima seção. Também é
possível utilizar o atributo dtypes para exibir o schema de todo o
dataframe, como no exemplo a seguir.
>>> iso3166.dtypes
Country object
Alpha2Code object
Alpha3Code object
NumericCode int64
dtype: object
>>> iso3166.NumericCode.dtype
dtype('int64')
>>> iso3166.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 245 entries, 0 to 244
Data columns (total 4 columns):
Country 245 non-null object
Alpha2Code 244 non-null object
Alpha3Code 245 non-null object
NumericCode 245 non-null int64
dtypes: int64(1), object(3)
memory usage: 7.7+ KB
>>> right
key1 key2 C D
0 K0 K0 C0 D0
1 K1 K0 C1 D1
2 K1 K0 C2 D2
3 K2 K0 C3 D3
Carga
Após extraídos e tratados, os dados estão preparados para ser inseridos
no seu destino. Uma das possibilidades de carregamento dos dados é
armazená-los em bancos de dados, relacionais ou não, ou data
warehouses. Neste caso, um dos pontos a considerar é a modelagem,
que precisa ser feita de forma adequada.
Outra possibilidade é carregar os dados em um repositório de dados.
Nesse caso, é necessário analisar previamente com qual periodicidade a
informação será acessada, permitindo assim a seleção de um
repositório adequado. Quando o acesso constante é necessário,
utilizamos repositórios conhecidos como hot storage, que permitem
particionamento e oferecem melhor performance. Para informações
que serão consultadas com menor frequência, podemos utilizar
repositórios do tipo cold storage, refletindo em um menor custo de
armazenamento, porém com maior latência. O data lake é um exemplo
desse repositório. Como exemplos de repositórios de dados
amplamente utilizados, podemos citar S3 Bucket (AWS – Amazon Web
Service), Cloud Storage (GCP – Google Cloud Platform) e Azure Blob
Container (Microsoft Azure).
Em decorrência da difusão dos serviços de nuvem, cada vez mais
precisamos realizar processos de extração, transformação e carga de
dados entre repositórios localizados, a fim de atender às necessidades
dos diferentes clientes, possibilitando um maior ganho de
produtividade e facilitando o trabalho de análise, nesse nosso mundo
cada vez mais complexo.
54. Web scraping
Francisco Hugo Siqueira Rosa
>>> print(soup)
Já esse último trecho apresenta todo o HTML obtido pela função read().
Os próximos passos são determinados pelo entendimento dessa
resposta.
Vamos pegar o título da primeira pergunta disponível na página.
Analisando um pouco, veremos que o título está dentro de uma
hierarquia de tags. Vamos escolher a tag ‘mãe’ que representa aquele
trecho da informação a recuperar. Note que as perguntas estão
separadas pela tag <div class=‘question-summary’>, e todas as
informações da pergunta estão dentro dessa tag. O título da pergunta
está dentro de uma tag <h3> e dentro dela existe um link (tag <a>).
f'{url_stackoverflow_questions}{str(number_of_pages)}')
... html = response.read().decode('utf-8')
... soup = BeautifulSoup(html, 'html.parser')
... except HTTPError as e:
... print(f"Erro: {e.reason}, STATUS: {e.reason}")
... except URLError as e:
... print(f"Erro: {e.reason}")
... except Exception as e:
... print(f"Tipo da exceção: {type(e)}, Erro: {e.args[0]}
")
... question_elem = soup.findAll('div', {'class': 'question-
summary'})
... for elem in question_elem:
... try:
... title_question = elem.find('h3').find('a', {
... 'class': 'question-
hyperlink'}).get_text().strip()
... description_question = elem.find('div', {
... 'class': 'excerpt'}).get_text().strip()
... number_votes = elem.find('span', {
... 'class': 'vote-count-post'}).get_text().strip()
... number_answers = elem.find(class_=[
... 'status answered', 'status unanswered',
... 'status answered-accepted'
... ]).strong.get_text().strip()
... dict_question['title'] = title_question
... dict_question['description'] = description_question
... dict_question['votes'] = number_votes
... dict_question['answers'] = number_answers
... list_questions.append(dict_question.copy())
... except Exception as e:
... print(f"ERRO: {e}")
... continue
... number_of_pages += 1
... if number_of_pages >= 11:
... break
... print(list_questions)
Como a saída é muito comprida, não será demonstrada aqui.
Utilizando o while True e um contador, conseguimos avançar nas
páginas e recuperar todas as perguntas. No exemplo anterior, paramos
na página 10, mas é possível continuar até o final.
É importante ressaltar que existem muitas outras formas de fazer a
mesma coisa e diversas customizações disponíveis. Sugerimos que você
leia a documentação do BeautifulSoup e pratique sempre que possível.
70 <https://pypi.org/project/beautifulsoup4/>.
71 <https://pypi.org/project/urllib3/>.
55. Manipulação e tratamento de
dados
Luiz Paulo O. Paula
Bruno Hanai
Davi Frazão
Joan Davi
NumPy
O NumPy é o pacote fundamental para a computação científica com
Python. Essa biblioteca fornece um array multidimensional de alto
desempenho que é utilizado como base para diversas operações,
principalmente em manipulação de dados. Esta seção apresentará
exemplos de uso da manipulação de arrays e matrizes.
Se você instalou o Anaconda, o NumPy já está pronto para uso. Caso
contrário, pode acessar <http://www.numpy.org/> e seguir as
instruções de instalação. Com o NumPy já instalado na sua máquina,
você poderá importá-lo:
Saída:
array([1, 2, 3, 4, 5])
O atributo shape exibe que o array criado possui uma única dimensão
com cinco elementos.
Saída:
(5,)
É possível ainda criar arrays com o método arange, indicando o número
de elementos do array.
Saída:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Reshape de arrays
O método reshape permite alterar o formato do array, transformando
um objeto unidimensional em um objeto multidimensional. Neste
exemplo, o array de dez elementos foi alterado para um formato de
duas linhas e cinco colunas:
Saída:
array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])
Split de arrays
Podemos dividir o array em partes com o método split, passando como
argumento o array e em quantas partes queremos dividi-lo:
>>> np.array_split(array, 2)
Saída:
[array([0, 1, 2, 3, 4]), array([5, 6, 7, 8, 9])]
>>> A = np.array([[1,0],[0,1]])
>>> B = np.array([[1,2],[3,4]])
Soma
Podemos trabalhar com somas em diferentes eixos, usando 0 para
linhas e 1 para colunas. Vejamos alguns exemplos. A seguir, faremos
uma soma de linhas:
Saída:
array([[4, 6]])
Saída:
array([[3],
[7]])
>>> A + B
Saída:
array([[2, 2],
[3, 5]])
Pandas
O pandas é um pacote construído sobre o NumPy e bastante utilizado
para manipular e analisar dados. Apresenta duas estruturas de dados
primárias. Os dataframes são matrizes multidimensionais que podemos
imaginar como uma tabela de dados relacionais, com linhas e colunas
rotuladas. Já a estrutura Series é uma única coluna, e dataframes
possuem um ou mais Series.
É comum o pandas ser importado sob o nome de “pd”:
Saída:
0 Espírito Santo
1 Minas Gerais
2 Rio de Janeiro
3 São Paulo
dtype: object
>>> dados = {
... 'col1': ['Ana','João','Fernanda','Maria'],
... 'col2': [56.0, 76.5, 85.0, 67.0],
... 'col3': [1.56, 1.76, 1.89, 1.66]}
>>> df = pd.DataFrame(dados)
>>> df
Saída:
>>> Medalhas_Rio2016 = {
... 'País' : [
... 'USA', 'Grã-Bretanha', 'China', 'Rússia', 'Alemanha',
'Japão',
... 'França', 'C. do Sul', 'Itália', 'Austrália',
'Brasil'],
... 'Ouro': [46, 27, 26, 19, 17, 12, 10, 9, 8, 8, 7],
... 'Prata': [37, 23, 18, 18, 10, 8, 18, 3, 12, 11, 6],
... 'Bronze': [38, 17, 26, 19, 15, 21, 14, 9, 8, 10, 6]}
>>> olimpiadas_rio2016 = pd.DataFrame(Medalhas_Rio2016)
>>> olimpiadas_rio2016
Saída:
Figura 55.1. Quadro de medalhas.
Fonte: os autores.
Saída:
(11,4)
Saída:
Criando colunas
A partir do dataframe criado, criaremos uma nova coluna Total, com o
total de medalhas de cada país:
Renomeando colunas
Agora renomearemos a coluna Total que foi anteriormente criada. O
parâmetro inplace informa que a alteração será diretamente no
dataframe original:
>>> olimpiadas_rio2016.rename(columns={'Total':'Total_Medalhas'},
inplace=True)
>>> olimpiadas_rio2016
Saída (com a coluna alterada em destaque):
Removendo colunas
Muitas vezes, o conjunto de dados terá informações irrelevantes ou
redundantes. Podemos excluir colunas que não serão utilizadas por
meio do método drop, que receberá no exemplo três parâmetros: o
primeiro é a coluna que queremos excluir, o segundo informa que as
alterações devem ser feitas no dataframe original, sem criar cópias, e o
terceiro indica que a exclusão deve ser aplicada na coluna.
Saída:
>>> olimpiadas_rio2016[:3]
Saída:
Tanto no loc quanto no iloc podemos usar lista para realizar buscas
(como nos exemplos anteriores), mas também é possível, em ambos,
utilizar um slice object, por meio do operador :, um único rótulo (loc) ou
um único inteiro (iloc) é uma lista de valores booleanos (True ou False)
do mesmo tamanho do eixo que está sendo buscado.
Método apply
O método apply aplica uma função a cada elemento de um
determinado eixo do dataframe, ou a uma Series. Para escolher em qual
eixo aplicaremos a função, utilizamos o argumento axis, que pode
receber os valores 0 ou 1, onde 0 aplica a função a cada coluna e 1
aplica a função a cada linha. Para entender melhor, vejamos alguns
exemplos:
Saída:
País USA
Ouro 46
Name: 0, dtype: object
País Grã-Bretanha
Ouro 27
Name: 1, dtype: object
País China
Ouro 26
Name: 2, dtype: object
0 None
1 None
2 None
dtype: object
Neste exemplo, aplicamos a função print() nas três primeiras linhas das
colunas País e Ouro. Quando configuramos o argumento axis=1,
estamos aplicando a função print() em cada linha. Note que são
retornadas três Series, uma para cada linha, onde cada Series contém as
duas colunas selecionadas. Já quando configuramos axis=0, temos o
seguinte resultado:
Saída:
0 USA
1 Grã-Bretanha
2 China
Name: País, dtype: object
0 46
1 27
2 26
Name: Ouro, dtype: object
País None
Ouro None
dtype: object
No exemplo anterior, o que nos é retornado são duas Series, uma para
cada coluna selecionada, já que agora estamos aplicando a função
print() para cada coluna.
Este capítulo apresentou alguns exemplos de códigos de manipulação
de dados usando as bibliotecas NumPy e pandas. Para exemplos
adicionais, consulte o repositório GitHub73 deste livro.
72 <https://github.com/jornada-colaborativa/livro-python>.
73 <https://github.com/jornada-colaborativa/livro-python>.
56. Visualização de dados
Tatiana Escovedo
Marcos Alexandre Castro
# Importação do pandas
import pandas as pd
# URL de leitura dos dados
url_dados = 'https://archive.ics.uci.edu/ml/machine-learning-
databases/iris/iris.data'
# Labels dos atributos do dataset
atributos = ['comprimento_sepala', 'largura_sepala',
'comprimento_petala', 'largura_petala', 'especie']
# Carga do dataset por meio do csv
iris = pd.read_csv(url_dados, names=atributos)
# Exibe as primeiras linhas do dataset
iris.head()
Com isso, temos o nosso dataset importado e pronto para uso. Agora
vamos iniciar a visualização.
Matplotlib
Não por acaso a Matplotlib será a primeira biblioteca a ser utilizada
aqui. Além de apresentar um baixo nível de implementação e, portanto,
possibilitar um alto nível de customização dos trabalhos, fornece uma
API que é utilizada por diversas outras bibliotecas de plotagem gráfica.
É uma excelente biblioteca para criar gráficos básicos, como de linhas,
de barras, histogramas e muitos outros.
O primeiro que criaremos será o gráfico conhecido como scatter plot,
no qual são marcados os pontos de cruzamento entre duas variáveis.
Para isso, é necessário importar o módulo pyplot do Matplotlib,
atribuindo a ele um alias para que ele possa ser mais facilmente
referenciado ao longo do código.
# Importação do pyplot
import matplotlib.pyplot as plt
# Plotando o gráfico de comprimento x largura da sépala
plt.scatter(iris['comprimento_sepala'], iris['largura_sepala'])
# Incluindo título do gráfico e rótulos dos eixos
plt.title('Iris: comprimento x largura da sépala')
plt.xlabel('comprimento_sepala')
plt.ylabel('largura_sepala')
plt.show()
Figura 56.4. Scatter plot do dataset Iris com marcadores por espécies.
Fonte: os autores.
Vamos agora trabalhar com um tipo diferente: o gráfico de linhas (line
plot). Este gráfico é utilizado geralmente quando estamos interessados
em expressar a evolução de uma variável com o passar do tempo.
Imagine que nosso dataset Iris esteja representado na ordem em que as
amostras foram coletadas ao longo do tempo e queremos visualizar a
variação dos atributos dos exemplos coletados ao longo do tempo.
Neste caso, faríamos:
# Plotando o histograma
plt.hist(iris['comprimento_sepala'], bins=7, edgecolor='black')
# Incluindo título do gráfico e legenda
plt.title('Distribuição de comprimento da sépala')
plt.xlabel('comprimento_sepala')
plt.ylabel('Frequência')
plt.show()
Figura 56.6. Histograma do atributo comprimento da sépala do dataset Iris.
Fonte: os autores.
Por sua vez, o gráfico de barras (bar plot) apresenta retângulos (barras)
com uma das dimensões proporcional à quantidade a ser representada
e a outra arbitrária, mas igual para todas as barras. Essas barras são
dispostas paralelamente às outras, horizontal ou verticalmente. Os
exemplos a seguir ilustram gráficos de barras (o primeiro, com barras
verticais, também chamado de gráfico de colunas; e o segundo, com
barras horizontais) para um dataset simples, que representa a profissão
dos respondentes de uma pesquisa fictícia.
# Atribuindo os valores de x e y
profissoes = [
'Engenheiro', 'Médico', 'Professor', 'Vendedor',
'Administrador', 'Outros']
quantidade = [15, 22, 25, 28, 33, 39]
# Criando o gráfico de barras
plt.bar(profissoes, quantidade)
# Incluindo título do gráfico e legenda
plt.title("Profissão dos respondentes")
plt.xlabel("Profissão")
plt.ylabel("Número de respondentes")
plt.show()
Figura 56.7. Exemplo de gráfico de barras verticais, ou gráfico de colunas.
Fonte: os autores.
O exemplo a seguir mostra como fazer para plotar em uma única figura
os gráficos dos dois últimos exemplos, cada um como um subplot:
Pandas
A biblioteca pandas oferece alguns métodos de visualização,
construídos com base na Matplotlib, que servem para uma visualização
rápida dos dados. Apesar da sintaxe mais simples do que a da biblioteca
Matplotlib, os métodos herdados pelo pandas são mais limitados
quanto às possibilidades de customização. Por esse motivo, caso você
queira ter mais flexibilidade para customizar seus gráficos,
recomendamos o uso direto da biblioteca Matplotlib.
As estruturas do pandas (Series e dataframe) possuem um método plot,
que também é um atributo e pode ser utilizado para a geração de
diversos tipos de gráficos, encadeado com os respectivos métodos (hist
para histograma, pie para gráfico de pizza, entre outros). A seguir,
veremos alguns exemplos com o dataset Iris, utilizado na seção
anterior.
Seaborn
Seaborn também é uma biblioteca de visualização baseada em
Matplotlib que oferece uma interface de alto nível para a criação de
gráficos atrativos, profissionais e com análises estatísticas. Com ela, é
possível escrever em uma linha um gráfico que precisaria de diversas
linhas usando apenas Matplotlib. Assim como na biblioteca pandas, os
métodos de visualização da biblioteca Seaborn têm uma sintaxe mais
simples, porém são mais limitados, sendo necessário utilizar a
biblioteca Matplotlib para customizações adicionais.
Todos os exemplos a seguir utilizarão o dataset Iris, que já carregamos
previamente. Você notará que os dois primeiros exemplos de gráficos
com Seaborn produzem o mesmo resultado de alguns gráficos que
criamos anteriormente com Matplotlib. Vamos começar importando a
biblioteca e configurando o estilo dos gráficos:
Plotly
Até aqui vimos algumas opções muito interessantes para visualização
de dados e produzimos imagens com qualidade satisfatória para
publicação e até para emprego em produtos orientados a dados.
Veremos agora a biblioteca Plotly, especializada na criação de gráficos
interativos e com qualidade de publicação, que é uma excelente opção
para a entrega final ao usuário. Diferentemente das etapas de
desenvolvimento, exploração e visualização de dados, em que
precisamos de soluções simples e rápidas, na hora de publicar os
resultados e convencer outras pessoas (principalmente de áreas não
ligadas diretamente a tecnologia), precisamos de gráficos amigáveis,
que possibilitem a interação com o usuário.
Vamos trazer alguns dos exemplos vistos anteriormente, mas agora
construídos com a biblioteca Plotly. Vejamos como fica um gráfico de
barras:
# Importando o dash
import dash
# Importando demais componentes de desenvolvimento web
import dash_core_components as dcc
import dash_html_components as html
# Criando a aplicação (nos moldes do Flask)
app = dash.Dash()
# Criando o título do nosso dashboard
titulo = html.H1('Meu primeiro dashboard!!!')
Figura 56.26. Aplicação web estilo dashboard criada com o módulo Dash.
Fonte: os autores.
74 <https://github.com/jornada-colaborativa/livro-python>.
75 <https://pandas.pydata.org/pandas-
docs/stable/reference/api/pandas.DataFrame.plot.html>.
76 <https://pandas.pydata.org/pandas-docs/stable/user_guide/visualization.html>.
77 <https://seaborn.pydata.org/>.
78 <https://dash.plotly.com/introduction>.
79 <https://plotnine.readthedocs.io/en/stable/index.html>.
80 <https://docs.bokeh.org/en/latest/index.html>.
81 <http://www.pygal.org/en/latest/index.html>.
57. Conceitos e aplicações de big
data
Sidnei Santiago
Lucas Pastana
Machine learning
É definido como aprendizado de máquina o campo de estudo de
algoritmos que podem aprender a realizar tarefas sem terem sido
programados explicitamente para isso. Para que tal objetivo seja
atingido, são utilizados modelos com dados de exemplo – chamados de
dados de treino (ou training data) –, tomando decisões sem
programação específica para tal problema. A ideia é que novos dados
sejam colocados para teste de tais modelos, obtendo, no final,
predições e outputs que tragam a informação necessária para uma
finalidade específica. O campo de machine learning e suas contribuições
para a inteligência artificial serão vistos com mais detalhes no Capítulo
59 – Machine Learning – Conceitos e Modelos.
Deep learning
Outra abordagem proveniente da evolução das tecnologias de
aprendizagem e um subcampo de machine learning é a chamada
aprendizagem profunda, ou deep learning, que tem como principal
inspiração as redes neurais que compõem o cérebro humano e sua
distribuição de neurônios. Tais redes neurais são compostas por uma
série de camadas, conectadas umas com as outras, a fim de transitar a
informação recebida e gerar uma informação ao final do
processamento – que é definido por um cálculo dos pesos atribuídos
para cada dado que se conecta ao neurônio. Tal definição será mais
bem estudada no Capítulo 60 – Deep Learning, responsável por
explicar e exemplificar esse campo em específico.
Classificação
Um exemplo típico de problema de classificação é a detecção de
clientes com perfis fraudulentos. Imagine a seguinte situação: um
determinado cliente deseja obter um empréstimo de R$ 10.000,00. O
gestor desse sistema poderia se perguntar: “será que esse cliente vai
pagar o empréstimo?”. Ou ainda: “qual é o melhor modelo de
financiamento para esse cliente (juros, prazo etc.)?”. Este é um
problema típico de classificação, pois deseja-se colocar o cliente em
uma das possíveis classes do problema, por exemplo, bom
pagador/mau pagador ou juros/prazo/outros.
A classificação é uma das categorias de problemas de machine learning
mais importantes e mais populares, e o objetivo do algoritmo é
aprender uma regra geral que mapeie as entradas nas saídas
corretamente. Conforme já mencionamos, os dados de entrada podem
ser divididos em dois grupos: X, com os atributos a serem utilizados na
determinação da classe de saída, e Y, que representa a classe de saída
(o atributo para o qual se deseja fazer a predição do valor da classe),
sendo que, em problemas de classificação, o Y é sempre categórico.
Informalmente, um problema de classificação pode ser definido como a
busca por uma função matemática que permita associar corretamente
cada exemplo Xi de um conjunto de dados a um único rótulo
categórico, Yi, denominado classe. Essa função, uma vez identificada,
poderá ser aplicada a novos dados para prever suas respectivas classes.
A Figura 59.4 ilustra esse problema:
Figura 59.4. Problema de classificação.
Fonte: Escovedo; Koshiyama (2020).
Regressão
O problema de classificação, apresentado anteriormente, pode ser
considerado um subtipo do problema de regressão, pois ambos são
bem similares. Sua principal diferença consiste na saída do modelo: na
classificação o resultado é categórico e na regressão o resultado é
numérico (contínuo ou discreto). Um exemplo de problema de
regressão é a predição do valor estimado das vendas em uma nova filial
de uma determinada cadeia de lojas. Se essa pergunta for mapeada em
um problema de classificação, as respostas possíveis poderiam ser:
alto/médio/baixo. Se mapeada em um problema de regressão, as
respostas poderiam ser valores monetários. As tarefas de separação em
conjuntos de treino e teste são feitas de forma equivalente para ambos
os problemas.
Assim como na classificação, a regressão consiste em realizar
aprendizado supervisionado a partir de dados históricos. Além do tipo
do resultado de saída do modelo, os dois problemas também se
diferem quanto às métricas utilizadas para a avaliação de saída: na
regressão, verifica-se a distância ou o erro entre a saída do modelo e a
saída desejada. A saída do modelo é um valor numérico que deve ser o
mais próximo possível do valor desejado, e a diferença entre esses
valores fornece uma medida de erro de estimação do algoritmo.
Podemos definir um problema de regressão como: dado um conjunto
de n padrões, cada um deles composto por variáveis explicativas
(independentes) e por uma variável resposta contínua ou discreta
(dependente), busca-se construir um modelo de regressão que estime o
valor mais esperado para a variável resposta dado um novo padrão i.
Assim, seja di o valor de resposta desejada para o padrão i e seja zi a
resposta predita do algoritmo, obtida a partir da entrada do padrão i,
então di – zi é o erro observado para o objeto i. O processo de
treinamento do modelo de regressão tem por objetivo “corrigir” esse
erro observado e, para tal, busca ajustar os parâmetros do modelo de
forma a aproximar as saídas preditas dos valores de saída desejados.
Dentre as diversas métricas de avaliação utilizadas para problemas de
regressão, uma das mais usadas é a RMSE (Root Mean Square Error, ou
raiz do erro quadrático médio). Quanto menor o valor de RMSE, melhor
é o modelo de regressão analisado. Outra métrica muito utilizada é o
coeficiente de determinação, ou R2. Quanto mais próximo de 1, melhor
é o ajuste do modelo.
Clusterização
O problema de clusterização (ou agrupamento) tem o objetivo de
agrupar os dados de interesse ou separar os registros de um conjunto
de dados em subconjuntos ou grupos (clusters), de tal forma que
elementos em um cluster compartilhem um conjunto de propriedades
comuns que os diferencie dos elementos de outros clusters. São os
problemas de aprendizagem não supervisionada mais comuns. Um
exemplo desse problema seria imaginar que quiséssemos determinar
localidades promissoras para abertura de novas filiais de uma loja.
Nesse caso, os bairros de uma cidade poderiam ser agrupados em
localidades mais ou menos promissoras.
Nesse tipo de problema, os grupos são formados de acordo com
alguma medida de similaridade, de forma que elementos pertencentes
a um dado grupo devem ser mais similares entre si, ou seja,
compartilham um conjunto maior de propriedades comuns, do que em
relação aos pertencentes a outros grupos. Sendo assim, é necessário
definir o que significa que duas observações são similares ou diferentes
dentro do domínio do nosso problema. Essa informação geralmente é
obtida por meio da consulta a especialistas do negócio, que auxiliam na
interpretação dos resultados, mas também existem métricas que
aferem o resultado do agrupamento, tais como o coeficiente de
silhouette. Alguns algoritmos de clusterização requerem que o usuário
forneça o número de clusters a formar, enquanto outros buscam
detectar a quantidade de clusters naturais existentes no conjunto de
dados de entrada.
Os algoritmos de clusterização podem ser divididos em duas categorias:
partitivos ou hierárquicos. Os algoritmos partitivos dividem o conjunto
de dados em k clusters e produzem agrupamentos simples, tentando
fazer os clusters tão compactos e separados quanto possível. Eles
funcionam bem quando os clusters são compactos, densos e bastante
separados uns dos outros. Entretanto, quando existem grandes
diferenças nos tamanhos e geometrias dos diferentes clusters, podem
dividir desnecessariamente grandes clusters para minimizar a distância
total calculada.
O funcionamento dos algoritmos partitivos pode ser resumido como:
✓ Inicialmente, k objetos são escolhidos aleatoriamente como os
centros dos k clusters.
✓ Os objetos são divididos entre os k clusters de acordo com a
medida de similaridade adotada, de modo que cada objeto fique
no cluster que forneça o menor valor de distância entre o objeto e
o centro do cluster.
✓ Uma estratégia iterativa determina se os objetos devem mudar de
cluster, fazendo com que cada cluster contenha somente os
elementos mais similares entre si.
✓ Após a divisão inicial, há duas possibilidades na escolha do
“elemento” que vai representar o centro do cluster e que será a
referência para o cálculo da medida de similaridade:
• Média dos objetos que pertencem ao cluster (centroide ou
centro de gravidade do cluster).
• O objeto que se encontra mais próximo ao centro de gravidade
daquele cluster (medoide).
Já os algoritmos hierárquicos criam uma decomposição hierárquica do
conjunto de dados, representada por um dendrograma, uma árvore que
iterativamente divide o conjunto de dados em subconjuntos menores
até que cada subconjunto consista em somente um objeto. Cada nó da
árvore representa um cluster do conjunto de dados, e a união dos
clusters em um determinado nível da árvore corresponde ao cluster no
nível exatamente acima. A figura a seguir ilustra um dendrograma:
Associação
O problema de associação pode ser exemplificado pela oferta de novos
serviços e produtos a clientes. Por exemplo, em um sistema de e-
commerce, poderíamos nos perguntar: “quem observa esse produto
tem interesse em ver qual outro?”. Ou ainda: “quem observa esse
produto costuma comprar qual outro?”. Apesar de ser considerado um
problema de aprendizado não supervisionado e ser bastante utilizado
na prática, o detalhamento desse tipo de problema, cujo algoritmo
mais conhecido é o Apriori, está fora do escopo deste livro.
Para cada um dos problemas de machine learning – classificação,
regressão, clusterização e agrupamento – existem diversos algoritmos
que podem ser utilizados. Entretanto, vale a pena mencionar o
conhecido teorema “não existe almoço grátis”: não existe um algoritmo
de aprendizado que seja superior a todos os demais quando
considerados todos os problemas possíveis. A cada problema, os
algoritmos disponíveis devem ser experimentados a fim de identificar
aqueles que obtêm melhor desempenho. Em seguida, apresentaremos
alguns dos modelos mais utilizados para aprendizagem supervisionada
e não supervisionada.
Árvore de decisão
A árvore de decisão é inspirada na forma como humanos tomam
decisões e, por este motivo, um dos modelos mais simples de se
entender. Uma das principais vantagens deste algoritmo é a
apresentação visual da informação, facilitando o entendimento pelo ser
humano. As árvores podem ser usadas para problemas de classificação
(árvores de classificação) ou regressão (árvores de regressão). De forma
resumida, uma árvore de decisão usa amostras das características dos
dados para criar regras de decisão no formato de árvore, mapeando os
dados em um conjunto de regras que podem ser usadas para uma
decisão.
As árvores de decisão costumam ter bons resultados e boa
interpretabilidade, e podem realizar automaticamente a seleção de
variáveis para compor suas estruturas. Cada nó interno representa uma
decisão sobre uma característica, que determina como os dados serão
particionados pelos seus nós filhos. Para aplicar o modelo a um novo
exemplo, basta testar os valores dos atributos em cada nó da árvore e
percorrê-la até se atingir um nó folha, que representará a classe ou o
valor predito, dependendo de o problema ser de classificação ou de
regressão. A Figura 59.8 ilustra uma árvore de classificação.
Regressão linear
A regressão linear é um algoritmo utilizado apenas para problemas de
regressão e, resumidamente, consiste em escolher coeficientes para
construir uma reta que minimize a soma dos quadrados dos erros (SQE)
entre os valores reais dos exemplos de treinamento e essa reta.
Observe um exemplo de regressão linear para o problema de se estimar
o faturamento esperado para uma filial em um bairro, considerando a
renda per capita desse bairro, ilustrado na Figura 59.10.
Figura 59.10. Exemplo de regressão linear.
Fonte: Escovedo; Koshiyama (2020).
Regressão logística
A regressão logística, apesar do nome, é um algoritmo utilizado
exclusivamente para problemas de classificação, mas seu
funcionamento lembra muito o funcionamento do algoritmo de
regressão linear. A regressão logística é usada para estimar valores
discretos de classes binárias (valores como 0/1, sim/não,
verdadeiro/falso) com base em um conjunto de variáveis
independentes. Internamente, a regressão logística calcula a
probabilidade de ocorrência de um evento, ajustando os dados a uma
função logit, que mapeia a saída em valores entre 0 e 1.
De forma similar à regressão linear, a regressão logística usa uma
equação como representação: os valores de entrada X são combinados
linearmente usando coeficientes para prever um valor de saída y. A
diferença é que o valor de saída é modelado em valor de classe binário
em vez de um valor numérico.
A regressão logística modela a probabilidade da classe padrão do
problema. Por exemplo, se estivermos modelando o perfil de um
cliente (bom ou mau pagador) dado seu salário, podemos escolher
considerar a classe “bom pagador” como padrão e modelar a
probabilidade de uma entrada X pertencer à classe padrão. Os
melhores coeficientes resultarão em um modelo que vai prever um
valor muito próximo de 1 para a classe padrão e um valor muito
próximo de 0 para a outra classe. Após determinados os coeficientes e
construir a equação resultante, basta utilizá-la para fazer predições
para novos exemplos.
Naïve Bayes
O Naïve Bayes (Bayes Ingênuo) é um dos métodos mais utilizados para
classificação, por ser computacionalmente rápido e por necessitar de
poucos dados de treinamento. Por esse motivo, é um modelo
especialmente adequado quando o problema tem um grande número
de atributos (características). Basicamente, este modelo determina a
probabilidade de um exemplo pertencer a uma determinada classe.
O Naïve Bayes é chamado de ingênuo (naïve) porque desconsidera
completamente qualquer correlação existente entre os atributos do
dataset. Por exemplo, em um problema de classificação de animais, se
determinado animal é considerado um “gato” se tiver bigodes, rabo e
aproximadamente 30 cm de altura, o algoritmo não vai levar em
consideração a correlação entre esses fatores e tratará cada um deles
de forma independente.
Além disso, este modelo foi assim batizado por ser baseado no
Teorema de Bayes, estando relacionado com o cálculo de
probabilidades condicionais. O Teorema de Bayes determina a
probabilidade de um evento com base em um conhecimento prévio (a
priori) que pode estar relacionado a esse evento.
Formalmente, seja X(A1, A2, …, An, C) um conjunto de dados. Considere
que c1, c2, …, cn são as classes do problema (valores possíveis do
atributo alvo C) e que R é um novo exemplo que deve ser classificado.
Sejam ainda a1, a2, …, ak os valores que R assume para os atributos
previsores A1, A2, …, An, respectivamente. Resumidamente, o algoritmo
consiste em dois passos:
1. Calcular as probabilidades condicionais P(C=ci|R), i = 1, 2, …, k.
2. Indicar como saída do algoritmo a classe c tal que P(C=c|R) seja
máxima, quando considerados todos os valores possíveis do
atributo alvo C.
A intuição por trás do algoritmo é dar mais peso para as classes mais
frequentes, considerando que os atributos são estatisticamente
independentes entre si. Apesar de isso não ocorrer em muitos casos
práticos, o método mostra-se bastante efetivo mesmo quando os
atributos não são estatisticamente independentes.
K-means
Um dos algoritmos de clusterização mais conhecido e utilizado é o K-
means, baseado em distâncias. Este algoritmo exige que seja informado
previamente o parâmetro k (número de clusters ou grupos) e atribui
cada observação a um dos k clusters. A Figura 59.15 ilustra os
resultados deste algoritmo em uma mesma base de dados,
considerando três diferentes valores de k:
Single-Layer Perceptron
Rosenblatt (1957) desenvolveu o que conhecemos hoje como modelo
Single-Layer Perceptron, ou seja, a rede neural artificial mais antiga.
Este modelo possui um número fixo de entradas, processamento e uma
única saída. Uma das particularidades deste modelo é ser um
classificador linear, ou seja, ele realizará a classificação se o conjunto de
dados puder ser linearmente separado em categorias binárias. Após
esse processo, as entradas ponderadas serão passadas pela função de
ativação, e o modelo será considerado ativado se a soma for maior que
o limiar de ativação.
Os pesos são inicializados de forma aleatória. É feito um certo ajuste de
pesos se a resposta esperada pela rede não for a correta. Nesse caso, o
algoritmo procura por um novo conjunto de pesos ideais para que a
rede possa classificar corretamente as entradas. A arquitetura básica do
modelo Single-Layer Perceptron pode ser vista na Figura 60.2 a seguir.
Figura 60.2. Arquitetura do modelo Single-Layer Perceptron.
Fonte: adaptado de Beale; Jackson (2017).
Multilayer Perceptron
Como vimos anteriormente, as características do modelo Single-Layer
Perceptron são limitadas à classificação de conjuntos linearmente
separáveis. Este modelo, portanto, é incapaz de classificar conjuntos
não lineares ou conjuntos com mais de uma classe.
Adicionando mais de uma camada de neurônios na rede para resolver
as limitações encontradas no modelo Single-Layer Perceptron, foi
criado o modelo Multilayer Perceptron, capaz de classificar conjuntos de
dados não linearmente separáveis, como, por exemplo, o problema
XOR. A Figura 60.4 ilustra um conjunto de dados não linearmente
separável.
Figura 60.4. Conjunto de dados não linearmente separável.
Fonte: Raibolt-Silva (2018).
Autoencoders
Proposta originalmente por Rumelhart, Hinton e Williams (1985),
autoencoders são redes neurais artificiais não supervisionadas que
possuem como objetivo aprender de forma eficiente representações de
dados, ou seja, aprender codificações. Com isso, é possível copiar os
dados de entrada para os dados de saída da rede.
O interesse em realizar tal atividade está diretamente ligado à
compressão de dados, que é uma técnica fundamental para reduzir o
espaço em disco utilizado para armazenar informações. A fim de evitar
a cópia perfeita duplicando o dado de entrada, os parâmetros dos
autoencoders são configurados de forma restrita, forçando-os a
reconstruir o dado de entrada de forma aproximada. Assim, os aspectos
relevantes dos dados copiados são preservados.
A compressão de dados consiste em reduzir a quantidade necessária de
bytes para que uma determinada informação possa ser representada e
pode, portanto, ser utilizada para a compactação de arquivos e para a
redução de dimensionalidade. Os autoencoders também vêm sendo
utilizados para o aprendizado de modelos generativos, que serão
abordados no próximo tópico deste capítulo. A arquitetura básica de
um Autoencoder pode ser vista na Figura 60.15.
Figura 60.15. Arquitetura básica do modelo de autoencoder.
Fonte: a autora.
Considerações finais
Este capítulo apresentou a temática deep learning e seus principais
modelos. O conteúdo abordado buscou apresentar os principais tópicos
e conceitos de forma resumida e introdutória. Para maiores
aprofundamentos, recomendamos os seguintes livros: “Deep learning”
(GOODFELLOW; BENGIO; COURVILLE, 2016); “Neural networks and
deep learning” (NIELSEN, 2015); e “TensorFlow 1.x Deep Learning
Cookbook” (GULLI; KAPOOR, 2017).
# Import OpenCV
import cv2 as cv
# Check Version
cv.__version__
Saída:
'4.2.0'
O primeiro passo que daremos para realizar a detecção e descrição de
características utilizando o descritor binário local ORB é importar todas
as bibliotecas de que faremos uso neste exemplo prático, a saber:
image1 = cv.imread(filename='image1.jpg',
flags=cv.IMREAD_GRAYSCALE)
Note que, ao importarmos a imagem, utilizamos o parâmetro flags =
cv.IMREAD_GRAYSCALE porque no OpenCV a configuração padrão do
modo de cor é o RBG. Portanto, para trabalhar com descritores binários
locais (ou também com descritores de recursos locais), precisamos
fazer uma conversão no padrão do modo de cor, de RGB para escala de
cinza. Agora utilizaremos o algoritmo ORB:
ORB = cv.ORB_create()
Saída:
output = cv.drawKeypoints(
image=image1, keypoints=keypoints1[:50],
outImage=None, flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
plt.figure()
plt.axis('off')
plt.imshow(output)
plt.show()
Saída:
image2 = cv.imread(filename='image2.jpg',
flags=cv.IMREAD_GRAYSCALE)
Saída:
matches = BFMatcher.match(queryDescriptors=descriptors1,
trainDescriptors=descriptors2)
output = cv.drawMatches(
img1=image1, keypoints1=keypoints1,
img2=image2, keypoints2=keypoints2,
matches1to2=matches[:30], outImg=None,
flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
plt.figure()
plt.axis('off')
plt.imshow(output)
plt.show()
Saída:
Figura 61.4. Primeiras 30 correspondências detectadas entre ambas as imagens pelo
algoritmo.
Fonte: a autora.
83 <http://dlib.net>.
84 <https://github.com/jrosebr1/imutils>.
85 <https://scikit-image.org>.
86 <https://opencv.org/>.
87 <https://github.com/jornada-colaborativa/livro-python>.
88 <https://pypi.org/project/opencv-python>.
62. Scikit-Learn
Carlos Eduardo Silva Castro
Tatiana Escovedo
Cassius T. C. Mendes
A API Estimator
Os algoritmos de machine learning no Scikit-Learn sempre serão
implementados utilizando a API Estimator, que em geral segue as
etapas:
✓ Escolha uma classe de modelo importando a classe apropriada do
Scikit-Learn.
✓ Escolha os hiperparâmetros do modelo instanciando esta classe
com os valores desejados.
✓ Organize os dados em uma matriz de recursos X e vetor de destino
Y.
✓ Ajuste o modelo aos seus dados chamando o método fit() da
instância do modelo.
✓ Aplique o modelo aos novos dados:
• No aprendizado supervisionado, geralmente estamos
interessados na predição de rótulos para dados desconhecidos.
Para tal, use o método predict().
• No aprendizado não supervisionado, geralmente estamos
interessados em descobrir associações ou propriedades dos
dados. Para tal, use o método transform() ou o método
predict().
Validação de modelos
Quando vamos treinar e validar nosso modelo de aprendizado
supervisionado, uma das alternativas seria utilizar a estratégia train-
test-split, que consiste em separar nosso dataset em dois subconjuntos
menores para serem utilizados como dados de treino e dados de
validação (ou teste). Uma razão comumente utilizada nessa estratégia
é dividir o conjunto de dados inicial em um subconjunto contendo, por
exemplo, 70% dos dados e outro contendo 30%. O conjunto contendo
70% seria utilizado para treinar o modelo e o conjunto de 30% seria
utilizado para validar o modelo.
Uma alternativa a essa estratégia é a técnica de cross validation, ou
validação cruzada. De forma simplificada, esta técnica consiste em
dividir o conjunto de dados em um número n de subconjuntos
(partições ou folders) contendo aproximadamente o mesmo número de
elementos. Durante essa estratégia, escolhe-se 1 dos n folders para
validação, treina-se o modelo nos n-1 folders restantes, validando-se o
modelo no folder escolhido inicialmente. Esse processo é repetido,
escolhendo-se um novo folder diferente dos que já foram escolhidos
para a validação do modelo e treinando-o com os demais, e continua
até que se tenha selecionado cada um dos folders para validação. O
resultado será um array contendo n medidas resultantes da avaliação,
do qual geralmente extraímos a sua média para calcular o desempenho
do modelo. As técnicas de validação cruzada mais utilizadas são as que
usam cinco ou dez partições. Apesar da validação cruzada em geral ser
mais adequada para validação de modelos do que a estratégia train-
test-split, ela pode ser difícil de ser utilizada quando dispomos de
poucos dados para treinamento.
# Imports
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import ConfusionMatrixDisplay
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
# Parâmetros
num_folds = 10
scoring = 'accuracy'
89 <https://scikit-learn.org/>.
90 <https://scikit-learn.org/stable/user_guide.html>.
91 <https://github.com/jornada-colaborativa/livro-python>.
92 <https://www.kaggle.com/uciml/pima-indians-diabetes-database>.
93 <http://scikit-learn.org/stable/modules/pipeline.html>.
94 <http://scikit-learn.org/stable/modules/classes.html#module-sklearn.pipeline>.
95<https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.h
tml>.
96 <https://scikit-learn.org/stable/modules/grid_search.html>.
63. TensorFlow
Cláudio Henrique Franco Gomes
Instalação
A biblioteca TensorFlow pode ser instalada utilizando o gerenciador de
pacotes de sua preferência:
Tensores
Tensores são arrays multidimensionais com um tipo uniforme. Similares
aos arrays da biblioteca NumPy, a conversão de tf.Tensor para np.array
é transparente para o desenvolvedor. Todos os tensores são imutáveis
(como strings em Python), não sendo possível atualizar o valor de um
tensor, apenas criar um novo. Tensores podem residir em memória
acelerada, como uma GPU (Graphics Processing Unit) ou uma TPU
(Tensor Processing Unit). O TensorFlow fornece diversos módulos de
operações com tensores, como tf.math, tf.linalg, tf.bitwise e tf.strings.
É possível utilizar tensores para criar equações entre variáveis
unidimensionais, muito embora o desempenho desse tipo de operação
não seja significativo. Na verdade, devido ao custo de cópia de dados
entre a memória RAM e a GPU, é provável que se tenha um
desempenho pior. No exemplo a seguir, calculamos mil vezes o décimo
número da série de Fibonacci utilizando Python e TensorFlow. O
resultado dá ganho de desempenho para o Python, que supera em
aproximadamente 38 vezes o TensorFlow.
import tensorflow as tf
import numpy as np
from time import time
t0 = time()
for k in range(1000):
x = [0, 1]
for i in range(10):
x = [x[1], sum(x)]
print("Executado em {0:.5f} seg".format(time() - t0))
with tf.device('GPU:0'):
x0 = tf.Variable([[0, 1], [1, 1]], dtype=tf.int64)
x1 = tf.constant([[0, 1], [1, 1]], dtype=tf.int64)
t0 = time()
for repetir in range(1000):
for contar_ate in range(10):
x0 = tf.matmul(x0, x1)
print("Executado em {0:.5f} seg".format(time() - t0))
m = np.random.rand(1000,1000)
n = np.random.rand(1000,1000)
t0 = time()
for i in range(1000):
np.matmul(m, n)
print("Executado em {0:.5f}".format(time() - t0))
Executado em 60.63266
with tf.device('GPU:0'):
mc = tf.constant(m)
nc = tf.constant(n)
t0 = time()
for i in range(1000):
tf.matmul(mc, nc)
print("Executado em {0:.5f}".format(time() - t0))
Executado em 0.09447
Carregamento de dados
Um modelo em TensorFlow espera receber dados nos seguintes
formatos: NumPy array ou lista de arrays ou lista de listas; tensor do
TensorFlow ou lista de tensores; dicionário mapeando os nomes das
entradas em seus respectivos tensores ou arrays, caso o modelo tenha
entradas nomeadas; tf.data.dataset; generator ou Keras.utils.Sequence.
Ou seja, qualquer formato de dados que tenha conversão automática
para qualquer um desses formatos (como é o caso dos dataframes de
pandas e Dask) pode servir de entrada para o treinamento de um
modelo projetado em TensorFlow.
No pacote tf.Keras.preprocessing podemos encontrar diversas funções
para carregamento de dados em diversos formatos. Vamos explorar a
seguir algumas formas de carregar dados com a biblioteca TensorFlow.
A fim de carregar um arquivo no formato CSV, primeiro definimos o
nome e a URL onde se encontra o arquivo. A seguir, por meio da função
get_file, carregamos o conteúdo desse arquivo para a memória, o qual
agora pode ser carregado para um dataframe pandas, que será
transformado em um dataset do módulo tf.data.
import tensorflow as tf
FILENAME = "train.csv"
SITE_URL = "https://storage.Googleapis.com/tf-datasets/titanic/"
FILE_URL = SITE_URL + FILENAME
file_path = tf.keras.utils.get_file(FILENAME, FILE_URL)
dataset = tf.data.experimental.make_csv_dataset(
file_path, batch_size=8, label_name='survived',
na_value="None", num_epochs=1, ignore_errors=True
)
train_dataset = dataset.shuffle(len(df)).batch(64)
model.fit(train_dataset, epochs=100)
import pathlib
data_dir = tf.keras.utils.get_file(
origin='https://url.de.exemplo/flower_photos.tgz',
fname='flower_photos',
untar=True
)
data_dir = pathlib.Path(data_dir)
import tensorflow.keras.preprocessing.image as I
image_generator = I.ImageDataGenerator(rescale=1./255)
train_generator = image_generator.flow_from_directory(
directory=str(data_dir), batch_size=32, shuffle=True,
target_size=(128, 128), classes = list(CLASS_NAMES)
)
Pré-processamento
Conforme mencionado anteriormente, é possível utilizar os generators
do módulo tensorflow.keras.preprocessing para fazer o pré-
processamento e a expansão da base de dados. A definição do
generator ImageDataGenerator conforme a versão 2.2 da biblioteca
TensorFlow é a seguinte:
tensorflow.keras.preprocessing.image.ImageDataGenerator(
featurewise_center=False, samplewise_center=False,
featurewise_std_normalization=False,
samplewise_std_normalization=False,
zca_whitening=False, zca_epsilon=1e-06, rotation_range=0,
width_shift_range=0.0, height_shift_range=0.0,
brightness_range=None,
shear_range=0.0, zoom_range=0.0, channel_shift_range=0.0,
fill_mode='nearest', cval=0.0, horizontal_flip=False,
vertical_flip=False, rescale=None, preprocessing_function=None,
data_format=None, validation_split=0.0, dtype=None
)
import tensorflow.keras.preprocessing.image as I
train_datagen = I.ImageDataGenerator(
width_shift_range=0.1, height_shift_range=0.1,
horizontal_flip=True, rotation_range=15
)
train_generator = train_datagen.flow(x_train, y_train,
batch_size=64)
import numpy as np
import tensorflow as tf
features = np.random.randint(50, 100, size=(10,3))
labels = np.random.randint(0, 2, size=(10,1))
ds = tf.data.Dataset.from_tensor_slices((features, labels))
ds = ds.map(lambda feature, label: feature // 3,28084)
ds = ds.filter(lambda x: tf.math.greater_equal(x[1], 20))
list(ds.as_numpy_iterator())
[array([17, 24, 25]),
array([27, 23, 31]),
array([19, 21, 29]),
array([32, 28, 19]),
array([18, 26, 30]),
array([18, 20, 22])]
Modelagem
O TensorFlow disponibiliza uma API high-level que pode ser encontrada
no módulo tensorflow.keras e uma API low-level que permite ao
desenvolvedor escrever o grafo que descreve o relacionamento entre as
features dos dados. Por exemplo, uma camada densa, escrita com a API
low-level, poderia se parecer com o seguinte código:
class Dense:
def __init__(self, input, output, callback,
kernel_initializer):
self.w = tf.Variable(kernel_initializer([input, output]))
self.b = tf.Variable(kernel_initializer([output]))
self.f = callback
def __call__(self, x):
if callable(self.f):
return self.f(tf.add(tf.matmul(x, self.w), self.b))
else:
return tf.add(tf.matmul(x, self.w), self.b)
tensorflow.keras.layers.Dense(
units, activation=None, use_bias=True,
kernel_initializer='glorot_uniform',
bias_initializer='zeros', kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None, kernel_constraint=None,
bias_constraint=None,
**kwargs
)
import tensorflow.keras.layers as L
def layer_type1(inputs, filters, kernel_size=(3, 3),
dropout_rate=0):
x = L.Conv2D(
filters, kernel_size, padding="same", activation="relu"
)(inputs)
if dropout_rate > 0:
x = L.Dropout(dropout_rate)(x)
return x
def layer_type2(inputs, filters, kernel_size=(3, 3),
dropout_rate=0):
x = layer_type1(inputs, filters)
x = L.Conv2D(
filters, kernel_size, padding="same", activation="relu"
)(x)
if dropout_rate > 0:
x = L.Dropout(dropout_rate)(x)
x = L.Add()([x, inputs])
return x
Métricas
A fim de verificar a qualidade de um modelo, isto é, o quanto o modelo
consegue classificar corretamente um conjunto de dados ou gerar o
dado mais próximo da expectativa, precisamos medir os resultados.
Para isso, o TensorFlow disponibiliza diversos algoritmos para calcular o
que chamamos de métricas. O pacote tf.Keras.metrics concentra as
métricas mais utilizadas pela maioria dos problemas resolvidos com
aprendizado de máquina. Vamos tratar de algumas delas a seguir.
A acurácia é uma métrica que determina com qual frequência as
predições do modelo equivalem às expectativas. Essa métrica possui
variações dependendo da quantidade de rótulos. Por exemplo, para
dois rótulos, o cálculo é feito com o total de acertos pelo total de
amostras. Para diversos rótulos, o cálculo é o total de acertos pelo total
de amostras por rótulo. Essa métrica é utilizada para problemas de
classificação. A seguir, temos três exemplos de como essa métrica pode
ser utilizada no treinamento de modelos com TensorFlow.
A AUC (Area Under Curve) é utilizada para calcular a área sob a curva
ROC (Receiver Operating Characteristic). Essa métrica gera quatro
variáveis: verdadeiros positivos (TP), verdadeiros negativos (TN), falsos
positivos (FP) e falsos negativos (FN), os quais constituem a matriz de
confusão (confusion matrix), ilustrada pela figura a seguir:
model.compile(
'sgd', loss='mse', metrics=
[tf.keras.metrics.BinaryCrossentropy()])
model.compile(
'sgd', loss='mse', metrics=
[tf.keras.metrics.CategoricalCrossentropy()])
model.compile(
'sgd', loss='mse', metrics=[tf.keras.metrics.Mean()])
model.compile(
'sgd', loss='mse', metrics=
[tf.keras.metrics.MeanSquaredError()])
Falamos aqui de cinco tipos de métricas para dar uma visão ampla dos
algoritmos de métricas oferecidos pelo TensorFlow. Outros algoritmos
são disponibilizados no pacote tf.Keras.metrics, além da possibilidade
de o desenvolvedor escrever sua própria métrica derivada da classe
Metric desse pacote.
O TensorFlow talvez não possa ser definido como uma simples
biblioteca, em decorrência da quantidade de ferramentas, pacotes e
recursos que disponibiliza. Também não é preciso mencionar que está
em constante desenvolvimento e dificilmente um livro ou mesmo um
portal wiki consiga acompanhar todas as suas atualizações.
Recomendamos fortemente que o leitor inclua em seu caminho de
conhecimento o portal do TensorFlow99 e o seu GitHub100. O Google
disponibiliza alguns cursos gratuitos dessa plataforma, bem como o
TensorFlow Developer Certificate.
97 <https://www.tensorflow.org/>.
98 <https://github.com/jornada-colaborativa/livro-python>.
99 <https://www.tensorflow.org/>.
100 <https://github.com/tensorflow>.
64. PyTorch
Reinaldo Maciel
História
Ao longo dos anos de desenvolvimento do deep learning, muitos
profissionais perceberam claramente que existia um trade-off a ser
efetuado: o aumento da usabilidade afetava negativamente a
velocidade e vice-versa. Olhando o TensorFlow, por exemplo, é possível
notar que o uso da biblioteca implica em ser mais verboso, escrever
mais código e ser cada vez menos flexível quando há modelos mais
complexos.
Diversos frameworks populares, como Caffe, TensorFlow e Theano,
disponibilizam uma boa velocidade computacional, mas ao custo de
usabilidade e flexibilidade. Foi então que surgiu o PyTorch dentro dos
laboratórios do Facebook AI. Foi introduzido em 2016, por meio de um
artigo produzido por Adam Paszke, Sam Gross, Soumith Chintala e
Gregory Chanan explorando sua usabilidade e flexibilidade.
No próprio nome da biblioteca está a sua base: Torch, que é uma rede
neural implementada em Lua que data de 2002. Outra grande
referência do PyTorch é o Chainer, criado no Japão em 2015. Apesar de
ser recente, o Chainer foi uma das primeiras bibliotecas de redes
neurais a oferecer dinamismo na criação, no treinamento e na operação
de redes neurais. A combinação de Torch com as ideias centrais do
Chainer fizeram o PyTorch crescer em popularidade ao longo dos
últimos anos.
Instalação
A instalação do PyTorch é extremamente simples. Você pode navegar
até o site102 e ir até a área Quick Start Locally. Nessa área existe uma
caixa dinâmica de seleção onde você vai poder escolher algumas
configurações existentes na sua máquina e então um comando será
apresentado, com o qual você poderá fazer a instalação da biblioteca:
import torch
x = torch.zeros(4, 3)
print(x)
Saída:
x = torch.rand(4, 3)
print(x)
Saída:
y = torch.rand(4, 3)
print(x + y)
Saída:
import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
É possível ver que são importadas as bibliotecas torch (é ela que vamos
usar para transformar o NumPy array em tensor); torch.nn, já que
usaremos uma rede neural para criar o modelo; NumPy para criarmos o
array de dados e, por fim, Matplotlib para visualizarmos os resultados.
# Hiperparametros
tamanho_entrada = 1
tamanho_saida = 1
epocas = 60
learning_rate = 0.001
# Dados de exemplo
x_train = np.array(
[[3.3], [4.4], [5.5], [6.71], [6.93],
[4.168], [9.779], [6.182], [7.59], [2.167],
[7.042], [10.791], [5.313], [7.997], [3.1]], dtype=np.float32)
y_train = np.array(
[[1.7], [2.76], [2.09], [3.19], [1.694],
[1.573], [3.366], [2.596], [2.53], [1.221],
[2.827], [3.465], [1.65], [2.904], [1.3]], dtype=np.float32)
# Treinamento do modelo
for epoca in range(epocas):
# Converte os arrays numpy em tensores PyTorch
entradas = torch.from_numpy(x_train)
targets = torch.from_numpy(y_train)
# Foward na rede neural
saidas = model(entradas)
loss = criterio(saidas, targets)
# Backwards na rede neural
otimizador.zero_grad()
loss.backward()
otimizador.step()
if (epoca+1) % 5 == 0:
print ('Epoca [{}/{}], Perdas: {:.4f}'.format(
epoca+1, epocas, loss.item()
))
101 <https://pytorch.org/>.
102 <https://pytorch.org/>.
65. Keras
João Pedro Prates da Conceição Galhianne
O Keras103 é uma API de alto nível para redes neurais escrita em Python
para ser utilizada inicialmente tendo como base o backend Theano.
Hoje suporta múltiplos backends, sendo eles o CNTK e o TensorFlow.
Instalação
A instalação do Keras tem como pré-requisito a instalação do
TensorFlow, Theano ou CNTK. Para instalar o Keras em si, basta utilizar
os comandos no terminal:
$ cd keras
$ sudo Python setup.py install
Prática
Atualmente, o dataset MNIST (Modified National Institute of Standards
and Technology) é comum na utilização de estudos envolvendo deep
learning. Tal dataset contém imagens de caracteres numéricos escritos
à mão. As imagens contidas nele foram tratadas usando um dataset
mais antigo (NIST). Dentre os tratamentos feitos, houve ajuste da
resolução de cada imagem para 28x28 pixels e a conversão para uma
escala cinza.
A utilização do dataset MNIST em larga escala como um padrão para
estudos de modelos de deep learning e machine learning fez com que os
resultados para tal dataset atingissem valores ótimos, não
representando mais um desafio hoje em dia. Devido a esse avanço,
novos datasets foram sendo utilizados, como o EMNIST (que contém
imagens de letras e números feitos à mão). Outro dos datasets mais
recomendados para esse aprendizado é o Fashion MNIST104, que contém
imagens de roupas de uma loja chamada Zalando.
Utilizaremos este dataset para criarmos um modelo que consiga
classificar diferentes tipos de roupas, separando-as em categorias como
camiseta, calça, tênis, casaco, entre outras. O dataset encontra-se
disponível no GitHub ou também na própria biblioteca do Keras, como
utilizaremos no código ao longo do capítulo. Esta prática foi inspirada
em tutorial disponível no link <https://www.tensorflow.org/tutorials/k
eras/classification?hl=pt-br>. Utilizaremos o Keras por meio do
TensorFlow. Então precisamos fazer as seguintes importações:
import tensorflow
from tensorflow import keras
dados_originais = keras.datasets.fashion_mnist
((imagens_treino, gabarito_treino),
(imagens_teste, gabarito_teste)) = dados_originais.load_data()
Numérico Classe
0 Camiseta/Top
1 Calça
2 Suéter
3 Vestido
4 Casaco
5 Sandália
6 Camisa
7 Tênis
8 Bolsa
9 Bota
Iniciando o modelo
Existem duas maneiras principais de realizar um modelo utilizando o
Keras: a Sequential e a Funtional API. Neste exemplo, nos
concentraremos na Sequential.
A Sequential funciona como uma lista de camadas pela qual nossos
dados passarão durante o treinamento. Sendo assim, precisaremos de
uma camada de entrada, algumas camadas de processamento e uma
camada de saída. Portanto, podemos fazer uma analogia da seguinte
forma:
modelo_sequential = sequential(
camada de entrada,
camadas de processamento,
camada de saída
)
modelo = keras.Sequential([keras.layers.Flatten(input_shape=(28,
28))])
Com isso criamos nossa camada 0. Para prosseguirmos com nossa rede
neural, criaremos mais camadas que darão sequência a essa. A camada
seguinte é conhecida como Dense. Tal camada se conectará com todas
as saídas obtidas na Flatten, como mostra a imagem a seguir:
Figura 65.2. Interconexão entre as camadas Flatten e Dense.
Fonte: o autor.
modelo = keras.Sequential([
keras.layers.Flatten(input_shape=(28, 28)),
keras.layers.Dense(256, activation='relu'),
])
modelo = keras.Sequential([
keras.layers.Flatten(input_shape=(28, 28)),
keras.layers.Dense(256, activation='relu'),
keras.layers.Dense(10, activation='softmax')
])
modelo.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
resultado = modelo.fit(imagens_treino, gabarito_treino)
Apurando o modelo
Ao buscar um resultado melhor, podemos adicionar mais camadas de
processamento em nossa rede (como mais uma Dense com ReLU) e
variar o número de neurônios da rede.
Além disso, podemos treinar o modelo mais vezes para que ele
“reforce” o que aprendeu (atualizando valores de pesos e vieses),
tornando-se mais eficaz. Treiná-lo mais vezes significa aumentar o
número de épocas. Para tal, devemos colocar um parâmetro a mais no
método fit do modelo:
resultado = modelo.fit(imagens_treino, gabarito_treino , epochs=6)
Epoch 1/6
1500/1500 [==============================] - 4s 3ms/step - loss:
4.6419 - accuracy: 0.7269
Epoch 2/6
1500/1500 [==============================] - 4s 3ms/step - loss:
0.6294 - accuracy: 0.7849
Epoch 3/6
1500/1500 [==============================] - 4s 3ms/step - loss:
0.5452 - accuracy: 0.8132
Epoch 4/6
1500/1500 [==============================] - 5s 3ms/step - loss:
0.5091 - accuracy: 0.8270
Epoch 5/6
1500/1500 [==============================] - 4s 3ms/step - loss:
0.4831 - accuracy: 0.8350
Epoch 6/6
1500/1500 [==============================] - 4s 3ms/step - loss:
0.4742 - accuracy: 0.8386
Podemos treinar a rede neural por várias épocas, mas como saberemos
o valor correto para não correr o risco de termos underfitting ou
overfitting? Uma maneira possível é traçando um gráfico dos resultados
de treino e teste. Podemos traçar esse gráfico, por exemplo, para
acurácia e perda. Os códigos e as figuras a seguir mostram como seriam
os resultados obtidos:
plt.xlabel('Época')
plt.ylabel('Acurácia')
plt.legend(['Treino', 'Validação'])
plt.plot(resultado.history['accuracy'])
plt.plot(resultado.history['val_accuracy'])
plt.show()
plt.xlabel('Épocas')
plt.ylabel('Perda')
plt.plot(resultado.history['loss'], '--')
plt.plot(resultado.history['val_loss'])
plt.legend(['Treino', 'Validação'])
plt.show()
Figura 65.6. Evolução da perda por épocas.
Fonte: o autor.
import tensorflow
from tensorflow import keras
import matplotlib.pyplot as plt
dados_originais = keras.datasets.fashion_mnist
((imagens_treino, gabarito_treino),
(imagens_teste, gabarito_teste)) = dados_originais.load_data()
classes_por_nome = ['Camiseta/Top', 'Calça', 'Suéter', 'Vestido',
'Casaco',
'Sandália', 'Camisa', 'Tênis', 'Bolsa', 'Bota']
plt.imshow(imagens_treino[0])
plt.title(classes_por_nome[gabarito_treino[0]])
plt.colorbar()
plt.show()
imagens_treino = imagens_treino/float(255)
modelo = keras.Sequential([
keras.layers.Flatten(input_shape=(28, 28)),
keras.layers.Dense(256, activation='relu'),
keras.layers.Dense(64, activation='relu'),
keras.layers.Dropout(0.15),
keras.layers.Dense(10, activation='softmax')
])
modelo.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
resultado = modelo.fit(imagens_treino, gabarito_treino,
epochs=10, validation_split=0.2)
plt.xlabel('Época')
plt.ylabel('Acurácia')
plt.plot(resultado.history['accuracy'], '--')
plt.plot(resultado.history['val_accuracy'])
plt.legend(['Treino', 'Validação'])
plt.xlabel('Épocas')
plt.ylabel('Perda')
plt.plot(resultado.history['loss'], '--')
plt.plot(resultado.history['val_loss'])
plt.legend(['Treino', 'Validação'])
plt.show()
103 <https://keras.io/>.
104 <https://github.com/zalandoresearch/fashion-mnist>.
66. Desenvolvimento de chatbot
Eric Gomes
105 <https://github.com/jornada-colaborativa/livro-python>.
PARTE X.
AUTOMAÇÃO DE PROCESSOS
67. Serverless
Alexandro Angelo Romeira
106 <https://github.com/jornada-colaborativa/livro-python>.
68. CI e CD
Davi Luis de Oliveira
venv/
__pycache__/
Com nosso código criado, precisamos testá-lo. Crie uma pasta tests e
coloque dentro um arquivo test_app.py, que consiste em um código
onde vamos importar o index do app.py para depois criar uma
verificação do valor retornado.
Com o Heroku CLI, você pode logar no Heroku via terminal e depois
criar um container para hospedar seu site.
Depois que você criar o seu token, deverá adicioná-lo, além do nome da
aplicação, no secrets (Settings/Options/Secrets/New repository secret)
do repositório no GitHub. No nosso caso, os valores eram os seguintes:
HEROKU_API_TOKEN a0a5406a-1dba-4d2f-8d10-4d3eb1f976b1
HEROKU_APP_NAME jornada-python
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r
requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or
undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --
statistics
# exit-zero treats all errors as warnings. The GitHub
editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-
line-length=127 --statistics
- name: Test with pytest
run: |
export PYTHONPATH=.
pytest
project_name/
README.md
setup.py
requirements.txt
package_name/
__init__.py
file1_name.py
file2_name.py
project_name/
README.md
setup.py
requirements.txt
package_name/
__init__.py
module1_name/
__init__.py
file1_name.py
file2_name.py
module2_name/
__init__.py
file1_name.py
file2_name.py
Dedico mais um livro aos amores da minha vida: meus filhos Lucas e
Luisa e minha esposa Keila. Agradeço a Deus essa nova conquista e
parabenizo o time organizador pelo comprometimento na curadoria e
aos coautores pela dedicação e excelência que resultou em mais uma
obra incrível para nossa série da Jornada Colaborativa. Agradecimento
especial para o amigo André Guilhon pela maestria na liderança do time
e ao Rodrigo Isensee pelo papo superanimado em Joinville que foi o
grande pontapé para iniciar este livro. Agradeço a meus familiares e
amigos da SulAmérica, Jornada Colaborativa e AdaptNow pelas
oportunidades de aprendizado e aos milhares de alunos, leitores e
participantes das minhas palestras pela grande receptividade e troca de
experiências que me tornam uma pessoa melhor a cada dia.
Antonio Muniz
Fundador e Líder da Jornada Colaborativa
Muita gratidão por estar fazendo parte deste projeto incrível, repleto de
pessoas maravilhosas, em especial, ao amigo Antonio Muniz, pela
idealização do projeto e pela confiança depositada em mim, e aos meus
amigos organizadores e coautores deste livro pela parceria. Agradeço à
minha família, por sempre terem zelado para que eu tivesse a melhor
educação possível, e a todos os professores que fizeram parte da minha
trajetória de aprendizado. Um agradecimento especial ao meu marido e
maior apoiador de todas as horas, Marcos Kalinowski, e para todos os
meus alunos e ex-alunos, que me motivam diariamente a ser uma
pessoa e profissional cada vez melhor.
Tatiana Escovedo
Organizadora e coautora do Jornada Python
Sobre os organizadores e
coautores
107 <https://setuptools.readthedocs.io/en/latest/setuptools.html>.
108 <https://pythonwheels.com/>.
109 <https://test.pypi.org/>.
110 <https://pypi.org/>.
111 <https://test.pypi.org/project/my_package>.