Curso Intensivo de Python - 3a - Eric Matthes
Curso Intensivo de Python - 3a - Eric Matthes
Eric Matthes
Novatec
Copyright © 2023 by Eric Matthes. Title of English-language original: Python Crash Course,
3rd Edition: A Hands-On, Project-Based Introduction to Programming, ISBN 9781718502703,
published by No Starch Press Inc. 245 8th Street, San Francisco, California, United States
94103. The Portuguese Language 3rd Edition Copyright © 2023 by Novatec Editora Ltda
under license by No Starch Press Inc. All rights reserved.
Copyright © 2023 por Eric Matthes. Título original em Inglês: Python Crash Course, 3rd
Edition: A Hands-On, Project-Based Introduction to Programming, ISBN 9781718502703,
publicado pela No Starch Press Inc. 245 8th Street, San Francisco, California, United States
94103. 3ª Edição em Português Copyright © 2023 pela Novatec Editora Ltda sob licença da
No Starch Press Inc. Todos os direitos reservados.
© Novatec Editora Ltda. [2023].
Todos os direitos reservados e protegidos pela Lei 9.610 de 19/02/1998. É proibida a
reprodução desta obra, mesmo parcial, por qualquer processo, sem prévia autorização, por
escrito, do autor e da Editora.
Editor: Rubens Prates GRA20230404
Tradução: Cibelle Ravaglia
Revisão gramatical: Alexandra Resende
ISBN do impresso: 978-85-7522-843-2
ISBN do ebook: 978-85-7522-844-9
Histórico de impressões:
Maio/2023 Primeira edição
Novatec Editora Ltda.
Rua Luís Antônio dos Santos 110
02460-000 – São Paulo, SP – Brasil
Tel.: +55 11 2959-6529
E-mail: [email protected]
Site: https://novatec.com.br
Twitter: twitter.com/novateceditora
Facebook: facebook.com/novatec
LinkedIn: linkedin.com/in/novatec
GRA20230404
Ao meu pai, que sempre teve tempo para responder minhas
perguntas sobre programação, e a Ever, que está começando a me
fazer perguntas.
Sumário
Capítulo 5 Instruções if
Um simples exemplo
Testes condicionais
Verificando a igualdade
Ignorando letras maiúsculas e minúsculas ao verificar a igualdade
Verificando a diferença
Comparações numéricas
Verificando múltiplas condições
Verificando se um valor está em uma lista
Verificando se um valor não está em uma lista
Expressões booleanas
Instruções if
Instruções if simples
Instruções if-else
Sequência if-elif-else
Usando múltiplos blocos elif
Omitindo o bloco else
Testando múltiplas condições
Usando instruções if com listas
Verificando elementos especiais
Verificando se uma lista não está vazia
Usando múltiplas listas
Estilizando suas instruções if
Recapitulando
Capítulo 6 Dicionários
Um dicionário simples
Trabalhando com dicionários
Acessando valores em um dicionário
Adicionando novos pares chave-valor
Começando com um dicionário vazio
Modificando valores em um dicionário
Removendo pares chave-valor
Um dicionário de objetos parecidos
Usando get() para acessar valores
Percorrendo um dicionário com um loop
Percorrendo todos os pares chave-valor com um loop
Percorrendo todas as chaves de um dicionário com um loop
Percorrendo as chaves de um dicionário com um loop em uma
ordem específica
Percorrendo todos os valores de um dicionário com um loop
Aninhamento
Uma lista de dicionários
Uma lista em um dicionário
Um dicionário em um dicionário
Recapitulando
Capítulo 8 Funções
Definindo uma função
Passando informações para uma função
Argumentos e parâmetros
Passando argumentos
Argumentos posicionais
Argumentos nomeados
Valores default
Chamadas de função equivalentes
Evitando erros de argumento
Valores de retorno
Retornando um valor simples
Definindo um argumento como opcional
Retornando um dicionário
Usando uma função com um loop while
Passando uma lista
Modificando uma lista em uma função
Evitando que uma função modifique uma lista
Passando um número arbitrário de argumentos
Misturando argumentos posicionais e arbitrários
Usando argumentos nomeados arbitrários
Armazenando suas funções em módulos
Importando um módulo inteiro
Importando funções específicas
Usando as para atribuir um alias a uma função
Usando as para atribuir um alias a um módulo
Importando todas as funções em um módulo
Estilizando funções
Recapitulando
Capítulo 9 Classes
Criando e usando uma classe
Criando a classe Dog
Método __init__()
Como criar uma instância a partir de uma classe
Trabalhando com classes e instâncias
Classe Car
Definindo um valor default para um atributo
Modificando valores de atributos
Herança
Método __init__() para uma classe-filha
Definindo atributos e métodos para a classe-filha
Sobrescrevendo métodos a partir da classe-pai
Instâncias como atributos
Modelando objetos do cotidiano
Importando classes
Importando uma única classe
Armazenando múltiplas classes em um módulo
Importando múltiplas classes de um módulo
Importando um módulo inteiro
Importando todas as classes de um módulo
Importando um módulo para um módulo
Usando aliases
Definindo o próprio fluxo de estudo e de trabalho
Biblioteca padrão do Python
Estilizando classes
Recapitulando
Capítulo 13 Alienígenas!
Revisando o projeto
Criando o primeiro alienígena
Criando a classe Alien
Criando uma instância do alienígena
Criando a frota alienígena
Criando uma fileira de alienígenas
Refatorando _create_fleet()
Adicionando fileiras
Movendo a frota
Movendo os alienígenas para a direita
Criando configurações para a direção da frota
Verificando se um alienígena alcançou a borda
Fazendo a frota descer e mudar de direção
Disparando contra os alienígenas
Detectando colisões de projéteis
Criando projéteis maiores para testes
Repopulando a frota
Criando projéteis mais rápidos
Refatorando _update_bullets()
Encerrando o jogo
Detectando colisões entre espaçonaves e alienígenas
Respondendo à colisões entre espaçonaves e alienígenas
Alienígenas que alcançam a parte inferior da tela
Game Over!
Identificando quais partes do jogo devem ser executadas
Recapitulando
Capítulo 14 Pontuação
Adicionando o botão Play
Criando uma classe Button
Desenhando o botão na tela
Iniciando o jogo
Reiniciando o jogo
Desativando o botão Play
Ocultando o cursor do mouse
Passando de nível
Modificando as configurações de velocidade
Redefinindo a velocidade
Pontuação
Exibindo a pontuação
Criando um scoreboard
Atualizando a pontuação conforme os alienígenas são abatidos
Redefinindo a pontuação
Assegurando que todos os ataques sejam pontuados
Aumentando os valores dos pontos
Arredondando a pontuação
Pontuações máximas
Exibindo o nível
Exibindo a quantidade de espaçonaves
Recapitulando
Recursos online
A No Starch Press tem mais informações disponíveis online sobre
este livro em https://nostarch.com/python-crash-course-3rd-edition.
Disponibilizo também um conjunto abrangente de recursos
complementares em https://ehmatthes.github.io/pcc_3e. Esses recursos
incluem:
• Instruções de configuração – Apesar de serem idênticas às do
livro, as instruções online de configuração incluem links ativos de
todas as etapas diferentes em que você pode clicar. Se tiver
problemas com a configuração, consulte este recurso.
• Atualizações – O Python, como todas as linguagens, está em
constante evolução. Eu mantenho um conjunto detalhado de
atualizações. Então, se algo não estiver funcionando, confira aqui
se as instruções foram alteradas.
• Soluções dos exercícios – Você deve passar um tempão
tentando resolver os exercícios nas seções “Faça você mesmo”. No
entanto, se ficar empacado e não conseguir fazer nenhum
progresso, confira as soluções online para a maioria dos
exercícios.
• Folhas de dicas – Disponibilizo online um conjunto completo de
folhas de dicas para download e breve referência dos principais
conceitos.
Noções básicas
Primeiros passos
Versões do Python
Toda linguagem de programação evolui à medida que ideias e novas
tecnologias surgem, e os desenvolvedores Python têm
reiteradamente contribuído para que a linguagem fique mais versátil
e poderosa. No momento em que eu escrevia este livro, a versão
mais recente era o Python 3.11. No entanto, tudo aqui deve ser
executado no Python 3.9 ou posterior. Nesta seção, descobriremos
se o Python já está instalado em seu sistema e se é necessário
instalar uma versão mais recente. O Apêndice A também contém
informações complementares sobre como instalar a versão mais
recente do Python em cada sistema operacional principal.
Python no Windows
Em geral, o Windows não vem com o Python, portanto é bem
provável que precisemos instalá-lo e depois instalar o VS Code.
Instalando o Python
Primeiro, verifique se o Python está instalado em seu sistema. Para
abrir a janela de comando, digite command no menu Iniciar, depois
clique no aplicativo Prompt de Comando. Na janela do terminal,
digite python em letras minúsculas. Se receber como resposta um
prompt do Python (>>>), o Python já está instalado em seu sistema.
Se você vir uma mensagem de erro informando que o python não é
um comando reconhecido ou se a Microsoft Store for aberta, o
Python não está instalado. Feche a Microsoft Store se abrir
automaticamente; é melhor fazer o download de um instalador
oficial do que usar a versão da Microsoft.
Se o Python não estiver instalado em seu sistema ou se você vir
uma versão anterior ao Python 3.9, será necessário fazer o
download de um instalador Python para Windows. Acesse
https://python.org e passe o mouse sobre o link Downloads. Você verá
um botão de download com a versão mais recente do Python. Clique
no botão para iniciar automaticamente o download do instalador
correto para o seu sistema. Após o download do arquivo, execute o
instalador. Não se esqueça de selecionar a opção Add Python to
PATH, pois isso facilita a configuração adequada do seu sistema. A
Figura 1.1 mostra esta opção selecionada.
Figura 1.1: Não se esqueça de selecionar a caixa de seleção Add
Python to PATH.
Instalando o VS Code
Você pode fazer o download do instalador do VS Code em
https://code.visualstudio.com. Clique no botão Download for Windows e
execute o instalador. Ignore as seções a seguir sobre macOS e Linux
e siga as etapas em “Executando um programa Hello World” na
página 40.
Python no macOS
O Python não vem instalado por padrão nas versões mais recentes
do macOS. Assim, você precisará instalá-lo caso ainda não tenha
instalado. Nesta seção, instalaremos a versão mais recente do
Python e, em seguida, instalaremos o VS Code e verificaremos se
está adequadamente configurado.
NOTA O Python 2 era incluído em versões mais antigas do macOS,
mas é uma versão desatualizada, não devemos usá-la.
Instalando o VS Code
Para instalar o editor VS Code é necessário fazer o download do
instalador em https://code.visualstudio.com. Clique na seta à direita do
botão Download, e vá para a pasta Downloads. Arraste o
instalador do Visual Studio Code para a pasta Aplicativos e clique
duas vezes no instalador para executá-lo.
Ignore a seção a seguir sobre Python no Linux e siga as etapas em
“Executando um programa Hello World” na página 40.
Python no Linux
Como os sistemas Linux são arquitetados para programação, o
Python já está instalado na maioria dos computadores Linux. As
pessoas que escrevem e mantêm o Linux esperam que você faça sua
própria programação em algum momento e o encorajam a fazê-lo.
Por esta razão, há muito pouco para instalar e apenas algumas
configurações a se fazer para começar a programar.
Instalando o VS Code
No Ubuntu Linux é possível instalar o VS Code no Ubuntu Software
Center. Clique no ícone do Ubuntu Software em seu menu e procure
por vscode. Clique no aplicativo Visual Studio Code (às vezes
chamado de code) e clique em Instalar. Uma vez instalado, procure
em seu sistema por VS Code e inicie o aplicativo.
Executando um programa Hello World
Com uma versão recente do Python e do VS Code instalada, você
está quase pronto para executar seu primeiro programa Python
escrito em um editor de texto. Mas antes precisamos instalar uma
extensão Python para VS Code.
Executando hello_world.py
Antes de escrever seu primeiro programa, crie uma pasta chamada
python_work em sua área de trabalho para seus projetos. É melhor
usar letras minúsculas e underscores em vez de espaços em nomes
de arquivos e pastas, já que o Python usa essas convenções de
nomenclatura. É possível criar essa pasta em outro lugar que não
seja a área de trabalho, porém, será mais fácil seguir algumas
etapas posteriores se você salvar a pasta python_work direto na
área de trabalho.
Abra o VS Code e feche a guia Introdução se ainda estiver aberta.
Crie um novo arquivo clicando em Arquivo4Novo Arquivo ou
pressionando CTRL+N (⌘+N no macOS). Salve o arquivo como
hello_world.py em sua pasta python_work. A extensão .py informa
ao VS Code que seu arquivo está escrito em Python e como executar
o programa e realçar o texto de maneira útil.
Após salvar o arquivo, digite a seguinte linha no editor:
hello_world.py
print("Hello Python world!")
Para executar seu programa, selecione ExecutarExecutar sem
Depuração ou pressione CTRL+F5. Uma tela de terminal deve
aparecer na parte inferior da janela do VS Code, mostrando a saída
do seu programa:
Hello Python world!
Provavelmente, você verá uma saída adicional mostrando o
interpretador Python usado para executar seu programa. Caso
queira simplificar as informações exibidas para ver somente a saída
do seu programa, consulte o Apêndice B. No Apêndice B, você
também pode encontrar sugestões úteis sobre como utilizar o
VS Code com mais eficiência.
Caso não veja essa saída, algo pode ter dado errado no programa.
Verifique todos os caracteres digitados na linha. Você digitou sem
querer o print com P maiúsculo? Esqueceu de uma ou ambas as
aspas ou parênteses? As linguagens de programação trabalham com
uma sintaxe muito específica e, caso não digite tudo de forma
correta, receberá erros. Se não conseguir executar o programa, veja
as sugestões na próxima seção.
Resolução de problemas
Se não conseguir executar o hello_world.py, veja a seguir algumas
sugestões que você pode tentar e que também são boas soluções
gerais para qualquer problema de programação:
• Quando um programa tem um erro significativo, o Python exibe
uma traceback, um relatório de erros. O Python examina o arquivo
e tenta identificar o problema. Verifique o traceback; isso pode lhe
fornecer uma pista sobre o problema que está impedindo a
execução do programa.
• Saia um pouco da frente do computador, faça uma breve pausa e
tente novamente. Lembre-se de que a sintaxe é indispensável na
programação. Ou seja, algo tão simples como aspas ou parênteses
incompatíveis pode impedir que um programa seja executado de
forma correta. Releia as partes relevantes deste capítulo, examine
seu código e tente identificar o erro.
• Recomece tudo. É bem provável que não seja necessário
desinstalar nenhum software, mas talvez seja uma boa excluir seu
arquivo hello_world.py e recriá-lo do zero.
• Peça a outra pessoa para seguir as etapas deste capítulo, em seu
computador ou em um computador diferente, e observe
atentamente o que ela faz. Talvez você tenha se esquecido de um
pequeno passo que outra pessoa não esqueceu.
• Confira as instruções adicionais de instalação no Apêndice A;
alguns dos detalhes incluídos no Apêndice podem ajudá-lo a
resolver seu problema.
• Ache alguém que conheça Python e peça para ajudá-lo a
configurar. Caso pergunte às pessoas, pode descobrir que talvez
conheça alguém que use Python.
• As instruções de configuração deste capítulo também estão
disponíveis no site que acompanha este livro em
https://ehmatthes.github.io/pcc_3e. Talvez a versão online dessas
instruções lhe sirva melhor, pois você pode simplesmente recortar
e colar o código e clicar nos links para os recursos necessários.
• Peça ajuda online. O Apêndice C disponibiliza uma série de
recursos, como fóruns e sites de bate-papo ao vivo, em que é
possível pedir ajuda de soluções para pessoas que já resolveram o
problema que você está enfrentando.
Nunca se preocupe em incomodar programadores experientes. Todo
programador já se sentiu perdido em algum momento, e a maioria
deles fica contente em ajudá-lo a configurar devidamente seu
sistema. Contanto que você consiga expor claramente o que está
tentando fazer, o que já tentou e os resultados que está obtendo, há
uma boa chance de alguém ajudá-lo. Conforme mencionado na
introdução, a comunidade Python é muito amigável e prestativa com
iniciantes.
O Python deve executar sem problemas em qualquer computador
moderno. Problemas iniciais de configuração podem ser frustrantes,
mas vale a pena resolvê-los. Assim que executar o hello_world.py,
você pode começar a aprender Python e suas atividades de
programação se tornarão mais interessantes e satisfatórias.
No Windows
Você pode usar o comando cd, de alterar o diretório, navegar pelo
sistema de arquivos em uma janela de comando. O comando dir, de
diretório, mostra todos os arquivos que existem no diretório atual.
Abra uma nova janela de terminal e digite os seguintes comandos
para executar hello_world.py:
C:\> cd Desktop\python_work
C:\Desktop\python_work> dir
hello_world.py
C:\Desktop\python_work> python hello_world.py
Hello Python world!
Primeiro, use o comando cd para navegar até a pasta python_work,
que está na pasta Desktop. Em seguida, use o comando dir para
verificar se hello_world.py está nessa pasta. Depois, execute o
arquivo usando o comando python hello_world.py.
A maioria dos seus programas executará perfeita e diretamente em
seu editor. Apesar disso, conforme suas atividades ficam mais
complexas, você desejará executar alguns de seus programas em
um terminal.
No macOS e no Linux
Executar um programa Python em uma sessão de terminal é a
mesma coisa no Linux e no macOS. Você pode usar o comando cd,
de alterar o diretório, navegar pelo sistema de arquivos em uma
janela de comando. O comando ls, de Lista, mostra todos os
arquivos não ocultos e existentes do diretório atual.
Abra uma nova janela de terminal e digite os seguintes comandos
para executar hello_world.py:
~$ cd Desktop/python_work/
~/Desktop/python_work$ ls
hello_world.py
~/Desktop/python_work$ python3 hello_world.py
Hello Python world!
Primeiro, use o comando cd para navegar até a pasta python_work,
que está na pasta Desktop. Em seguida, use o comando ls para
verificar se hello_world.py está nessa pasta. Depois, execute o
arquivo usando o comando python3 hello_world.py.
A maioria dos seus programas executará perfeita e diretamente em
seu editor. Apesar disso, conforme suas atividades ficam mais
complexas, você desejará executar alguns de seus programas em
um terminal.
Recapitulando
Neste capítulo, você aprendeu um pouco sobre o Python e instalou o
Python em seu sistema, se não tivesse instalado. Você também
instalou um editor de texto para escrever com facilidade código
Python. Executamos trechos de código Python em uma sessão de
terminal, e você executou seu primeiro programa, hello_world.py.
Provavelmente, você aprendeu também um pouco sobre solução de
problemas.
No próximo capítulo, você aprenderá sobre os diferentes tipos de
dados com os quais pode trabalhar em seus programas Python e
começará também a usar variáveis.
2 capítulo
Variáveis
Vamos tentar usar uma variável com o hello_world.py. Adicione uma
nova linha no início do arquivo e modifique a segunda linha:
hello_world.py
message = "Hello Python world!"
print(message)
Execute esse programa para ver o que acontece. Você deve ver a
mesma saída que antes:
Hello Python world!
Adicionamos uma variável chamada message. Toda variável guarda um
valor, informação associada a essa variável. Nesse caso, o valor é o
texto "Hello Python world!".
Adicionar uma variável ocasiona um pouco mais de trabalho para o
interpretador Python. Ao processar a primeira linha, o interpretador
associa a variável message com o texto "Hello Python world!". Ao chegar à
segunda linha, exibe na tela o valor associado a message.
Vamos incrementar esse programa modificando o hello_world.py
para que exiba uma segunda mensagem. Adicione uma linha em
branco ao hello_world.py e, em seguida, acrescente duas novas
linhas de código:
message = "Hello Python world!"
print(message)
Strings
Como a maioria dos programas define e agrega algum tipo de dado
e, em seguida, faz algo de útil com ele, isso ajuda a classificar
diferentes tipos de dados. O primeiro tipo de dados que veremos é a
string. À primeira vista, strings são bem simples, mas podemos usá-
las das mais variadas formas.
Uma string é uma série de caracteres. No Python, seja lá o que
estiver entre aspas é considerado uma string, e podemos usar aspas
simples ou duplas em torno das strings desse jeito:
"This is a string."
'This is also a string.'
Essa flexibilidade possibilita que usemos aspas e apóstrofos em
nossas strings:
'I told my friend, "Python is my favorite language!"'
"The language 'Python' is named after Monty Python, not the snake."
"One of Python's strengths is its diverse and supportive community."
Vamos explorar algumas formas de usar as strings.
É
É essencial considerar espaços em branco porque muitas vezes
queremos comparar duas strings para determinar se são iguais. Por
exemplo, podemos ter uma situação importante envolvendo
verificação dos nomes de usuário das pessoas que logaram em um
site. Espaços em branco extras também podem gerar confusão em
situações mais simples. Felizmente, o Python facilita a remoção de
espaços em branco extras dos dados inseridos pelas pessoas.
O Python pode procurar espaços em branco extras nos lados direito
e esquerdo de uma string. A fim de garantir que não tenha nenhum
espaço em branco no lado direito de uma string, use o método
rstrip():
1 >>> favorite_language = 'python '
2 >>> favorite_language
'python '
3 >>> favorite_language.rstrip()
'python'
4 >>> favorite_language
'python '
O valor associado ao favorite_language 1 contém espaço em branco extra
no final da string. Ao solicitarmos esse valor ao Python em uma
sessão de terminal, podemos ver o espaço no final do valor 2.
Quando o método rstrip() itera na variável favorite_language 3, esse
espaço extra é removido. No entanto, é removido apenas
temporariamente. Caso solicite o valor de favorite_language mais uma
vez, a string terá a mesma aparência de quando foi inserida,
incluindo o espaço em branco extra 4.
Para remover o espaço em branco da string de forma definitiva, é
necessário associar o valor removido ao nome da variável:
>>> favorite_language = 'python '
1 >>> favorite_language = favorite_language.rstrip()
>>> favorite_language
'python'
A fim de remover o espaço em branco da string, retiramos o espaço
em branco do lado direito da string e associamos esse novo valor à
variável original 1. Em programação é comum alterar o valor de uma
É
variável. É assim que o valor de uma variável pode ser atualizado à
medida que um programa é executado ou em resposta à entrada do
usuário.
É possível também removermos o espaço em branco do lado
esquerdo de uma string com o método lstrip(), ou de ambos os lados
ao mesmo tempo usando o strip():
1 >>> favorite_language = ' python '
2 >>> favorite_language.rstrip()
' python'
3 >>> favorite_language.lstrip()
'python '
4 >>> favorite_language.strip()
'python'
Nesse exemplo, começamos com um valor que tem espaços em
branco no início e no final 1. Em seguida, removemos o espaço extra
do lado direito 2, do lado esquerdo 3, e de ambos os lados 4. Testar
essas funções de remoção pode ajudá-lo a se familiarizar com a
manipulação de strings. Em um contexto real, essas funções de
remoção são usadas quase sempre para limpar entradas de usuário
antes que sejam armazenadas em um programa.
Removendo prefixos
Ao trabalhar com strings, outra tarefa comum é remover um prefixo.
Considere um URL com o típico prefixo https://. Queremos remover
esse prefixo para que possamos focar somente a parte da URL que
os usuários precisam inserir em uma barra de endereços. Vejamos
como fazer isso:
>>> nostarch_url = 'https://nostarch.com'
>>> nostarch_url.removeprefix('https://')
'nostarch.com'
Digite o nome da variável seguido por um ponto e, em seguida, o
método removeprefix(). Dentro dos parênteses insira o prefixo que
deseja remover da string original.
Assim como os métodos para remover espaços em branco, o
deixa a string original inalterada. Se quiser manter o
removeprefix()
novo valor com o prefixo removido, reatribua-o à variável original ou
atribua-o a uma nova variável:
>>> simple_url = nostarch_url.removeprefix('https://')
Quando vemos um URL em uma barra de endereço e a parte
https:// não é mostrada, o navegador provavelmente está usando
um método como o removeprefix() nos bastidores.
Números
Na programação, os números são usados com bastante frequência
para armazenar pontuação de jogos, representar dados em
visualizações, armazenar informações em aplicativos web e assim
por diante. O Python trata os números de diversas formas
diferentes, dependendo de como estão sendo usados. Analisaremos
primeiramente como o Python lida com números inteiros, já que os
inteiros são os mais simples de se trabalhar.
Inteiros
No Python é possível somar (+), subtrair (-), multiplicar (*) e dividir
(/) números inteiros.
>>> 2+3
5
>>> 3-2
1
>>> 2*3
6
>>> 3/2
1.5
Caso execute essas operações na sessão do terminal, o Python
simplesmente retorna o resultado da operação. O Python usa dois
símbolos de multiplicação para representar expoentes:
>>> 3 ** 2
9
>>> 3 ** 3
27
>>> 10 ** 6
1000000
O Python também suporta a ordem de precedência das operações,
logo podemos usar inúmeras operações em uma expressão.
Podemos também utilizar parênteses para modificar a ordem das
operações para que o Python possa avaliar a expressão na ordem
especificada. Por exemplo:
>>> 2 + 3*4
14
>>> (2 + 3) * 4
20
Nesses exemplos, o espaçamento não impacta como o Python avalia
as expressões; os espaços simplesmente nos ajudam a identificar
mais rápido as operações que têm prioridade quando estamos lendo
o código.
Floats
O Python chama qualquer número com um ponto decimal de
número de ponto flutuante [float]. Esse termo é usado na maioria
das linguagens de programação e refere-se ao fato de que um ponto
decimal pode aparecer em qualquer posição de um número. Deve-se
arquitetar minuciosamente toda linguagem de programação para
lidar de forma adequada com os números decimais, de modo que
tenham o devido comportamento, independentemente de onde o
ponto decimal apareça.
Na maioria dos casos podemos utilizar floats sem nos preocuparmos
como se comportam. Basta digitar os números que quer usar, e o
Python provavelmente fará o que você espera:
>>> 0.1 + 0.1
0.2
>>> 0.2 + 0.2
0.4
>>> 2 * 0.1
0.2
>>> 2 * 0.2
0.4
No entanto, fique atento de que às vezes você pode obter um
número arbitrário de casas decimais como resposta:
>>> 0.2 + 0.1
0.30000000000000004
>>> 3 * 0.1
0.30000000000000004
Isso ocorre em todas as linguagens, sendo pouco preocupante. O
Python tenta encontrar uma maneira de representar o resultado com
a maior precisão possível, o que às vezes é difícil, pois os
computadores precisam representar números internamente. Por ora,
apenas ignore as casas decimais extras; nos projetos da Parte II,
aprenderemos como lidar com as casas extras quando for
necessário.
Inteiros e floats
Ao realizar a divisão de dois números quaisquer, mesmo que sejam
inteiros que resultem em um número inteiro, sempre obteremos um
float:
>>> 4/2
2.0
Caso misture um inteiro e um float em qualquer outra operação,
você também obterá um float:
>>> 1 + 2.0
3.0
>>> 2 * 3.0
6.0
>>> 3.0 ** 2
9.0
O padrão Python é um float em qualquer operação que use um float,
mesmo que a saída seja um número inteiro.
Underscores em números
Ao escrever números grandes, é possível agrupar dígitos usando
underscores para tornar os números grandes mais legíveis:
>>> universe_age = 14_000_000_000
Ao printar um número que foi definido com underscores, o Python
exibe apenas os dígitos:
>>> print(universe_age)
14000000000
O Python ignora os underscore ao armazenar esses tipos de valores.
Mesmo se não agruparmos os dígitos em três, o valor ainda não será
afetado. Para o Python, 1000 é o mesmo que 1_000, que é o mesmo
que 10_00. Esse recurso funciona com números inteiros e floats.
Atribuição múltipla
Podemos atribuir valores a mais de uma variável usando somente
uma única linha de código. Isso pode ajudar a sintetizar seus
programas, facilitando a legibilidade deles; você usará essa técnica
com mais frequência ao inicializar um conjunto de números.
Por exemplo, veja como inicializar as variáveis x, y, e z para zero:
>>> x, y, z = 0, 0, 0
É necessário separar os nomes das variáveis com vírgulas e fazer o
mesmo com os valores, e o Python atribuirá cada valor à sua
respectiva variável. Desde que o número de valores corresponda ao
número de variáveis, o Python irá combiná-las de forma correta.
Constantes
Uma constante é uma variável cujo valor permanece o mesmo
durante a vida de um programa. O Python não tem tipos de
constantes built-in, mas os programadores Python usam letras
maiúsculas para indicar que uma variável deve ser tratada como
uma constante e nunca ser alterada:
MAX_CONNECTIONS = 5000
Quando quiser tratar uma variável como uma constante em seu
código, escreva o nome da variável com todas as letras maiúsculas.
Comentários
Os comentários são um recurso de grande utilidade na maioria das
linguagens de programação. Até agora, tudo o que você escreveu
em seus programas foi código Python. À medida que seus
programas ficam mais extensos e mais complicados, é necessário
adicionar notas em seus programas que descrevam sua abordagem
geral para o problema que está resolvendo. Um comentário
possibilita que você escreva notas em seu próprio idioma, dentro de
seus programas.
Recapitulando
Neste capítulo, aprendemos como trabalhar com variáveis e a usar
nomes de variáveis descritivos e resolver erros de nome e de sintaxe
quando surgem. Aprendemos o que são strings e como exibi-las
usando letras minúsculas, maiúsculas e inicias maiúsculas. A
princípio, utilizamos espaços em branco para organizar a saída de
forma ordenada e aprendemos a remover elementos desnecessários
de uma string. Depois, começamos a trabalhar com números inteiros
e floats e aprendemos algumas forma de trabalhar com dados
numéricos. Além disso, aprendemos a escrever comentários
explicativos para que você e outras pessoas leiam seu código de
modo mais fácil. Por último, lemos a filosofia de manter seu código o
mais simples possível, sempre que possível.
No Capítulo 3 aprenderemos como armazenar coleções de
informações em estruturas de dados chamadas listas, e também a
trabalhar com uma lista, manipulando qualquer informação que
esteja nela.
3 capítulo
Introdução às listas
motorcycles[0] = 'ducati'
print(motorcycles)
Aqui, definimos a lista motorcycles, com 'honda' sendo o primeiro
elemento. Em seguida, mudamos o valor do primeiro elemento para
'ducati'. A saída mostra que o primeiro elemento foi alterado, ao passo
que o restante da lista permanece o mesmo:
['honda', 'yamaha', 'suzuki']
['ducati', 'yamaha', 'suzuki']
É possível alterar o valor de qualquer elemento em uma lista, não
somente do primeiro item.
motorcycles.append('honda')
motorcycles.append('yamaha')
motorcycles.append('suzuki')
print(motorcycles)
A lista decorrente é exatamente igual às listas dos exemplos
anteriores:
['honda', 'yamaha', 'suzuki']
Criar listas assim é bastante comum, porque muitas vezes não
saberemos os dados que os usuários querem armazenar em um
programa até que o programa seja executado. Para que seus
usuários assumam o controle, comece definindo uma lista vazia para
armazenar os valores dos usuários. Em seguida, anexe com o
método append() cada valor novo fornecido à lista que acabou de criar.
motorcycles.insert(0, 'ducati')
print(motorcycles)
Nesse exemplo, inserimos o valor 'ducati' no início da lista. O método
insert() abre um espaço na posição 0 e armazena o valor 'ducati' nesse
local:
['ducati', 'honda', 'yamaha', 'suzuki']
Essa operação desloca todos os outros valores uma posição à direita
na lista.
del motorcycles[0]
print(motorcycles)
Aqui, usamos a instrução del para remover o primeiro elemento,
'honda', da lista de motos:
['honda', 'yamaha', 'suzuki']
['yamaha', 'suzuki']
É possível remover um elemento de qualquer posição em uma lista
usando a instrução del se você souber seu índice. Por exemplo, veja
como remover o segundo elemento, 'yamaha', da lista:
motorcycles = ['honda', 'yamaha', 'suzuki']
print(motorcycles)
del motorcycles[1]
print(motorcycles)
A segunda moto é deletada da lista:
['honda', 'yamaha', 'suzuki']
['honda', 'suzuki']
Em ambos os exemplos, não podemos mais acessar o valor que foi
removido da lista após usarmos a instrução del.
2 popped_motorcycle = motorcycles.pop()
3 print(motorcycles)
4 print(popped_motorcycle)
Começamos definindo e mostrando a lista motorcycles 1. Depois,
removemos um valor da lista e atribuímos esse valor à variável
popped_motorcycle 2. Exibimos a lista 3 a fim de mostrar um valor
removido dela. Em seguida, exibimos o valor removido 4 para provar
que ainda temos acesso ao valor.
A saída demonstra que o valor 'suzuki' foi removido do final da lista e
agora está atribuído à variável popped_motorcycle:
['honda', 'yamaha', 'suzuki']
['honda', 'yamaha']
suzuki
Qual é a serventia desse método pop()? Imagine que as motos da
lista estão armazenadas em ordem cronológica, conforme a data de
compra. Nesse caso, podemos utilizar o método pop() para exibir
explicações sobre a última moto que compramos:
motorcycles = ['honda', 'yamaha', 'suzuki']
last_owned = motorcycles.pop()
print(f"The last motorcycle I owned was a {last_owned.title()}.")
A saída é uma frase simples sobre a moto recém-comprada:
The last motorcycle I owned was a Suzuki.
first_owned = motorcycles.pop(0)
print(f"The first motorcycle I owned was a {first_owned.title()}.")
Começamos removendo a primeira moto da lista e, em seguida,
exibimos uma mensagem a respeito. A saída é uma frase simples
com a primeira moto comprada:
The first motorcycle I owned was a Honda.
Lembre-se de que sempre que usar o método pop(), o elemento com
o qual está trabalhando não fica mais armazenado na lista.
Caso esteja inseguro se deve usar a instrução del ou o método pop(),
pense o seguinte: quando quiser deletar um elemento de uma lista e
não for nunca mais for usá-lo, recorra à instrução del. Agora, se
desejar usar o elemento mesmo ao removê-lo, recorra ao método
pop().
motorcycles.remove('ducati')
print(motorcycles)
Aqui, o método remove() solicita ao Python que identifique onde 'ducati'
aparece na lista e remova esse elemento:
['honda', 'yamaha', 'suzuki', 'ducati']
['honda', 'yamaha', 'suzuki']
Podemos também utilizar o método remove() para trabalhar com um
valor que está sendo removido de uma lista. Removeremos o valor
'ducati' e exibiremos a justificativa para removê-lo da lista:
1 motorcycles = ['honda', 'yamaha', 'suzuki', 'ducati']
print(motorcycles)
2 too_expensive = 'ducati'
3 motorcycles.remove(too_expensive)
print(motorcycles)
4 print(f"\nA {too_expensive.title()} is too expensive for me.")
Após definir a lista 1, atribuímos o valor 'ducati' a uma variável
chamada too_expensive 2. Depois, usamos essa variável para informar
ao Python qual valor remover da lista 3. O valor 'ducati' foi removido
da lista 4, mas ainda está acessível pela variável too_expensive,
possibilitando exibir uma justificativa sobre porque removemos 'ducati'
da lista de motos:
['honda', 'yamaha', 'suzuki', 'ducati']
['honda', 'yamaha', 'suzuki']
A Ducati is too expensive for me.
cars.reverse()
print(cars)
Veja que o reverse() não reordena em ordem alfabética; o método
simplesmente inverte a ordem da lista:
['bmw', 'audi', 'toyota', 'subaru']
['subaru', 'toyota', 'audi', 'bmw']
O método reverse() altera permanentemente a ordem de uma lista,
mas podemos reverter para a ordem original a qualquer momento
aplicando reverse() à mesma lista uma segunda vez.
Recapitulando
Neste capítulo, aprendemos o que são listas e como trabalhar com
os elementos individuais de uma lista. Aprendemos como definir
uma lista e como adicionar e remover elementos dela, a ordenar
listas permanente e temporariamente para exibi-las, e também como
encontrar o tamanho de uma lista e como evitar erros de índice ao
trabalhar com listas.
No Capítulo 4, você aprenderá a trabalhar com elementos em uma
lista de modo mais eficiente. Ao percorrer cada elemento em uma
lista usando somente algumas linhas de código, você conseguirá
trabalhar com eficiência, mesmo quando sua lista contiver milhares
ou milhões de elementos.
4 capítulo
Esquecendo da indentação
Sempre indente a linha após a instrução for em um loop. Você pode
até se esquecer da indentação, mas o Python não:
magiians.py
magicians = ['alice', 'david', 'carolina']
for magician in magicians:
1 print(magician)
O print() 1 deveria estar indentado, mas não está. Ao esperar um
bloco indentado e não encontrá-lo, o Python o alerta em qual linha
houve um problema:
File "magicians.py", line 3
print(magician)
^
IndentationError: expected an indented block after 'for' statement on line 2
Em geral, você consegue resolver esse tipo de erro de indentação
fazendo a indentação da linha ou das linhas imediatamente após a
instrução for.
Indentando desnecessariamente
Se você indentar sem querer uma linha que não precisa ser
indentada, o Python o alerta sobre a indentação inesperada:
hello_world.py
message = "Hello Python world!"
print(message)
Não precisamos indentar o print(), pois não faz parte de um loop;
logo, o Python relata o seguinte erro:
File "hello_world.py", line 2
print(message)
^
IndentationError: unexpected indent
Faça a indentação somente quando tiver uma razão específica para
tal, assim você evita erros de indentação inesperada. Até agora, nos
programas que estamos escrevendo, as únicas linhas que devemos
indentar são as ações que queremos repetir para cada elemento
dentro de um loop for.
Esquecendo os dois-pontos
Os dois-pontos no final de uma instrução for informam ao Python
para interpretar a próxima linha como o início de um loop:
magicians = ['alice', 'david', 'carolina']
1 for magician in magicians
print(magician)
Se, por um acaso, você esquecer os dois-pontos 1, receberá um erro
de sintaxe porque o Python não sabe exatamente o que você está
tentando fazer:
File "magicians.py", line 2
for magician in magicians
^
SyntaxError: expected ':'
O Python não sabe se você simplesmente esqueceu os dois-pontos
ou se pretendia escrever um pouco mais de código para um loop
mais complexo. Se conseguir identificar uma possível correção, o
interpretador sugerirá uma, como adicionar dois-pontos no final de
uma linha, como fez com a resposta expected ':'. Alguns erros podem
ser corrigidos fácil e claramente, graças às sugestões dos tracebacks
do Python. Alguns erros são mais difíceis de resolver, mesmo quando
a eventual correção envolve um único caractere. Não se sinta mal
quando levar um tempo para corrigir um erro; lembre-se: você não é
o único que está passando por isso.
print(squares)
Começamos com uma lista vazia chamada squares. Em seguida,
solicitamos ao Python que percorra cada valor de 1 a 10 usando a
função range(). Dentro do loop, o valor atual é elevado à segunda
potência, ou elevado ao quadrado, e atribuído à variável square 1.
Cada novo valor de square é então anexado à lista squares 2. Por
último, quando o loop termina de executar, a lista de quadrados é
exibida:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Para escrever esse código de forma mais concisa, omita a variável
temporária square e anexe cada novo valor diretamente à lista:
squares = []
for value in range(1,11):
squares.append(value**2)
print(squares)
Essa linha faz o mesmo trabalho que as linhas dentro do loop for na
listagem anterior. Cada valor no loop é elevado ao quadrado e
imediatamente anexado à lista de quadrados.
Você pode recorrer a qualquer uma dessas abordagens quando
estiver criando listas mais complexas. Às vezes, usar uma variável
temporária facilita a leitura do seu código; outras vezes, aumenta
extensiva e desnecessariamente o código. Foque primeiro em
escrever um código que você claramente entenda e execute o que
você quer. Em seguida, procure abordagens mais eficientes
conforme revisar seu código.
List comprehensions
A abordagem anterior para gerar a lista square consistia em usar três
ou quatro linhas de código. Uma list comprehension possibilita gerar
essa mesma lista com somente uma linha de código. Uma list
comprehension combina o loop for e a criação de elementos novos
em uma linha e anexa automaticamente cada elemento novo. Nem
sempre as lists comprehensions são apresentadas aos iniciantes,
mas resolvi inclui-las porque você provavelmente as verá assim que
começar a examinar o código de outras pessoas.
O próximo exemplo cria a mesma lista de números quadrados que
vimos anteriormente, mas com uma list comprehension:
squares.py
squares = [value**2 for value in range(1, 11)]
print(squares)
Para usar essa sintaxe, comece com um nome descritivo para a lista,
como squares. Em seguida, abra um conjunto de colchetes e defina a
expressão para os valores que quer armazenar na lista nova. Neste
exemplo, a expressão é value**2, que eleva o valor ao quadrado.
Depois, escreva um loop for para gerar os números desejados que
quer inserir na expressão e feche os colchetes. Neste exemplo, o
loop for é for value in range(1, 11), que fornece os valores de 1 a 10 na
expressão value**2. Repare que não temos dois-pontos no final da
instrução for.
O resultado é a mesma lista de números quadrados que vimos
antes:
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
É necessário prática para escrever as próprias lists comprehensions,
mas com o tempo, assim que estiver familiarizado com as listas
comuns, você perceberá que vale a pena. Quando escrever três ou
quatro linhas de código para gerar listas começar a se tornar
repetitivo, pense em escrever as próprias list comprehensions.
my_foods.append('cannoli')
friend_foods.append('ice cream')
NOTA Por ora, não se preocupe com detalhes deste exemplo. Caso
esteja tentando trabalhar com uma cópia de uma lista e
observa um comportamento inesperado, verifique se está
copiando a lista com uma fatia, como no primeiro exemplo.
FAÇA VOCÊ MESMO
4.10 Fatias: Use um dos programas que escreveu neste capítulo, adicione diversas
linhas ao final do programa para executarem o seguinte:
• Exiba a mensagem: Os três primeiros elementos da lista são:. Em seguida, use uma
fatia para exibir os três primeiros elementos da lista desse programa.
• Exiba a mensagem: O três elementos que ficam no meio da lista são:. Depois, use
uma fatia para exibir os três elementos do meio da lista.
• Exiba a mensagem: Os três últimos elementos da lista são: Em seguida, utilize uma
fatia para exibir os três últimos elementos da lista.
4.11 Minhas pizzas, suas pizzas: Comece com o programa do Exercício 4.1 (página 90).
Faça uma cópia da lista de pizzas e a nomeie como friend_pizzas. Em seguida, siga as
etapas:
• Adicione uma pizza nova à lista original.
• Adicione uma pizza diferente à lista friend_pizzas.
• Prove que tem duas listas separadas. Exiba a mensagem: Minhas pizzas favoritas
são:. E, em seguida, use um loop for para exibir a primeira lista. Exiba a mensagem:
Minhas pizzas favoritas são:. E, em seguida, use um loop for para exibir a segunda
lista. Garanta que cada pizza nova seja armazenada na lista adequada.
4.12 Mais loops: Nesta seção, todas as versões de food.py evitaram o uso de loops for
para exibição, a fim de economizar espaço. Escolha uma versão de food.py e escreva
dois loops for para exibir cada lista de alimentos.
Tuplas
As listas funcionam perfeitamente para armazenar coleções de
elementos que podem mudar ao longo da vida de um programa. A
capacidade de modificar listas é indispensável quando estamos
trabalhando com uma lista de usuários em um site ou uma lista de
personagens em um jogo. No entanto, às vezes queremos criar uma
lista de elementos que não podem ser modificados. Com as tuplas
podemos fazer exatamente isso. O Python trata valores que não
podem mudar como imutáveis, e uma lista imutável se chama tupla.
Modified dimensions:
400
100
Quando comparadas às listas, as tuplas são estruturas de dados
simples. Use as tuplas quando quiser armazenar um conjunto de
valores que não devem ser alterados ao longo da vida de um
programa.
Guia de estilo
Quando alguém quer realizar uma mudança na linguagem Python, é
necessário recorrer a uma Python Enhancement Proposal (PEP),
Proposta de Aprimoramento do Python. Uma das PEPS mais antigas
é a PEP 8, que orienta os programadores sobre como estilizar seu
código. Apesar de ser relativamente extensa, boa parte da PEP 8 diz
respeito à estruturas de codificação mais complexas do que as que
vimos até agora.
O guia de estilo Python foi elaborado com base no entendimento de
que o código é lido com mais frequência do que escrito. Escrevemos
o código uma vez e começamos a lê-lo ao depurá-lo. Ao adicionar
uma funcionalidade a um programa, você passará mais tempo lendo
seu código. Outros programadores também leem seu código quando
você o compartilha.
Dada a escolha entre um código mais fácil de escrever ou um código
mais fácil de ler, os programadores Python quase sempre irão
incentivá-lo a escrever um código mais fácil de ler. As diretrizes a
seguir o ajudarão a escrever um código claro desde o início.
Indentação
A PEP 8 nos recomenda quatro espaços por nível de indentação. O
uso de quatro espaços aumenta a legibilidade, deixando espaço para
diversos níveis de indentação em cada linha.
Em um documento de processamento de texto, como um
documento word, as pessoas costumam usar tabulações em vez de
espaços para o recuo. Apesar de isso funcionar perfeitamente com
documentos de texto, o Python se confunde quando misturamos
tabulações com espaços. Todo editor de texto fornece configuração
que possibilita usar a tecla TAB, mas depois acaba convertendo cada
tabulação em um número definido de espaços. Sem sombra de
dúvidas, você deve usar a tecla TAB. No entanto, verifique a
configuração do seu editor para inserir espaços em vez de
tabulações em seu documento.
Misturar tabulações e espaços em seu arquivo pode ocasionar
problemas dificílimos de identificar. Caso ache que tem uma mistura
de tabulações e espaços, você pode converter as tabulações de um
arquivo em espaços com a maioria dos editores.
Comprimento da linha
Muitos programadores Python recomendam que cada linha deve ter
menos de 80 caracteres. Essa diretriz se deve ao fato de que,
antigamente, a maioria dos computadores comportava apenas
79 caracteres em uma única linha de terminal. Hoje em dia, é
possível inserir linhas mais extensas, porém há outras razões para se
adotar o comprimento de linha padrão de 79 caracteres.
Em geral, os programadores profissionais abrem diversos arquivos
na mesma tela, assim o comprimento de linha padrão possibilita com
que consigam enxergar todas as linhas de dois ou três arquivos
abertos lado a lado na mesma tela. A PEP 8 recomenda também
limitar todos os comentários a 72 caracteres por linha, já que
algumas das ferramentas que geram documentação automática para
projetos maiores adicionam caracteres de formatação no início de
cada linha comentada.
As diretrizes do PEP 8 para comprimento de linha não são imutáveis
como as tuplas, algumas equipes preferem um limite de
99 caracteres. Não se preocupe tanto com o comprimento das linhas
em seu código enquanto estiver aprendendo, mas esteja ciente de
que as pessoas que trabalham de forma colaborativa quase sempre
seguem as diretrizes da PEP 8. A maioria dos editores possibilita que
você configure um realce visual, normalmente uma linha vertical na
tela, que mostra onde se estabelecem esses limites.
NOTA O Apêndice B mostra como configurar seu editor de texto
para que sempre insira quatro espaços sempre que você
pressiona a tecla TAB, assim como uma linha-guia vertical
para ajudá-lo com o limite de 79 caracteres.
Linhas em branco
Para agrupar partes de seu programa visualmente, use linhas em
branco. Devemos usar linhas em branco para organizar arquivos,
mas não podemos usá-las em excesso. Seguindo os exemplos deste
livro, você deve encontrar o ponto adequado de equilíbrio. Por
exemplo, caso tenha cinco linhas de código que criam uma lista e
outras três que fazem alguma coisa com essa lista, é apropriado
inserir uma linha em branco entre essas duas seções. No entanto,
recomenda-se não inserir três ou quatro linhas em branco entre as
duas seções.
Embora não influenciem a execução do código, as linhas em branco
influenciam a legibilidade dele. O interpretador Python utiliza a
indentação horizontal para interpretar o significado do código, mas
desconsidera o espaçamento vertical.
Recapitulando
Neste capítulo, aprendemos como trabalhar eficientemente com os
elementos de uma lista. Aprendemos como percorrer uma lista com
um loop for, como o Python utiliza a indentação para estruturar um
programa e como evitar alguns erros comuns de indentação. Você
aprendeu a criar listas numéricas simples, bem como algumas
operações que podem ser efetuadas em listas numéricas. Aprendeu
também como fatiar uma lista para trabalhar com um subconjunto
de elementos e como usar uma fatia para copiarmos devidamente
uma lista. Estudamos também as tuplas, que fornecem grau de
proteção a um conjunto de valores que não devem ser alterados, e
como estilizar seu código cada vez mais complexo para facilitar a
leitura.
No Capítulo 5, aprendermos como responder adequadamente a
diferentes condições com a instrução if. Veremos como organizar
conjuntos relativamente complexos de testes condicionais para
responder adequada e precisamente ao tipo de situação ou
informação desejadas. Aprenderemos também a usar as instruções if
percorrendo uma lista para executar ações específicas com
elementos selecionados dessa mesma lista.
5
capítulo
Instruções if
Um simples exemplo
O exemplo a seguir mostra como os testes if possibilitam responder
devidamente à situações especiais. Imagine que você tem uma lista
de carros e quer exibir o nome de cada carro. Como os carros são
nomes próprios, a maioria dos nomes deve ser exibida com a
primeira letra em maiúsculo. Apesar disso, o valor 'bmw' deve ser
exibido em letras maiúsculas. O código a seguir percorre uma lista
de nomes de carros com um loop for e procura o valor 'bmw'. Sempre
que for 'bmw', o valor será exibido com todas as letras maiúsculas em
vez de ser exibido com a primeira letra maiúscula:
cars.py
cars = ['audi', 'bmw', 'subaru', 'toyota']
Testes condicionais
No cerne de cada instrução if reside uma expressão que pode ser
avaliada como True ou False, chamada de teste condicional. O Python
utiliza os valores True e False a fim de decidir se o código em uma
instrução if deve ser executado. Se um teste condicional for avaliado
como True, o Python executará o código após a instrução if. Se o
teste for avaliado como False, o Python desconsiderará o código após
a instrução if.
Verificando a igualdade
Boa parte dos testes condicionais compara o valor atual de uma
variável com um valor específico de interesse. O teste condicional
mais simples analisa se o valor de uma variável é igual ao valor de
interesse:
>>> car = 'bmw'
>>> car == 'bmw'
True
A primeira linha define o valor de car como 'bmw' recorrendo a um
único sinal de igual, como já vimos tantas vezes. A próxima linha
verifica se o valor de car é 'bmw', recorrendo a um sinal de igual duplo
(==). Esse operador de igualdade retorna True se os valores à
esquerda e à direita do operador forem correspondentes e False se
não forem. Neste exemplo, os valores correspondem, logo o Python
retorna True.
Se o valor de car fosse diferente de 'bmw', o teste retornaria False:
>>> car = 'audi'
>>> car == 'bmw'
False
Um único sinal de igual equivale a uma declaração. Podemos ler essa
primeira linha de código assim: “O valor atribuído a car é igual a
'audi'”. Em contrapartida, um duplo sinal de igual equivale a uma
pergunta: “O valor de car é igual a 'bmw'?” A maioria das linguagens
de programação utiliza sinais de igual dessa forma.
Verificando a diferença
Quando queremos determinar se dois valores não são iguais,
podemos usar o operador de diferença (!=). Vejamos outra instrução
if para examinar como usar o operador de diferença. Armazenaremos
um ingrediente do pedido de uma pizza em uma variável e
exibiremos uma mensagem, caso o pedido não contenha anchovas:
toppings.py
requested_topping = 'mushrooms'
if requested_topping != 'anchovies':
print("Hold the anchovies!")
Esse código compara o valor de request_topping com o valor 'anchovies'.
Se esses dois valores não corresponderem, o Python retornará True e
executará o código após a instrução if. Se os dois valores
corresponderem, o Python retornará False e não executará o código
após a instrução if.
Como o valor de request_topping não é 'anchovies', a função print() é
executada:
Hold the anchovies!
A maioria das expressões condicionais testará a igualdade, mas, às
vezes, você achará melhor testar a diferença.
Comparações numéricas
Testar valores numéricos é bastante simples. Por exemplo, o código
seguinte verifica se uma pessoa tem 18 anos:
>>> age = 18
>>> age == 18
True
Podemos testar também se dois números não são iguais. Por
exemplo, o código a seguir exibe uma mensagem se a resposta
fornecida não estiver correta:
magic_number.py
answer = 17
if answer != 42:
print("That is not the correct answer. Please try again!")
O teste condicional passa, já o valor de answer (17) não é igual a 42.
Como o teste foi aprovado, o bloco de código indentado é
executado:
That is not the correct answer. Please try again!
É
É possível também incluir diferentes comparações matemáticas sem
suas declarações condicionais, como menor que, menor ou igual a,
maior que e maior ou igual a:
>>> age = 19
>>> age < 21
True
>>> age <= 21
True
>>> age > 21
False
>>> age >= 21
False
Cada comparação matemática pode ser usada como parte de uma
instrução if, que pode ajudá-lo a detectar as condições exatas de
interesse.
Expressões booleanas
Em algum momento, à medida que aprende mais sobre
programação, você vai se deparar com o termo expressão booleana.
Expressão booleana é somente outra palavra para teste condicional.
Um valor booleano é True ou False, assim como o valor de uma
expressão condicional depois de avaliada.
Os valores booleanos costumam ser usados para acompanhar
determinadas condições, como se um jogo estiver sendo executado
ou se um usuário tem permissão para editar determinado conteúdo
em um site:
game_active = True
can_edit = False
Os valores booleanos fornecem um modo eficaz para rastrear o
estado de um programa ou uma condição específica e importante
em seu programa.
Instruções if
Assim que entender os testes condicionais, você será capaz de
escrever instruções if. Por mais que existam diversos tipos diferentes
de instruções if, escolher qual usar depende do número de condições
que precisamos testar. Já vimos e discutimos alguns exemplos de
instruções if em testes condicionais, mas, agora, vamos nos
aprofundar no assunto.
Instruções if simples
O tipo mais simples de instrução if tem um teste e uma ação:
if teste_conditional:
faça alguma coisa
É possível inserir qualquer teste condicional na primeira linha, e
basicamente qualquer ação no bloco indentado após o teste. Se o
teste condicional for avaliado como True, o Python executará o código
após a instrução if. Se o teste for avaliado como False, o Python
ignorará o código após a instrução if.
Digamos que temos uma variável representando a idade de uma
pessoa e queremos saber se essa pessoa tem idade para votar. O
código a seguir testa se a pessoa pode votar:
voting.py
age = 19
if age >= 18:
print("You are old enough to vote!")
O Python verifica se o valor de age é maior ou igual a 18. Como é
maior, o Python executa o print() indentado:
You are old enough to vote!
A indentação desempenha o mesmo papel nas instruções if e nos
loops for. Todas as linhas indentadas depois de uma instrução if serão
executadas se o teste for aprovado, e todo o bloco de linhas
indentadas será ignorado se o teste não for aprovado.
Podemos inserir quantas linhas de código quisermos no bloco
seguinte à instrução if. Vamos adicionar outra linha de saída se a
pessoa tiver idade suficiente para votar, perguntando se já se
registrou para votar1:
age = 19
if age >= 18:
print("You are old enough to vote!")
print("Have you registered to vote yet?")
O teste condicional passa e ambas instruções prints() são indentadas
e as linhas são exibidas:
You are old enough to vote!
Have you registered to vote yet?
Se o valor de age for menor que 18, o programa não gerará
nenhuma saída.
Instruções if-else
Queremos executar uma ação quando um teste condicional passa,
mas não é sempre que queremos executar a mesma ação. Muitas
vezes, em outros casos, queremos executar uma ação diferente.
Com a sintaxe if-else do Python isso é possível. Um bloco if-else se
parece com uma instrução if simples, exceto que a instrução else
permite definir uma ação ou um conjunto de ações que são
executadas quando o teste condicional falha.
Mostraremos a mesma mensagem exibida anteriormente se a
pessoa tiver idade suficiente para votar, mas, desta vez,
adicionaremos uma mensagem para quem não tiver idade para
votar:
age = 17
1 if age >= 18:
print("You are old enough to vote!")
print("Have you registered to vote yet?")
2 else:
print("Sorry, you are too young to vote.")
print("Please register to vote as soon as you turn 18!")
Caso o teste condicional 1 passe, o primeiro bloco de print() indentado
será executado. Caso o teste seja avaliado como False, o bloco else 2 é
executado. Como age é menor que 18 anos, desta vez, o teste
condicional falha e o código no bloco else é executado:
Sorry, you are too young to vote.
Please register to vote as soon as you turn 18!
Esse código executa porque temos somente duas situações possíveis
para avaliar: uma pessoa tem idade para votar ou não. A estrutura
if-else funciona bem em situações nas quais queremos que o Python
sempre execute uma das duas ações possíveis. Em uma sequência if-
else simples como essa, uma dessas duas ações sempre será
executada
Sequência if-elif-else
Precisaremos com frequência testar mais de duas situações possíveis
e, para avaliá-las, podemos usar a sintaxe if-elif-else do Python. O
Python executa somente um bloco em uma sequência if-elif-else. E
executa cada teste condicional consecutivamente, até que um passe.
Quando um teste passa, o código seguinte a esse teste é executado
e o Python desconsidera o restante dos testes.
Muitas situações reais envolvem mais de duas condições possíveis.
Por exemplo, imagine um parque de diversões que cobra preços
diferentes de entrada para diferentes faixas etárias:
• A entrada para menores de 4 anos é gratuita.
• A entrada para qualquer pessoa entre 4 e 18 anos custa US$25.
• A entrada para maiores de 18 anos custa U$S40.
Como podemos usar uma instrução if para estipular o preço da
entrada de uma pessoa? O código a seguir testa a faixa etária de
uma pessoa e, em seguida, exibe uma mensagem com preço de
entrada:
amusement_park.py
age = 12
1 if age < 4:
print("Your admission cost is $0.")
2 elif age < 18:
print("Your admission cost is $25.")
3 else:
print("Your admission cost is $40.")
O teste if 1 verifica se uma pessoa tem menos de 4 anos. Quando o
teste passa, uma mensagem adequada é exibida, e o Python
desconsidera o resto dos testes. A linha elif 2 é realmente outro teste
if, executado apenas se o teste anterior falhar. Nesse ponto da
sequência, sabemos que a pessoa tem, pelo menos, 4 anos porque o
primeiro teste falhou. Se a pessoa for menor de 18 anos, uma
mensagem adequada é exibida, e o Python desconsidera o bloco else.
Se ambos os testes if e elif falharem, o Python executa o código no
bloco else 3.
Neste exemplo, o teste if 1 é avaliado como False, então seu bloco de
código não é executado. No entanto, o teste elif é avaliado como True
(12 é menor que 18),então seu código é executado. A saída é uma
frase, informando ao usuário o preço da entrada:
Your admission cost is $25.
Qualquer idade acima 17 anos faria com que os dois primeiros testes
falhassem. Nessas situações, o bloco else seria executado e o preço
da entrada seria de US$40.
Em vez de exibir o preço de entrada dentro do bloco if-elif-else, seria
mais sucinto definir somente o preço dentro da sequência if-elif-else e,
em seguida, inserir um único print() que execute após a avaliação da
sequência.
age = 12
if age < 4:
price = 0
elif age < 18:
price = 25
else:
price = 40
if age < 4:
price = 0
elif age < 18:
price = 25
elif age < 65:
price = 40
else:
price = 20
if age < 4:
price = 0
elif age < 18:
price = 25
elif age < 65:
price = 40
elif age >= 65:
price = 20
if 'mushrooms' in requested_toppings:
print("Adding mushrooms.")
1 if 'pepperoni' in requested_toppings:
print("Adding pepperoni.")
if 'extra cheese' in requested_toppings:
print("Adding extra cheese.")
print("\nFinished making your pizza!")
Começamos com uma lista contendo os ingredientes do pedido. A
primeira instrução if verifica se a pessoa quer sua pizza com
mushrooms (cogumelos). Se sim, exibe-se uma mensagem
confirmando esse ingrediente. O teste para pepperoni 1 é outra
instrução simples if, não uma instrução elif ou else, assim esse teste é
executado independentemente de o teste anterior ter passado ou
não. A última instrução if verifica se o pedido tem extra cheese
(queijo extra), indiferente dos resultados dos dois primeiros testes.
Esses três testes independentes são efetuados sempre que esse
programa é executado.
Neste exemplo, como todas as condições são avaliadas, cogumelos e
queijo extra são adicionados à pizza:
Adding mushrooms.
Adding extra cheese.
if 'mushrooms' in requested_toppings:
print("Adding mushrooms.")
elif 'pepperoni' in requested_toppings:
print("Adding pepperoni.")
elif 'extra cheese' in requested_toppings:
print("Adding extra cheese.")
if requested_toppings:
for requested_topping in requested_toppings:
print(f"Adding {requested_topping}.")
print("\nFinished making your pizza!")
else:
print("Are you sure you want a plain pizza?")
Agora, começamos com uma lista vazia dos ingredientes solicitados.
Em vez de avançar direto para o loop for, primeiro fazemos uma
verificação rápida. Quando o nome de uma lista é utilizado em uma
instrução if, o Python retorna True se a lista contiver pelo menos um
elemento; uma lista vazia é avaliada como False. Se request_toppings
passar no teste condicional, executamos o mesmo loop for do
exemplo anterior. Se o teste condicional falhar, exibimos uma
mensagem perguntando ao cliente se realmente quer uma pizza
normal, sem ingredientes adicionais.
Como nesse caso a lista está vazia, a saída pergunta se o usuário
quer uma pizza normal.
Are you sure you want a plain pizza?
Se a lista não estiver vazia, a saída exibirá cada ingrediente
solicitado adicionado à pizza.
Dicionários
Um dicionário simples
Imagine um jogo de alienígenas com cores e valores de pontos
diferentes. O simples dicionário a seguir armazena informações
sobre um determinado alienígena:
alien.py
alien_0 = {'color': 'green', 'points': 5}
print(alien_0['color'])
print(alien_0['points'])
O dicionário alien_0 armazena a cor e o valor dos pontos do
alienígena. As duas últimas linhas acessam e exibem essas
informações, conforme podemos ver:
green
5
Como acontece com a maioria dos conceitos novos de programação,
utilizar dicionários requer prática. Após trabalhar um pouco com os
dicionários, você perceberá como podem efetivamente modelar
situações reais.
new_points = alien_0['points']
print(f"You just earned {new_points} points!")
Uma vez definido o dicionário, extraímos o valor associado à chave
'points' do dicionário. Esse valor é então atribuído à variável new_points.
A última linha exibe uma afirmação sobre quantos pontos o jogador
acabou de ganhar:
You just earned 5 points!
Caso execute esse código sempre que um alienígena for abatido,
poderá acessar o valor desses pontos.
Adicionando novos pares chave-valor
Como são estruturas dinâmicas, podemos adicionar novos pares
chave-valor a um dicionário a qualquer momento. Para adicionar um
novo par chave-valor, forneça o nome do dicionário seguido pela
nova chave entre colchetes, com o valor novo.
Vamos adicionar duas informações novas ao dicionário alien_0: as
coordenadas x e y do alienígena, que nos ajudarão a apresentá-lo
em determinada posição na tela. Vamos posicionar o alienígena na
borda esquerda da tela, 25 pixels abaixo da parte superior. Em geral,
como as coordenadas da tela começam no canto superior esquerdo,
posicionaremos o alienígena na borda esquerda da tela, definindo a
coordenada x em 0 e 25 pixels a partir da parte superior, e a
coordenada y com o valor 25 positivo, como a seguir:
alien.py
alien_0 = {'color': 'green', 'points': 5}
print(alien_0)
alien_0['x_position'] = 0
alien_0['y_position'] = 25
print(alien_0)
Começamos definindo o mesmo dicionário com o qual estamos
trabalhando. Depois, exibimos esse dicionário, apresentando um
snapshot de suas informações. Em seguida, adicionamos um novo
par chave-valor ao dicionário: a chave 'x_position' e o valor 0. Fazemos
o mesmo para a chave 'y_position'. Quando exibimos o dicionário
modificado, vemos os dois pares adicionais de chave-valor:
{'color': 'green', 'points': 5}
{'color': 'green', 'points': 5, 'x_position': 0, 'y_position': 25}
A versão final do dicionário contém quatro pares chave-valor. Os dois
pares originais especificam a cor e o valor do ponto, e os outros dois
especificam a posição do alienígena.
Os dicionários preservam a ordem em que foram definidos. Ao exibir
um dicionário ou percorrer seus elementos, você verá os elementos
na mesma ordem em que foram adicionados ao dicionário.
alien.py
alien_0 = {}
alien_0['color'] = 'green'
alien_0['points'] = 5
print(alien_0)
Primeiro, definimos um dicionário vazio chamado alien_0. Depois,
adicionamos valores para cor e pontos. O resultado é o dicionário
usado nos exemplos anteriores:
{'color': 'green', 'points': 5}
Normalmente, recorremos a dicionários vazios quando armazenamos
dados fornecidos pelo usuário em um dicionário ou quando
escrevemos código que gera automaticamente um número
significativo de pares chave-valor.
1 del alien_0['points']
print(alien_0)
A instrução del 1 informa ao Python para deletar a chave 'points' do
dicionário alien_0 e também remover o valor associado a essa chave.
A saída mostra que a chave 'points' e seu valor de 5 foram excluídos
do dicionário, mas o restante do dicionário permanece inalterado:
{'color': 'green', 'points': 5}
{'color': 'green'}
1 language = favorite_languages['sarah'].title()
print(f"Sarah's favorite language is {language}.")
Para conferir qual linguagem Sarah escolheu, basta acessar o valor
em:
favorite_languages['sarah']
Usamos essa sintaxe para extrair a linguagem favorita de Sarah do
dicionário 1 e atribuí-la à variável language. Aqui, como criamos uma
variável nova, o print() se torna mais limpo. A saída exibe a linguagem
favorita de Sarah:
Sarah's favorite language is C.
Você pode usar essa mesma sintaxe com qualquer pessoa
representada no dicionário.
Usando get() para acessar valores
Usar chaves entre colchetes para acessar o valor que estamos
interessados em um dicionário pode ocasionar um problema em
potencial: se a chave solicitada não existir, receberemos um erro.
Vejamos o que acontece quando tentamos acessar o valor de pontos
de um alienígena que não tem valor definido de pontos:
alien_no_points.py
alien_0 = {'color': 'green', 'speed': 'slow'}
print(alien_0['points'])
O Python retorna um traceback, mostrando um KeyError:
Traceback (most recent call last):
File "alien_no_points.py", line 2, in <module>
print(alien_0['points'])
~~~~~~~^^^^^^^^^^
KeyError: 'points'
No Capítulo 10, vamos aprender mais como lidar com esses tipos
gerais de erro. Para dicionários especificamente, podemos usar o
método get() a fim de definirmos um valor padrão que será retornado
se a chave solicitada não existir.
O método get() exige uma chave como primeiro argumento. Como
segundo argumento opcional, podemos passar o valor a ser
retornado caso a chave não exista:
alien_0 = {'color': 'green', 'speed': 'slow'}
Key: first
Value: enrico
Key: last
Value: fermi
Percorrer com um loop todos os pares chave-valor funciona bem,
ainda mais com dicionários como o favorite_languages.py, exemplo
da página 135, que armazena o mesmo tipo de informação para
muitas chaves diferentes. Se percorrermos com um loop o dicionário
favorite_languages, obteremos o nome de cada pessoa no dicionário e
sua linguagem de programação favorita. Como as chaves sempre
referenciam o nome de uma pessoa e o valor é sempre uma
linguagem, usaremos as variáveis name e language no loop em vez de
key e value, assim fica mais fácil acompanhar o que está acontecendo
dentro do loop:
favorite_languages.py
favorite_languages = {
'jen': 'python',
'sarah': 'c',
'edward': 'rust',
'phil': 'python',
}
1 if name in friends:
2 language = favorite_languages[name].title()
print(f"\t{name.title()}, I see you love {language}!")
Primeiro, criamos uma lista de amigos para exibir uma mensagem.
Dentro do loop, exibimos o nome de cada pessoa. Em seguida,
verificamos se o name com o qual estamos trabalhando consta na
lista friends 1. Se constar, determinamos a linguagem favorita da
pessoa usando o nome do dicionário e o valor atual de name como
chave 2. Depois, exibimos uma mensagem especial, incluindo uma
referência à linguagem escolhida por cada um. O nome de todos é
mostrado, mas nossos amigos recebem uma mensagem especial:
Hi Jen.
Hi Sarah.
Sarah, I see you love C!
Hi Edward.
Hi Phil.
Phil, I see you love Python!
Podemos também utilizar o método keys() para verificar se uma
determinada pessoa participou da pesquisa. Desta vez, vamos
verificar se Erin participou:
favorite_languages = {
-- trecho de código omitido --
}
Aninhamento
Não raro, queremos armazenar múltiplos dicionários em uma lista ou
uma lista de itens como um valor em um dicionário. Chamamos isso
de aninhamento. Podemos aninhar dicionários dentro de uma lista,
uma lista de itens dentro de um dicionário ou até mesmo um
dicionário dentro de outro dicionário. O aninhamento é um recurso
poderoso, como veremos nos próximos exemplos.
# Resume o pedido
1 print(f"You ordered a {pizza['crust']}-crust pizza "
"with the following toppings:")
Um dicionário em um dicionário
Podemos aninhar um dicionário dentro de outro dicionário, mas o
código pode se tornar complexo se fizermos isso. Por exemplo, se
tiver um site com diversos usuários, cada um com um nome de
usuário único, você poderá usar os nomes de usuário como chaves
em um dicionário. Você pode armazenar informações sobre cada
usuário usando um dicionário como valor associado ao nome de
usuário. Na lista a seguir, armazenamos três informações sobre cada
usuário: nome, sobrenome e localização. Acessaremos essas
informações percorrendo com um loop os nomes de usuário e o
dicionário de informações associado a cada nome de usuário:
many_users.py
users = {
'aeinstein': {
'first': 'albert',
'last': 'einstein',
'location': 'princeton',
},
'mcurie': {
'first': 'marie',
'last': 'curie',
'location': 'paris',
},
Username: mcurie
Full name: Marie Curie
Location: Paris
Veja que a estrutura do dicionário de cada usuário é idêntica. Ainda
que não seja exigência do Python, essa estrutura facilita trabalhar
com dicionários aninhados. Se o dicionário de cada usuário tivesse
chaves diferentes, o código dentro do loop for seria mais complexo.
Recapitulando
Neste capítulo, aprendemos como definir um dicionário e como
trabalhar com as informações armazenadas em um dicionário.
Estudamos como acessar e modificar elementos individuais em um
dicionário e como percorrer todas as informações em um dicionário
com um loop. Aprendemos a percorrer os pares chave-valor de um
dicionário, suas chaves e seus valores com um loop e também como
aninhar diversos dicionários em uma lista, aninhar listas em um
dicionário e aninhar um dicionário em outro dicionário.
No próximo capítulo, estudaremos os loops while e como aceitar
entradas de pessoas que estão usando seus programas. Será um
capítulo incrível, porque aprenderemos como tornar nossos
programas interativos: eles serão capazes de responder à entrada do
usuário.
7 capítulo
name = input(prompt)
print(f"\nHello, {name}!")
Esse exemplo nos mostra uma das formas de criar uma string
multilinha. A primeira linha atribui a primeira parte da mensagem à
variável prompt. Na segunda linha, o operador += pega a string
atribuída ao prompt e adiciona a string nova no final.
Agora, o prompt se estende por duas linhas, novamente com espaço
após o ponto de interrogação para maior clareza:
If you share your name, we can personalize the messages you see.
What is your first name? Eric
Hello, Eric!
Usando int() para receber entradas numéricas
Quando usamos a função input(), o Python interpreta tudo o que o
usuário insere como uma string. Vejamos a sessão do interpretador,
que pergunta a idade do usuário:
>>> age = input("How old are you? ")
How old are you? 21
>>> age
'21'
Embora o usuário digite o número 21, quando solicitamos o valor de
age, o Python retorna '21', a string que representa o valor numérico
digitado. Sabemos que o Python interpretou a entrada como uma
string, pois o número agora está entre aspas. Caso queira apenas
exibir a entrada, o código funciona bem. Mas caso tente usar a
entrada como um número, receberá um erro:
>>> age = input("How old are you? ")
How old are you? 21
1 >>> age >= 18
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
2 TypeError: '>=' not supported between instances of 'str' and 'int'
Como tentamos utilizar a entrada para fazer uma comparação
numérica 1, o Python gera um erro porque não consegue comparar
uma string com um inteiro: a string '21' atribuída à age não pode ser
comparada com o valor numérico 18 2.
É possível resolver esse problema usando a função int(), que converte
a string de entrada em um valor numérico. Isso possibilita que a
comparação seja executada com sucesso:
>>> age = input("How old are you? ")
How old are you? 21
1 >>> age = int(age)
>>> age >= 18
True
Neste exemplo, quando inserimos 21 no prompt, o Python interpreta
o número como uma string, mas o valor é convertido em uma
representação numérica por int() 1. Desse modo, o Python consegue
executar o teste condicional: comparar age (que agora representa o
valor numérico 21) e 18 a fim de verificar se age é maior ou igual
a 18. O teste é avaliado como True.
Como usamos a função int () em um programa propriamente dito?
Imagine um programa que verificar se as pessoas têm altura
suficiente para andar em uma montanha-russa:
rollercoaster.py
height = input("How tall are you, in inches? ")
height = int(height)
Operador módulo
Um mecanismo que ajuda muito a trabalhar com informações
numéricas é o operador módulo (%), que divide um número por
outro número e retorna o resto:
>>> 4 % 3
1
>>> 5 % 3
2
>>> 6 % 3
0
>>> 7 % 3
1
O operador módulo não informa o resultado da divisão entre dois
valores e sim o resto da divisão entre dois valores.
Quando um número é divisível por outro número, o resto dessa
divisão é 0. Logo, o operador módulo sempre retorna 0. Podemos
usar esse operador para determinar se um número é par ou ímpar:
even_or_odd.py
number = input("Enter a number, and I'll tell you if it's even or odd: ")
number = int(number)
if number % 2 == 0:
print(f"\nThe number {number} is even.")
else:
print(f"\nThe number {number} is odd.")
Como números pares são sempre divisíveis por dois, se o módulo de
um número divido por dois for zero (aqui, if number % 2 == 0) o número
é par. Caso contrário, é ímpar.
Enter a number, and I'll tell you if it's even or odd: 42
message = ""
while message != 'quit':
message = input(prompt)
print(message)
Primeiro, definimos um prompt que informa ao usuário duas opções:
inserir uma mensagem ou inserir o valor de saída (nesse caso, 'quit').
Depois, definimos uma variável message para acompanhar qualquer
valor inserido pelo usuário. Definimos message como uma string vazia,
"". Desse modo, o Python consegue realizar uma verificação assim
que iterar a linha while. Na primeira vez que o programa é executado
e o Python itera a instrução while, é necessário comparar o valor de
message com 'quit', ainda que o usuário não tenha fornecido nenhuma
entrada. Caso não tenha nada para efetuar a comparação, o Python
não será capaz de continuar executando o programa. Para solucionar
esse problema, fizemos questão de atribuir um valor inicial à message.
Ainda que seja uma string vazia, o Python consegue entendê-la,
possibilitando efetuar a comparação que faz o loop while ser
executado. Esse loop while é executado enquanto o valor de message
não for 'quit'.
Na primeira passagem pelo loop, message é apenas uma string vazia,
então o Python entra no loop. Em message = input(prompt), o Python
exibe o prompt e espera o usuário fornecer uma entrada. Seja lá o
que for fornecido pelo usuário, essa entrada é atribuída à message e
exibida; depois, o Python reavalia a condição na instrução while.
Contanto que o usuário não tenha digitado a palavra 'quit', o prompt
é exibido mais uma vez e o Python espera por mais entradas.
Quando o usuário finalmente digita 'quit', o Python interrompe a
execução do loop while e o programa termina:
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello everyone!
Hello everyone!
message = ""
while message != 'quit':
message = input(prompt)
if message != 'quit':
print(message)
Agora, o programa executa uma breve verificação, antes de
apresentar a mensagem e apenas a exibe se não corresponder ao
valor de quit:
Tell me something, and I will repeat it back to you:
Enter 'quit' to end the program. Hello everyone!
Hello everyone!
active = True
1 while active:
message = input(prompt)
if message == 'quit':
active = False
else:
print(message)
Definimos a variável active como True para que o programa inicie com
o status ativo. Isso simplifica ainda mais a instrução while porque
nenhuma comparação é feita na instrução; a lógica é calculada em
outras partes do programa. Desde que a variável active permaneça
como True, o loop continuará executando 1.
Na instrução if dentro do loop while, verificamos o valor de message
assim que o usuário insere sua entrada. Se o usuário digitar 'quit',
definimos active como False e o loop while para de executar. Se o
usuário inserir algo diferente de 'quit', exibimos sua entrada como
uma mensagem.
Esse programa tem a mesma saída do exemplo anterior, em que
inserimos o teste condicional diretamente na instrução while. No
entanto, agora que temos uma flag para sinalizar se o programa
geral está com status ativo, seria fácil adicionar mais testes (como
instruções elif) para eventos que devem definir active como False. Isso
é de grande ajuda em programas complicados como jogos, nos
quais pode haver muitos eventos que devem interromper a execução
do programa. Sempre que qualquer um desses eventos definir a flag
active como False, o loop principal do jogo será encerrado, uma
mensagem Game Over poderá ser exibida e o jogador pode ter a
opção de jogar mais uma vez.
cities.py
prompt = "\nPlease enter the name of a city you have visited:"
prompt += "\n(Enter 'quit' when you are finished.) "
1 while True:
city = input(prompt)
if city == 'quit':
break
else:
print(f"I'd love to go to {city.title()}!")
Um loop que começa com while True 1 será executado
incessantemente, a menos que seja interrompido por uma instrução
break. Nesse programa, o loop continua solicitando ao usuário que
insira o nome das cidades em que esteve até que seja fornecido 'quit'.
Quando 'quit' é inserido, a instrução break é executada, fazendo com
que o Python saia do loop:
Please enter the name of a city you have visited:
(Enter 'quit' when you are finished.) New York
I'd love to go to New York!
print(current_number)
Primeiro, definimos current_number como 0. Por ser menor que 10, o
Python entra no loop while. Uma vez dentro do loop, incrementamos
o contador em 1 1, assim o valor de current_number é 1. Em seguida, a
instrução if verifica o módulo de current_number e 2. Se o módulo for 0
(significando que current_number é divisível por 2), a instrução continue
informa ao Python para ignorar o restante do loop e retornar ao
início. Se o número atual não for divisível por 2, o restante do loop
será executado e o Python exibirá o número atual:
1
3
5
7
9
print(pets)
Começamos com uma lista com diversas instâncias de 'cat'. Depois de
exibir a lista, o Python entra no loop while porque encontra o valor 'cat'
na lista pelo menos uma vez. Uma vez dentro do loop, o Python
remove a primeira instância de 'cat', retorna à linha while e entra
novamente no loop quando identifica que 'cat' ainda está na lista. O
Python remove cada instância de 'cat' até que o valor não esteja mais
na lista, ponto em que o sai do loop e exibe a lista mais uma vez:
['dog', 'cat', 'dog', 'goldfish', 'cat', 'rabbit', 'cat']
['dog', 'dog', 'goldfish', 'rabbit']
while polling_active:
# Solicita o nome e a resposta do participante
1 name = input("\nWhat is your name? ")
response = input("Which mountain would you like to climb someday? ")
Funções
greet_user()
Esse exemplo apresenta a estrutura mais simples de uma função. A
primeira linha utiliza a palavra reservada def para informar ao Python
que estamos definindo uma função. Trata-se da definição da função,
que informa ao Python o nome da função e, se for o caso, que tipo
de informação a função precisa para realizar sua tarefa. Os
parênteses armazenam essas informações. Nesse caso, o nome da
função é greet_user(), e como não precisa de informações para realizar
sua tarefa, seus parênteses estão vazios. (Mesmo assim, os
parênteses são necessários.) Por último, a definição termina em
dois-pontos.
Quaisquer linhas indentadas depois de def greet_user() constituem o
corpo da função. Na segunda linha, o texto é um comentário
chamado docstring, que explica o que a função faz. Ao gerar
documentação para as funções em seus programas, o Python
procura uma string logo após a definição da função. Em geral, essas
strings são inseridas entre aspas triplas, possibilitando escrever
múltiplas linhas.
A linha print("Hello!") é a única linha de código concreto no corpo dessa
função. Logo, greet_user() tem somente uma tarefa: print("Hello!").
E para usarmos essa função, devemos chamá-la. Uma chamada de
função informa ao Python para executar o código na função. Para
chamar uma função, escreva o nome da função, seguido por
qualquer informação necessária entre parênteses. Aqui, como
nenhuma informação é necessária, chamar nossa função é tão
simples quanto inserir greet_user(). Como esperado, a função exibe
Hello!:
Hello!
greet_user('jesse')
A inserção de greet_user('jesse') chama greet_user() e fornece à função as
informações necessárias para executar o print(). A função aceita o
nome passado, exibindo o cumprimento para esse nome:
Hello, Jesse!
Da mesma forma, inserir greet_user('sarah') chama greet_user(), passa
'sarah' e exibe Hello, Sarah! Podemos chamar greet_user() e lhe passar
qualquer nome quantas vezes quisermos para que a função gere a
saída previsível todas as vezes.
Argumentos e parâmetros
Na função anterior greet_user(), definimos greet_user() a fim de exigir um
valor para a variável username. Assim que chamamos a função e
fornecemos as informações (o nome de uma pessoa), o
cumprimento adequado foi exibido.
A variável username na definição de greet_user() é um exemplo de
parâmetro, uma informação que a função precisa para realizar sua
tarefa. O valor 'jesse' em greet_user('jesse') é um exemplo de argumento.
Um argumento é uma informação passada de uma chamada de
função para uma função. Ao chamarmos uma função, inserimos
entre parênteses o valor com o qual queremos que a função
trabalhe. Nesse caso, o argumento 'jesse' foi passado para a função
greet_user(), e o valor foi atribuído ao parâmetro username.
NOTA As pessoas às vezes falam de argumentos e parâmetros de
forma intercambiável. Não se surpreenda se vir as variáveis
de uma definição de função serem chamadas de argumentos
ou as variáveis em uma chamada de função serem chamadas
de parâmetros.
FAÇA VOCÊ MESMO
8.1 Mensagem: Escreva uma função chamada display_message() que exiba uma frase
contando a todo mundo o que você está aprendendo neste capítulo. Chame a função e
verifique se a mensagem é adequadamente exibida.
8.2 Livro favorito: Escreva uma função chamada favorite_book() que aceite um
parâmetro, title. A função deve exibir uma mensagem como: Um dos meus livros
favoritos é Alice no País das Maravilhas. Chame a função e lembre-se de incluir o título
de um livro como argumento na chamada da função.
Passando argumentos
Dado que uma definição de função pode ter diversos parâmetros,
uma chamada de função pode precisar de vários argumentos. É
possível passar argumentos para as funções de inúmeras formas.
Pode-se utilizar argumentos posicionais, que precisam estar na
mesma ordem em que os parâmetros foram escritos. Podemos usar
argumentos nomeados, em que cada argumento é composto de um
nome de variável e de um valor, e listas e dicionários de valores.
Examinaremos cada um deles.
Argumentos posicionais
Ao chamarmos uma função, o Python verifica a correspondência de
cada argumento na chamada da função com um parâmetro na
definição da função. A forma mais simples de fazer isso é tomar
como base a ordem dos argumentos fornecidos. Os valores desse
tipo de correspondência são chamados de argumentos posicionais.
Vejamos como isso funciona. Imagine uma função que exibe
informações sobre animais de estimação. A função nos informa o
tipo de cada animal e o nome, conforme mostrado aqui:
pets.py
1 def describe_pet(animal_type, pet_name):
"""Exibe as informações sobre um animal de estimação"""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
2 describe_pet('hamster', 'harry')
A definição revela que essa função precisa de um tipo de animal e
seu nome 1. Ao chamarmos describe_pet(), precisamos fornecer um tipo
de animal e um nome, nessa mesma ordem. Por exemplo, na
chamada de função, o argumento 'hamster' é atribuído ao parâmetro
animal_type e o argumento 'harry' é atribuído ao parâmetro pet_name 2.
No corpo da função, esses dois parâmetros são utilizados para exibir
informações sobre o animal de estimação que está sendo
apresentado.
A saída exibe um hamster chamado Harry:
I have a hamster.
My hamster's name is Harry.
describe_pet('hamster', 'harry')
describe_pet('dog', 'willie')
Nessa segunda chamada de função, passamos a describe_pet() os
argumentos 'dog' e 'willie'. Como no conjunto anterior de argumentos
que usamos, o Python verifica a correspondência de 'dog' com o
parâmetro animal_type e 'willie' com o parâmetro pet_name. Como
anteriormente, a função realiza sua tarefa, mas, desta vez, exibe
valores para um cachorro chamado Willie. Agora, temos um hamster
chamado Harry e um cachorro chamado Willie:
I have a hamster.
My hamster's name is Harry.
I have a dog.
My dog's name is Willie.
Chamar uma função diversas vezes é uma forma ultraeficiente de
trabalhar. Basta escrever uma vez na função o código que
representa um animal de estimação. Desse modo, sempre que
quiser representar um novo animal de estimação, chame a função
com as informações do novo animal de estimação. Mesmo que o
código que descreve um animal de estimação fosse incrementado
em 10 linhas, ainda poderíamos representar um novo animal de
estimação com apenas uma linha chamando a função novamente.
describe_pet('harry', 'hamster')
Nesta chamada de função, enumeramos primeiro o nome e depois o
tipo de animal. Dessa vez, como o argumento 'harry' é enumerado
primeiro, esse valor é atribuído ao parâmetro animal_type. Da mesma
maneira, 'hamster' é atribuído à pet_name. Agora, temos um “harry”
chamado “Hamster”:
I have a harry.
My harry's name is Hamster.
No caso de obter resultados estranhos como esse, confira se a
ordem dos argumentos em sua chamada de função corresponde à
ordem dos parâmetros na definição da função.
Argumentos nomeados
Um argumento nomeado é um par nome-valor que passamos para
uma função. Como associamos diretamente o nome e o valor dentro
do argumento, quando passamos o argumento para a função, não
há confusão (não teremos um harry chamado Hamster). Graças aos
argumentos nomeados, não precisamos nos preocupar em ordenar
corretamente os argumentos na chamada de função, já que o papel
de cada valor na chamada de função fica explícito.
Vamos reescrever pets.py com argumentos nomeados para chamar
describe_pet():
def describe_pet(animal_type, pet_name):
"""Exibe as informações sobre um animal de estimação"""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(animal_type='hamster', pet_name='harry')
A função describe_pet() não mudou. Mas quando chamamos a função,
informamos explicitamente ao Python a correspondência entre cada
parâmetro e cada argumento. Ao ler a chamada de função, o Python
já sabe atribuir o argumento 'hamster' ao parâmetro animal_type e o
argumento 'harry' a pet_name. A saída mostra corretamente que temos
um hamster chamado Harry.
A ordem dos argumentos nomeados não é importante porque o
Python sabe onde cada valor deve ir. As duas chamadas de função
correspondem à:
describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')
Valores default
Ao escrevermos uma função, podemos definir um valor default para
cada parâmetro. Se um argumento para um parâmetro for fornecido
na chamada da função, o Python usará o valor do argumento. Caso
contrário, utilizará o valor default do parâmetro. Portanto, ao
definirmos um valor default para um parâmetro, podemos descartar
o argumento correspondente que normalmente escreveríamos na
chamada da função. O uso de valores default pode simplificar suas
chamadas de função e elucidar como suas funções são normalmente
usadas.
Por exemplo, se constatar que a maioria das chamadas para
describe_pet() estão sendo usadas para representar cachorros, você
pode definir o valor default de animal_type como 'dog'. Agora, qualquer
um que chamar describe_pet() para um cachorro pode omitir essa
informação:
def describe_pet(pet_name, animal_type='dog'):
"""Exibe informação sobre o animal de estimação"""
print(f"\nI have a {animal_type}.")
print(f"My {animal_type}'s name is {pet_name.title()}.")
describe_pet(pet_name='willie')
Mudamos a definição de describe_pet() para incluir um valor default,
'dog', para animal_type. Agora, quando a função é chamada sem
animal_type especificado, o Python sabe usar o valor 'dog' para esse
parâmetro:
I have a dog.
My dog's name is Willie.
Repare que a ordem dos parâmetros na definição da função teve
que ser alterada. Como o valor default torna desnecessário
especificar uma animal como argumento, o único argumento
restante na chamada da função é o nome do animal de estimação. O
Python ainda interpreta o valor como um argumento posicional. Ou
seja, se a função for chamada apenas com o nome de um animal de
estimação, esse argumento fará correspondência com primeiro
parâmetro enumerado na definição da função. Por isso, é necessário
que o primeiro parâmetro seja pet_name.
Agora, a forma mais simples de usar essa função é fornecer apenas
o nome de um cachorro na chamada da função:
describe_pet('willie')
Essa chamada de função teria a mesma saída do exemplo anterior.
Como o único argumento fornecido é 'willie', a correspondência é feita
com o primeiro parâmetro na definição, pet_name. E como nenhum
argumento é fornecido a animal_type, o Python usa o valor default 'dog'.
Para representar um animal que não seja um cachorro, podemos
usar uma chamada de função como esta:
describe_pet(pet_name='harry', animal_type='hamster')
Como um argumento explícito para animal_type é fornecido, o Python
ignorará o valor default do parâmetro.
describe_pet()
O Python reconhece que algumas informações estão faltando na
chamada da função e o traceback nos informa que:
Traceback (most recent call last):
1 File "pets.py", line 6, in <module>
2 describe_pet()
^^^^^^^^^^^^^^
3 TypeError: describe_pet() missing 2 required positional arguments:
'animal_type' and 'pet_name'
Primeiro, o traceback indica a localização do problema 1,
possibilitando-nos reaver e verificar o que deu algo errado com
nossa chamada de função. Em seguida, a chamada de função
responsável pelo erro é exibida 2. Por último, o traceback informa
que faltam dois argumentos na chamada e apresenta o nome dos
argumentos ausentes 3. Caso essa função estivesse em um arquivo
separado, poderíamos possivelmente reescrever a chamada de
maneira adequada, sem precisar abrir esse arquivo e ler o código da
função.
O Python é tão bom que lê o código da função para nós e nos
informa os nomes dos argumentos que precisamos fornecer. Ou
seja, mais um incentivo para que você nomeie suas variáveis e
funções com nomes descritivos. Caso adote essa prática, as
mensagens de erro do Python podem não apenas ajudá-lo como
também ajudar outra pessoa que use seu código.
Caso forneça um número excessivo de argumentos, você receberá
um traceback parecido que pode ajudá-lo a fazer a correspondência
adequada entre a chamada de função e a definição de função.
Valores de retorno
Nem sempre uma função precisa exibir sua saída diretamente. Em
vez disso, pode processar alguns dados e retornar um valor ou
conjunto de valores. O valor que a função retorna se chama valor de
retorno. A instrução return recebe um valor de dentro de uma função
e o reenvia para a linha que chamou a função. Os valores de retorno
possibilitam transferir grande parte do trabalho repetitivo às funções,
simplificando assim o corpo do seu programa.
Retornando um dicionário
Uma função pode retornar qualquer tipo de valor que quisermos,
incluindo estruturas de dados mais complicadas, como listas e
dicionários. Por exemplo, a função a seguir recebe partes de um
nome e retorna um dicionário que representa uma pessoa:
person.py
def build_person(first_name, last_name):
"""Retorna um dicionário de informações sobre uma pessoa"""
1 person = {'first': first_name, 'last': last_name}
2 return person
while True:
print("\nPlease tell me your name:")
print("(enter 'q' at any time to quit)")
2 def show_completed_models(completed_models):
""" Exibe todos os modelos impressos"""
print("\nThe following models have been printed:")
for completed_model in completed_models:
print(completed_model)
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
Definimos a função print_models() com dois parâmetros: uma lista de
designs que precisam ser impressos e uma lista de modelos
concluídos 1. Dadas essas duas listas, a função simula a impressão
de cada design, deixando a lista de designs não impressos vazia e
preenchendo a lista de modelos concluídos. Em seguida, definimos a
função show_completed_models() com um parâmetro: a lista de modelos
concluídos 2. Com essa lista, o show_completed_models() exibe o nome de
cada modelo impresso.
Apesar de esse programa ter a mesma saída que a versão sem
funções, o código fica mais estruturado. O código que executa a
maior parte da tarefa foi passado para duas funções separadas,
facilitando o entendimento da parte principal do programa. Observe
o corpo do programa e veja como é mais fácil entender o que está
acontecendo:
unprinted_designs = ['phone case', 'robot pendant', 'dodecahedron']
completed_models = []
print_models(unprinted_designs, completed_models)
show_completed_models(completed_models)
Definimos uma lista de designs não impressos e uma lista vazia que
armazenará os modelos concluídos. Depois, como já definimos
nossas duas funções, tudo o que temos a fazer é chamá-las e passar
os argumentos adequados às duas. Chamamos print_models() e
passamos as duas listas necessárias a essa função; como esperado,
print_models() simula a impressão dos designs. Em seguida, chamamos
show_completed_models() e passamos a lista de modelos concluídos para
que a função possa informar os modelos impressos. Os nomes
descritivos das funções facilitam com que outras pessoas leiam e
compreendem esse código, mesmo sem comentários.
É mais fácil incrementar e manter esse programa do que a versão
sem funções. No caso de precisarmos imprimir mais designs
posteriormente, basta chamar print_models() de novo. Se percebermos
que é necessário modificar o código de impressão, podemos alterá-
lo apenas uma vez, e nossas alterações serão reproduzidas em todos
os trechos em que a função for chamada. Trata-se de uma técnica
mais eficiente do que ter que atualizar o código separadamente em
todo o programa.
Nesse exemplo, observamos também a ideia de que cada função
deve ter uma tarefa específica. A primeira função imprime cada
design e a segunda exibe os modelos concluídos. Isso é mais prático
do que usar uma função para executar ambas as tarefas. Se estiver
escrevendo uma função e perceber que a função está executando
muitas tarefas diferentes, tente dividir o código em duas funções.
Lembre-se de que sempre podemos chamar uma função a partir de
outra função, e isso é vantajoso quando dividimos uma tarefa
complexa em uma série de etapas.
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
O asterisco no nome do parâmetro *toppings informa ao Python para
criar uma tupla chamada toppings, contendo todos os valores que essa
função recebe. O print() no corpo da função gera a saída mostrando
que o Python pode lidar com uma chamada de função de um valor e
com uma chamada de três valores. Chamadas diferentes são
tratadas de forma parecida. Observe que o Python empacota os
argumentos em uma tupla, mesmo que a função receba somente
um valor:
('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')
Agora, podemos substituir a chamada print() por um loop que
percorre a lista de ingredientes e apresenta a pizza que está sendo
pedida:
def make_pizza(*toppings):
"""Sintetiza a pizza que estamos prestes a fazer"""
print("\nMaking a pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')
A função responde adequadamente, quer receba um valor ou três
valores:
Making a pizza with the following toppings:
- pepperoni
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Na definição da função, o Python atribui o primeiro valor que recebe
ao parâmetro size. Todos os outros valores que vêm depois são
armazenados na tupla toppings. Primeiro, as chamadas de função
incluem um argumento para o tamanho, seguido de quantos
ingredientes forem necessários.
Agora, cada pizza tem um tamanho e uma quantidade de
ingredientes, e cada informação é exibida no local adequado,
mostrando o tamanho primeiro e os ingredientes depois:
Making a 16-inch pizza with the following toppings:
- pepperoni
pizza.py
def make_pizza(size, *toppings):
"""Sintetiza a pizza que estamos prestes a fazer"""
print(f"\nMaking a {size}-inch pizza with the following toppings:")
for topping in toppings:
print(f"- {topping}")
Agora, vamos criar um arquivo separado chamado making_pizzas.py
no mesmo diretório que pizza.py. Esse arquivo importa o módulo
que acabamos de criar e, em seguida, chama duas vezes make_pizza():
making_pizzas.py
import pizza
1 pizza.make_pizza(16, 'pepperoni')
pizza.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Quando lê esse arquivo, a linha import pizza informa ao Python para
abrir o arquivo pizza.py e copiar todas as funções dele para esse
programa. Na verdade, não vemos o código sendo copiado entre
arquivos, pois o Python copia o código nos bastidores, pouco antes
da execução do programa. Tudo o que precisamos saber é que
qualquer função definida no pizza.py agora estará disponível em
making_pizzas.py.
Para chamar uma função de um módulo importado, digite o nome
do módulo, pizza, seguido pelo nome da função, make_pizza(),
separados por um ponto 1. O código a seguir gera a mesma saída
que o programa original, que não importou um módulo:
Making a 16-inch pizza with the following toppings:
- pepperoni
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Com essa sintaxe, não é necessário usar a notação de ponto ao
chamar uma função. Já que importamos explicitamente a função
make_pizza() na instrução import, podemos chamá-la pelo nome quando
usamos a função.
mp(16, 'pepperoni')
mp(12, 'mushrooms', 'green peppers', 'extra cheese')
Nesse programa, a instrução import mostrada aqui renomeia a função
make_pizza() para mp(). Sempre que quisermos chamar make_pizza(),
basta escrever mp(), e o Python executará o código em make_pizza(),
evitando qualquer confusão com outra função make_pizza() que
possamos ter escrito no arquivo de programa.
Vejamos a sintaxe geral para fornecer um alias:
from nome_módulo import nome_função as nf
p.make_pizza(16, 'pepperoni')
p.make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
O módulo pizza recebe o alias p na instrução import, mas todas as
funções do módulo mantêm seus nomes originais. Chamar as
funções escrevendo p.make_pizza() não é apenas mais conciso do que
pizza.make_pizza(), como também redireciona sua atenção do nome do
módulo e possibilita que você foque os nomes descritivos de suas
funções. Esses nomes de função, que informam claramente o que
cada função faz, são mais importantes para a legibilidade do código
do que utilizar o nome completo do módulo.
Vejamos a sintaxe geral dessa abordagem:
import nome_módulo as nm
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')
Na instrução import, o asterisco solicita ao Python que copie todas as
funções do módulo pizza para esse arquivo de programa. Como cada
função é importada, podemos chamar cada função pelo nome sem
usar a notação de ponto. No entanto, é melhor não usar essa
abordagem quando estivermos trabalhando com módulos maiores
que não escrevemos: se o módulo tiver um nome de função que
corresponda a um nome existente de um projeto, poderemos obter
resultados inesperados. Pode ser que o Python veja diversas funções
ou variáveis com o mesmo nome e, em vez de importar todas as
funções separadamente, sobrescreverá as funções.
A melhor abordagem é importar a função ou funções desejadas, ou
importar todo o módulo e usar a notação de ponto. Isso resulta em
um código limpo, fácil de ler e entender. Incluí esta seção para que
você reconheça instruções import quando as vir no código de outras
pessoas:
from nome_módulo import *
Estilizando funções
É necessário que você se lembre de alguns detalhes quando estiver
estilizando funções. Funções devem ter nomes descritivos, e esses
nomes devem usar letras minúsculas e underscores. Os nomes
descritivos nos ajudam a entender o que o código está tentando
fazer. O nome dos módulos também devem seguir essas
convenções.
Cada função deve ter um comentário que explique de forma concisa
o que faz. Esse comentário deve aparecer logo após a definição da
função e deve usar o formato docstring. Quando uma função está
bem documentada, outros programadores podem usá-la lendo
apenas a descrição na docstring. Os programadores também devem
ser capazes de confiar que o código funciona conforme descrito e,
desde que saibam o nome da função, os argumentos necessários e o
tipo de valor retornado, eles devem conseguir usá-la em seus
programas.
Caso especifique um valor default para um parâmetro, nenhum
espaço deverá ser usado em ambos os lados do sinal de igual:
def nome_função(parâmetro_0, parâmetro_1='valor_default')
A mesma convenção deve ser utilizada para argumentos nomeados
em chamadas de função:
fnome_função(valor_0, parâmetro_1='valor')
A PEP 8 (https://www.python.org/dev/peps/pep-0008 ) recomenda um limite de
linhas de código com 79 caracteres para que cada linha fique visível
em uma janela do editor de tamanho razoável. Se um conjunto de
parâmetros fizer com que a definição de uma função tenha mais de
79 caracteres, pressione ENTER após o parêntese de abertura na
linha de definição. Na próxima linha, pressione a tecla TAB duas
vezes para separar a lista de argumentos do corpo da função, que só
será indentada um nível.
A maioria dos editores alinha automaticamente quaisquer linhas
adicionais de argumentos para corresponder à indentação
estabelecida na primeira linha:
def nome_função(
parâmetro_0, parâmetro_1, parâmetro_2,
parâmetro_3, parâmetro_4, parâmetro_5):
corpo da função...
Se o programa ou módulo tiver mais de uma função, podemos
separar cada uma por duas linhas em branco para facilitar a
visualização de onde uma função termina e a próxima começa.
Todas as instruções import devem ser escritas no início de um arquivo.
A única exceção é quando escrevemos comentários no início do
arquivo para descrever o programa como um todo.
Recapitulando
Neste capítulo, aprendemos a escrever funções e a passar
argumentos, assim as funções têm acesso às informações de que
precisam para realizar suas tarefas. Estudamos como usar
argumentos posicionais e nomeados e também a aceitar um número
arbitrário de argumentos. Vimos funções que exibem saída e que
retornam valores, e como usar funções com listas, dicionários,
instruções if e loops while. Aprendemos também como armazenar
funções em arquivos separados chamados módulos, com o intuito de
que os arquivos de programa sejam mais simples e fáceis de
entender. Por último, vimos como estilizar funções para que os
programas fiquem mais estruturados, facilitando a legibilidade de
todas as pessoas.
Um dos objetivos como programador é desenvolver um código
simples que execute o que queremos. As funções nos ajudam com
isso, já que possibilitam escrever blocos de códigos que funcionam
para usá-los quando precisarmos. Quando sabemos que uma função
executa adequadamente uma tarefa, podemos confiar que
continuará funcionando e podemos passar para a próxima tarefa de
programação.
Com as funções, temos a possibilidade de escrever o código uma vez
e, depois, reutilizá-los quantas vezes bem entendermos. Se
precisarmos executar o código em uma função, basta escrevermos
uma chamada de linha e a função executa sua tarefa. Se
precisarmos modificar o comportamento de uma função, basta
modificar um bloco de código, e essa mudança passa a valer em
todas as partes que chamamos essa função.
Funções facilitam a legibilidade dos programas, ao passo que bons
nomes de função sintetizam o que cada parte de um programa faz.
Para termos uma noção mais rápida do funcionamento de um
programa, é melhor lermos uma série de chamadas de função do
que uma série extensa de blocos de código.
Funções também facilitam o teste e a depuração do código. Quando
você atribui a maior parte das tarefas do programa a um conjunto
de funções, em que cada uma delas executa uma tarefa específica,
fica mais fácil de testar e manter o código escrito. Ou seja, podemos
escrever um programa separado para chamar e testar cada função
em todas as situações que encontrarmos, pois teremos certeza de
que executarão corretamente sempre que as chamarmos.
No Capítulo 9, vamos aprender a escrever classes. As classes
combinam funções e dados em um pacote elegante que pode ser
usado de formas flexíveis e eficientes.
capítulo 9
Classes
4 def sit(self):
"""Simula um cachorro sentado em resposta a um comando"""
print(f"{self.name} is now sitting.")
def roll_over(self):
"""Simula um cachorro rolando em resposta a um comando"""
print(f"{self.name} rolled over!")
Podemos observar muitos detalhes aqui, mas não se preocupe. No
decorrer deste livro, você verá essa estrutura e terá muito tempo
para se acostumar. De início, definimos uma classe chamada Dog 1.
Por convenção, nomes com a primeira letra maiúscula se referem às
classes em Python. A definição de classe não leva parênteses porque
estamos criando essa classe do zero. Em seguida, escrevemos
docstrings para descrever o que a classe faz.
Método __init__()
Uma função que parte de uma classe é um método. Tudo o que
aprendemos sobre funções também vale para os métodos; por ora, a
única diferença prática é a maneira como chamaremos os métodos.
O método 2 __init__() é um método especial que o Python executa
automaticamente sempre que criamos uma instância nova com base
na classe Dog. Esse método tem dois underscores principais à
esquerda e dois à direita, convenção que ajuda a evitar que os
nomes de método default do Python entrem em conflito com os
nomes criados do método. Não se esqueça de usar dois underscores
em cada lado de __init__(). Caso use apenas um de cada lado, o
método não será chamado automaticamente quando a classe for
usada, resultando em erros difíceis de identificar.
Definimos o método __init__() com três parâmetros: self, name e age. O
parâmetro self é necessário na definição do método e deve vir
primeiro, antes dos outros parâmetros. Deve ser incluído na
definição porque quando Python chamar esse método mais tarde
(para criar uma instância de Dog), a chamada de método passará
automaticamente o argumento self. Toda chamada de método
associada a uma instância passa automaticamente self, referência à
própria instância; isso concede à instância individual acesso aos
atributos e aos métodos da classe. Ao criarmos uma instância de
Dog, o Python chamará o método __init__() da classe Dog. Vamos
passar um nome e uma idade para Dog() como argumentos; o self é
passado automaticamente, não precisamos passá-lo. Sempre que
quisermos criar uma instância da classe Dog, basta fornecer valores
para os dois últimos parâmetros, name e age.
As duas variáveis definidas no corpo do método __init__() têm o
prefixo self 3. Qualquer variável prefixada com self fica disponível para
todos os métodos da classe, e também conseguiremos acessar essas
variáveis por meio de qualquer instância criada a partir da classe. A
linha self.name = name aceita o valor associado ao parâmetro name e o
atribui à variável name que, em seguida, é anexada à instância que
está sendo criada. O mesmo processo ocorre com self.age = age.
Chamamos variáveis como essa, acessíveis por meio de instâncias de
atributos.
A classe Dog tem dois outros métodos definidos: sit() e roll_over() 4. Já
que esses métodos não precisam de informações adicionais para
serem executados, apenas os definimos para ter um parâmetro, self.
As instâncias que criarmos mais tarde terão acesso a esses métodos.
Dito de outro modo, serão capazes de sentar e rolar. Por ora, sit() e
roll_over() não ajudam muito. Simplesmente exibem uma mensagem
dizendo se o cachorro está sentado ou rolando. Contudo, podemos
ampliar esse conceito à situações realistas: se essa classe fizesse
parte de um jogo de computador, esses métodos teriam o código
para fazer a animação de um cachorro sentar e rolar. Caso essa
classe fosse escrita para controlar um robô, esses métodos
orientariam os movimentos para fazer com que um cachorro robótico
sentasse e rolasse.
1 my_dog = Dog('Willie', 6)
Acessando atributos
Para acessar os atributos de uma instância, use a notação de ponto.
Vejamos como acessamos o valor do atributo name 2 de my_dog:
my_dog.name
No Python, usamos regularmente a notação de ponto. Essa sintaxe
demonstra como o Python encontra o valor de um atributo. Aqui, o
Python confere a instância my_dog e, em seguida, encontra o atributo
name associado a my_dog. É o mesmo atributo referenciado como
self.name na classe Dog. Empregamos a mesma abordagem para
trabalhar com o atributo age 3.
A saída é um resumo do que sabemos sobre my_dog:
My dog's name is Willie.
My dog is 6 years old.
Chamando métodos
Após criarmos uma instância a partir da classe Dog, podemos utilizar
a notação de ponto para chamar qualquer método definido nessa
mesma classe. Vamos fazer com que nosso cachorro sente e role.
class Dog:
-- trecho de código omitido --
my_dog = Dog('Willie', 6)
my_dog.sit()
my_dog.roll_over()
Para chamar um método, forneça o nome da instância (nesse caso,
my_dog) e o método que quer chamar, separados por um ponto. Ao
ler my_dog.sit(), o Python procura o método sit() na classe Dog e executa
o código. O Python interpreta a linha my_dog.roll_over() da mesma
forma.
Agora, Willie faz o que mandamos:
Willie is now sitting.
Willie rolled over!
Essa sintaxe é uma mão na roda. Quando atributos e métodos
recebem nomes descritivos apropriados como name, age, sit() e
roll_over(), podemos facilmente deduzir o que um bloco de código,
mesmo um que nunca vimos antes, deve fazer.
my_dog = Dog('Willie', 6)
your_dog = Dog('Lucy', 3)
Classe Car
Escreveremos uma classe nova representando um carro. Nossa
classe armazenará informações sobre o tipo de carro com o qual
estamos trabalhando e terá um método que resume essas
informações:
car.py
class Car:
"""Simples tentativa de representar um carro"""
2 def get_descriptive_name(self):
"""Retorna um nome descritivo, formatado elegantemente"""
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
3 my_new_car = Car('audi', 'a4', 2024)
print(my_new_car.get_descriptive_name())
Na classe Car, primeiro definimos o método __init__() com o parâmetro
self 1, assim como fizemos com a classe Dog. Fornecemos também
três outros parâmetros: make, model e year. O método __init__() recebe
esses parâmetros e os atribui aos atributos que serão associados às
instâncias criadas a partir dessa classe. Ao criarmos uma instância
nova de Car, é necessário especificar uma marca, modelo e ano para
nossa instância.
Definimos um método chamado get_descriptive_name() 2 que insere year,
make e model de um carro em uma string, representando o carro de
forma elegante. Assim, não precisaremos exibir o valor de cada
atributo individualmente. Nesse método, para trabalhar com os
valores dos atributos, utilizamos self.make, self.model e self.year. Fora da
classe, criamos uma instância da classe Car e a atribuímos à variável
my_new_car 3. Em seguida, chamamos get_descriptive_name() para mostrar
que tipo de carro temos:
2024 Audi A4
Vamos deixar as coisas mais interessantes adicionando um atributo
que muda ao longo do tempo. Adicionaremos um atributo que
armazena a quilometragem geral do carro. No código,
representaremos essa quilometragem como milhas.
def get_descriptive_name(self):
-- trecho de código omitido --
2 def read_odometer(self):
"""Exibe uma frase mostrando a quilometragem do carro, em milhas"""
print(f"This car has {self.odometer_reading} miles on it.")
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
Utilizamos a notação de ponto a fim de acessar o atributo
odometer_reading do carro e definimos seu valor diretamente. Essa linha
informa ao Python para receber a instância my_new_car, encontrar o
atributo odometer_reading associado e definir o valor desse atributo
como 23:
2024 Audi A4
This car has 23 miles on it.
Às vezes, queremos acessar atributos de modo direto como fizemos,
mas, outras vezes, queremos escrever um método que atualize seu
valor.
1 my_new_car.update_odometer(23)
my_new_car.read_odometer()
A única alteração em Car é a adição de update_odometer(). Esse método
recebe um valor de quilometragem e o atribui a self.odometer_reading.
Com a instância my_new_car, chamamos update_odometer() com 23 como
argumento 1. Isso define a leitura do hodômetro como 23 e
read_odometer() exibe a leitura:
2024 Audi A4
This car has 23 miles on it.
Podemos incrementar o método update_odometer() para executar uma
tarefa adicional sempre que a leitura do hodômetro for alterada.
Adicionaremos um pouco de lógica para garantir que ninguém tente
reverter a leitura do hodômetro:
class Car:
-- trecho de código omitido --
2 my_used_car.update_odometer(23_500)
my_used_car.read_odometer()
my_used_car.increment_odometer(100)
my_used_car.read_odometer()
O método novo increment_odometer() aceita uma quantidade de milhas,
e adiciona esse valor a self.odometer_reading. Primeiro, criamos um carro
usado, my_used_car 1. Definimos o hodômetro 23.500 chamando
update_odometer() e passando 23_500 2. Por último, chamamos
increment_odometer() e passamos 100 para adicionar as 100 milhas que
percorremos entre comprar o carro e registrá-lo:
2019 Subaru Outback
This car has 23500 miles on it.
This car has 23600 miles on it.
Podemos alterar esse método para rejeitar incrementos negativos,
assim ninguém consegue usar essa função para reverter o
hodômetro.
NOTA Podemos utilizar esses métodos para controlar a forma
como os usuários do programa atualizam valores como uma
leitura do hodômetro, mas qualquer pessoa com acesso ao
programa pode definir a leitura do hodômetro em qualquer
valor, acessando diretamente o atributo. A segurança eficaz
exige extrema atenção aos detalhes, além de verificações
básicas como as que acabamos de mostrar.
FAÇA VOCÊ MESMO
9.4 Pessoas atendidas: Comece com o seu programa do Exercício 9.1 (página 208).
Adicione um atributo chamado number_served com um valor default de 0. Crie uma
instância chamada restaurant a partir dessa classe. Exiba o número de clientes que o
restaurante atendeu e, em seguida, altere este valor e o exiba novamente.
Adicione um método chamado set_number_served() que possibilita definir o número de
clientes atendidos. Chame esse método com um novo número e exiba mais uma vez o
valor.
Adicione um método chamado increment_number_served(), o qual possibilita aumentar
o número de clientes atendidos. Chame esse método com qualquer número que quiser
e que possa representar quantos clientes foram atendidos em, digamos, um dia de
atividade comercial.
9.5 Tentativas de login: Adicione um atributo chamado login_attempts à sua classe
User do Exercício 9.3 (página 209). Crie um método chamado
increment_login_attempts() que incrementa o valor de login_attempts em 1. Crie outro
método chamado reset_login_attempts() que redefine o valor de login_attempts para 0.
Crie uma instância da classe User e chame increment_login_attempts() diversas vezes.
Exiba o valor de login_attempts para verificar que foi incrementado corretamente e, em
seguida, chame reset_login_attempts(). Exiba login_attempts novamente a fim de ter
certeza de que foi redefinido para 0.
Herança
Nem sempre é preciso começar do zero ao escrever uma classe. Se
a classe que estiver escrevendo for uma versão especializada de
outra classe escrita, recorra à herança. Quando uma classe herda de
outra, assume os atributos e métodos da primeira classe. A classe
original é chamada de classe-pai, e a classe nova é a classe-filha. A
classe-filha pode herdar qualquer um, ou todos os atributos e
métodos da classe-pai, mas também pode definir atributos e
métodos novos por conta própria.
def get_descriptive_name(self):
"""Retorna um nome descritivo, formatado elegantemente"""
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
def read_odometer(self):
"""Exibe uma frase mostrando a quilometragem do carro"""
print(f"This car has {self.odometer_reading} miles on it.")
2 class ElectricCar(Car):
"""Representa aspectos de um carro, específicos para veículos elétricos"""
class ElectricCar(Car):
"""Representa aspectos de um carro, específicos para veículos elétricos"""
2 def describe_battery(self):
"""Exibe uma frase descrevendo o tamanho da bateria"""
print(f"This car has a {self.battery_size}-kWh battery.")
def fill_gas_tank(self):
"""Carros elétricos não têm tanques de gasolina"""
print("This car doesn't have a gas tank!")
Agora, se alguém tentar chamar fill_gas_tank() com um carro elétrico, o
Python ignorará o método fill_gas_tank() em Car e executará esse
código. Ao recorrermos à herança, podemos fazer nossas classes-
filhas reterem o que precisamos e sobrescrever qualquer coisa não
necessária à classe-pai.
class Battery:
"""Simples tentativa de modelar uma bateria para um carro elétrico"""
2 def describe_battery(self):
"""Exibe uma frase descrevendo o tamanho da bateria"""
print(f"This car has a {self.battery_size}-kWh battery.")
class ElectricCar(Car):
"""Representa aspectos de um carro, específicos para veículos elétricos
class Battery:
-- trecho de código omitido --
def get_range(self):
"""Exibe frase sobre a distância que o carro percorre com essa bateria"""
if self.battery_size == 40:
range = 150
elif self.battery_size == 65:
range = 225
class ElectricCar(Car):
-- trecho de código omitido --
Importando classes
Conforme adiciona mais funcionalidade às suas classes, seus
arquivos podem ficar extensos, mesmo quando usa adequadamente
a herança e a composição. Segundo a filosofia geral do Python,
precisamos manter os arquivos o mais organizados possível. Para
ajudar, o Python possibilita armazenar classes em módulos e, em
seguida, importar as classes que precisamos para o programa
principal.
class Car:
"""Simples tentativa de representar um carro"""
def get_descriptive_name(self):
"""Retorna um nome descritivo, formatado elegantemente"""
long_name = f"{self.year} {self.make} {self.model}"
return long_name.title()
def read_odometer(self):
""" Exibe uma frase mostrando a quilometragem do carro"""
print(f"This car has {self.odometer_reading} miles on it.")
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
A instrução import 1 informa ao Python para abrir o módulo car e
importar a classe Car. Agora, podemos utilizar a classe Car como se
estivesse definida nesse arquivo. A saída é a mesma que já vimos:
2024 Audi A4
This car has 23 miles on it.
Importar classes é um jeito eficaz de programar. Imagine o tamanho
desse arquivo de programa se toda a classe Car estivesse nele. Ao
transferir a classe para um módulo e, em seguida, importá-lo, ainda
temos a mesma funcionalidade, mas o arquivo principal do programa
fica limpo e fácil de ler. Armazenamos também a maior parte da
lógica em arquivos separados; uma vez que nossas classes
funcionem como esperado, podemos abrir um pouco mão desses
arquivos e focarmos a lógica de alto nível do programa principal.
class Car:
-- trecho de código omitido --
class Battery:
"""Simples tentativa de modelar uma bateria para um carro elétrico"""
def describe_battery(self):
"""Exibe uma frase descrevendo o tamanho da bateria"""
print(f"This car has a {self.battery_size}-kWh battery.")
def get_range(self):
"""Exibe uma frase sobre a distância que o carro
consegue percorrer com essa bateria"""
if self.battery_size == 40:
range = 150
elif self.battery_size == 65:
range = 225
class ElectricCar(Car):
"""Modela aspectos de um carro, específicos para veículos elétricos"""
my_cars.py
1 from car import Car, ElectricCar
class Battery:
-- trecho de código omitido --
class ElectricCar(Car):
-- trecho de código omitido --
A classe ElectricCar precisa de acesso à sua classe-pai Car, então
importamos Car diretamente para o módulo. Se esquecermos essa
linha, o Python gerará um erro quando tentarmos importar o módulo
electric_car. Precisamos também atualizar o módulo Car para que
contenha somente a classe Car:
car.py
"""Classe que pode ser usada para representar um carro"""
class Car:
-- trecho de código omitido --
Agora, podemos importar de cada módulo separadamente e criar
qualquer tipo de carro que precisamos:
my_cars.py
from car import Car
from electric_car import ElectricCar
Usando aliases
Como vimos no Capítulo 8, ao usarmos módulos, os aliases podem
ser bastante úteis para organizar o código dos projetos. Podemos
também usar aliases ao importar classes.
Como exemplo, imagine um programa em que você queira criar
muitos carros elétricos. Talvez seja entediante digitar (e ler) ElectricCar
repetidas vezes. Podemos associar um alias a ElectricCar r quando
declaramos o import:
from electric_car import ElectricCar as EC
Agora, podemos usar esse alias sempre que quisermos criar um
carro elétrico:
my_leaf = EC('nissan', 'leaf', 2024)
Podemos também associar um alias a um módulo. Vejamos como
importar o módulo electric_car inteiro com um alias:
import electric_car as ec
Agora, é possível usar esse alias de módulo com o nome completo
da classe:
my_leaf = ec.ElectricCar('nissan', 'leaf', 2024)
Definindo o próprio fluxo de estudo e de trabalho
Conforme podemos ver, o Python disponibiliza muitas opções para
estruturar o código em um projeto grande. É indispensável conhecer
todas essas possibilidades, pois, assim, você consegue estabelecer
as melhores formas de organizar seus projetos, bem como entender
os projetos de outras pessoas.
De início, mantenha sua estrutura de códigos simples. Tente fazer
tudo em um arquivo e transfira suas classes para módulos
separados, assim que tudo estiver funcionando. Se gostar de como
os módulos e arquivos interagem, tente armazenar suas classes em
módulos quando começar um projeto. Encontre uma abordagem que
possibilite escrever um código que funcione e comece a partir daí.
Estilizando classes
Vale a pena elucidar algumas questões de estilo relacionadas às
classes, sobretudo à medida que seus programas se tornam mais
complicados.
Nomes de classes devem seguir o padrão CamelCase. Basta escrever
a primeira letra do nome em maiúsculo e não usar underscores.
Nomes de instâncias e módulos devem ser escritos em letras
minúsculas, com underscores entre as palavras.
Crie uma docstring imediatamente após definir cada classe. A
docstring deve ser uma breve descrição do que a classe faz, siga as
mesmas convenções de formatação que usou para escrever as
docstrings das funções. Crie também uma docstring imediatamente
após criar cada módulo, descrevendo para que as classes de um
módulo podem ser usadas.
Pode-se usar linhas em branco para organizar o código, mas não
abuse delas. Dentro de uma classe, é possível usar uma linha em
branco entre os módulos e, dentro de um módulo, pode-se usar
duas linhas em branco para separar as classes.
Caso seja necessário importar um módulo de uma biblioteca padrão
e um módulo que escrever, insira primeiro a instrução import do
módulo da biblioteca padrão. Em seguida, adicione uma linha em
branco e a instrução import do módulo que escreveu. Em programas
com diversas instruções import, essa convenção facilita ver de onde se
origina os diferentes módulos usados no programa.
Recapitulando
Neste capítulo, aprendemos a escrever nossas próprias classes.
Aprendemos a armazenar informações em uma classe usando
atributos e como escrever métodos que atribuem às classes o
comportamento necessário. Estudamos os métodos __init__() que
criam instâncias a partir de suas classes justamente com os atributos
que queremos. Vimos como alterar os atributos de uma instância
diretamente e por meio de métodos. Aprendemos que a herança
pode simplificar a criação de classes que estão relacionadas entre si
e a usar instâncias de uma classe como atributos em outra classe
para manter cada classe simples.
Vimos como armazenar classes em módulos e importar as classes
necessárias para os arquivos em que serão usadas, mantendo,
assim, os projetos organizados. Começamos a aprender sobre a
biblioteca padrão do Python e vimos um exemplo do módulo random.
Por último, aprendemos a estilizar classes usando as convenções
pytônicas.
No Capítulo 10 aprenderemos a trabalhar com arquivos, assim
podemos salvar o trabalho que fizemos em um programa e o
trabalho que permitimos que os usuários fizessem. Veremos também
as exceções, classe especial do Python, arquitetada para ajudá-lo a
responder a erros, quando eles surgirem.
10
capítulo
Arquivos e exceções
1 path = Path('pi_digits.txt')
2 contents = path.read_text()
print(contents)
Para trabalhar com o conteúdo de um arquivo, é necessário informar
ao Python o path (caminho) do arquivo. Um path é a localização
exata de um arquivo ou pasta em um sistema. O Python fornece um
módulo chamado pathlib que facilita o trabalho com arquivos e
diretórios, independentemente do sistema operacional usado por
você ou pelos usuários do seu programa. Em geral, módulos como
esse, que fornecem funcionalidades específicas, são chamados de
biblioteca. O nome pathlib se origina das palavras inglesas path e
library, caminho e biblioteca, respectivamente.
Começamos importando a classe Path do pathlib. Podemos fazer muita
coisa com um objeto Path que aponta para um arquivo. Por exemplo,
podemos verificar se o arquivo existe antes de trabalhar com ele, ler
o conteúdo do arquivo ou escrever dados novos no arquivo. Aqui,
criamos um objeto Path representando o arquivo pi_digits.txt, que
atribuímos à variável path 1. Como esse arquivo é salvo no mesmo
diretório que o arquivo .py que estamos escrevendo, o nome do
arquivo é tudo o que o Path precisa para acessá-lo.
NOTA O VS Code procura arquivos na pasta recentemente mais
acessada. Se estiver usando o VS Code, comece abrindo a
pasta onde está armazenando os programas deste capítulo.
Por exemplo, caso esteja salvando seus arquivos de programa
em uma pasta chamada chapter_10, pressione Ctr+O(⌘+O
no macOS) e abra essa pasta.
Uma vez que temos um objeto Path representando pi_digits.txt,
recorremos ao método read_text() para ler todo o conteúdo do
arquivo 2. O conteúdo do arquivo é retornado como uma única
string, que atribuímos à variável contents. Ao exibirmos o valor de
contents, vemos todo o conteúdo do arquivo de texto:
3.1415926535
8979323846
2643383279
A única diferença entre essa saída e o arquivo original é a linha em
branco extra no final da saída. A linha em branco aparece, pois
read_text() retorna uma string vazia quando chega ao final do arquivo;
essa string vazia aparece como uma linha em branco.
Podemos remover a linha em branco extra usando rstrip() na string
contents:
from pathlib import Path
path = Path('pi_digits.txt')
contents = path.read_text()
contents = contents.rstrip()
print(contents)
Lembre-se do Capítulo 2, em que o método rstrip() do Python remove,
ou retira, quaisquer caracteres de espaço em branco do lado direito
de uma string. Agora, a saída corresponde exatamente ao conteúdo
do arquivo original:
3.1415926535
8979323846
2643383279
É possível remover o caractere de quebra de linha quando lemos o
conteúdo do arquivo, usando o método rstrip() imediatamente após
chamar read_text():
contents = path.read_text().rstrip()
Essa linha informa ao Python para chamar o método read_text() no
arquivo com o qual estamos trabalhando. Em seguida, aplica o
método rstrip() à string que read_text() retorna. A string limpa é então
atribuída à variável contents. Essa abordagem se chama
encadeamento de métodos, e você a verá com frequência na
programação.
2 lines = contents.splitlines()
for line in lines:
print(line)
Começamos lendo todo o conteúdo do arquivo, como fizemos
antes 1. Caso planeje trabalhar com as linhas individuais em um
arquivo, não é necessário remover nenhum espaço em branco ao ler
o arquivo. O método splitlines() retorna uma lista de todas as linhas no
arquivo, e atribuímos essa lista à variável lines 2. Depois, percorremos
essas linhas com o loop e exibimos cada uma delas:
3.1415926535
8979323846
2643383279
Como não modificamos nenhuma das linhas, a saída corresponde
exatamente ao arquivo de texto original.
path = Path('pi_digits.txt')
contents = path.read_text()
lines = contents.splitlines()
pi_string = ''
1 for line in lines:
pi_string += line
print(pi_string)
print(len(pi_string))
Começamos lendo o arquivo e armazenando cada linha de
algarismos em uma lista, assim como no exemplo anterior. Em
seguida, criamos uma variável, pi_string, para armazenar os
algarismos de pi. Escrevemos um loop que adiciona cada linha de
algarismos à pi_string 1. Exibimos essa string e também mostramos
seu comprimento:
3.1415926535 8979323846 2643383279
36
A variável pi_string contém o espaço em branco que estava ao lado
esquerdo dos algarismos em cada linha, mas podemos removê-los
usando lstrip() em cada linha:
-- trecho de código omitido --
for line in lines:
pi_string += line.lstrip()
print(pi_string)
print(len(pi_string))
Agora, temos uma string contendo pi com até 30 casas decimais. A
string tem 32 caracteres, pois também inclui o 3 inicial e um ponto
decimal:
3.141592653589793238462643383279
32
path = Path('pi_million_digits.txt')
contents = path.read_text()
lines = contents.splitlines()
pi_string = ''
for line in lines:
pi_string += line.lstrip()
print(f"{pi_string[:52]}...")
print(len(pi_string))
A saída mostra que, de fato, temos uma string contendo pi com até
um milhão de casas decimais.
3.14159265358979323846264338327950288419716939937510...
1000002
O Python não tem limite inerente à quantidade de dados com os
quais podemos trabalhar; é possível trabalhar com a quantidade de
dados que a memória do seu sistema conseguir suportar.
pi_birthday.py
-- trecho de código omitido --
for line in lines:
pi_string += line.strip()
Escrevendo em um arquivo
Uma das formas mais simples de salvar dados é escrevê-los em um
arquivo. Ao escrevermos um texto em um arquivo, a saída ainda fica
disponível, mesmo após fecharmos o terminal com a saída do
programa. É possível examinar a saída após um programa terminar
de ser executado, e podemos também compartilhar os arquivos de
saída com outras pessoas. Podemos também escrever programas
que releiam o texto da memória e podemos trabalhar mais uma vez
com esses dados posteriormente.
path = Path('programming.txt')
path.write_text("I love programming.")
O método write_text() recebe um único argumento: a string que
queremos escrever no arquivo. Esse programa não tem saída de
terminal, mas se abrirmos o arquivo programming.txt, veremos uma
linha:
programming.txt
I love programming.
Esse arquivo se comporta como qualquer outro arquivo de seu
computador. Conseguimos abri-lo, escrever um texto novo nele,
copiar dele, colar nele, e assim por diante.
NOTA O Python só consegue escrever strings em um arquivo de
texto. Se quiser armazenar dados numéricos em um arquivo
de texto, será necessário primeiro converter os dados para o
formato de string com a função str().
path = Path('programming.txt')
path.write_text(contents)
Definimos uma variável chamada contents para armazenar todo o
conteúdo do arquivo. Na próxima linha, utilizamos o operador +=
para adicionar essa string à variável. Podemos fazer isso quantas
vezes forem necessárias, a fim de criar strings de qualquer
comprimento. Nesse caso, incluímos caracteres de quebra de linha
no final de cada linha, para garantir que cada instrução apareça na
própria linha.
Se executarmos o programa e abrirmos programming.txt, veremos
cada uma dessas linhas no arquivo de texto:
I love programming.
I love creating new games.
I also love working with data.
Podemos também usar espaços, tabulações e linhas em branco para
formatar a saída, assim como fazemos com a saída do terminal. Não
há limite para o comprimento de strings, e é assim que muitos
documentos gerados por computador são criados.
Exceções
O Python utiliza objetos especiais chamados exceções para gerenciar
erros ocorridos durante a execução de um programa. Sempre que
ocorre um erro que faz com que Python fique indeciso sobre como
agir em seguida, gera-se um objeto de exceção. Caso desenvolva
um código para abordar a exceção, o programa continuará em
execução. Se não tentar abordar a exceção, o programa será
suspenso e mostrará um traceback, incluindo um relatório da
exceção lançada.
As exceções são tratadas com blocos try-except. Um bloco try-except
solicita que Python tome alguma ação, mas também informa ao
Python o que fazer se uma exceção for lançada. Quando usamos
blocos try-except, os programas continuam executando, mesmo
quando as coisas começam a dar errado. Em vez de tracebacks, que
podem deixar os usuários confusos, os usuários verão mensagens
amigáveis de erro, escritas por você.
while True:
1 first_number = input("\nFirst number: ")
if first_number == 'q':
break
2 second_number = input("Second number: ")
if second_number == 'q':
break
3 answer = int(first_number) / int(second_number)
print(answer)
Esse programa solicita que o usuário insira um first_number 1 e, se o
usuário não inserir q para sair, um second_number 2. Em seguida,
dividimos esses dois números para obter answer 3. Como o programa
não toma nenhuma ação para lidar com erros, solicitar uma divisão
por zero provocará uma falha:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
Traceback (most recent call last):
File "division_calculator.py", line 11, in <module>
answer = int(first_number) / int(second_number)
~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~
ZeroDivisionError: division by zero
Não é nada bom quando um programa falha, mas é pior permitir
que os usuários vejam tracebacks. Usuários não técnicos ficarão
confusos e, em um ambiente malicioso, hackers coletarão mais
informações do que queremos. Por exemplo, eles saberão o nome
do arquivo do programa e verão uma parte do código que não está
funcionando corretamente. Às vezes, um hacker habilidoso pode
usar essas informações para definir que tipo de ataque lançar contra
seu código.
Bloco else
É possível que o programa fique mais resistente a erros, envolvendo
a linha que pode gerar erros em um wrapper contendo um bloco try-
except. Como o erro ocorre na linha que executa a divisão, é nela que
vamos inserir o bloco try-except. No exemplo a seguir, também
podemos ver um bloco else. Qualquer código que dependa da
execução bem-sucedida do bloco try deve ser inserido no bloco else:
-- trecho de código omitido --
while True:
-- trecho de código omitido --
if second_number == 'q':
break
1 try:
answer = int(first_number) / int(second_number)
2 except ZeroDivisionError:
print("You can't divide by 0!")
3 else:
print(answer)
Instruímos o Python para tentar finalizar a operação de divisão em
um bloco try 1, que tem somente o código que pode causar um erro.
Qualquer código que dependa da execução bem-sucedida do bloco
try é adicionado ao bloco else. Nesse caso, se a operação de divisão
for bem-sucedida, usamos o bloco else para exibir o resultado 3.
O bloco except informa ao Python como responder quando um
ZeroDivisionError é lançado 2. Se a execução do bloco try não for bem-
sucedida devido a um erro de divisão por zero, exibimos uma
mensagem amigável, informando ao usuário como evitar esse tipo
de erro. O programa continua sendo executado e o usuário nunca vê
um traceback:
Give me two numbers, and I'll divide them.
Enter 'q' to quit.
First number: 5
Second number: 0
You can't divide by 0!
First number: 5
Second number: 2
2.5
First number: q
O único código que deve ser inserido em um bloco try é aquele que
pode fazer com que uma exceção seja lançada. Às vezes, você terá
um código adicional que deve ser executado apenas se a execução
do bloco try for bem-sucedida; esse código deve ser inserido no
bloco else. O bloco except informa ao Python o que fazer se
determinada exceção for lançada quando tenta executar o código no
bloco try.
Quando prevemos prováveis fontes de erros, podemos desenvolver
programas robustos que continuam a ser executados mesmo quando
encontram dados inválidos e recursos ausentes. Assim, o código fica
resistente a erros de usuários inocentes e ataques maliciosos.
path = Path('alice.txt')
contents = path.read_text(encoding='utf-8')
Repare que estamos usando read_text() de forma ligeiramente
diferente do que já vimos antes. O argumento encoding é necessário
quando a codificação padrão do sistema não corresponde à
codificação do arquivo que está sendo lido. É mais provável que isso
aconteça ao ler um arquivo que não foi criado em seu sistema.
Como não consegue ler um arquivo ausente, o Python lança uma
exceção:
Traceback (most recent call last):
1 File "alice.py", line 4, in <module>
2 contents = path.read_text(encoding='utf-8')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/.../pathlib.py", line 1056, in read_text
with self.open(mode='r', encoding=encoding, errors=errors) as f:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^
File "/.../pathlib.py", line 1042, in open
return io.open(self, mode, buffering, encoding, errors, newline)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
^^^^^^^^
3 FileNotFoundError: [Errno 2] No such file or directory: 'alice.txt'
Temos um traceback mais extenso do que os que vimos
anteriormente, assim analisaremos como podemos entender
tracebacks mais complexos. Em geral, é melhor começar com o final
do traceback. Na última linha, podemos ver que uma exceção
FileNotFoundError foi lançada 3. Isso é importante, pois a linha nos
informa que tipo de exceção usar no bloco except que vamos escrever.
Se voltarmos, perto do início do traceback 1, podemos ver que o erro
ocorreu na linha 4 do arquivo alice.py. A próxima linha mostra a
linha de código que causou o erro 2. O restante do traceback mostra
códigos das bibliotecas envolvidas na abertura e leitura de arquivos.
Via de regra, não é necessário ler ou entender todas as linhas de um
traceback.
Para lidar com o erro que está sendo lançado, o bloco try começará
com a linha identificada como problemática no traceback. Em nosso
exemplo, essa é a linha que contém read_text():
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text(encoding='utf-8')
1 except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
Nesse exemplo, o código no bloco try gera um FileNotFoundError,
portanto, escrevemos um bloco except que corresponde a esse erro 1.
O Python executa o código nesse bloco quando o arquivo não pode
ser encontrado, e o resultado é uma mensagem de erro amigável
em vez de um traceback:
Sorry, the file alice.txt does not exist.
Como o arquivo não existe, o programa não tem mais nada para
verificar. Por isso, vemos esse tipo de saída. Vamos desenvolver esse
exemplo e ver como o tratamento de exceções pode ajudar quando
estamos trabalhando com mais de um arquivo.
Analisando textos
É possível analisar arquivos de texto contendo livros inteiros. Muitas
obras clássicas da literatura estão disponíveis como arquivos de
texto simples porque são de domínio público. Os textos usados nesta
seção são provenientes do Projeto Gutenberg (https://gutenberg.org). O
Projeto Gutenberg faz a curadoria de uma coleção de obras literárias
disponíveis em domínio público. É um excelente recurso, caso esteja
interessado em trabalhar com textos literários em seus projetos de
programação.
Vamos extrair o texto de Alice in Wonderland e tentar contar o
número de palavras dele. Para fazer isso, usaremos o método de
string split(), que por padrão divide uma string onde quer que
encontre qualquer espaço em branco:
from pathlib import Path
path = Path('alice.txt')
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Conta o número aproximado de palavras no arquivo
1 words = contents.split()
2 num_words = len(words)
print(f"The file {path} has about {num_words} words.")
Movi o arquivo alice.txt para o diretório correto, desta vez, o bloco try
funcionará. Pegamos a string contents, que agora contém todo o texto
de Alice in Wonderland como uma string extensa, e usamos split() a
fim de gerar uma lista de todas as palavras do livro 1. Se usarmos
len() nesta lista 2, a função nos fornece uma boa aproximação do
número de palavras do texto original. Por último, exibimos uma
mensagem informando quantas palavras foram encontradas no
arquivo. Inserimos esse código no bloco else, já que esse código só é
executado se o código no bloco try tiver sido executado com sucesso.
A saída nos informa quantas palavras existem em alice.txt:
The file alice.txt has about 29594 words.
Embora a contagem seja um pouco alta, visto que estamos usando
um arquivo de texto com informações extras fornecidas pelo editor,
temos uma boa aproximação do tamanho de Alice in Wonderland.
def count_words(path):
1 """ Conta o número aproximado de palavras em um arquivo
try:
contents = path.read_text(encoding='utf-8')
except FileNotFoundError:
print(f"Sorry, the file {path} does not exist.")
else:
# Conta o número aproximado de palavras no arquivo:
words = contents.split()
num_words = len(words)
print(f"The file {path} has about {num_words} words.")
path = Path('alice.txt')
count_words(path)
A maior parte desse código está inalterada. Apenas indentamos e
transferimos o código para o corpo de count_words(). Como é bom
hábito manter os comentários atualizados quando alteramos um
programa, convertemos o comentário em uma docstring e o
reescrevemos um pouco 1.
Agora, podemos escrever um breve loop para contar as palavras em
qualquer texto que quisermos analisar. Para isso, armazenamos os
nomes dos arquivos que queremos analisar em uma lista e, depois,
chamamos count_words() para cada arquivo na lista. Tentaremos contar
as palavras dos livros Alice in Wonderland, Siddhartha, Moby Dick e
Little Women, todos disponíveis em domínio público. Não inclui de
propósito siddhartha.txt no diretório que contém word_count.py,
para que vermos como nosso programa lida com um arquivo
ausente:
from pathlib import Path
def count_words(filename):
-- trecho de código omitido --
Armazenando dados
Muitos de seus programas solicitarão aos usuários que forneçam
informações específicas. É possível permitir que os usuários
armazenem preferências em um jogo ou forneçam dados para uma
visualização. Seja lá qual for o foco do seu programa, você
armazenará as informações fornecidas pelos usuários em estruturas
de dados, como listas e dicionários. Quando os usuários fecham um
programa, quase sempre desejamos salvar as informações inseridas.
Uma forma simples de fazer isso é armazenar dados usando o
módulo json.
O módulo json possibilita converter estruturas Python simples de
dados em strings formatadas em JSON e, em seguida, possibilita
carregar os dados desse arquivo na próxima vez que o programa for
executado. Podemos também utilizar o json para compartilhar dados
entre diferentes programas Python. Melhor ainda, o formato de
dados JSON não é específico do Python, ou seja, podemos
compartilhar dados armazenados no formato JSON com pessoas que
trabalham com diferentes linguagens de programação. É um formato
conveniente e portátil, e é fácil de aprender.
NOTA O formato JSON (JavaScript Object Notation, Notação de
Objeto JavaScript, em tradução livre) foi inicialmente
desenvolvido para JavaScript. No entanto, tornou-se um
formato comum usado por muitas linguagens, incluindo o
Python.
1 path = Path('numbers.json')
2 contents = json.dumps(numbers)
path.write_text(contents)
Primeiro, importamos o módulo json e, em seguida, criamos uma lista
de números com a qual trabalhar. Depois, escolhemos um nome de
arquivo para armazenar a lista de números 1. É de praxe usar a
extensão de arquivo .json para sinalizar que os dados no arquivo
estão armazenados no formato JSON. Em seguida, usamos a função
json.dumps () 2 para gerar uma string contendo a representação JSON
dos dados com os quais estamos trabalhando. Uma vez que temos
essa string, a escrevemos no arquivo com o mesmo método
write_text() que usamos antes.
1 path = Path('numbers.json')
2 contents = path.read_text()
3 numbers = json.loads(contents)
print(numbers)
Fizemos questão de ler do mesmo arquivo que escrevemos 1. Como
o arquivo de dados é somente um arquivo de texto com formatação
específica, podemos lê-lo com o método read_text() 2. Em seguida,
passamos o conteúdo do arquivo para json.loads() 3. Essa função aceita
uma string formatada em JSON e retorna um objeto Python (nesse
caso, uma lista), que atribuímos a numbers. Por último, exibimos a
lista de números acessados e verificamos que é a mesma lista criada
em number_writer.py:
[2, 3, 5, 7, 11, 13]
É a forma mais simples de compartilhar dados entre dois programas.
2 path = Path('username.json')
contents = json.dumps(username)
path.write_text(contents)
1 path = Path('username.json')
contents = path.read_text()
2 username = json.loads(contents)
path = Path('username.json')
1 if path.exists():
contents = path.read_text()
username = json.loads(contents)
print(f"Welcome back, {username}!")
2 else:
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
print(f"We'll remember you when you come back, {username}!")
Podemos usar muitos métodos úteis com objetos Path. O método
exists() retorna True se existir um arquivo ou pasta e False se não existir.
Aqui, usamos path.exists() para descobrir se um nome de usuário já foi
armazenado 1. Se username.json existir, carregamos o nome de
usuário e exibimos um cumprimento personalizado.
Se o arquivo username.json não existir 2, solicitamos um nome de
usuário e armazenamos o valor que o usuário fornece. Exibimos
também mensagem familiar de que nos lembraremos deles quando
retornarem.
Qualquer que seja o bloco executado, o resultado é um nome de
usuário e um cumprimento adequado. Se for a primeira vez que o
programa é executado, a saída será:
What is your name? Eric
We'll remember you when you come back, Eric!
Caso contrário:
Welcome back, Eric!
Essa é a saída que veremos se o programa já foi executado pelo
menos uma vez. Mesmo que os dados desta seção sejam apenas
uma única string, o programa executaria perfeitamente com
quaisquer dados que possam ser convertidos em uma string
formatada em JSON.
Refatoração
Não raro, chegamos a um ponto em que nosso código funciona sem
problemas, mas reconhecemos que podemos melhorá-lo se o
dividirmos em uma série de funções com tarefas específicas. Esse
processo se chama refatoração. A refatoração contribui para um
código mais limpo, mais fácil de entender e de incrementar.
É possível refatorar remember_me.py transferindo a maior parte de
sua lógica para uma ou mais funções. Como o foco de
remember_me.py está em cumprimentar o usuário, passaremos
todo o nosso código existente para uma função chamada greet_user():
remember_me.py
from pathlib import Path
import json
def greet_user():
1 """Cumprimenta o usuário pelo nome"""
path = Path('username.json')
if path.exists():
contents = path.read_text()
username = json.loads(contents)
print(f"Welcome back, {username}!")
else:
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
print(f"We'll remember you when you come back, {username}!")
greet_user()
Agora, como estamos usando uma função, reescrevemos os
comentários em uma docstring que retrata como o programa
funciona atualmente 1. Trata-se de um arquivo um pouco mais
limpo. Contudo, a função greet_user() está fazendo mais do que
apenas cumprimentar o usuário – também está acessando um nome
de usuário armazenado, se existir, e solicitando um novo nome de
usuário, se não existir.
Refatoraremos greet_user() para que não execute tantas tarefas
diferentes. Vamos começar passando o código para acessar um
nome de usuário armazenado para uma função separada:
from pathlib import Path
import json
def get_stored_username(path):
1 """Obtém o nome de usuário armazenado, se disponível"""
if path.exists():
contents = path.read_text()
username = json.loads(contents)
return username
else:
2 return None
def greet_user():
"""Cumprimenta o usuário pelo nome"""
path = Path('username.json')
username = get_stored_username(path)
3 if username:
print(f"Welcome back, {username}!")
else:
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
print(f"We'll remember you when you come back, {username}!")
greet_user()
A função nova get_stored_username() 1 tem um propósito claro, como
mencionado na docstring. Essa função acessa um nome de usuário
armazenado e o retorna, se encontrar um. Se o path passado para
get_stored_username() não existir, a função retorna None 2. Trata-se de
uma prática recomendada: uma função deve retornar o valor
esperado, ou deve retornar None. Isso nos possibilita realizar um
teste simples com o valor de retorno da função. Exibimos uma
mensagem de boas-vindas ao usuário se a tentativa de acessar um
nome de usuário for bem-sucedida 3 e, se não for, solicitamos um
novo nome de usuário.
Devemos fatorar mais um bloco de código de greet_user(). Se o nome
de usuário não existir, devemos passar o código que solicita um novo
nome de usuário para uma função específica:
from pathlib import Path
import json
def get_stored_username(path):
"""Obtém o nome de usuário armazenado, se disponível"""
-- trecho de código omitido --
def get_new_username(path):
"""Solicita um novo nome de usuário"""
username = input("What is your name? ")
contents = json.dumps(username)
path.write_text(contents)
return username
def greet_user():
"""Cumprimenta o usuário pelo nome"""
path = Path('username.json')
1 username = get_stored_username(path)
if username:
print(f"Welcome back, {username}!")
else:
2 username = get_new_username(path)
print(f"We'll remember you when you come back, {username}!")
greet_user()
Cada função dessa versão final de remember_me.py tem um
propósito específico e claro. Ao chamarmos greet_user(), a função
exibe uma mensagem adequada: ou dá as boas-vindas mais uma
vez ao usuário existente ou cumprimenta um novo usuário. A função
faz isso chamando get_stored_username() 1, responsável apenas por
acessar um nome de usuário armazenado, se existir. Por último, se
necessário, greet_user() chama get_new_username() 2, responsável apenas
por obter um novo nome de usuário e armazená-lo. Essa
compartimentação de tarefas é parte imprescindível para escrever
um código nítido, que será fácil de manter e incrementar.
Recapitulando
Neste capítulo, aprendemos a trabalhar com arquivos. Aprendemos a
ler todo o conteúdo de um arquivo e, em seguida, analisar o
conteúdo uma linha de cada vez, se necessário. Vimos como
escrever a quantidade de texto que quisermos em um arquivo e
também como lidar com as exceções que você provavelmente verá
em seus programas. Por fim, aprendemos como armazenar
estruturas de dados Python para que possamos salvar as
informações fornecidas pelo usuário. Assim, os usuários não
precisam fazer tudo de novo sempre que executarem um programa.
No Capítulo 11, veremos formas eficientes de testar o código. Isso o
ajudará a confiar que o código que desenvolve está correto e a
identificar bugs introduzidos conforme incrementa os programas que
escreve.
11
capítulo
Atualizando o pip
O Python vem com uma ferramenta chamada pip, usada para
instalar pacotes de terceiros. Como ajuda a instalar pacotes de
recursos externos, o pip é atualizado com frequência para resolver
possíveis problemas de segurança. Assim sendo, vamos começar
atualizando o pip.
Abra uma nova janela de terminal e digite o seguinte comando:
$ python -m pip install --upgrade pip
1 Requirement already satisfied: pip in /.../python3.11/site-packages (22.0.4)
-- trecho de código omitido --
2 Successfully installed pip-22.1.2
A primeira parte deste comando, python -m pip, informa ao Python
para executar o módulo pip. A segunda parte, install --upgrade, solicita
que o pip atualize um pacote já instalado. A última parte, pip,
especifica qual pacote de terceiros deve ser atualizado. A saída
mostra que minha versão atual do pip, version 22.0.4 1, foi
substituída pela versão mais recente no momento em que eu
escrevia este livro, 22.1.2 2.
Podemos utilizar esse comando para atualizar qualquer pacote de
terceiros instalado em nosso sistema:
$ python -m pip install --upgrade nome_pacote
Instalando o pytest
Agora que o pip está atualizado, podemos instalar o pytest:
$ python -m pip install --user pytest
Collecting pytest
-- trecho de código omitido --
Successfully installed attrs-21.4.0 iniconfig-1.1.1 ...pytest-7.x.x
Dessa vez, ainda estamos usando o comando core pip install, sem a
flag --upgrade. Em vez disso, estamos utilizando a flag --user, que
instrui o Python para instalar esse pacote apenas no usuário atual. A
saída mostra que a versão mais recente do pytest foi instalada com
sucesso, com uma série de outros pacotes dos quais o pytest
depende.
É possível usar o seguinte comando para instalar muitos pacotes de
terceiros:
$ python -m pip install --user package_name
test_name_function.py
from name_function import get_formatted_name
1 def test_first_last_name():
"""Nomes como 'Janis Joplin' funcionam?"""
2 formatted_name = get_formatted_name('janis', 'joplin')
3 assert formatted_name == 'Janis Joplin'
Antes de executarmos o teste, analisaremos mais detalhadamente
essa função. O nome de um arquivo de teste é importante; deve
começar com test_. Quando solicitamos que execute os testes que
escrevemos, o pytest procurará por qualquer arquivo que comece com
test_, e executará todos os testes que encontrar nesse arquivo.
No arquivo de teste, primeiro, importamos a função que queremos
testar: get_formatted_name(). Em seguida, definimos uma função de
teste: nesse caso, test_first_last_name() 1. Temos um bom motivo para
usar um nome de função mais extenso do que os que já usamos.
Primeiro, as funções de teste precisam começar com a palavra test,
seguida por um underscore. Qualquer função que comece com test_
será detectada pelo pytest e executada como parte do processo de
teste.
Além do mais, os nomes dos testes devem ser mais extensos e
descritivos do que um típico nome de função. Nunca chamamos uma
função sozinha; o pytest encontrará a função e a executará para você.
Os nomes das funções de teste devem ser extensos o suficiente para
que, se virmos o nome da função em um relatório de teste,
possamos ter uma boa noção de qual comportamento estava sendo
testado.
Em seguida, chamamos a função que estamos testando 2. Aqui,
chamamos get_formatted_name() com os argumentos 'janis' e 'joplin', do
mesmo jeito que fizemos quando executamos names.py. Atribuímos
o valor de retorno dessa função a formatted_name.
Por último, fizemos uma asserção 3. Uma asserção é uma afirmação
sobre uma condição. Aqui, estamos afirmando que o valor de
formatted_name deve ser 'Janis Joplin'.
Executando um teste
Se executarmos o arquivo test_name_function.py diretamente, não
receberemos nenhuma saída porque nunca chamamos a função de
teste. Ao contrário, faremos com que o pytest execute o arquivo de
teste para nós.
Para tal, abra uma janela de terminal e navegue até a pasta que
contém o arquivo de teste. Caso esteja usando o VS Code, pode
abrir a pasta que contém o arquivo de teste e usar o terminal
integrado na janela do editor. Na janela do terminal, digite o
comando pytest. Você deve ver isso:
$ pytest
========================= test session starts
=========================
1 platform darwin -- Python 3.x.x, pytest-7.x.x, pluggy-1.x.x
2 rootdir: /.../python_work/chapter_11
3 collected 1 item
4 test_name_function.py . [100%]
========================== 1 passed in 0.00s
==========================
Vamos tentar compreender essa saída. Antes de mais nada, vemos
algumas informações sobre o sistema em que o teste está sendo
executado 1. Estou executando o teste em um sistema macOS. Ou
seja, você pode ver algumas saídas diferentes aqui. Mais importante
ainda, podemos ver quais versões do Python, do pytest e de outros
pacotes estão sendo usados para executar o teste.
Em seguida, vemos o diretório onde o teste está sendo executado 2:
no meu caso, python_work/chapter_11. É possível ver que o pytest
encontrou um teste para executar 3, e é possível ver o arquivo de
teste que está sendo executado 4. O único ponto após o nome do
arquivo nos informa que um único teste passou e o 100% mostra
claramente que todos os testes foram executados. Um projeto
grande pode ter centenas ou milhares de testes, e os pontos e o
indicador de porcentagem concluída podem ajudar a monitorar o
progresso geral da execução do teste
A última linha nos informa que um teste passou, e levou menos
de 0,01 segundo para executar o teste.
Essa saída indica que a função get_formatted_name() sempre funcionará
para nomes com um primeiro nome e sobrenome, a menos que
modifiquemos a função. Ao modificar get_formatted_name(), podemos
executar o este teste novamente. Se o teste passar, sabemos que a
função ainda funcionará para nomes como Janis Joplin.
NOTA Se não tiver certeza de como navegar para o local certo no
terminal, confira “Executando programas Python em um
terminal” na página 43. Além do mais, caso veja uma
mensagem de que o comando pytest não foi encontrado, use o
comando python -m pytest.
Um teste que falha
Como é um teste que falha? Modificaremos get_formatted_name() para
que possa lidar com nomes do meio, mas faremos isso de um jeito
que a função lance um erro para nomes com somente um primeiro
nome e sobrenome, como Janis Joplin.
Vejamos uma nova versão de get_formatted_name() que requer um
argumento para nome do meio:
name_function.py
def get_formatted_name(first, middle, last):
"""Gera um nome completo, elegantemente formatado"""
full_name = f"{first} {middle} {last}"
return full_name.title()
Essa versão deve funcionar para pessoas com nomes do meio, mas
quando a testamos, verificamos que quebramos a função para
pessoas com apenas um primeiro nome e sobrenome.
Dessa vez, a execução do pytest fornece a seguinte saída:
$ pytest
========================= test session starts
=========================
-- trecho de código omitido --
1 test_name_function.py F [100%]
2 ============================== FAILURES
===============================
3 ________________________ test_first_last_name _________________________
def test_first_last_name():
"""Nomes como 'Janis Joplin' funcionam?"""
4> formatted_name = get_formatted_name('janis', 'joplin')
5E TypeError: get_formatted_name() missing 1 required positional
argument: 'last'
test_name_function.py:5: TypeError
======================= short test summary info
=======================
FAILED test_name_function.py::test_first_last_name - TypeError:
get_formatted_name() missing 1 required positional argument: 'last'
========================== 1 failed in 0.04s
==========================
Aqui, temos muitas informações, porque precisamos saber muitas
coisas quando um teste falha. O primeiro item da saída é um único
F 1, que nos informa que um teste falhou. Em seguida, vemos uma
seção que foca FAILURES 2, porque os testes com falha são geralmente
o foco mais importante em uma execução de teste. Depois, vemos
que test_first_last_name() foi a função de teste que falhou 3. Um colchete
angular 4 indica a linha de código que causou a falha do teste. O E
na próxima linha 5 mostra o erro real que causou a falha: um TypeError
devido a um argumento posicional obrigatório ausente, last. Podemos
ver que no final, as informações mais importantes são repetidas em
um resumo menor. Assim, quando estivermos executando muitos
testes, podemos ter uma noção rápida de quais testes falharam e
por quê.
def test_first_last_name():
-- trecho de código omitido --
def test_first_last_middle_name():
"""Nomes como 'Wolfgang Amadeus Mozart' funcionam?"""
1 formatted_name = get_formatted_name(
'wolfgang', 'mozart', 'amadeus')
2 assert formatted_name == 'Wolfgang Amadeus Mozart'
Nomeamos essa função nova como test_first_last_middle_name(). O nome
da função deve começar com test_ para que seja executada
automaticamente quando executamos o pytest. Nomeamos a função
para que fique claro qual comportamento de get_formatted_name()
estamos testando. Como resultado, se o teste falhar, saberemos
imediatamente que tipos de nomes são afetados.
Para testar a função, chamamos get_formatted_name() com um primeiro
nome, nome do meio e sobrenome 1 e, depois, fazemos uma
asserção 2 de que o nome completo retornado corresponde ao nome
completo (primeiro nome, sobrenome e nome do meio) que
esperamos. Quando executamos o pytest mais uma vez, ambos os
testes passam:
$ pytest
========================= test session starts
=========================
-- trecho de código omitido --
collected 2 items
1 test_name_function.py .. [100%]
========================== 2 passed in 0.01s
==========================
Os dois pontos 1 sinalizam que dois testes passaram, o que também
fica claro na última linha de saída. Maravilha! Agora sabemos que a
função ainda funciona para nomes como Janis Joplin, e podemos ficar
seguros de que funcionará também para nomes como Wolfgang Amadeus
Mozart.
Variedade de asserções
Até agora vimos somente um tipo de asserção: uma afirmação de
que uma string tem um valor específico. Ao escrever um teste, é
possível fazer qualquer afirmação que possa ser expressa como uma
instrução condicional. Se a condição for True como esperado, sua
asserção sobre como essa parte do programa se comporta será
constatada; pode ficar tranquilo de que erros não existem. Se a
condição presumida como True for na realidade False, o teste falhará e
você saberá que existe um problema para resolver. A Tabela 11.1
demonstra alguns dos tipos mais úteis de asserções usadas que
podemos incluir em seus testes iniciais.
Tabela 11.1: Instruções de asserção comumente usadas em testes
Asserção Afirmação
assert a == b Afirma que dois valores são iguais.
assert a != b Afirma que dois valores não são iguais.
assert a Afirma que a avalia como True.
assert not a Afirma que a avalia como False.
assert elemento in lista Afirma que um elemento está em uma lista.
assert elemento not in lista Afirma que um elemento não está em uma lista.
2 def show_question(self):
"""Mostra a pergunta da pesquisa"""
print(self.question)
4 def show_results(self):
"""Mostra todas as respostas fornecidas"""
print("Survey results:")
for response in self.responses:
print(f"- {response}")
Essa classe começa com uma pergunta de pesquisa que você
forneceu, 1 e inclui uma lista vazia para armazenar respostas. A
classe tem métodos para exibir a pergunta da pesquisa 2, adicionar
uma resposta nova à lista de respostas 3 e exibir todas as respostas
armazenadas na lista 4. Para criar uma instância a partir dessa
classe, tudo o que precisamos fornecer é uma pergunta. Após ter
uma instância representando uma pesquisa específica, exiba a
pergunta da pesquisa com show_question(), armazene uma resposta
com store_response() e mostra os resultados com show_results().
A fim de exemplificar que a classe AnonymousSurvey funciona,
escreveremos um programa que use a classe:
language_survey.py
from survey import AnonymousSurvey
Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q
1 def test_store_single_response():
"""Testa se uma única resposta está devidamente armazenada"""
question = "What language did you first learn to speak?"
2 language_survey = AnonymousSurvey(question)
language_survey.store_response('English')
3 assert 'English' in language_survey.responses
Começamos importando a classe que queremos testar,
AnonymousSurvey. A primeira função de teste averiguará se, quando
armazenamos uma resposta à pergunta da pesquisa, a resposta
acabará na lista de respostas da pesquisa. Um bom nome descritivo
para essa função é test_store_single_response() 1. Se esse teste falhar,
saberemos pelo nome da função, no resumo do teste, que houve um
problema ao armazenar uma única resposta à pesquisa.
Para testar o comportamento de uma classe, é necessário criar uma
instância da classe. Criamos uma instância chamada language_survey 2
com a pergunta "What language did you first learn to speak?". Armazenamos
uma única resposta, English, com o método store_response(). Em seguida,
verificamos que a resposta foi devidamente armazenada, pela
asserção de que English está na lista language_survey.responses 3.
Por padrão, executar o comando pytest sem argumentos executará
todos os testes que o pytest identificar no diretório atual. Para
concentrar os testes em um arquivo, passe o nome do arquivo de
teste que quiser executar. Aqui, executaremos apenas um teste que
escrevemos para AnonymousSurvey:
$ pytest test_survey.py
========================= test session starts
=========================
-- trecho de código omitido --
test_survey.py . [100%]
========================== 1 passed in 0.01s
==========================
É um bom ponto de partida, mas uma pesquisa só serve de alguma
coisa se gerar mais de uma resposta. Vamos averiguar se três
respostas podem ser devidamente armazenadas. Para isso,
adicionamos outro método a TestAnonymousSurvey:
from survey import AnonymousSurvey
def test_store_single_response():
-- trecho de código omitido --
def test_store_three_responses():
"""Testa se três respostas individuais estão devidamente armazenadas"""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
1 responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
language_survey.store_response(response)
Usando fixtures
Em test_survey.py, criamos uma instância nova de AnonymousSurvey em
cada função de teste. Funciona sem problemas no breve exemplo
com o qual estamos trabalhando, mas em um projeto real, com
dezenas ou centenas de testes, seria problemático.
Nos testes, uma fixture ajuda a definir um ambiente de teste. Em
geral, isso significa criar um recurso que é usado por mais de um
teste. Criamos uma fixture no pytest escrevendo uma função com o
decorator @pytest.fixture. Um decorator é uma diretiva inserida pouco
antes de uma definição de função; o Python aplica essa diretiva à
função antes de ser executada a fim de alterar como o código da
função se comporta. Não se preocupe se isso parecer complicado; é
possível começar a usar decorators de pacotes de terceiros antes de
você mesmo aprender a escrevê-los.
Vamos usar uma fixture para criar uma única instância de pesquisa
que pode ser usada em ambas as funções de teste em
test_survey.py:
import pytest
from survey import AnonymousSurvey
1 @pytest.fixture
2 def language_survey():
"""Uma pesquisa que estará disponível para todas as funções de teste"""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
return language_survey
3 def test_store_single_response(language_survey):
"""Testa se uma única resposta está devidamente armazenada"""
4 language_survey.store_response('English')
assert 'English' in language_survey.responses
5 def test_store_three_responses(language_survey):
"""Testa se três respostas individuais estão devidamente armazenadas"""
responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
6 language_survey.store_response(response)
Recapitulando
Neste capítulo, aprendemos a escrever testes para funções e classes
usando ferramentas no módulo pytest. Vimos como escrever funções
de teste que averiguam comportamentos específicos que suas
funções e classes devem exibir e como as fixtures podem ser
utilizadas para criar eficientemente recursos que podem ser usados
em múltiplas funções de teste em um arquivo de teste.
Apesar do teste ser um tópico fundamental para se aprender a
programar, muitos programadores iniciantes não são expostos a ele.
Como programador iniciante, você não precisa escrever testes para
todos os projetos simples que cria. Mas assim que começar a
trabalhar em projetos com empenho significativo de
desenvolvimento, será necessário testar os comportamentos críticos
de suas funções e classes. Desse modo, você ficará mais seguro de
que as tarefas novas em seus projetos não comprometerão as partes
que já funcionam e terá mais liberdade para melhorar seu código.
Caso quebre sem querer uma funcionalidade existente, você saberá
no mesmo instante e conseguirá corrigi-la com facilidade. Responder
a um teste executado com falha é bem mais fácil do que responder
a um bug reportado por um usuário descontente.
Você será mais respeitado por outros programadores se incluir no
código alguns testes iniciais. Eles se sentirão mais à vontade de
testar seu código e estarão mais dispostos a trabalhar com você em
projetos. Caso queira contribuir com um projeto em que outros
programadores estão trabalhando, espera-se que você mostre que
seu código passa nos testes existentes e, via de regra, espera-se
que você escreva testes para qualquer comportamento novo que
introduzir no projeto.
Brinque com testes em seu código para se familiarizar com os
processos de teste. Escreva testes para os comportamentos mais
críticos de suas funções e classes, mas não tenha como objetivo a
cobertura completa em projetos iniciais, a menos que tenha uma
razão específica.
II
parte
Projetos
Visualização de dados
Os projetos de Visualização de Dados começam no Capítulo 15:
você aprenderá a gerar dados e criar uma série de visualizações
funcionais e valiosas usando o Matplotlib e o Plotly. No Capítulo 16,
você aprenderá a acessar dados de fontes online e a fornecê-los a
um pacote de visualização a fim de gerar gráficos de dados
meteorológicos e um mapa da atividade global de terremotos. Por
último, no Capítulo 17, você aprenderá como escrever um
programa para fazer download e visualizar dados de modo
automático. Aprender como gerar visualizações lhe possibilita
explorar o campo da ciência de dados, uma das áreas de
programação com maior demanda atualmente.
Aplicações web
No projeto de Aplicações web (Capítulos 18, 19 e 20), você usará
o pacote Django para desenvolver uma aplicação simples web, que
possibilita aos usuários manter um registro sobre os diferentes
tópicos que estão aprendendo. Os usuários criarão uma conta com
nome de usuário e senha, digitarão um tópico e, em seguida,
imputarão os dados sobre o que estão aprendendo. Implantaremos
também nossa aplicação em um servidor remoto para que qualquer
pessoa em qualquer lugar do mundo consiga acessá-la.
Após concluir esse projeto, você será capaz de começar a
desenvolver suas aplicações simples web e estará pronto para se
dedicar a conteúdos mais aprofundados sobre como desenvolver
aplicações com o Django.
12 capítulo
Instalando o Pygame
Antes de começar a programar, instale o Pygame. Faremos a
instalação da mesma forma que fizemos com o pytest no
Capítulo 11: com o pip. Caso tenha pulado o Capítulo 11 ou precise
refrescar a memória sobre o que é pip, veja a seção “Instalando o
pytest com pip” na página 263.
Para instalar o Pygame, digite o seguinte comando no prompt do
terminal:
$ python -m pip install --user pygame
Se você usar um comando diferente de python para executar
programas ou iniciar uma sessão de terminal, como python3, não se
esqueça de usar esse comando.
import pygame
class AlienInvasion:
"""Classe geral para gerenciar ativos e comportamento do jogo"""
def __init__(self):
"""Inicializa o jogo e crie recursos do jogo"""
1 pygame.init()
def run_game(self):
"""Inicia o loop principal do jogo"""
3 while True:
# Observa eventos de teclado e mouse
4 for event in pygame.event.get():
5 if event.type == pygame.QUIT:
sys.exit()
if __name__ == '__main__':
# Cria uma instância do jogo e execute o jogo
ai = AlienInvasion()
ai.run_game()
Primeiro, importamos os módulos sys e pygame. O módulo pygame tem
a funcionalidade necessária para desenvolvermos um jogo.
Usaremos ferramentas no módulo sys para encerrar jogo quando o
jogador desistir de jogar.
O jogo Invasão Alienígena começa como uma classe chamada
AlienInvasion. No método __init__(), a função pygame.init() inicializa as
configurações de background de que o Pygame precisa para
funcionar adequadamente 1. Depois, chamamos
para criar uma janela de exibição 2, na qual
pygame.display.set_mode()
desenharemos todos os elementos gráficos do jogo. O argumento
(1200, 800) é uma tupla que define as dimensões da janela do jogo,
que terá 1.200 pixels de largura por 800 pixels de altura. (Podemos
ajustar esses valores dependendo do tamanho da tela.) Atribuímos
esta janela de exibição ao atributo self.screen, de modo que esteja
disponível em todos os métodos da classe.
O objeto que atribuímos a self.screen é chamado de superfície. No
Pygame, uma superfície é a parte da tela em que um elemento do
jogo pode ser exibido. Cada elemento do jogo, como um alienígena
ou uma espaçonave, é a própria superfície. A superfície retornada
pelo display.set_mode() representa toda a janela do jogo. Ao
habilitarmos o loop de animação do jogo, essa superfície será
redesenhada a cada passagem pelo loop, de modo que possa ser
atualizada com quaisquer mudanças desencadeadas pela entrada do
usuário.
O jogo é controlado pelo método run_game(). Este método contém um
loop while 3, executado continuamente. O loop while contém um loop
de eventos e um código que gerencia as atualizações de tela. Um
evento é uma ação que o usuário executa durante o jogo, como
pressionar uma tecla ou mover o mouse. Para que nosso programa
responda a eventos, escrevemos um loop de eventos que possa
‘ouvir’ eventos e realizar tarefas apropriadas dependendo dos tipos
de eventos ocorridos. O loop for 4 aninhado dentro do loop while é um
loop de eventos.
Para acessar os eventos que o Pygame detecta, usaremos a função
pygame.event.get(). Essa função retorna uma lista de eventos ocorridos
desde a última vez que foi chamada. Qualquer evento de teclado ou
mouse fará com que este loop for seja executado. Dentro do loop,
escreveremos uma série de instruções if para detectar e responder a
eventos específicos. Por exemplo, quando o jogador clica no botão
fechar da janela do jogo, um evento pygame.QUIT é detectado e
chamamos o sys.exit() para sair do jogo 5.
A chamada para pygame.display.flip() 6 informa ao Pygame para deixar a
tela desenhada mais recente visível. Nesse caso, o Pygame
simplesmente desenha uma tela vazia em cada passagem pelo loop
while, deletando a tela antiga, de modo que somente a nova tela
fique visível. Quando movemos os elementos do jogo, o
pygame.display.flip() atualiza continuamente a exibição, mostrando as
novas posições dos elementos do jogo e ocultando as antigas,
criando a ilusão de movimento suave.
No final do arquivo, criamos uma instância do jogo e chamamos o
run_game(). Colocamos o run_game() em um bloco if que só executa se o
arquivo for chamado diretamente. Ao executar o arquivo
alien_invasion.py, você deverá ver uma janela vazia do Pygame.
alien_invasion.py
def __init__(self):
""" Inicializa o jogo e cria recursos de jogo"""
pygame.init()
self.clock = pygame.time.Clock()
-- trecho de código omitido --
Após inicializar o pygame, criamos uma instância da classe Clock, a
partir do módulo pygame.time. Depois, vamos fazer o relógio funcionar
no final do loop while em run_game():
def run_game(self):
""" Inicia o loop principal do jogo"""
while True:
-- trecho de código omitido --
pygame.display.flip()
self.clock.tick(60)
O método tick() recebe um argumento: a taxa de frames do jogo.
Aqui, estou usando um valor de 60, logo, o Pygame fará o possível
para que o loop seja executado exatamente 60 vezes por segundo.
NOTA O relógio do Pygame deve ajudar o jogo a ser executado de
forma consistente na maioria dos sistemas. Caso o jogo
execute de forma menos consistente em seu sistema, tente
inserir valores diferentes para a taxa de frames. Se não
conseguir encontrar uma boa taxa de frames em seu sistema,
é possível omitir o relógio e ajustar as configurações do jogo
para que funcione bem em seu sistema.
def run_game(self):
-- trecho de código omitido --
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# Redesenha a tela durante cada passagem pelo loop
2 self.screen.fill(self.bg_color)
class AlienInvasion:
"""Classe geral para gerenciar ativos e comportamento do jogo"""
def __init__(self):
"""Inicializa o jogo e crie recursos de jogo"""
pygame.init()
self.clock = pygame.time.Clock()
1 self.settings = Settings()
2 self.screen = pygame.display.set_mode(
(self.settings.screen_width, self.settings.screen_height))
pygame.display.set_caption("Alien Invasion")
def run_game(self):
-- trecho de código omitido --
# Redesenha a tela durante cada passagem pelo loop.
3 self.screen.fill(self.settings.bg_color)
class Ship:
"""Classe para cuidar da espaçonave"""
def __init__(self, ai_game):
"""Inicializa a espaçonave e defina sua posição inicial"""
1 self.screen = ai_game.screen
2 self.screen_rect = ai_game.screen.get_rect()
5 def blitme(self):
"""Desenha a espaçonave em sua localização atual"""
self.screen.blit(self.image, self.rect)
O Pygame é eficiente porque possibilita tratar todos os elementos do
jogo como retângulos (rects), mesmo que não tenham exatamente o
formato de retângulos. Tratar um elemento como um retângulo é
eficiente, pois os retângulos são formas geométricas simples. Por
exemplo, caso seja necessário descobrir se dois elementos do jogo
podem colidir, o Pygame pode fazer isso mais rápido se tratar cada
objeto como um retângulo. Essa abordagem normalmente funciona
bem o suficiente para que ninguém que jogue o jogo perceba que
não estamos trabalhando com a forma exata de cada elemento do
jogo. Nesta classe, trataremos a espaçonave e a tela como
retângulos.
Importamos o módulo pygame antes de definir a classe. O método
__init__() de Ship recebe dois parâmetros: o self para referência e uma
referência à instância atual da classe AlienInvasion. Isso fornecerá à Ship
acesso a todos os recursos do jogo definidos em AlienInvasion. Depois,
atribuímos a tela a um atributo de Ship 1, para que possamos acessá-
lo facilmente em todos os métodos desta classe. Acessamos o
atributo rect da tela usando o método get_rect() e o atribuímos a
self.screen_rect 2. Isso nos possibilita colocar a espaçonave no local
correto na tela.
Para subir a imagem, chamamos o pygame.image.load() 3 e fornecemos a
localização da imagem da espaçonave. Esta função retorna uma
superfície representando a espaçonave, que atribuímos a self.image.
Quando a imagem é carregada, chamamos o get_rect() para acessar o
atributo rect da superfície da espaçonave, de modo que possamos
usá-lo posteriormente para posicionar a espaçonave.
Ao trabalhar com um objeto rect, é possível usar as coordenadas x
e y das bordas superior, inferior, esquerda e direita do retângulo,
bem como o centro, para posicionar o objeto. Podemos definir
qualquer um desses valores a fim de determinar a posição atual do
rect. Ao centralizar um elemento do jogo, trabalhe com os atributos
center, centerx ou centery de um rect. Quando estiver trabalhando em
uma borda da tela, trabalhe com os atributos top, bottom, left ou right.
Há também atributos que combinam essas propriedades, como
midbottom, midtop, midleft e midright. Ao ajustar o posicionamento
horizontal ou vertical do rect, é possível utilizar somente os atributos
x e y, as coordenadas x e y do canto superior esquerdo. Por causa
desses atributos, não precisamos fazer cálculos. Antes, os
desenvolvedores tinham que fazer cálculos manualmente. Você
usará esses atributos com frequência.
NOTA No Pygame, a origem (0, 0) está no canto superior
esquerdo da tela e as coordenadas aumentam à medida que
você desce e se desloca à direita. Em uma tela de 1200×800,
a origem está no canto superior esquerdo e o canto inferior
direito tem as coordenadas (1200, 800). Essas coordenadas
se referem à janela do jogo, não à tela física.
Vamos posicionar a espaçonave no centro inferior da tela. Para tal,
faça com que o valor de self.rect.midbottom corresponda ao atributo
midbottom do rect 4 da tela. O Pygame usa esses atributos rect para
posicionar a imagem da espaçonave de forma centralizada e
horizontal, alinhada à parte inferior da tela.
Por último, definimos o método blitme() 5, que desenha a imagem na
tela na posição especificada pelo self.rect.
Desenhando a espaçonave na tela
Agora, vamos atualizar alien_invasion.py para criar uma espaçonave
e chamar o método blitme() da espaçonave:
alien_invasion.py
-- trecho de código omitido --
from settings import Settings
from ship import Ship
class AlienInvasion:
"""Classe geral para gerenciar ativos e comportamento do jogo"""
def __init__(self):
-- trecho de código omitido --
pygame.display.set_caption("Alien Invasion")
1 self.ship = Ship(self)
def run_game(self):
trecho de código omitido -
# Redesenha a tela durante cada passagem pelo loop
self.screen.fill(self.settings.bg_color)
2 self.ship.blitme()
pygame.display.flip()
self.clock.tick(60)
-- trecho de código omitido --
Importamos Ship e criamos uma instância de Ship após a criação da
tela 1. Chamar Ship() exige um argumento: uma instância de
AlienInvasion. Aqui, o argumento self se refere à instância atual de
AlienInvasion. Esse é o parâmetro que dá acesso à Ship aos recursos do
jogo, como o objeto screen. Atribuímos esta instância de Ship à self.ship.
Após preencher o background, desenhamos a espaçonave na tela
chamando o ship.blitme(), para que a espaçonave apareça em cima do
background 2.
Ao executar alien_invasion.py, você deverá ver uma tela de jogo
vazia com a espaçonave no centro inferior, conforme mostrado na
Figura 12.2.
Método _check_events()
Moveremos o código que gerencia eventos para um método
separado chamado _check_events(). Isso simplificará o run_game() e
isolará o loop de gerenciamento de eventos. Isolar o loop de eventos
possibilita gerenciar eventos separadamente de outros aspectos do
jogo, como atualizar a tela.
Vejamos a classe AlienInvasion com o novo método _check_events(), que
afeta somente o código em run_game():
alien_invasion.py
def run_game(self):
"""Inicia o loop principal do jogo"""
while True:
1 self._check_events()
2 def _check_events(self):
"""Responde as teclas pressionadas e a eventos de mouse"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
Criamos um novo método _check_events() 2 e movemos as linhas que
verificam se o jogador clicou para fechar a janela neste novo
método.
Para chamar um método de dentro de uma classe, use a notação de
ponto com a variável self e o nome do método 1. Chamamos o
método de dentro do loop while em run_game().
Método _update_screen()
A fim de simplificar ainda mais o run_game(), moveremos o código que
atualiza a tela para um método separado chamado _update_screen():
alien_invasion.py
def run_game(self):
"""Inicia o loop principal do jogo"""
while True:
self._check_events()
self._update_screen()
self.clock.tick(60)
def _check_events(self):
-- trecho de código omitido --
def _update_screen(self):
"""Atualiza as imagens na tela e mude para a nova tela"""
self.screen.fill(self.settings.bg_color)
self.ship.blitme()
pygame.display.flip()
Movemos o código que desenha o background e a espaçonave e vira
a tela para _update_screen(). Agora o corpo do loop principal em
run_game() é mais simples. É fácil ver que estamos procurando novos
eventos, atualizando a tela e marcando o relógio em cada passagem
pelo loop.
Caso já tenha desenvolvido diversos jogos, você provavelmente
começará dividindo seu código em métodos como esses. Mas, caso
nunca tenha se deparado com um projeto como esse, é bem
provável que você não saiba como estruturar seu código a princípio.
A abordagem usada lhe fornece uma noção de um processo de
desenvolvimento na prática: você começa escrevendo o código da
forma mais simples possível e, em seguida, refatora à medida que
seu projeto fica mais complexo.
Agora que reestruturamos o código para facilitar a adição de mais
códigos, podemos trabalhar nos aspectos dinâmicos do jogo!
Pilotando a espaçonave
Agora forneceremos ao jogador a capacidade de deslocar a
espaçonave para a direita e para a esquerda. Vamos escrever um
código que responde quando o jogador pressiona a tecla de seta
para a direita ou para a esquerda. Focaremos o primeiro movimento
para a direita e, depois, empregaremos os mesmos princípios para
controlar o movimento para a esquerda. À medida que escreve mais
código, você aprenderá a controlar o movimento das imagens na
tela e a responder à entrada do usuário.
Movimento contínuo
Quando o jogador permanece com a tecla de seta para a direita
pressionada, queremos que a espaçonave continue se movendo para
a direita até o jogador soltar a tecla. Faremos com que o jogo
detecte um evento pygame.KEYUP para sabermos quando a tecla de
seta para a direita não estiver pressionada. Assim, usaremos os
eventos KEYDOWN e KEYUP com uma flag chamada moving_right para
implementar o movimento contínuo.
Quando a flag moving_right for False, a espaçonave ficará imóvel.
Quando o jogador pressionar a tecla de seta para a direita,
definiremos a flag como True e, quando o jogador soltar a tecla,
definiremos mais uma vez a flag como False.
Como a classe Ship controla todos os atributos da espaçonave,
forneceremos a ela um atributo chamado moving_right e um método
update() para verificar o status da flag moving_right. O método update()
mudará a posição da espaçonave se a flag estiver definida como True.
Chamaremos esse método uma vez em cada passagem pelo loop
while a fim de atualizar a posição da espaçonave.
ship.py
class Ship:
"""Classe para gerenciar a espaçonave"""
# Flag de movimento; começa com uma espaçonave que não está se movendo
1 self.moving_right = False
2 def update(self):
"""Atualiza a posição da espaçonave com base na flag de movimento"""
if self.moving_right:
self.rect.x += 1
def blitme(self):
-- trecho de código omitido --
Adicionamos um atributo self.moving_right ao método __init__() e, a
princípio, definimos como False 1. Em seguida, adicionamos update(),
que desloca a espaçonave para a direita se a flag for True 2. O
método update() será chamado fora da classe, logo não é considerado
um método auxiliar.
Agora precisamos modificar o _check_events() para que o moving_right
seja definido como True quando a tecla de seta para a direita for
pressionada e False quando a tecla for solta:
alien_invasion.py
def _check_events(self):
"""Responde as teclas pressionadas e a eventos de mouse"""
for event in pygame.event.get():
-- trecho de código omitido --
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
1 self.ship.moving_right = True
2 elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
Aqui, modificamos a forma como o jogo responde quando o jogador
pressiona a tecla de seta para a direita: em vez de mudar a posição
da espaçonave diretamente, apenas definimos moving_right como
True 1. Em seguida, adicionamos um novo bloco elif, que responde aos
eventos KEYUP 2. Quando o jogador solta a tecla de seta para a
direita (K_RIGHT), definimos moving_right como False.
Depois, modificamos o loop while em run_game() para que chame o
método update() da espaçonave em cada passagem pelo loop:
alien_invasion.py
def run_game(self):
"""Inicia o loop principal do jogo"""
while True:
self._check_events()
self.ship.update()
self._update_screen()
self.clock.tick(60)
A posição da espaçonave será atualizada depois de verificarmos os
eventos do teclado e antes de atualizarmos a tela. Isso possibilita
que a posição da espaçonave seja atualizada em resposta à entrada
do jogador e assegura que a posição atualizada seja usada ao
desenhar a espaçonave na tela.
Quando você executa alien_invasion.py e mantém pressionada a
tecla de seta para a direita, a espaçonave deve se mover
continuamente para a direita até que você solte a tecla.
def update(self):
"""Atualiza a posição do espaçonave com base nas flags de movimento"""
if self.moving_right:
self.rect.x += 1
if self.moving_left:
self.rect.x -= 1
Em __init__() adicionamos uma flag self.moving_left. Em update() utilizamos
dois blocos if separados, em vez de um elif, viabilizando que o valor
rect.x da espaçonave seja aumentado e depois reduzido quando
ambas as teclas de seta são pressionadas. O resultado: a
espaçonave permanece imóvel. Se usássemos elif para o movimento
à esquerda, a tecla de seta para a direita sempre teria prioridade.
Usar dois blocos if torna os movimentos mais precisos, já que o
jogador pode pressionar momentaneamente ambas as teclas ao
mudar de direção.
Temos que fazer duas adições ao_check_events():
alien_invasion.py
def _check_events(self):
"""Responde as teclas pressionadas e a eventos de mouse"""
for event in pygame.event.get():
-- trecho de código omitido --
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True
elif event.key == pygame.K_LEFT:
self.ship.moving_left = True
def __init__(self):
-- trecho de código omitido --
# Configurações da espaçonave
self.ship_speed = 1.5
Definimos o valor inicial de ship_speed como 1.5. Quando se desloca, a
posição da espaçonave é ajustada em 1.5 pixels (em vez de 1 pixel)
em cada passagem pelo loop.
Estamos usando um float para a configuração de velocidade a fim de
termos um controle mais preciso da velocidade do espaçonave
quando posteriormente aumentarmos o ritmo do jogo. No entanto,
atributos rect como x armazenam somente valores inteiros. Ou seja,
precisamos fazer algumas modificações em Ship:
ship.py
class Ship:
"""Classe para gerenciar a espaçonave"""
def __init__(self, ai_game):
"""Inicializa a espaçonave e defina sua posição inicial"""
self.screen = ai_game.screen
1 self.settings = ai_game.settings
-- trecho de código omitido --
# Flags de movimento; começa com uma espaçonave que não está se movendo
self.moving_right = False
self.moving_left = False
def update(self):
"""Atualiza a posição da espaçonave com base nas flags de movimento"""
# Atualiza o valor x da espaçoave, não o rect
if self.moving_right:
3 self.x += self.settings.ship_speed
if self.moving_left:
self.x -= self.settings.ship_speed
def blitme(self):
-- trecho de código omitido --
Criamos um atributo setting para Ship, de modo que possamos usá-lo
em update() 1. Como estamos ajustando a posição da espaçonave por
frações de pixel, é necessário atribuir a posição a uma variável que
possa ter um float atribuído. É possível usar float para definir um
atributo de um rect, só que o rect manterá apenas a parte inteira
desse valor. A fim de rastrear a posição da espaçonave com
precisão, definimos um novo self.x 2. Usamos a função float() a fim de
converter o valor de self.rect.x em um float e atribuir esse valor a self.x.
Agora, quando mudamos a posição da espaçonave em update(), o
valor de self.x é ajustado pela quantidade armazenada em
settings.ship_speed 3. Após self.x ser atualizado, utilizamos o novo valor
para atualizar self.rect.x, que controla a posição da espaçonave 4.
Apenas a parte inteira de self.x será atribuída a self.rect.x, o que é bom
para exibir a espaçonave.
Em seguida, podemos mudar o valor de ship_speed, e qualquer valor
maior que 1 fará com que a espaçonave se mova mais rápido. Isso
ajudará com que a espaçonave responda rápido o suficiente para
abater alienígenas, e nos permitirá mudar o ritmo do jogo à medida
que o jogador progride na jogabilidade.
Refatorando o _check_events()
O método _check_events() ficará cada vez maior conforme
desenvolvemos o jogo. Por isso, vamos dividi-lo em dois métodos
separados: um que lida com eventos KEYDOWN e outro que lida com
eventos KEYUP:
alien_invasion.py
def _check_events(self):
"""Responde as teclas pressionadas e a eventos de mouse"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
self._check_keydown_events(event)
elif event.type == pygame.KEYUP:
self._check_keyup_events(event)
Breve recapitulação
Na próxima seção, adicionaremos a capacidade de atirar, em um
novo arquivo chamado bullet.py e faremos algumas mudanças em
alguns dos arquivos que já estamos usando. No momento, temos
três arquivos contendo diversas classes e métodos. Para esclarecer
como o projeto está organizado, revisaremos cada um desses
arquivos antes de adicionarmos outras funcionalidades.
alien_invasion.py
O arquivo principal, alien_invasion.py, contém a classe AlienInvasion.
Essa classe cria inúmeros atributos importantes usados em todo o
jogo: as configurações são atribuídas à settings, a superfície de
exibição principal é atribuída à screen e uma instância da espaçonave
também é criada nesse arquivo. O loop principal do jogo, um loop
while, também é armazenado neste módulo. O loop while chama
_check_events(), ship.update() e _update_screen(). Marca também o relógio em
cada passagem pelo loop.
O método _check_events() detecta eventos relevantes, como teclas
pressionadas e soltas, e processa cada um desses tipos de eventos
com os métodos _check_keydown_events() e _check_keyup_events(). Por ora,
esses métodos gerenciam o movimento da espaçonave. A classe
AlienInvasion também contém _update_screen(), que redesenha a tela em
cada passagem pelo loop principal.
O arquivo alien_invasion.py é o único arquivo que você precisa
executar quando quiser jogar o jogo Invasão Alienígena. Os outros
arquivos, settings.py e ship.py, contêm código importado para este
arquivo.
settings.py
O arquivo settings.py contém a classe Settings. Essa classe tem
apenas um método __init__(), que inicializa os atributos controlando a
aparência do jogo e a velocidade da espaçonave.
ship.py
O arquivo ship.py contém a classe Ship. A classe Ship tem um método
__init__(), um método update() para gerenciar a posição da espaçonave
e um método blitme() para desenhar a espaçonave na tela. A imagem
da espaçonave é armazenada em ship.bmp, que fica na pasta de
images.
Disparando balas
Agora, vamos adicionar a capacidade de atirar. Escrevermos um
código que abre fogo, representado por um pequeno retângulo,
quando o jogador pressionar a barra de espaço. Desse modo, os
projéteis se deslocaram verticalmente na tela até desaparecerem na
parte superior da tela.
Adicionando os projéteis
No final do método __init__(), atualizaremos settings.py para incluir os
valores necessários para nova classe Bullet:
settings.py
def __init__(self):
-- trecho de código omitido --
# Configurações do projétil
self.bullet_speed = 2.0
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = (60, 60, 60)
Essas configurações criam projéteis cinza escuro com largura de
3 pixels e altura de 15 pixels. Os projéteis se deslocarão um pouco
mais rápido do que as espaçonaves.
class Bullet(Sprite):
"""Classe para gerenciar os projéteis disparados da espaçonave"""
bullet.py
def update(self):
"""Desloca o projétil verticalmente pela tela
# Atualiza a posição exata da projétil
1 self.y -= self.settings.bullet_speed
# Atualiza a posição do react
2 self.rect.y = self.y
def draw_bullet(self):
"""Desenha o projétil na tela"""
3 pygame.draw.rect(self.screen, self.color, self.rect)
O método update() gerencia a posição do projétil. Quando disparado,
um projétil se move verticalmente pela tela, o que corresponde a um
valor decrescente da coordenada y. Para atualizar a posição,
subtraímos a quantidade armazenada em settings.bullet_speed de self.y 1.
Depois, usamos o valor de self.y para definir o valor de self.rect.y 2.
A configuração bullet_speed nos possibilita aumentar a velocidade dos
projéteis à medida que o jogo avança ou conforme necessário para
refinar o comportamento do jogo. Uma vez que um projétil é
disparado, nunca mudamos o valor de sua coordenada x, logo o
projétil se deslocará verticalmente em linha reta, mesmo que a
espaçonave se mova.
Quando queremos desenhar um projétil, chamamos draw_bullet(). A
função draw.rect() preenche a parte da tela definida pelo rect do projétil
com a cor armazenada em self.color 3.
alien_invasion.py
-- trecho de código omitido --
from ship import Ship
from bullet import Bullet
Em seguida, criaremos o grupo que armazena os projéteis em
__init__():
alien_invasion.py
def __init__(self):
-- trecho de código omitido --
self.ship = Ship(self)
self.bullets = pygame.sprite.Group()
Depois, precisamos atualizar a posição dos projéteis em cada
passagem pelo loop while:
alien_invasion.py
def run_game(self):
"""Inicia o loop principal do jogo"""
while True:
self._check_events()
self.ship.update()
self.bullets.update()
self._update_screen()
self.clock.tick(60)
Ao chamarmos o update() em um grupo, o grupo automaticamente
chama o update() para cada sprite no grupo. A linha self.bullets.update()
chama o bullet.update() para cada projétil que inserimos no grupo bullets.
Disparando projéteis
Em AlienInvasion é necessário mudar _check_keydown_events() para disparar
um projétil quando o jogador pressionar a barra de espaço.
Não é necessário alterar _check_keyup_events(), pois nada acontece
quando a barra de espaço não é mais pressionada. É necessário
também modificar _update_screen() a fim de garantir que cada projétil
seja desenhado na tela antes de chamarmos flip().
É necessário nos esforçamos um pouco mais para fazer um projétil
disparar. Logo, escreveremos um novo método, _fire_bullet().
alien_invasion.py
def _check_keydown_events(self, event):
-- trecho de código omitido --
elif event.key == pygame.K_q:
sys.exit()
1 elif event.key == pygame.K_SPACE:
self._fire_bullet()
def _fire_bullet(self):
"""Cria um novo projétil e o adiciona ao grupo projéteis"""
2 new_bullet = Bullet(self)
3 self.bullets.add(new_bullet)
def _update_screen(self):
"""Atualiza as imagens na tela e muda para a nova tela"""
self.screen.fill(self.settings.bg_color)
4 for bullet in self.bullets.sprites():
bullet.draw_bullet()
self.ship.blitme()
pygame.display.flip()
-- trecho de código omitido --
Chamamos quando a barra de espaço é pressionada 1. Em
_fire_bullet()
_fire_bullet() criamos uma instância de Bullet e a chamamos de
new_bullet 2. Depois, a adicionamos ao grupo bullets usando o método
add() 3. O método add() é semelhante ao append(), mas é escrito
especificamente para grupos Pygame.
O método bullets.sprites() retorna uma lista de todos os sprites no bullets.
Para desenhar todos os projéteis disparados na tela, percorremos os
sprites em bullets por meio de um loop e chamamos draw_bullet() em
cada um deles 4. Inserimos esse loop antes da linha que desenha a
espaçonave, para que os projéteis não comecem na frente da
espaçonave.
Agora, quando executar o alien_invasion.py, você poderá deslocar a
espaçonave para a direita e para a esquerda e disparar quantos
projéteis quiser. As projéteis passam na tela e desaparecem quando
alcançam a parte superior, conforme mostrado na Figura 12.3. É
possível alterar o tamanho, a cor e a velocidade dos projéteis em
settings.py.
self._update_screen()
self.clock.tick(60)
Ao utilizarmos um loop for com uma lista (ou um grupo no Pygame),
o Python espera que a lista permaneça com o mesmo tamanho,
contanto que o loop esteja em execução. Ou seja, não podemos
remover itens de uma lista ou grupo dentro de um loop for. Assim,
temos que fazer uma cópia do grupo no loop. Usamos o método
copy() para configurar o loop for 1, assim podemos modificar o grupo
original bullets dentro do loop. Verificamos se cada projétil
desapareceu da parte superior da tela 2. Se sim, os removemos do
bullets 3. Inserimos um print() para exibir a quantidade de projéteis
atuais no jogo e verificamos se estão sendo deletados quando
alcançam a parte superior da tela 4.
Se esse código executar sem problemas, é possível ver na saída no
terminal, conforme os projéteis são disparados, que o número de
projéteis reduz a zero após desaparecerem na parte superior da tela.
Após verificar se os projéteis estão sendo deletados corretamente,
remova o print(). Se optar por não removê-lo, o jogo ficará bem lento,
pois leva mais tempo para gravar a saída no terminal do que para
desenhar gráficos na janela do jogo.
Recapitulando
Neste capítulo, aprendemos a fazer um planejamento para um jogo
e a estrutura básica de um jogo desenvolvido com o Pygame.
Aprendemos a definir a cor de um background e armazenar as
configurações em uma classe separada, em que é possível ajustá-las
com mais facilidade. Vimos como desenhar uma imagem na tela e
fornecer ao jogador controle sobre o movimento dos elementos do
jogo. Desenvolvemos elementos que se movem por conta própria,
como projéteis voando pela tela, e deletamos objetos que não são
mais necessários. Aprendemos a refatorar código reiteradamente em
um projeto para facilitar o desenvolvimento contínuo.
No Capítulo 13, adicionaremos alienígenas ao jogo Invasão
Alienígena. No final do capítulo, você poderá abrir fogo contra os
alienígenas e, quem sabe, antes de atingirem sua espaçonave!
13
capítulo
Alienígenas!
Revisando o projeto
Ao começar uma fase nova de desenvolvimento em um projeto
grande, é sempre uma boa ideia rever o planejamento e explicitar o
que quer realizar com o código que você está prestes a escrever.
Neste capítulo, faremos o seguinte:
• Vamos adicionar um único alienígena ao canto superior esquerdo
da tela, com distanciamento apropriado ao redor.
• Vamos preencher a parte superior da tela com o máximo de
alienígenas que conseguirmos inserir horizontalmente. Depois,
criaremos fileiras adicionais de alienígenas até que tenhamos uma
frota completa.
• Faremos a frota se deslocar lateralmente e para baixo até que
toda a frota seja abatida, um alienígena abata uma espaçonave ou
um alienígena se choque contra o solo. Se toda a frota for abatida,
criaremos uma frota nova. Se um alienígena abater uma
espaçonave ou se chocar contra o solo, destruiremos a
espaçonave e criaremos uma frota nova.
• Vamos restringir o número de espaçonaves que o jogador pode
usar e encerrar o jogo quando o jogador tiver utilizado o número
atribuído de espaçonaves.
Vamos refinar esse planejamento à medida que implementarmos
funcionalidades, mas isso é específico o suficiente para começarmos
a escrever código.
Em um projeto, quando começar a trabalhar em uma nova série de
funcionalidades, revise também seu código existente. Em geral,
como cada fase nova contribui sucessivamente com a complexidade
de um projeto, é melhor limpar qualquer código desorganizado ou
ineficiente. Como estamos refatorando à medida que avançamos,
não precisamos refatorar nenhum código neste momento.
class Alien(Sprite):
"""Classe para representar um único alienígena na frota"""
self._create_fleet()
Criamos um grupo para armazenar a frota de alienígenas, e
chamamos _create_fleet(), que estamos prestes a escrever.
Veja o método new_create_fleet():
alien_invasion.py
def _create_fleet(self):
"""Cria a frota de alienígenas"""
# Cria um alienígena
alien = Alien(self)
self.aliens.add(alien)
Nesse método, estamos criando uma instância de Alien e depois a
adicionando ao grupo que armazenará a frota. O alienígena será
posicionado na área default superior esquerda da tela.
Para fazer com que o alienígena apareça, precisamos chamar o
método draw() do grupo em _update_screen():
alien_invasion.py
def _update_screen(self):
-- trecho de código omitido --
self.ship.blitme()
self.aliens.draw(self.screen)
pygame.display.flip()
Ao chamar draw() em um grupo, o Pygame desenha cada elemento
no grupo na posição definida por seu atributo rect. O método draw()
exige um argumento: uma superfície na qual desenhar os elementos
do grupo. A Figura 13.2 mostra o primeiro alienígena na tela.
Figura 13.2: O primeiro alienígena aparece.
Agora que o primeiro alienígena aparece devidamente, vamos
escrever o código para desenhar uma frota inteira.
1 current_x = alien_width
2 while current_x < (self.settings.screen_width - 2 * alien_width):
3 new_alien = Alien(self)
4 new_alien.x = current_x
new_alien.rect.x = current_x
self.aliens.add(new_alien)
5 current_x += 2 * alien_width
Obtemos a largura do alienígena do primeiro alienígena que criamos
e definimos uma variável chamada current_x 1, que se refere à posição
horizontal do próximo alienígena que pretendemos posicionar na
tela. A princípio, ajustamos para uma largura alienígena, deslocando
o primeiro alienígena na frota da borda esquerda da tela.
Em seguida, começamos o loop while 2; continuaremos adicionando
alienígenas enquanto há espaço suficiente para inserir um. A fim de
determinar se há espaço para posicionar outro alienígena, faremos
uma comparação de current_x com algum valor máximo. Vejamos uma
primeira tentativa de definir esse loop:
while current_x < self.settings.screen_width:
Talvez funcione, mas isso posicionaria o último alienígena na fileira
da extremidade direita da tela. Desse modo, adicionamos uma
pequena margem no lado direito da tela. Contanto que haja, pelo
menos, duas larguras de alienígenas de espaço na borda direita da
tela, entramos no loop e adicionamos outro alienígena à frota.
Sempre que houver espaço horizontal suficiente para continuar o
loop, queremos fazer duas coisas: criar um alienígena na posição
correta e definir a posição horizontal do próximo alienígena na
fileira. Criamos um alienígena e o atribuímos a new_alien 3. Em
seguida, definimos a posição horizontal precisa para o valor atual de
current_x 4. Posicionamos também o rect do alienígena nesse mesmo
valor x, e adicionamos o alienígena novo ao grupo self.aliens.
Por último, incrementamos o valor de current_x 5. Adicionamos duas
larguras de alienígenas à posição horizontal, para passar pelo
alienígena que acabamos de adicionar e também permitir algum
espaço entre os alienígenas. O Python reavaliará a condição no início
do loop while e decidirá se há espaço para outro alienígena. Quando
não houver mais espaço, o loop terminará e teremos uma fileira
completa de alienígenas.
Ao executarmos Invasão Alienígena agora, devemos ver a primeira
fileira de alienígenas aparecer, como na Figura 13.3.
Refatorando _create_fleet()
Se o código que desenvolvemos até agora fosse o necessário para
criar uma frota, provavelmente deixaríamos _create_fleet() como está.
No entanto, temos mais trabalho a fazer. Assim, limparemos um
pouco o método. Adicionaremos um novo método auxiliar,
_create_alien(), e o chamaremos a partir de _create_fleet():
alien_invasion.py
def _create_fleet(self):
-- trecho de código omitido --
while current_x < (self.settings.screen_width - 2 * alien_width):
self._create_alien(current_x)
current_x += 2 * alien_width
Adicionando fileiras
Para terminar a frota, continuaremos adicionando mais fileiras até
ficarmos sem espaço. Usaremos um loop aninhado – faremos um
wrapper de outro loop while em torno do atual. O loop interno
posicionará horizontalmente os alienígenas em uma fileira,
concentrando-se nos valores x dos alienígenas. O loop externo
posicionará verticalmente os alienígenas, concentrando-se nos
valores y. Pararemos de adicionar fileiras quando chegarmos perto
da parte inferior da tela, deixando espaço suficiente para a
espaçonave e um pouco de espaço para começar a disparar contra
os alienígenas.
Vejamos como aninhar os dois loops while em_create_fleet():
def _create_fleet(self):
"""Cria a frota de alienígenas"""
# Cria um alienígena e continua adicionando alienígenas
# até que não haja mais espaço
# O distanciamento entre os alienígenas é de uma largura
# de alienígena e uma altura de alienígena
alien = Alien(self)
1 alien_width, alien_height = alien.rect.size
Movendo a frota
Agora, vamos fazer a frota de alienígenas se mover para a direita
por toda a tela até que alcance a borda e, em seguida, fazê-la
descer uma determinada distância e mover-se na outra direção.
Continuaremos esse movimento até que todos os alienígenas sejam
abatidos ou até que algum deles colida com a espaçonave ou
chegue à parte inferior da tela. Começaremos fazendo a frota se
mover para a direita.
def update(self):
"""Move o alienígena para a direita"""
1 self.x += self.settings.alien_speed
2 self.rect.x = self.x
Criamos um parâmetro settings em __init__() para que possamos
acessar a velocidade do alienígena em update(). Sempre que
atualizarmos a posição de um alienígena, vamos movê-lo para a
direita conforme a distância armazenada em alien_speed. Rastreamos a
posição exata do alienígena com o atributo self.x, que pode
armazenar valores floats 1. Depois, usamos o valor de self.x para
atualizar a posição do rect 2 do alienígena.
No loop while principal, já temos chamadas para atualizar as posições
da espaçonave e dos projéteis. Agora, adicionaremos uma chamada
para atualizar também a posição de cada alienígena:
alien_invasion.py
while True:
self._check_events()
self.ship.update()
self._update_bullets()
self._update_aliens()
self._update_screen()
self.clock.tick(60)
Como estamos prestes a escrever um pouco de código para
gerenciar o movimento da frota, criamos um método novo chamado
_update_aliens(). Atualizamos as posições dos alienígenas após os
projéteis serem atualizados, pois em breve verificaremos se algum
projétil abateu algum alienígena.
O lugar em que inserimos esse método no módulo não é crítico. No
entanto, para manter o código organizado, vou inseri-lo logo após
_update_bullets() para coincidir com a ordem das chamadas de método
no loop while. Vejamos a primeira versão de _update_aliens():
alien_invasion.py
def _update_aliens(self):
"""Atualiza as posições de todos os alienígenas na frota"""
self.aliens.update()
Utilizamos o método update() no grupo aliens, que chama o método
update() de cada alienígena. Agora, quando executar Invasão
Alienígena, você deve ver a frota se mover para a direita e
desaparecer na lateral da tela.
def update(self):
"""Desloca o alienígena para a direita ou para a esquerda"""
2 self.x += self.settings.alien_speed * self.settings.fleet_direction
self.rect.x = self.x
Podemos chamar o método novo check_edges() em qualquer alienígena
a fim de verificarmos se ele está na borda esquerda ou direita. Se o
atributo right de seu rect for maior ou igual ao atributo right do rect da
tela, o alienígena ficará na borda direita. Agora, se seu valor left for
menor ou igual a 0, o alienígena ficará na borda esquerda. 1. Em vez
de inserir esse teste condicional em um bloco if, o inserimos
diretamente na instrução return. Esse método retornará True se o
alienígena estiver na borda direita ou esquerda, e False se não estiver
em nenhuma das bordas.
Modificamos o método upadte() para possibilitar o movimento à
esquerda ou à direita, multiplicando a velocidade do alienígena pelo
valor de fleet_direction 2. Se fleet_direction for 1, o valor de alien_speed será
somado à posição atual do alienígena, deslocando-o para a direita;
se fleet_direction for -1, o valor será subtraído da posição do alienígena,
deslocando-o para a esquerda.
def _change_fleet_direction(self):
"""Faz toda a frota descer e mudar de direção"""
for alien in self.aliens.sprites():
3 alien.rect.y += self.settings.fleet_drop_speed
self.settings.fleet_direction *= -1
Em _check_fleet_edges() percorremos a frota com um loop e chamamos
check_edges() em cada alienígena 1. Se check_edges() retornar True,
sabemos que um alienígena está em uma borda e toda a frota
precisa mudar de direção; assim, chamamos _change_fleet_direction() e
saímos do loop 2. Em _change_fleet_direction() percorremos todos os
alienígenas com um loop e fizemos cada um descer pela tela com a
configuração fleet_drop_speed 3; em seguida, alteramos o valor de
fleet_direction multiplicando seu valor atual por -1. A linha que muda a
direção da frota não faz parte do loop for. Apesar de querermos
mudar a posição vertical de cada alienígena, só queremos mudar a
direção da frota uma vez.
Vejamos as alterações de _update_aliens():
alien_invasion.py
def _update_aliens(self):
"""Verifica se a frota está na borda e, em seguida, atualiza as posições"""
self._check_fleet_edges()
self.aliens.update()
Modificamos o método chamando _check_fleet_edges() antes de atualizar
a posição de cada alienígena.
Agora, quando executar o jogo, a frota deve se mover para frente e
para trás entre as bordas da tela e descer sempre que alcançar uma
borda. Podemos então começar a abater alienígenas e ficarmos
atentos em qualquer alienígena que acerte a espaçonave ou alcance
a parte inferior da tela.
Repopulando a frota
A principal funcionalidade do Invasão Alienígena é que os
alienígenas são implacáveis: sempre que a frota é destruída, uma
frota nova deve aparecer.
Para fazer com que uma frota nova de alienígenas apareça após uma
frota ser destruída, primeiro verificamos se o grupo aliens está vazio.
Se estiver, chamamos _create_fleet(). Realizaremos essa verificação no
final de _update_bullets(), porque é onde os alienígenas são destruídos
individualmente.
alien_invasion.py
def _update_bullets(self):
-- trecho de código omitido --
1 if not self.aliens:
# Destrói os projéteis existentes e cria uma frota nova
2 self.bullets.empty()
self._create_fleet()
Verificamos se o grupo aliens está vazio 1. Um grupo vazio é avaliado
como False. Trata-se de uma forma simples de verificar se o grupo
está vazio. Se sim, descartamos todos os projéteis existentes usando
o método empty(), que remove todos os sprites restantes de um
grupo 2. Chamamos também _create_fleet(), que preenche a tela com
alienígenas novamente.
Agora, uma frota nova aparece assim que a frota atual é destruída.
settings.py
# Configurações dos projéteis
self.bullet_speed = 2.5
self.bullet_width = 3
-- trecho de código omitido --
O melhor valor para essa configuração depende de sua experiência
no jogo, então encontre um valor que funcione para você. É possível
também ajustar outras configurações.
Refatorando _update_bullets()
Vamos refatorar _update_bullets() para que não realize tantas tarefas
diferentes. Vamos transferir o código que lida com colisões
alienígenas e projéteis para um método separado:
alien_invasion.py
def _update_bullets(self):
-- trecho de código omitido --
# Descarta os projéteis que desapareceram
for bullet in self.bullets.copy():
if bullet.rect.bottom <= 0:
self.bullets.remove(bullet)
self._check_bullet_alien_collisions()
def _check_bullet_alien_collisions(self):
"""Responde à colisões alienígenas"""
# Remove todos os projéteis e os alienígenas que tenham colidido
collisions = pygame.sprite.groupcollide(
self.bullets, self.aliens, True, True)
if not self.aliens:
# Destrói os projéteis existentes e cria uma frota nova
self.bullets.empty()
self._create_fleet()
Criamos um método novo, _check_bullet_alien_collisions(), para detectar
colisões entre projéteis e alienígenas e para responder
adequadamente à destruição de toda a frota. Isso impede que
_update_bullets() aumente muito e simplifica desenvolvimentos
posteriores.
Encerrando o jogo
Qual é a graça e o desafio de um jogo se não perdermos? Se o
jogador não disparar contra a frota rápido o bastante, faremos com
que os alienígenas destruam a espaçonave quando fizerem contato.
Ao mesmo tempo, restringiremos a quantidade de espaçonaves que
um jogador pode usar, e destruiremos a espaçonave quando um
alienígena chegar à parte inferior da tela. O jogo encerrará quando o
jogador tiver usado todas as suas espaçonaves.
def reset_stats(self):
"""Inicializa as estatísticas que podem mudar durante o jogo"""
self.ships_left = self.settings.ship_limit
Criaremos uma instância de GameStats durante todo o tempo em que
Invasão Alienígena estiver em execução, mas será necessário
redefinir algumas estatísticas sempre que o jogador iniciar um jogo
novo. Para fazer isso, inicializaremos a maioria das estatísticas no
método reset_stats(), e não diretamente em __init__(). Vamos chamar
esse método a partir de __init__() para que as estatísticas sejam
adequadamente definidas quando a instância de GameStats for
criada 1. No entanto, também podemos chamar reset_stats() sempre
que o jogador iniciar um jogo novo. Por ora, temos somente um
dado estatístico, ships_left, cujo valor mudará durante o jogo.
O número de espaçonaves com os quais o jogador começa a jogar
deve ser armazenado em settings.py como ship_limit:
settings.py
# Configurações da espaçonave
self.ship_speed = 1.5
self.ship_limit = 3
Precisamos também fazer algumas alterações em alien_invasion.py
para criar uma instância de GameStats. Primeiro, atualizaremos as
instruções import na parte superior do arquivo:
alien_invasion.py
import sys
from time import sleep
import pygame
alien_invasion.py
def __init__(self):
-- trecho de código omitido --
self.screen = pygame.display.set_mode(
(self.settings.screen_width, self.settings.screen_height))
pygame.display.set_caption("Alien Invasion")
self.ship = Ship(self)
-- trecho de código omitido --
Criamos a instância após criar a janela do jogo, mas antes de definir
outros elementos, como a espaçonave.
Quando um alienígena abater uma espaçonave, subtrairemos 1 do
número de espaçonaves restantes, destruiremos todos os
alienígenas e projéteis existentes, criaremos uma frota nova e
reposicionaremos a espaçonave no meio da tela. Pausaremos
também o jogo por um momento para que o jogador possa ver a
colisão e reagrupar antes que uma frota nova apareça.
Vamos inserir parte desse código em um método novo chamado
_ship_hit(). Chamaremos esse método a partir de _update_alienens()
quando um alienígena abater uma espaçonave:
alien_invasion.py
def _ship_hit(self):
"""Responde à espaçonave sendo abatida por um alienígena"""
# Decrementa ships_left
1 self.stats.ships_left -= 1
# Pausa
4 sleep(0.5)
O método novo _ship_hit() coordena a resposta quando um alienígena
abate uma espaçonave. Dentro de _ship_hit(), o número de
espaçonaves restantes é reduzido em 1. Depois, esvaziamos os
grupos bullets e aliens 2.
Em seguida, criamos uma frota nova e centralizamos a
espaçonave 3. (Vamos adicionar o método center_ship() à Ship daqui a
pouco.) Em seguida, após as atualizações terem sido feitas,
adicionamos uma pausa em todos os elementos do jogo, mas antes
que quaisquer alterações sejam desenhadas na tela, assim, o
jogador consegue ver que sua espaçonave foi abatida 4. A chamada
sleep() pausa a execução do programa por meio segundo, tempo
suficiente para que o jogador veja que o alienígena abateu sua
espaçonave. Quando a função sleep() termina, a execução do código
passa para o método _update_screen(), que desenha a frota nova na
tela.
Em _update_aliens(),substituímos o print() por uma chamada para
_ship_hit() quando um alienígena abate uma espaçonave:
alien_invasion.py
def _update_aliens(self):
-- trecho de código omitido --
if pygame.sprite.spritecollideany(self.ship, self.aliens):
self._ship_hit()
Vejamos o método novo center_ship(), que pertence a ship.py:
ship.py
def center_ship(self):
"""Centraliza a espaçonave na tela"""
self.rect.midbottom = self.screen_rect.midbottom
self.x = float(self.rect.x)
Centralizamos a espaçonave da mesma forma que em __init__().
Depois de centralizá-la, redefinimos o atributo self.x, que nos
possibilita rastrear a posição exata da espaçonave.
NOTA Perceba que nunca criamos mais de uma espaçonave;
criamos somente uma instância da espaçonave para todo o
jogo e a recentralizamos sempre que a espaçonave é abatida.
A estatística ships_left nos relatará quando o jogador ficar sem
espaçonaves.
Execute o jogo, dispare contra alguns alienígenas, e deixe um
alienígena abater a espaçonave. O jogo deve pausar e uma frota
nova deve aparecer com a espaçonave centralizada na parte inferior
da tela novamente.
alien_invasion.py
def _update_aliens(self):
-- trecho de código omitido --
# Detecta colisões entre alienígenas e espaçonaves
if pygame.sprite.spritecollideany(self.ship, self.aliens):
self._ship_hit()
Game Over!
Por mais que Invasão Alienígena pareça mais completo agora, um
jogo nunca acaba. O valor de ships_left fica cada vez mais negativo.
Adicionaremos a flag game_active para que possamos encerrar o jogo
quando o jogador ficar sem espaçonaves. Definiremos essa flag no
final do método __init__() em AlienInvasion:
alien_invasion.py
def __init__(self):
-- trecho de código omitido --
# Inicializa Invasão Alienígena em um estado ativo
self.game_active = True
Agora adicionamos código a _ship_hit() que define game_active como False
quando o jogador tiver usado todas as suas espaçonaves:
alien_invasion.py
def _ship_hit(self):
""" Responde à espaçonave sendo abatida por um alienígena"""
if self.stats.ships_left > 0:
# Decrementa ships_left
self.stats.ships_left -= 1
-- trecho de código omitido --
# Pausa
sleep(0.5)
else:
self.game_active = False
A maior parte de _ship_hit() permanece inalterada. Movemos todo o
código existente para um bloco if. Desse modo, testamos e
garantimos que o jogador tenha, pelo menos, uma espaçonave
restante. Se tiver, criamos uma frota nova, fazemos uma pausa e
seguimos em frente. Se o jogador não tiver mais espaçonaves,
definimos game_active como False.
if self.game_active:
self.ship.update()
self._update_bullets()
self._update_aliens()
self._update_screen()
self.clock.tick(60)
No loop principal, sempre precisamos chamar _check_events(), mesmo
que o jogo esteja inativo. Por exemplo, ainda é necessário saber se
o usuário pressiona Q para sair do jogo ou clica no botão para fechar
a janela. Continuamos também atualizando a tela para que
possamos fazer mudanças nela enquanto esperamos se o jogador
inicia um jogo novo ou não. As chamadas restantes de função
precisam acontecer apenas quando o jogo está ativo, já que se o
jogo estiver inativo, não precisamos atualizar as posições dos
elementos do jogo.
Agora, ao jogar Invasão Alienígena, o jogo deve congelar quando
você tiver usado todas as suas espaçonaves.
Recapitulando
Neste capítulo, aprendemos a adicionar um grande número de
elementos idênticos a um jogo criando uma frota de alienígenas.
Usamos loops aninhados para criar uma grade de elementos e para
criar um conjunto grande de elementos que viabilizou movimentos
para o jogo ao chamarmos o método update() de cada elemento.
Vimos como controlar a direção dos objetos na tela e a responder a
situações específicas, como quando a frota alcança a parte inferior
da tela. Detectamos e respondemos a colisões quando projéteis
abateram alienígenas e alienígenas abateram espaçonaves.
Aprendemos também a rastrear estatísticas em um jogo e usar a
flag game_active para determinar quando o jogo termina.
No próximo e último capítulo deste projeto, adicionaremos um botão
Play a fim de que o jogador possa escolher quando iniciar o jogo e
se jogará mais uma vez quando o jogo acabar. Faremos com que o
jogo fique mais rápido sempre que o jogador abater toda a frota e
adicionaremos um sistema de pontuação. O resultado final será um
jogo totalmente pronto para jogar!
14
capítulo
Pontuação
class Button:
"""Classe para criar botões para o jogo"""
alien_invasion.py
def _update_screen(self):
-- trecho de código omitido --
self.aliens.draw(self.screen)
pygame.display.flip()
Para que o botão Play fique mais visível do que todos os outros
elementos na tela, o desenhamos após todos os outros elementos
serem desenhados, mas antes de alternarmos para uma tela nova.
Incluímos o botão em um bloco if, assim o botão só aparece quando
o jogo está inativo.
Agora, quando executarmos Invasão Alienígena, devemos ver um
botão Play no centro da tela, como mostrado na Figura 14.1.
Iniciando o jogo
Para iniciar um novo jogo quando o jogador clicar em Play,
adicionamos o seguinte bloco elif ao final de _check_events() a fim de
monitorarmos os eventos do mouse sobre o botão:
alien_invasion.py
def _check_events(self):
"""Responde às teclas pressionadas e aos eventos do mouse"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
-- trecho de código omitido --
1 elif event.type == pygame.MOUSEBUTTONDOWN:
2 mouse_pos = pygame.mouse.get_pos()
3 self._check_play_button(mouse_pos)
Apesar de o Pygame detectar um evento MOUSEBUTTONDOWN quando o
jogador clica em qualquer lugar da tela 1, queremos restringir nosso
jogo para responder aos cliques do mouse apenas sobre o botão
Play. Desse modo, usamos pygame.mouse.get_pos() que retorna uma
tupla contendo as coordenadas x e y do cursor do mouse quando o
botão do mouse é clicado 2. Enviamos esses valores para o método
novo _check_play_button() 3.
Vejamos o _check_play_button(), optei por inseri-lo após _check_events():
alien_invasion.py
def _check_play_button(self, mouse_pos):
"""Inicia um jogo novo quando o jogador clica em Play"""
1 if self.play_button.rect.collidepoint(mouse_pos):
self.game_active = True
Utilizamos o rect do método collidepoint() para verificar se o ponto do
clique do mouse se sobrepõe à região definida pelo rect do botão
Play 1. Se sim, definimos game_active como True e o jogo começa!
Neste momento, você deve conseguir iniciar o jogo e jogá-lo por
completo. Quando o jogo terminar, o valor de game_active deve se
tornar False e o botão Play deve reaparecer.
Reiniciando o jogo
O código do botão que acabamos de desenvolver funciona na
primeira vez que o jogador clicar em Play. No entanto, não funciona
mais depois que o primeiro jogo termina, já que as condições que
encerram o jogo não foram redefinidas.
Para reiniciar o jogo sempre que o jogador clicar em Play,
precisamos reiniciar as estatísticas do jogo, limpar os alienígenas e
projéteis anteriores, criar uma frota nova e centralizar a espaçonave,
como podemos ver:
alien_invasion.py
def _check_play_button(self, mouse_pos):
"""Inicia um jogo novo quando o jogador clica em Play"""
if self.play_button.rect.collidepoint(mouse_pos):
# Redefine as estatísticas do jogo
1 self.stats.reset_stats()
self.game_active = True
alien_invasion.py
def _check_play_button(self, mouse_pos):
"""Inicia um jogo novo quando o jogador clica em Play"""
1 button_clicked = self.play_button.rect.collidepoint(mouse_pos)
2 if button_clicked and not self.game_active:
# Redefine as estatísticas do jogo
self.stats.reset_stats()
-- trecho de código omitido --
A flag button_clicked armazena um valor True ou False 1, e o jogo será
reiniciado somente se Play for clicado e se o jogo não estiver ativo
no momento 2. Para testar esse comportamento, inicie um jogo novo
e clique repetidamente onde o botão Play deve estar. Se tudo
funcionar como esperado, clicar na área do botão Play não deve
afetar a jogabilidade.
Passando de nível
Em nosso jogo atual, uma vez que um jogador derruba toda a frota
alienígena, o jogador passa para um nível novo, mas a dificuldade
do jogo não. Vamos animar um pouco as coisas, possibilitando que o
jogo fique mais desafiador, aumentando sua velocidade sempre que
um jogador limpar a tela.
# Configurações da espaçonave
self.ship_limit = 3
# Configurações do alienígena
self.fleet_drop_speed = 10
2 self.initialize_dynamic_settings()
Continuamos a inicializar as configurações que permanecem
constantes no método __init__(). Adicionamos a configuração
speedup_scale 1 para controlar a rapidez com que o jogo acelera: um
valor de 2 dobrará a velocidade do jogo sempre que o jogador
passar para um nível novo; um valor de 1 manterá a velocidade
constante. Um valor como 1.1 deve aumentar a velocidade o
suficiente para que o jogo fique desafiador, mas não impossível de
jogar. Por último, chamamos o método initialize_dynamic_settings() para
inicializar os valores dos atributos que precisam mudar ao longo do
jogo 2.
Vejamos o código de initialize_dynamic_settings():
settings.py
def initialize_dynamic_settings(self):
"""Inicializa as configurações que mudam ao longo do jogo"""
self.ship_speed = 1.5
self.bullet_speed = 2.5
self.alien_speed = 1.0
Pontuação
Implementaremos um sistema de pontuação para acompanhar a
pontuação do jogo em tempo real e exibir a pontuação máxima, o
nível e a quantidade de espaçonaves restantes.
Como a pontuação é uma estatística de jogo, vamos adicionar um
atributo score à GameStats.
game_stats.py
class GameStats:
-- trecho de código omitido --
def reset_stats(self):
"""Inicializa as estatísticas que podem mudar durante o jogo"""
self.ships_left = self.ai_settings.ship_limit
self.score = 0
Para redefinir a pontuação sempre que um jogo novo começar,
inicializamos score em reset_stats() em vez de __init__().
Exibindo a pontuação
Para exibir a pontuação na tela, primeiro criamos uma classe nova,
Scoreboard. Por ora, essa classe apenas exibirá a pontuação atual.
Futuramente, vamos usá-la também para informar a pontuação
máxima, o nível e a quantidade de espaçonaves restantes. Vejamos
a primeira parte da classe; salve-a como scoreboard.py:
scoreboard.py
import pygame.font
class Scoreboard:
"""Classe para exibir informações de pontuação"""
scoreboard.py
def prep_score(self):
"""Transforma a pontuação em uma imagem renderizada"""
1 score_str = str(self.stats.score)
2 self.score_image = self.font.render(score_str, True,
self.text_color, self.settings.bg_color)
Criando um scoreboard
A fim de exibir a pontuação, criaremos uma instância de Scoreboard
em AlienInvasion. Primeiro, atualizaremos as instruções import:
alien_invasion.py
-- trecho de código omitido --
from game_stats import GameStats
from scoreboard import Scoreboard
-- trecho de código omitido --
Em seguida, criamos uma instância de Scoreboard em __init__():
alien_invasion.py
def __init__(self):
-- trecho de código omitido --
pygame.display.set_caption("Alien Invasion")
alien_invasion.py
def _update_screen(self):
-- trecho de código omitido --
self.aliens.draw(self.screen)
# Configurações de pontuação
self.alien_points = 50
Vamos aumentar o valor dos pontos de cada alienígena conforme o
jogo avança. Para garantir que esse valor de ponto seja redefinido
sempre que um jogo novo for iniciado, definimos o valor em
initialize_dynamic_settings().
if collisions:
self.stats.score += self.settings.alien_points
self.sb.prep_score()
-- trecho de código omitido --
Quando um projétil atinge um alienígena, o Pygame retorna um
dicionário collisions. Verificamos se o dicionário existe e, se existir, o
valor do alienígena é somado à pontuação. Depois, chamamos
prep_score() a fim de criar uma imagem nova para a pontuação
atualizada.
Agora, quando jogar Invasão Alienígena, você deve ser capaz de
acumular pontos!
Redefinindo a pontuação
No momento, estamos somente preparando uma nova pontuação
após um alienígena ser abatido, o que funciona na maior parte do
jogo. No entanto, ao iniciarmos um jogo novo, ainda veremos nossa
pontuação do jogo anterior até que o primeiro alienígena seja
abatido.
Podemos corrigir isso preparando a pontuação ao iniciar um jogo
novo:
alien_invasion.py
def _check_play_button(self, mouse_pos):
-- trecho de código omitido --
if button_clicked and not self.game_active:
-- trecho de código omitido --
# Redefine as estatísticas do jogo
self.stats.reset_stats()
self.sb.prep_score()
-- trecho de código omitido --
Chamamos prep_score() após redefinir as estatísticas do jogo ao iniciar
um jogo novo. Isso prepara o scoreboard com a pontuação de 0.
def __init__(self):
-- trecho de código omitido --
# A rapidez com que o jogo acelera
self.speedup_scale = 1.1
# Com que rapidez os valores dos pontos alienígenas aumentam
1 self.score_scale = 1.5
self.initialize_dynamic_settings()
def initialize_dynamic_settings(self):
-- trecho de código omitido --
def increase_speed(self):
"""Aumenta configurações de velocidade e valores dos pontos alienígenas"""
self.ship_speed *= self.speedup_scale
self.bullet_speed *= self.speedup_scale
self.alien_speed *= self.speedup_scale
settings.py
def increase_speed(self):
-- trecho de código omitido --
self.alien_points = int(self.alien_points * self.score_scale)
print(self.alien_points)
O novo valor de ponto deve aparecer no terminal sempre que
passarmos para o próximo nível.
NOTA Não deixe de remover o print() após verificar se o valor do
ponto está aumentando, ou isso pode afetar o desempenho
do jogo e distrair o jogador.
Arredondando a pontuação
Já que maioria dos jogos de tiro no estilo arcade apresenta as
pontuações como múltiplos de 10, faremos a mesma coisa com a
nossa. Além disso, formataremos a pontuação para incluir vírgulas
como separadores de números grandes. Faremos essa mudança em
Scoreboard:
scoreboard.py
def prep_score(self):
"""Transforma a pontuação em uma imagem renderizada"""
rounded_score = round(self.stats.score, -1)
score_str = f"{rounded_score:,}"
self.score_image = self.font.render(score_str, True,
self.text_color, self.settings.bg_color)
-- trecho de código omitido --
A função round() normalmente arredonda um float para um número
definido de casas decimais fornecido como o segundo argumento.
Apesar disso, quando passamos um número negativo como segundo
argumento, round() arredondará o valor para o 10, 100, 1.000 mais
próximo, e assim por diante. Esse código instruí o Python a
arredondar o valor de stats.score para o múltiplo mais próximo e
atribuí-lo a rounded_score.
Em seguida, usamos um especificador de formato na f-string para a
pontuação. Um especificador de formato é uma sequência especial
de caracteres que modifica a forma como o valor de uma variável é
apresentado. Aqui, a sequência:, instruí o Python a inserir vírgulas
em locais adequados no valor numérico fornecido. Isso resulta em
strings como 1,000,000 em vez de 1000000.
Agora, quando executamos o jogo, devemos ver uma pontuação
elegantemente formatada e arredondada, mesmo quando acumular
muitos pontos, conforme mostrado na Figura 14.3.
scoreboard.py
def prep_high_score(self):
"""Transforma a pontuação em uma imagem renderizada"""
1 high_score = round(self.stats.high_score, -1)
high_score_str = f"{high_score:,}"
2 self.high_score_image = self.font.render(high_score_str, True,
self.text_color, self.settings.bg_color)
Exibindo o nível
Para exibir o nível do jogador no jogo, é necessário primeiro um
atributo em GameStats representando o nível atual. Para redefinir o
nível no início de cada jogo novo, inicialize-o em reset_stats():
game_stats.py
def reset_stats(self):
"""Inicializa as estatísticas que podem mudar durante o jogo"""
self.ships_left = self.settings.ship_limit
self.score = 0
self.level = 1
Para que Scoreboard exiba o nível atual, chamamos um método novo,
prep_level(), a partir de __init__():
scoreboard.py
def __init__(self, ai_game):
-- trecho de código omitido --
self.prep_high_score()
self.prep_level()
Vejamos o prep_level():
scoreboard.py
def prep_level(self):
"""Transforma o nível em uma imagem renderizada"""
level_str = str(self.stats.level)
1 self.level_image = self.font.render(level_str, True,
self.text_color, self.settings.bg_color)
scoreboard.py
def show_score(self):
"""Desenha as pontuações e o nível na tela"""
self.screen.blit(self.score_image, self.score_rect)
self.screen.blit(self.high_score_image, self.high_score_rect)
self.screen.blit(self.level_image, self.level_rect)
Essa linha nova desenha a imagem do nível na tela.
Vamos incrementar stats.level e atualizar a imagem do nível em
_check_bullet_alien_collisions():
alien_invasion.py
def _check_bullet_alien_collisions(self):
-- trecho de código omitido --
if not self.aliens:
# Destrói os projéteis existentes e cria uma frota nova
self.bullets.empty()
self._create_fleet()
self.settings.increase_speed()
# Aumenta o nível
self.stats.level += 1
self.sb.prep_level()
Se uma frota for destruída, incrementamos o valor de stats.level e
chamamos prep_level() para garantir que o nível novo seja
adequadamente exibido.
A fim de garantir que a imagem do nível seja devidamente
atualizada no início de um jogo novo, chamamos também prep_level()
quando o jogador clica no botão Play:
alien_invasion.py
def _check_play_button(self, mouse_pos):
-- trecho de código omitido --
if button_clicked and not self.game_active:
-- trecho de código omitido --
self.sb.prep_score()
self.sb.prep_level()
-- trecho de código omitido --
Chamamos prep_level() logo após chamar prep_score().
1 class Ship(Sprite):
"""Classe para gerenciar a espaçonave"""
scoreboard.py
import pygame.font
from pygame.sprite import Group
scoreboard.py
def __init__(self, ai_game):
"""Inicializa os atributos de pontuação"""
self.ai_game = ai_game
self.screen = ai_game.screen
-- trecho de código omitido --
self.prep_level()
self.prep_ships()
Atribuímos a instância do jogo a um atributo, pois precisaremos dele
para criar algumas espaçonaves. Chamamos prep_ships() após a
chamada para prep_level().
Vamos conferir prep_ships():
scoreboard.py
def prep_ships(self):
"""Mostra as espaçonaves restantes"""
1 self.ships = Group()
2 for ship_number in range(self.stats.ships_left):
ship = Ship(self.ai_game)
3 ship.rect.x = 10 + ship_number * ship.rect.width
4 ship.rect.y = 10
5 self.ships.add(ship)
O método prep_ships() cria um grupo vazio, self.ships, a fim de
armazenar as instâncias da espaçonave 1. Para preencher esse
grupo, um loop é executado uma vez em cada espaçonave restante
do jogador 2. Dentro do loop, criamos uma espaçonave nova e
definimos o valor da coordenada x de cada espaçonave para que
apareçam uma ao lado da outra, com uma margem de 10 pixels ao
lado esquerdo do grupo de espaçonaves 3. Definimos o valor da
coordenada y 10 pixels abaixo da parte superior da tela a fim de que
as espaçonaves apareçam no canto superior esquerdo da tela 4. Em
seguida, adicionamos cada espaçonave nova ao grupo ships 5.
Agora, é necessário desenhar as espaçonaves na tela.
scoreboard.py
def show_score(self):
"""Desenha as pontuações, o nível e as espaçonaves na tela"""
self.screen.blit(self.score_image, self.score_rect)
self.screen.blit(self.high_score_image, self.high_score_rect)
self.screen.blit(self.level_image, self.level_rect)
self.ships.draw(self.screen)
Para exibir as espaçonaves na tela, chamamos draw() no grupo, e o
Pygame desenha cada uma delas.
Para mostrar quantas espaçonaves o jogador têm de início,
chamamos prep_ships() quando um jogo novo começa. Fazemos isso
em _check_play_button() em AlienInvasion:
alien_invasion.py
def _check_play_button(self, mouse_pos):
-- trecho de código omitido --
if button_clicked and not self.game_active:
-- trecho de código omitido --
self.sb.prep_level()
self.sb.prep_ships()
-- trecho de código omitido --
Chamamos também prep_ships() quando uma espaçonave é abatida,
assim, atualizamos a exibição de imagens da espaçonave quando o
jogador perde uma delas:
alien_invasion.py
def _ship_hit(self):
"""Responde à espaçonave sendo abatida por um alienígena"""
if self.stats.ships_left > 0:
# Decrementa ships_left e atualiza scoreboard
self.stats.ships_left -= 1
self.sb.prep_ships()
-- trecho de código omitido --
Chamamos prep_ships() após decrementar o valor de ships_left. Desse
modo, a quantidade correta de espaçonaves restantes é exibida
sempre que uma delas é destruída.
A Figura 14.6 mostra o sistema de pontuação completo, com as
espaçonaves restantes exibidas no canto superior esquerdo da tela.
Figura 14.6: O sistema completo de pontuação do Jogo Invasão
Alienígena.
FAÇA VOCÊ MESMO
14.5 Recorde de pontuação máxima: a pontuação máxima é redefinida sempre que um
jogador fecha e reinicia o jogo Invasão Alienígena. Corrija isso escrevendo a pontuação
máxima em um arquivo antes de chamar sys.exit() e lendo-a ao inicializar seu valor em
GameStats.
14.6 Refatoração: Procure métodos que estão fazendo mais de uma tarefa e refatore-os
a fim de organizar seu código e torná-lo eficiente. Por exemplo, transfira uma parte do
código em _check_bullet_alien_collisions(), que inicia um nível novo quando a frota de
alienígenas é destruída, para uma função chamada start_new_level(). Transfira também
as quatro chamadas de método separadas no método __init__() em Scoreboard para
um método chamado prep_images() para reduzir __init__(). O método prep_images()
também pode ajudar a simplificar_check_play_button() ou start_game() se você já tiver
refatorado _check_play_button().
NOTA Antes de tentar refatorar o projeto, confira o Apêndice D para saber como
restaurá-lo ao estado anterior caso introduza bugs durante a refatoração.
14.7 Desenvolvendo mais o jogo: Pense em uma forma de expandir Invasão Alienígena.
Por exemplo, é possível programar os alienígenas para que disparem contra sua
espaçonave. É possível também adicionar escudos para que sua espaçonave seja
protegida e que possam ser destruídos pelos projéteis de ambos os lados. Ou você
pode usar o módulo pygame.mixer para adicionar efeitos sonoros, como explosões e
sons de tiro.
14.8 Disparos laterais, versão final: Continue desenvolvendo Disparos Laterais, usando
tudo o que fizemos neste projeto. Adicione um botão Play, acelere a velocidade do jogo
adequadamente e desenvolva um sistema de pontuação. Faça questão de refatorar seu
código conforme o programa e procure oportunidades para personalizar o jogo além do
que foi mostrado neste capítulo.
Recapitulando
Neste capítulo, aprendemos a implementar um botão Play para
iniciar um jogo novo. Vimos também como detectar eventos do
mouse e ocultar o cursor em jogos ativos. É possível usar o que
aprendemos para criar outros botões, como um botão Help, para
exibir instruções sobre jogos. Além disso, aprendemos como
modificar a velocidade de um jogo à medida que avança, como
implementar um sistema de pontuação gradativa e como exibir
informações de formas textuais e não textuais.
15
capítulo
Gerando dados
Instalando a Matplotlib
Para utilizarmos a Matplotlib em nosso conjunto inicial de
visualizações, precisaremos instalá-la usando o pip, assim como
fizemos com o pytest no Capítulo 11 (confira “Instalando o pytest
com pip” na página 263).
Para instalar a Matplotlib, digite o seguinte comando em um prompt
de terminal:
$ python -m pip install --user matplotlib
Caso utilize um comando diferente de python para executar
programas ou iniciar uma sessão de terminal, como python3, o
comando será assim:
$ python3 -m pip install --user matplotlib
Para conferir os tipos de visualizações possíveis com a Matplotlib,
visite a página inicial da Matplotlib em https://matplotlib.org e clique em
Plot types. Ao clicar em uma visualização na galeria, é possível ver
o código usado para gerar o gráfico.
1 fig, ax = plt.subplots()
ax.plot(squares)
plt.show()
Primeiro, importamos o módulo pyplot usando o alias plt para não
precisarmos digitar pyplot repetidas vezes. (Como você verá essa
convenção com frequência em exemplos online, vamos usá-la aqui.)
O módulo pyplot contém uma série de funções que ajudam a gerar
diagramas e gráficos.
Criamos uma lista chamada squares para armazenar os dados que
plotaremos. Depois, adotamos outra convenção comum da
Matplotlib chamando a função subplots() 1. Essa função pode gerar um
ou mais gráficos na mesma figura. A variável fig representa toda a
figura, que é a coleção de gráficos gerados. A variável ax representa
um único gráfico na figura; usaremos essa variável na maioria das
vezes ao definir e personalizar um único gráfico.
Em seguida, usamos o método plot(), que tenta plotar os dados
fornecidos de maneira relevante. A função plt.show() abre o
visualizador da Matplotlib e exibe o gráfico, como mostrado na
Figura 15.1. O visualizador possibilita ampliar e navegar pelo gráfico,
sendo possível salvar todas as imagens do gráfico que quisermos
clicando no ícone do disquete.
Figura ١٥.1: Um dos gráficos mais simples que podemos gerar com
a Matplotlib.
fig, ax = plt.subplots()
1 ax.plot(squares, linewidth=3)
plt.show()
O parâmetro linewidth controla a espessura da linha que plot() gera 1.
Uma vez que um gráfico foi gerado, existem muitos métodos
disponíveis para modificá-los antes de apresentá-lo. O método
set_title() define um título geral para o gráfico 2. Os parâmetros fontsize,
que aparecem repetidas vezes pelo código, controlam o tamanho do
texto em diversos elementos do gráfico.
Os métodos set_xlabel() e set_ylabel() possibilitam definir um título para
cada um dos eixos 3, e o método tick_params() estiliza as marcações 4.
Aqui, tick_params() define o tamanho da fonte dos rótulos de marcação
de escala para 14 em ambos os eixos.
Como podemos ver na Figura 15.2, o gráfico resultante é mais fácil
de ler. O tipo de rótulo é maior, e as linhas do gráfico são mais
espessas. Não raro, vale a pena testar esses valores para ver o que
funciona melhor no gráfico resultante.
Figura 15.2: O gráfico fica bem mais fácil de ler agora.
Corrigindo o gráfico
Agora que conseguimos ler melhor o gráfico, podemos ver que os
dados não estão devidamente plotados. Veja no final do gráfico que
o quadrado de 4,0 é mostrado como 25! Vamos arrumar isso.
Ao fornecermos uma única sequência de números, o plot() faz a
suposição de que o primeiro ponto de dados corresponde a um
valor x de 0, só que nosso primeiro ponto corresponde a um valor x
de 1. É possível sobrescrever o comportamento default fornecendo
para plot() os valores de entrada e de saída usados para calcular os
quadrados:
mpl_squares.py
import matplotlib.pyplot as plt
input_values = [1, 2, 3, 4, 5]
squares = [1, 4, 9, 16, 25]
fig, ax = plt.subplots()
ax.plot(input_values, squares, linewidth=3)
input_values = [1, 2, 3, 4, 5]
squares = [1, 4, 9, 16, 25]
plt.style.use('seaborn')
fig, ax = plt.subplots()
-- trecho de código omitido --
Esse código gera o gráfico mostrado na Figura 15.4. Já que uma
grande variedade de estilos está disponível; brinque com esses
estilos para encontrar alguns de que goste.
scatter_squares.py
import matplotlib.pyplot as plt
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(2, 4)
plt.show()
Vamos estilizar a saída para que fique mais relevante. Vamos
adicionar um título, rotular os eixos e garantir que todo o texto seja
grande o suficiente para ler:
import matplotlib.pyplot as plt
plt.style.use('seaborn')
fig, ax = plt.subplots()
1 ax.scatter(2, 4, s=200)
plt.show()
Chamamos scatter() e usamos o argumentos para definir o tamanho
dos pontos usados para desenhar o gráfico 1. Agora, quando
executamos scatter_squares.py, veremos um único ponto no meio
do gráfico, conforme mostrado na Figura 15.5.
Figura 15.5: Plotando um único ponto.
x_values = [1, 2, 3, 4, 5]
y_values = [1, 4, 9, 16, 25]
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, s=100)
plt.style.use('seaborn')
fig, ax = plt.subplots()
2 ax.scatter(x_values, y_values, s=10)
plt.show()
Começamos com um intervalo de valores x contendo os números de
1 a 1.000 1. Em seguida, uma list comprehension gera os valores y
percorrendo os valores x com um loop (for x in x_values), elevando ao
quadrado cada número (x**2) e atribuindo os resultados a y_values.
Depois, passamos as listas de entrada e de saída para scatter() 2.
Como se trata de um conjunto grande de dados, usamos um
tamanho de ponto menor.
Antes de mostrar o gráfico, usamos o método axis() para especificar o
intervalo de cada eixo 3. O método axis() exige quatro valores: os
valores mínimo e máximo para o eixo x e para o eixo y. Aqui,
executamos o eixo x de 0 a 1.100 e o eixo y de 0 a 1.100.000. A
Figura 15.7 mostra o resultado.
plt.show()
O método ticklabel_format() possibilita sobrescrever o estilo default de
marcação de escala dos rótulos para qualquer gráfico.
Usando um colormap
Um colormap é uma sequência de cores em um gradiente que oscila
de uma cor inicial para uma cor final. Nas visualizações, os
colormaps são usados para destacar padrões nos dados. Por
exemplo, podemos transformar valores baixos em uma cor clara e
valores altos em uma cor mais escura. O uso de um colormap
garante que todos os pontos na visualização variem de forma suave
e com acurácia ao longo de uma escala de cores bem estruturada.
O módulo pyplot inclui um conjunto buit-in de colormaps. Para utilizar
um desses colormaps, precisamos especificar como o pyplot deve
atribuir uma cor a cada ponto no conjunto de dados. Veja como
atribuir uma cor a cada ponto, com base em seu valor y:
scatter_squares.py
-- trecho de código omitido --
plt.style.use('seaborn')
fig, ax = plt.subplots()
ax.scatter(x_values, y_values, c=y_values, cmap=plt.cm.Blues, s=10)
Passeios aleatórios
Nesta seção, utilizaremos o Python para gerar dados a partir de um
passeio aleatório e, em seguida, usaremos a Matplotlib para criar
uma representação visualmente elegante desses dados. Um passeio
aleatório é um caminho determinado por uma série de decisões
simples, e cada uma delas é deixada inteiramente ao acaso. Imagine
um passeio aleatório como o caminho que uma formiga confusa
tomaria se desse cada passo em uma direção aleatória.
Passeios aleatórios apresentam usos práticos na natureza, na física,
biologia, química e economia. Por exemplo, um grão de pólen que
flutua sobre uma gota de água vai de um lado para o outro pela
superfície aquosa porque é constantemente empurrado por
moléculas de água. Já que movimento molecular em uma gota de
água é aleatório, o caminho que um grão de pólen traça na
superfície é um passeio aleatório. O código que desenvolveremos a
seguir modela muitas situações do mundo cotidiano.
Criando a classe RandomWalk
Para criarmos um passeio aleatório, primeiro criaremos uma classe
RandomWalk, que tomará decisões aleatórias sobre qual direção o
passeio deve tomar. A classe deve receber três atributos: uma
variável para rastrear o número de pontos no passeio e duas listas
para armazenar as coordenadas x e y de cada ponto no passeio.
Precisaremos somente de dois métodos para a classe RandomWalk: o
método __init__() e fill_walk(), que calculará os pontos no passeio.
Começaremos com o método __init__():
random_walk.py
1 from random import choice
class RandomWalk:
"""Classe para gerar passeios aleatórios"""
Escolhendo direções
Vamos utilizar o método para determinar a sequência
fill_walk()
completa de pontos no passeio. Adicione esse método a
random_walk.py:
random_walk.py
def fill_walk(self):
"""Calcula todos os pontos do passeio"""
self.x_values.append(x)
self.y_values.append(y)
Primeiro, definimos um loop que é executado até que o passeio seja
preenchido com o número correto de pontos 1. A parte principal de
fill_walk() orienta o Python como simular quatro decisões aleatórias:
Será que o passeio vai para a direita ou para a esquerda? Qual é a
distância que o passeio percorrerá nessa direção? O passeio se
deslocará para cima ou para baixo? Qual é a distância que o passeio
percorrerá nessa direção.
Usamos choice([1, -1]) a fim de escolher um valor para x_direction, que
retorna 1 para movimento à direita ou -1 para movimento à
esquerda 2. Depois, choice([0, 1, 2, 3, 4]) seleciona aleatoriamente uma
distância a ser percorrida em determinada direção. Atribuímos esse
valor a x_distance. Incluir um 0 possibilita passos com movimento ao
longo de apenas um eixo.
Determinamos a extensão de cada passo nas direções x e y
multiplicando a direção do movimento pela distância escolhida 3 4.
Um resultado positivo para x_step significa movimento à direita, um
resultado negativo significa movimento à esquerda e 0 significa
movimento vertical. Um resultado positivo para y_step significa
movimento para cima, negativo significa movimento para baixo e 0
significa movimento horizontal. Se os valores de x_step e y_step
forem 0, o passeio não se movimenta; quando isso acontece,
continuamos interando com o loop 5.
A fim de obtermos o próximo valor x para o passeio, somamos o
valor em x_step ao último valor armazenado em x_values 6 e fazemos o
mesmo com os valores y. Ao termos as coordenadas do novo ponto,
as anexamos a x_values e a y_values.
Estilizando o passeio
Nesta seção, vamos personalizar nossos gráficos a fim de destacar
as particularidades importantes de cada passeio e reduzir o destaque
dos elementos que podem ocasionar distração. Para tanto,
identificamos as particularidades que queremos enfatizar, como onde
o passeio começou e terminou, e o caminho percorrido. Depois,
identificamos as particularidades a serem enfatizadas, como
marcações de escala e rótulos. O resultado deve ser uma
representação visual simples que transmita claramente o caminho
percorrido em cada passeio aleatório.
Colorindo os pontos
Vamos utilizar um colormap para mostrar a ordem dos pontos no
passeio e vamos remover o contorno preto de cada ponto para que
a cor dos pontos fique mais nítida. Para colorir os pontos conforme
sua posição no passeio, passamos ao argumento c uma lista
contendo a posição de cada ponto. Como os pontos são plotados em
ordem, a lista contém apenas os números de 0 a 4.999:
rw_visual.py
-- trecho de código omitido --
while True:
# Cria um random walk
rw = RandomWalk()
rw.fill_walk()
plt.show()
-- trecho de código omitido --
A fim de mostrar o ponto inicial, plotamos o ponto (0, 0) em verde e
em um tamanho maior (s=100) do que o restante dos pontos. Para
destacar o ponto final, plotamos os últimos valores x e y em
vermelho com um tamanho de 100 também. Não se esqueça de
inserir esse código logo antes da chamada para plt.show(), pois, assim,
os pontos inicial e final são desenhados em cima de todos os outros
pontos.
Ao executarmos esse código, conseguimos identificar exatamente
onde cada passeio começa e termina. Se os pontos finais não se
destacarem de modo evidente, ajuste a cor e o tamanho até se
destacarem.
Limpando os eixos
Removeremos os eixos desse gráfico para que não causem distração
do caminho de cada passeio. Vejamos como ocultar os eixos:
rw_visual.py
-- trecho de código omitido --
while True:
-- trecho de código omitido --
ax.scatter(rw.x_values[-1], rw.y_values[-1], c='red', edgecolors='none',
s=100)
# Remove os eixos
ax.get_xaxis().set_visible(False)
ax.get_yaxis().set_visible(False)
plt.show()
-- trecho de código omitido --
Para modificar os eixos, usamos os métodos ax.get_xaxis() e ax.get_yaxis()
a fim de obter cada eixo e, em seguida, encadeamos o método
set_visible() para que cada eixo ficasse invisível. À medida que trabalha
cada vez mais com visualizações, você verá muito esse tipo de
encadeamento de métodos para personalizar diferentes aspectos de
uma visualização.
Agora, execute rw_visual.py; você deve ver uma série de gráficos
sem eixos.
Instalando o Plotly
Instale Plotly usando pip, assim como fizemos com a Matplotlib:
$ python -m pip install --user plotly
$ python -m pip install --user pandas
O Plotly Express depende do pandas, uma biblioteca para trabalhar
de forma eficiente com dados. Ou seja, é necessário instalar o
pandas também. Se usou python3 ou outra coisa ao instalar a
Matplotlib, lembre-se de usar o mesmo comando aqui.
Para conferir quais tipos de visualizações são possíveis com o Plotly,
visite a galeria de tipos de gráficos em https://plotly.com/python. Cada
exemplo tem um código-fonte, assim você pode ver como Plotly gera
as visualizações.
class Die:
"""Classe que representa um único dado"""
def roll(self):
""""Retorna um valor aleatório entre 1 e o número de lados"""
2 return randint(1, self.num_sides)
O método __init__() recebe um argumento opcional 1. Com a classe
Die, quando uma instância de nosso dado é criada, o número de
lados será seis, se não incluirmos nenhum argumento. Se incluirmos
um argumento, esse valor definirá o número de lados no dado. (Os
dados são nomeados pelo número de lados: um dado de seis lados é
um D6, um dado de oito lados é um D8, e assim por diante.)
O método roll() usa a função randint() para retornar um número
aleatório entre 1 e o número de lados 2. Essa função pode retornar o
valor inicial (1), o valor final (num_sides) ou qualquer número inteiro
entre os dois.
Lançando o dado
Antes de criarmos uma visualização com base na classe Die,
lançaremos um D6, exibiremos os resultados e verificaremos se os
resultados parecem razoáveis:
die_visual.py
from die import Die
# Cria um D6
1 die = Die()
print(results)
Criamos uma instância de Die com os seis lados como default 1. Em
seguida, lançamos o dado 100 vezes 2 e armazenamos o resultado de
cada lançamento na lista results. Vejamos um exemplo do conjunto de
resultados:
[4, 6, 5, 6, 1, 5, 6, 3, 5, 3, 5, 3, 2, 2, 1, 3, 1, 5, 3, 6, 3, 6, 5, 4,
1, 1, 4, 2, 3, 6, 4, 2, 6, 4, 1, 3, 2, 5, 6, 3, 6, 2, 1, 1, 3, 4, 1, 4,
3, 5, 1, 4, 5, 5, 2, 3, 3, 1, 2, 3, 5, 6, 2, 5, 6, 1, 3, 2, 1, 1, 1, 6,
5, 5, 2, 2, 6, 4, 1, 4, 5, 1, 1, 1, 4, 5, 3, 3, 1, 3, 5, 4, 5, 6, 5, 4,
1, 5, 1, 2]
Ao verificarmos rapidamente esses resultados, podemos ver que a
classe Die parece estar funcionando. Como estamos vendo os valores
1 e 6, sabemos que os possíveis valores menores e maiores estão
sendo retornados, e como não vemos 0 ou 7, sabemos que todos os
resultados estão no intervalo adequado. Vemos também cada
número de 1 a 6, o que sinaliza que todos os resultados possíveis
estão representados. Determinaremos exatamente quantas vezes
cada número aparece.
Analisando os resultados
Vamos analisar os resultados do lançamento de um D6 contando
quantas vezes tiramos cada número:
die_visual.py
-- trecho de código omitido --
# Realiza alguns testes e armazena os resultados em uma lista
results = []
1 for roll_num in range(1000):
result = die.roll()
results.append(result)
# Analisa os resultados
frequencies = []
2 poss_results = range(1, die.num_sides+1)
for value in poss_results:
3 frequency = results.count(value)
4 frequencies.append(frequency)
print(frequencies)
Como não estamos mais exibindo os resultados, é possível aumentar
o número de lançamentos simulados para 1000 1. A fim de
analisarmos os lançamentos, criamos uma lista vazia frequencies para
armazenar o número de vezes que cada valor é tirado. Em seguida,
geramos todos os resultados possíveis que poderíamos obter; nesse
exemplo, são todos os números a partir de 1 até quantos lados die
tiver 2. Percorremos com um loop os valores possíveis, contamos
quantas vezes cada número aparece em results 3 e, em seguida,
anexamos esse valor à frequencies 4. Vamos exibir essa lista antes de
criarmos uma visualização:
[155, 167, 168, 170, 159, 181]
Ao que tudo indica, os resultados são razoáveis: vemos seis
frequências, uma para cada número possível quando lançamos
um D6. Vemos também que nenhuma frequência é
significativamente maior do que qualquer outra. Agora,
visualizaremos esses resultados.
Criando um histograma
Agora que temos os dados que queremos, podemos gerar uma
visualização com apenas algumas linhas de código usando o Plotly
Express:
die_visual.py
import plotly.express as px
# Visualiza os resultados
fig = px.bar(x=poss_results, y=frequencies)
fig.show()
Primeiro, importamos o módulo plotly.express, usando o alias
convencional px. Depois, usamos a função px.bar() para criar um
gráfico de barras. No uso mais simples dessa função, precisamos
somente passar um conjunto de valores x e um conjunto de valores
y. Aqui, os valores x são os resultados possíveis do lançamento de
um único dado, e os valores y são as frequências para cada
resultado possível.
A linha final chama fig.show(), que informa ao Plotly para renderizar o
gráfico resultante como um arquivo HTML e abrir esse arquivo em
uma nova guia do navegador. O resultado é mostrado na
Figura 15.12.
Figura 15.12: Gráfico inicia gerado pelo Plotly Express.
É um gráfico muito simples e, com certeza, não está completo. Mas
é exatamente assim que o Plotly Express deve ser usado; basta
escrever algumas linhas de código, verificar o visual do gráfico e
garantir que represente os dados da maneira que queremos. Se
gostar do que está vendo, pode passar para a personalização de
elementos do gráfico, como rótulos e estilos. Mas se quiser explorar
outros tipos possíveis de gráfico, pode seguir em frente agora, sem
passar tempo demais com a personalização. Sinta-se à vontade para
testar as coisas, alterando px.bar() para algo como px.scatter() ou px.line().
É possível encontrar uma lista completa dos tipos de gráficos
disponíveis em https://plotly.com/python/plotly-express.
Esse gráfico é dinâmico e interativo. Se alterarmos o tamanho da
janela do navegador, o gráfico será redimensionado para
corresponder ao espaço disponível. Se passarmos o mouse sobre
qualquer uma das barras, veremos um pop-up destacando os dados
específicos relacionados a essa barra.
Personalizando o o gráfico
Agora que sabemos que temos o tipo correto de gráfico e nossos
dados estão sendo representados com acurácia, podemos nos
concentrar em adicionar os rótulos e os estilos adequados para o
gráfico.
A primeira forma de personalizar um gráfico com o Plotly é usar
alguns parâmetros opcionais na chamada inicial que gera o gráfico,
nesse caso, px.bar(). Vejamos como adicionar um título geral e um
rótulo para cada eixo:
die_visual.py
-- trecho de código omitido --
# Visualiza os resultados
1 title = "Results of Rolling One D6 1,000 Times"
2 labels = {'x': 'Result', 'y': 'Frequency of Result'}
fig = px.bar(x=poss_results, y=frequencies, title=title, labels=labels)
fig.show()
De início, definimos o título que queremos, aqui atribuído a title 1.
Para definir os rótulos dos eixos, escrevemos um dicionário 2. As
chaves do dicionário se referem aos rótulos que queremos
personalizar, e os valores são os rótulos personalizados que
queremos usar. Aqui, fornecemos ao eixo x o rótulo Result e ao eixo y
o rótulo Frequency of Result. Agora, a chamada para px.bar() inclui os
argumentos opcionais title e labels.
Então, quando é gerado, o gráfico inclui um título e um rótulo
adequados para cada eixo, como mostrado na Figura 15.13.
Figura 15.13: Um simples gráfico de barras criado com Plotly.
# Visualiza os resultados
title = "Results of Rolling Two D6 Dice 1,000 Times"
labels = {'x': 'Result', 'y': 'Frequency of Result'}
fig = px.bar(x=poss_results, y=frequencies, title=title, labels=labels)
fig.show()
Após criarmos duas instâncias de Die, lançamos os dados e
calculamos a soma dos dois para cada lançamento 1. O menor
resultado possível (2) é a soma do menor número em cada dado. O
maior resultado possível (12) é a soma do maior número em cada
dado, que atribuímos a max_result 2. A variável max_result facilita mais a
leitura do código para gerar poss_results 3. Poderíamos ter escrito
range(2, 13), mas funcionaria apenas para dois dados D6. Ao modelar
situações reais, é melhor escrever um código que possa facilmente
modelar uma variedade de situações. Esse código nos possibilita
simular o lançamento de um par de dados com qualquer número de
lados.
Após executarmos esse código, devemos ver um gráfico que se
parece com o da Figura 15.14.
Figura 15.14: Resultados simulados do lançamento de dois dados
com seis lados 1.000 vezes.
Esse gráfico mostra a distribuição aproximada dos resultados que
provavelmente obteremos quando lançarmos um par de dados D6.
Conforme podemos ver, é menos provável que tiremos um 2 ou
um 12 e mais provável que tiremos um 7. Isso acontece porque
existem seis maneiras de tirar um 7: 1 e 6, 2 e 5, 3 e 4, 4 e 3, 5 e 2,
e 6 e 1.
Mais personalizações
Há um problema que devemos solucionar com o gráfico que
acabamos de gerar. Como agora existem 11 barras, as configurações
default de layout para o eixo x deixam algumas das barras sem
rótulo. Apesar de as configurações default funcionarem bem para a
maioria das visualizações, esse gráfico ficaria melhor visualmente
com todas as barras rotuladas.
O Plotly tem um método update_layout() que pode ser usado para fazer
uma ampla variedade de atualizações em uma figura após ser
criada. Veja como solicitar ao Plotly para fornecer a cada barra o
próprio rótulo:
dice_visual.py
-- trecho de código omitido --
fig = px.bar(x=poss_results, y=frequencies, title=title, labels=labels)
fig.show()
O método update_layout() atua no objeto fig, que representa o gráfico
geral. Aqui, utilizamos o argumento xaxis_dtick, que especifica a
distância entre as marcações de escala no eixo x. Como definimos
esse espaçamento em 1, cada barra é rotulada. Quando
executarmos dice_visual.py mais uma vez, deveremos ver um rótulo
em cada barra.
# Cria um D6 e um D10
die_1 = Die()
1 die_2 = Die(10)
# Analisa os resultados
-- trecho de código omitido --
# Visualiza os resultados
2 title = "Results of Rolling a D6 and a D10 50,000 Times"
labels = {'x': 'Result', 'y': 'Frequency of Result'}
-- trecho de código omitido --
Para criar um D10, passamos o argumento 10 ao criar a segunda
instância de Die 1 e alteramos o primeiro loop para simular
50.000 lançamentos em vez de 1.000. Alteramos também o título do
gráfico 2.
A Figura 15.15 mostra o gráfico resultante. Em vez de ter um
resultado mais provável, temos cinco deles. Isso ocorre porque
existe somente uma forma de tirar o menor valor (1 e 1) e o maior
valor (6 e 10), mas o dado menor limita o número de formas por
meio das quais podemos gerar os números intermediários. Existem
seis formas de tirar um 7, 8, 9, 10 ou 11, pois como são os
resultados mais comuns, temos probabilidade igual de tiramos um
deles.
Salvando os gráficos
Quando gostamos de um gráfico, sempre podemos salvá-lo como
um arquivo HTML em nosso navegador. Mas também podemos fazer
isso de maneira programada. Para salvar seu gráfico como arquivo
HTML, substitua a chamada fig.show() por uma para fig.write_html():
fig.write_html('dice_visual_d6d10.html')
O método write_html() exige um argumento: o nome do arquivo para
escrever. Se fornecermos apenas um nome de arquivo, esse arquivo
será salvo no mesmo diretório que o arquivo .py. É possível também
chamar savefig() com um objeto Path, e escrever o arquivo de saída em
qualquer lugar que queira em seu sistema.
1 path = Path('weather_data/sitka_weather_07-2021_simple.csv')
lines = path.read_text().splitlines()
2 reader = csv.reader(lines)
3 header_row = next(reader)
print(header_row)
Primeiro, importamos o Path e o módulo csv. Depois, criamos um
objeto Path que analisa a pasta weather_data e aponta para o
arquivo de dados meteorológicos específico com o qual queremos
trabalhar 1. Lemos o arquivo e encadeamos o método splitlines() para
obter uma lista de todas as linhas no arquivo, que atribuímos à lines.
Em seguida, criamos um objeto reader 2. Trata-se de um objeto que
pode ser usado para fazer o parse de cada linha no arquivo. Para
criar um objeto reader, chame a função csv.reader() e passe a ele a
lista de linhas do arquivo CSV.
Ao receber um objeto reader, a função next() retorna a próxima linha
do arquivo, começando no início dele. Como aqui chamamos next()
somente uma vez, temos a primeira linha do arquivo, que contém os
cabeçalhos do arquivo 3. Atribuímos os dados retornados a header_row.
Como podemos ver, header_row contém cabeçalhos significativos,
relacionados ao clima, que nos transmite quais informações cada
linha de dados armazena:
['STATION', 'NAME', 'DATE', 'TAVG', 'TMAX', 'TMIN']
O objeto reader processa a primeira linha de valores separados por
vírgulas no arquivo e armazena cada valor como um item em uma
lista. O cabeçalho STATION representa o código para a estação
meteorológica que registrou esses dados. A posição deste cabeçalho
nos informa que o primeiro valor em cada linha será o código da
estação meteorológica. O cabeçalho NAME indica que o segundo valor
em cada linha é o nome da estação meteorológica que efetuou o
registro. O restante dos cabeçalhos especifica quais tipos de
informações foram registradas em cada leitura. Por ora, os dados em
que estamos mais interessados são a data (DATE), a temperatura
máxima (TMAX) e a temperatura mínima (TMIN). Esse é um simples
conjunto de dados que contém apenas dados relacionados à
temperatura. Ao fazer o download de seus dados meteorológicos, é
possível optar por incluir uma série de outras medições relacionadas
à velocidade e à direção do vento e a dados de precipitação.
print(highs)
Criamos uma lista vazia chamada highs 1 e, em seguida, percorremos
com um loop as linhas restantes no arquivo 2. O objeto reader
continua de onde parou no arquivo CSV e retorna automaticamente
cada linha seguida de sua posição atual. Como já lemos a linha de
cabeçalho, o loop começará na segunda linha em que os próprios
dados começam. Em cada passagem pelo loop, extraímos os dados
do índice 4, correspondente ao cabeçalho TMAX, e os atribuímos à
variável high 3. Recorremos à função int() para converter os dados,
armazenados como uma string, em um formato numérico a fim de
que possamos usá-los. Depois, anexamos esse valor à highs.
A listagem a seguir mostra os dados agora armazenados em highs:
[61, 60, 66, 60, 65, 59, 58, 58, 57, 60, 60, 60, 57, 58, 60, 61, 63, 63, 70,
64, 59, 63, 61, 58, 59, 64, 62, 70, 70, 73, 66]
Extraímos a temperatura máxima para cada data e armazenamos
cada valor em uma lista. Agora, criaremos a visualização desses
dados.
path = Path('weather_data/sitka_weather_07-2021_simple.csv')
lines = path.read_text().splitlines()
-- trecho de código omitido --
# Formata o gráfico
2 ax.set_title("Daily High Temperatures, July 2021", fontsize=24)
3 ax.set_xlabel('', fontsize=16)
ax.set_ylabel("Temperature (F)", fontsize=16)
ax.tick_params(labelsize=16)
plt.show()
Passamos a lista de temperaturas máximas para plot() e color='red' a
fim de plotar os pontos com a cor vermelha 1. (Plotaremos as
temperaturas máximas na cor vermelha e as temperaturas mínimas
na cor azul.) Em seguida, especificamos alguns outros detalhes de
formatação, como o título, o tamanho da fonte e os rótulos 2, assim
como fizemos no Capítulo 15. Como ainda não adicionamos as
datas, não rotularemos o eixo x, mas ax.set_xlabel() modifica o
tamanho da fonte para que os rótulos default fiquem mais legíveis 3.
A Figura 16.1 mostra o gráfico resultante: um simples gráfico de
linhas das temperaturas máximas de julho de 2021, em Sitka,
Alasca.
Figura 16.1: Um gráfico de linhas mostrando as temperaturas
máximas diárias de julho de 2021, em Sitka, Alasca.
Módulo datetime
Adicionaremos datas ao nosso gráfico para que fique mais útil. A
primeira data do arquivo de dados meteorológicos está na segunda
linha:
"USW00025333","SITKA AIRPORT, AK US","2021-07-01",,"61","53"
Os dados serão lidos como uma string, logo é necessário uma
maneira de converter a string "2021-07-01" em um objeto que
represente essa data. É possível criar um objeto que represente
1º de julho de 2021, usando o método strptime() do módulo datetime.
Confira como o strptime() funciona em uma sessão de terminal:
>>> from datetime import datetime
>>> first_date = datetime.strptime('2021-07-01', '%Y-%m-%d')
>>> print(first_date)
2021-07-01 00:00:00
Primeiro, importamos a classe datetime do módulo datetime. Em
seguida, chamamos o método strptime() com a string contendo a data
que queremos processar como primeiro argumento. O segundo
argumento informa ao Python como a data está formatada. Nesse
exemplo, '%Y-' instruí ao Python que procure um ano de quatro
dígitos antes do primeiro traço; '%m-' indica um mês de dois dígitos
antes do segundo traço; e %d'' significa que a última parte da string
é o dia do mês, de 1 a 31.
O método strptime() pode receber uma variedade de argumentos para
determinar como interpretar a data. A Tabela 16.1 mostra alguns
desses argumentos.
Tabela 16.1: Argumentos para formatação de data e de hora do
módulo datetime
Argumento Significado
%A Nome do dia da semana, como Monday
%B Nome do mês, como January
%m Mês, como um número (de 01 a 12)
%d Dia do mês, como número (de 01 a 31)
%Y Ano com quatro dígitos, como 2019
%y Ano com dois dígitos, como 19
%H Hora, em formato 24 horas (de 00 a 23)
%I Hora em formato 12 horas (de 01 a 12)
%p AM ou PM
%M Minutos (de 00 a 59)
%S Segundos (de 00 a 61)
Plotando datas
É possível melhorar nosso gráfico extraindo as datas a partir das
leituras diárias de temperatura máxima e usar essas datas no eixo x:
sitka_highs.py
from pathlib import Path
import csv
from datetime import datetime
path = Path('weather_data/sitka_weather_07-2021_simple.csv')
lines = path.read_text().splitlines()
reader = csv.reader(lines)
header_row = next(reader)
# Extrai datas e temperaturas máximas
1 dates, highs = [], []
for row in reader:
2 current_date = datetime.strptime(row[2], '%Y-%m-%d')
high = int(row[4])
dates.append(current_date)
highs.append(high)
# Formata o gráfico
ax.set_title("Daily High Temperatures, July 2021", fontsize=24)
ax.set_xlabel('', fontsize=16)
4 fig.autofmt_xdate()
ax.set_ylabel("Temperature (F)", fontsize=16)
ax.tick_params(labelsize=16)
plt.show()
Criamos duas listas vazias a fim de armazenar as datas e as
temperaturas máximas do arquivo 1. Em seguida, convertemos os
dados que contêm as informações de data (row[2]) em um objeto
datetime 2 e o anexamos à date. Passamos as datas e os valores de
temperaturas máximas para plot() 3. A chamada para fig.autofmt_xdate() 4
desenha os rótulos de data na diagonal a fim de evitar que se
sobreponham. A Figura 16.2 mostra o gráfico melhorado.
Figura 16.2: O gráfico está mais pertinente, agora que tem datas no
eixo x.
# Formata o gráfico
4 ax.set_title("Daily High and Low Temperatures, 2021", fontsize=24)
-- trecho de código omitido --
Adicionamos a lista vazia lows para armazenar as temperaturas
mínimas 1 e, em seguida, extraímos e armazenamos a temperatura
mínima para cada data a partir da sexta posição em cada linha
(row[5]) 2. Adicionamos uma chamada a plot() para as temperaturas
mínimas e colorimos esses valores de azul 3. Por último, atualizamos
o título 4. A Figura 16.4 mostra o gráfico resultante.
Verificação de erros
Devemos conseguir executar o código sitka_highs_lows.py usando
dados de qualquer local. No entanto, algumas estações
meteorológicas coletam dados diferentes de outras, e algumas delas,
ocasionalmente, falham em coletar os dados e não conseguem
coletar alguns dos dados que deveriam. Dados ausentes podem
resultar em exceções que acarretam falhas em nossos programas, a
menos que os manipulemos de modo adequado.
Por exemplo, vejamos o que acontece quando tentamos gerar um
gráfico de temperatura para o Vale da Morte, Califórnia. Copie o
arquivo death_valley_2021_simple.csv para a pasta em que está
armazenando os dados dos programas deste capítulo.
Primeiro, executaremos o código para ver os cabeçalhos incluídos
nesse arquivo de dados:
death_valley_highs_lows.py
from pathlib import Path
import csv
path = Path('weather_data/death_valley_2021_simple.csv')
lines = path.read_text().splitlines()
reader = csv.reader(lines)
header_row = next(reader)
# Formata o gráfico
4 title = "Daily High and Low Temperatures, 2021\nDeath Valley, CA"
ax.set_title(title, fontsize=20)
ax.set_xlabel('', fontsize=16)
-- trecho de código omitido --
Sempre que examinamos uma linha, tentamos extrair a data e as
temperaturas máximas e mínimas 1. Se algum dado estiver ausente,
o Python lançará um ValueError e nós o manipularemos exibindo uma
mensagem de erro que inclua a data dos dados ausentes 2. Após
exibir o erro, o loop continuará processando a próxima linha. Caso
todos os dados de uma data sejam acessados sem erro, o bloco else
será executado e os dados serão anexados às listas adequadas 3.
Como estamos plotando informações para um local novo,
atualizamos o título para incluir o local no gráfico e utilizamos um
tamanho de fonte menor para acomodar o título mais extenso 4.
Agora, quando executarmos death_valley_highs_lows.py, veremos
que somente uma data tinha dados ausentes:
Missing data for 2021-05-04 00:00:00
Como o erro é tratado de forma apropriada, nosso código é capaz de
gerar um gráfico, que ignora os dados ausentes. A Figura 16.3
mostra o gráfico resultante.
Ao comparar esse gráfico com o gráfico de Sitka, é possível
constatar que o Vale da Morte é mais quente do que o sudeste do
Alasca, como esperado. Além do mais, a variação de temperaturas a
cada dia é maior no deserto. A altura da região sombreada confirma
isso.
Extraindo magnitudes
Podemos percorrer a lista contendo os dados sobre cada terremoto
com um loop e extrair qualquer informação que quisermos. Vamos
extrair a magnitude de cada terremoto:
eq_explore_data.py
-- trecho de código omitido --
all_eq_dicts = all_eq_data['features']
1 mags = []
for eq_dict in all_eq_dicts:
2 mag = eq_dict['properties']['mag']
mags.append(mag)
print(mags[:10])
Criamos uma lista vazia para armazenar as magnitudes e, depois,
percorremos com um loop a lista all_eq_dicts 1. Dentro deste loop,
cada terremoto é representado pelo dicionário eq_dict. A magnitude
de cada terremoto é armazenada na seção 'properties' desse dicionário,
com a chave 'mag' 2. Armazenamos cada magnitude na variável mag e
depois a anexamos à lista mags.
Exibimos as primeiras 10 magnitudes, assim podemos verificar se
estamos obtendo os dados corretos:
[1.6, 1.6, 2.2, 3.7, 2.92000008, 1.4, 4.6, 4.5, 1.9, 1.8]
Em seguida, vamos extrair os dados da localização de cada
terremoto para criarmos um mapa dos terremotos.
print(mags[:10])
print(lons[:5])
print(lats[:5])
Criamos listas vazias para as longitudes e latitudes. O código
eq_dict['geometry'] acessa o dicionário que representa o elemento de
geometria do terremoto 1. A segunda chave, 'coordinates', extrai a lista
de valores associados à 'coordinates'. Por último, o índice 0 solicita o
primeiro valor na lista de coordenadas, que corresponde à longitude
de um terremoto.
Ao exibirmos as primeiras 5 longitudes e latitudes, a saída mostra
que estamos extraindo os dados corretos:
[1.6, 1.6, 2.2, 3.7, 2.92000008, 1.4, 4.6, 4.5, 1.9, 1.8]
[-150.7585, -153.4716, -148.7531, -159.6267, -155.248336791992]
[61.7591, 59.3152, 63.1633, 54.5612, 18.7551670074463]
Com esses dados, podemos passar para o mapeamento de cada
terremoto.
Criando um mapa-múndi
Usando as informações que extraímos até agora, é possível criar um
mapa-múndi simples. Embora ainda não seja apresentável,
queremos assegurar que as informações sejam devidamente
exibidas antes de focarmos os problemas de estilo e apresentação.
Vejamos o mapa inicial:
eq_world_map.py
from pathlib import Path
import json
import plotly.express as px
Representando magnitudes
Um mapa de atividades sísmicas deve mostrar a magnitude de cada
terremoto. Podemos também incluir mais dados, agora que sabemos
que os dados estão sendo corretamente plotados.
-- trecho de código omitido --
# Lê os dados como uma string e os converte em um objeto Python
path = Path('eq_data/eq_data_30_day_m1.geojson')
contents = path.read_text()
-- trecho de código omitido --
Recapitulando
Neste capítulo, aprendemos a trabalhar com conjuntos de dados do
mundo real. Processamos arquivos CSV e GeoJSON e extraímos os
dados desejados. Com dados meteorológicos históricos, aprendemos
mais sobre como trabalhar com a Matplotlib, bem como usar o
módulo datetime e como plotar diversas séries de dados em um
gráfico. Plotamos dados geográficos em um mapa-múndi com o
Plotly e aprendemos a personalizar o estilo do mapa.
À medida que adquire experiência trabalhando com arquivos CSV e
JSON, você será capaz de processar quase todos os dados que
queira analisar. É possível fazer o download da maioria dos
conjuntos de dados online em um ou ambos esses formatos. Ao
trabalhar com esses formatos, é possível também aprender a
trabalhar com outros formatos de dados com mais facilidade.
No próximo capítulo, desenvolveremos programas que coletam
automaticamente os próprios dados de fontes online e, em seguida,
criaremos visualizações desses dados. Caso queira programar como
hobby, essas habilidades são divertidas. Agora, caso esteja
interessado em se tornar um programador profissional, essas
habilidades são essenciais.
17capítulo
Git e GitHub
Basearemos nossa visualização em informações do GitHub
(https://github.com), site que possibilita aos programadores colaborarem
com projetos de programação. Vamos utilizar a API do GitHub para
requisitar informações sobre projetos Python do site e, em seguida,
vamos gerar uma visualização interativa da popularidade relativa
desses projetos usando o Plotly.
O GitHub deve seu nome ao Git, sistema de controle de
versão/versionamento distribuído. Como o Git ajuda as pessoas a
gerenciarem as próprias tarefas em um projeto, as mudanças
efetuadas por uma pessoa não interferem nas alterações que outras
pessoas estão fazendo. Quando implementamos uma funcionalidade
nova em um projeto, o Git rastreia as alterações feitas em cada
arquivo. Assim que o código novo estiver funcionando, fazemos o
commit das mudanças feitas, e o Git registra o novo estado do
projeto. Se cometer um erro e quiser reverter suas mudanças, é
possível retornar facilmente a qualquer estado anterior da tarefa.
(Para saber mais sobre o controle de versão usando o Git, confira o
Apêndice D.) No GitHub, os projetos são armazenados em
repositórios, que contêm tudo associado ao projeto: o código,
informações sobre os colaboradores, quaisquer problemas ou
relatórios de bugs, e assim por diante.
Quando os usuários do GitHub gostam de um projeto, podem
marcá-lo com uma “estrela (starred)” para mostrar apoio e
acompanhar os projetos que talvez queiram usar. Neste capítulo,
desenvolveremos um programa para fazer o download automático
de informações sobre os projetos Python com mais estrelas no
GitHub e, depois, criaremos uma visualização informativa desses
projetos.
# Processa os resultados
print(response_dict.keys())
Primeiro, importamos o módulo requests. Depois, atribuímos o URL da
chamada de API à variável url 1. Como se trata de um URL extenso, o
dividimos em duas linhas. A primeira linha é a parte principal do
URL, e a segunda linha é a string da query. Incluímos mais uma
condição na string da query original: stars:>10000, que informa ao
GitHub para procurar somente repositórios Python que tenham mais
de 10.000 estrelas. Isso deve possibilitar que o GitHub retorne um
conjunto completo e consistente de resultados.
Atualmente, o GitHub está na terceira versão de sua API, por isso,
definimos cabeçalhos para a chamada de API que solicitam
explicitamente para usar essa versão da API e retornamos os
resultados no formato JSON 2. Em seguida, utilizamos requests a fim
de criar a chamada para a API 3. Chamamos get() e passamos o URL
e o cabeçalho que definimos, e atribuímos o objeto de resposta à
variável r.
O objeto de resposta tem um atributo chamado status_code, que nos
informa se a requisição foi bem-sucedida. (Um status code de
200 sinaliza uma resposta bem-sucedida.) Exibimos o valor de
status_code para garantir que a chamada foi realizada com sucesso 4.
Solicitamos à API para retornar as informações no formato JSON,
depois usamos o método json() para converter as informações em um
dicionário Python 5. Atribuímos o dicionário resultante a response_dict.
Por último, exibimos as chaves de response_dict e vemos a seguinte
saída:
Status code: 200
dict_keys(['total_count', 'incomplete_results', 'items'])
Como o status code é 200, sabemos que a requisição foi bem-
sucedida. O dicionário de respostas contém apenas três chaves:
'total_count', 'incomplete_results' e 'items'. Vamos conferir esse dicionário de
respostas.
3 Keys: 78
allow_forking
archive_url
archived
-- trecho de código omitido --
url
visiblity
watchers
watchers_count
No momento em que eu escrevia este livro, existiam apenas
248 repositórios Python com mais de 10.000 estrelas 1. Podemos ver
que o GitHub foi capaz de processar totalmente a chamada de API 2.
Nessa resposta, o GitHub retornou informações sobre os primeiros
30 repositórios que correspondem às condições de nossa query. Se
quisermos mais repositórios, é possível requisitar páginas adicionais
de dados.
A API do GitHub retorna muitas informações sobre cada repositório:
existem 78 chaves no repo_dict 3. Ao examinarmos essas chaves,
temos uma noção do tipo de informação que podemos extrair sobre
um projeto. (A única forma de saber quais informações estão
disponíveis por meio de uma API é ler a documentação ou examinar
as informações por meio de código, como estamos fazendo aqui.)
Extrairemos os valores para algumas das chaves do repo_dict:
python_repos.py
-- trecho de código omitido --
# Examina o primeiro repositório
repo_dict = repo_dicts[0]
Name: public-apis
Owner: public-apis
Stars: 191494
Repository: https://github.com/public-apis/public-apis
Description: A collective list of free APIs
Name: system-design-primer
Owner: donnemartin
Stars: 179952
Repository: https://github.com/donnemartin/system-design-primer
Description: Learn how to design large-scale systems. Prep for the system
design interview. Includes Anki flashcards.
-- trecho de código omitido --
Name: PayloadsAllTheThings
Owner: swisskyrepo
Stars: 37227
Repository: https://github.com/swisskyrepo/PayloadsAllTheThings
Description: A list of useful payloads and bypass for Web Application Security
and Pentest/CTF
Como projetos interessantes aparecem nesses resultados, talvez
valha a pena conferir alguns deles. Mas não fique muito tempo
fazendo isso, pois estamos prestes a criar uma visualização que
facilitará e muito a legibilidade dos resultados.
# Cria a visualização
4 fig = px.bar(x=repo_names, y=stars)
fig.show()
Importamos o Plotly Express e, em seguida, criamos a chamada de
API como temos feito. Continuamos exibindo o status da resposta de
chamada da API para sabermos se existe um problema 1. Ao
processarmos os resultados gerais, continuamos exibindo a
mensagem confirmando que recebemos um conjunto completo de
resultados 2. Removemos o resto das chamadas print(), já que não
estamos mais na fase exploratória; sabemos que temos os dados
que queremos.
Em seguida, criamos duas listas vazias 3 para armazenar os dados
que incluiremos no gráfico inicial. Precisaremos do nome de cada
projeto para rotular as barras (repo_names) e o número de estrelas a
fim de determinar a altura das barras (stars). No loop, anexamos o
nome de cada projeto e o número de estrelas que cada um tem
nessas listas.
Criamos a visualização inicial com apenas duas linhas de código 4.
Isso é compatível com a filosofia do Plotly Express de que devemos
conseguir ver a visualização o mais rápido possível antes de refinar
sua aparência. Aqui, usamos a função px.bar() para criar um gráfico
de barras. Passamos a lista repo_names como argumento x e stars como
argumento y.
A Figura 17.1 mostra o gráfico resultante. É possível constatar que
os primeiros projetos são significativamente mais populares do que
os outros, apesar de todos serem projetos importantes no
ecossistema Python.
Figura 17.1: Os projetos Python com mais estrelas no GitHub.
Estilizando o gráfico
Uma vez que sabemos que as informações no gráfico estão corretas,
o Plotly suporta diversas maneiras de estilizar e personalizar os
gráficos. Faremos algumas alterações na chamada px.bar() inicial e,
em seguida, faremos alguns ajustes adicionais no objeto fig depois
de criá-lo.
Começaremos a estilizar o gráfico adicionando um título e rótulos
para cada eixo:
python_repos_visual.py
-- trecho de código omitido --
# Cria a visualização
title = "Most-Starred Python Projects on GitHub"
labels = {'x': 'Repository', 'y': 'Stars'}
fig = px.bar(x=repo_names, y=stars, title=title, labels=labels)
1 fig.update_layout(title_font_size=28, xaxis_title_font_size=20,
yaxis_title_font_size=20)
fig.show()
Primeiro, adicionamos um título e rótulos para cada eixo, como
fizemos nos capítulos 15 e 16. Em seguida, usamos o método
fig.update_layout() a fim de modificar elementos específicos do gráfico 1.
O Plotly adota a convenção em que os aspectos de um elemento
gráfico são conectados por underscores. À medida que se
familiarizar com a documentação do Plotly, você começará a
identificar padrões consistentes em como os diferentes elementos de
um gráfico são nomeados e modificados. Nesse caso, definimos o
tamanho da fonte do título como 28 e o tamanho da fonte para cada
título do eixo como 20. O resultado é mostrado na Figura 17.2.
# Cria a visualização
title = "Most-Starred Python Projects on GitHub"
labels = {'x': 'Repository', 'y': 'Stars'}
4 fig = px.bar(x=repo_names, y=stars, title=title, labels=labels,
hover_name=hover_texts)
fig.update_layout(title_font_size=28, xaxis_title_font_size=20,
yaxis_title_font_size=20)
fig.show()
De início, definimos uma nova lista vazia, hover_texts, para armazenar
o texto que queremos exibir em cada projeto 1. No loop em que
processamos os dados, extraímos o proprietário e a descrição para
cada projeto 2. Já que o Plotly possibilita que usemos código HTML
dentro dos elementos de texto, geramos uma string para o rótulo
com uma quebra de linha (<br />) entre o nome de usuário do
proprietário do projeto e a descrição 3. Em seguida, anexamos esse
rótulo à lista hover_texts.
Na chamada px.bar(), adicionamos o argumento hover_name e o
É
passamos para hover_texts 4. É a mesma abordagem que usamos para
personalizar o rótulo para cada ponto no mapa de atividades
sísmicas globais. Como cria cada barra, o Plotly extraí os rótulos
dessa lista e apenas os exibe quando o usuário passa o mouse sobre
as barras. A Figura 17.3 mostra uma tooltip personalizada.
stars.append(repo_dict['stargazers_count'])
-- trecho de código omitido --
# Cria a visualização
title = "Most-Starred Python Projects on GitHub"
labels = {'x': 'Repository', 'y': 'Stars'}
fig = px.bar(x=repo_links, y=stars, title=title, labels=labels,
hover_name=hover_texts)
fig.update_layout(title_font_size=28, xaxis_title_font_size=20,
yaxis_title_font_size=20)
fig.show()
Atualizamos o nome da lista que estamos criando de repo_names para
repo_links. Assim, reportamos com mais acurácia o tipo de informação
que estamos consolidando para o gráfico 1. Depois, extraímos o URL
do projeto de repo_dict e o atribuímos à variável temporária repo_url 2.
Em seguida, geramos um link para o projeto 3. Usamos a tag âncora
HTML, que tem o formato <a href='URL'>link text</a> para gerar o link.
Em seguida, adicionamos esse link a repo_links.
Quando chamamos px.bar(), usamos repo_links para os valores x no
gráfico. Apesar de o resultado parecer o mesmo de antes, agora o
usuário pode clicar em qualquer um dos nomes do projeto na parte
inferior do gráfico para visitar a página inicial do projeto no GitHub.
Temos uma visualização interativa e informativa dos dados
acessados por meio de uma API!
fig.update_traces(marker_color='SteelBlue', marker_opacity=0.6)
fig.show()
No Plotly, um trace se refere a uma coleção de dados em um gráfico.
O método update_traces() pode receber diversos argumentos diferentes;
qualquer argumento que comece com marker_ afeta as marcações no
gráfico. Aqui, definimos a cor de cada marcação como 'SteelBlue';
qualquer cor CSS nomeada funcionará. Definimos também a
opacidade de cada marcação como 0,6. Uma opacidade de 1,0 será
totalmente opaca, e uma opacidade de 0 será completamente
invisível.
import requests
Recapitulando
Neste capítulo, aprendemos a usar APIs para escrever programas
independentes que coletam automaticamente os dados de que
precisam e usam esses dados para criar uma visualização. Utilizamos
a API do GitHub para explorar os projetos Python com mais estrelas,
e também vimos brevemente a API do Hacker News. Vimo como
usar o pacote Requests para executar automaticamente uma
chamada de API e como processar os resultados dessa chamada.
Tivemos também um contato rápido com algumas configurações do
Plotly que personalizam ainda mais a aparência dos gráficos que
geramos.
No próximo capítulo, usaremos o Django para criar uma aplicação
web como projeto final.
18
capítulo
Instalando o Django
Com o ambiente virtual ativado, digite o seguinte para atualizar o pip
e instalar o Django:
(ll_env)learning_log$ pip install --upgrade pip
(ll_env)learning_log$ pip install django
Collecting django
-- trecho de código omitido --
Installing collected packages: sqlparse, asgiref, django
Successfully installed asgiref-3.5.2 django-4.1 sqlparse-0.4.2
(ll_env)learning_log$
Como faz o download de recursos de variadas fontes, o pip é
atualizado com bastante frequência. É boa ideia atualizar o pip
sempre que criarmos um ambiente virtual novo.
Como agora estamos trabalhando em um ambiente virtual, o
comando para instalar o Django é o mesmo em todos os sistemas.
Não há necessidade de usar comandos extensos, como python -m pip
install nome_package, ou incluir a flag --user. Lembre-se de que o Django
estará disponível somente quando o ambiente ll_env estiver ativo.
NOTA O Django lança uma versão nova a cada oito meses. Ou
seja, talvez você veja uma versão mais recente quando
instalá-lo. Provavelmente, esse projeto funcionará como está
aqui, mesmo com versões mais recentes do Django. Caso
queira usar a mesma versão do Django que estamos usando
aqui, digite o comando pip install django==4.1.*. Isso instalará a
versão mais recente do Django 4.1. Se tiver algum problema
relacionado à versão que está usando, confira os recursos
online deste livro em https://ehmatthes.github.io/pcc_3e.
Visualizando o projeto
Vamos assegurar que o Django configurou devidamente o projeto.
Digite o comando runserver para visualizar o projeto em seu estado
atual:
(ll_env)learning_log$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
Definindo modelos
Vamos pensar em nossos dados por um momento. É necessário que
cada usuário crie uma série de tópicos em seu registro de
aprendizagem. Cada entrada inserida será vinculada a um tópico, e
essas entradas serão exibidas como texto. Além disso, será
necessário armazenarmos o timestamp de cada entrada, assim
conseguimos mostrar aos usuários quando criaram cada uma delas.
Vamos abrir o arquivo models.py e ver o conteúdo existente:
models.py
from django.db import models
class Topic(models.Model):
"""Um tópico que o usuário está aprendendo"""
1 text = models.CharField(max_length=200)
2 date_added = models.DateTimeField(auto_now_add=True)
3 def __str__(self):
""" Retorna uma representação de string do modelo"""
return self.text
Criamos uma classe chamada Topic, que herda de Model – uma classe-
pai incluída no Django que define a funcionalidade básica de um
modelo. Adicionamos dois atributos à classe Topic: text e date_added.
O atributo text é um CharField, um dado composto por caracteres ou
texto 1. Recorremos ao CharField quando queremos armazenar uma
pequena quantidade de texto, como um nome, um título ou uma
cidade. Ao definirmos um atributo CharField, temos que informar ao
Django quanto espaço deve reservar no banco de dados. Aqui,
atribuímos um max_length de 200 caracteres, que deve ser suficiente
para armazenar a maioria dos nomes de tópicos.
O atributo date_added é um DateTimeField, um dado que registrará uma
data e hora 2. Passamos o argumento auto_now_add=True, que informa
ao Django para definir automaticamente esse atributo com a data e
hora atuais sempre que o usuário criar um tópico novo.
É
É bom informar ao Django como queremos que o framework
represente uma instância de um modelo. Se um modelo tem um
método __str__(), o Django chama esse método sempre que for
necessário gerar uma saída referente a uma instância desse modelo.
Aqui, escrevemos um método __str__() que retorna o valor atribuído
ao atributo text 3.
Para ver os diferentes tipos de campos que podemos usar em um
modelo, confira a página “Model Field Reference” em
https://docs.djangoproject.com/en/4.1/ref/models/fields. Não precisa saber de
tudo nesse exato momento, mas isso será extremamente útil
quando você estiver desenvolvendo seus projetos Django.
Ativando modelos
Para utilizar nossos modelos, é necessário dizer ao Django para
incluir nossa aplicação no projeto geral. Basta abrir settings.py (no
diretório ll_project); veremos uma seção que informa ao Django
quais aplicações estão instalados no projeto:
settings.py
-- trecho de código omitido --
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
-- trecho de código omitido --
Adicione nossa aplicação a essa lista modificando INSTALLED_APPS para
que fique assim:
-- trecho de código omitido --
INSTALLED_APPS = [
# Minhas aplicações
'learning_logs',
Configurando um superusuário
O Django possibilita que criemos um superusuário, um usuário que
tem todos os privilégios disponíveis no site. Os privilégios de um
usuário controlam as ações que ele pode tomar. As configurações
mais restritivas de privilégio possibilitam que um usuário leia
somente informações públicas no site. No site, usuários registrados
normalmente têm o privilégio de ler os próprios dados privados e
algumas informações selecionadas disponíveis apenas para os
membros. Para administrar efetivamente um projeto, em geral, o
proprietário do site precisa ter acesso a todas as informações
armazenadas nele. Um bom administrador é meticuloso com as
informações confidenciais de seus usuários, já que eles confiam
muito nas aplicações que acessam.
Para criar um superusuário no Django, digite o seguinte comando e
responda aos prompts:
(ll_env)learning_log$ python manage.py createsuperuser
1 Username (leave blank to use 'eric'): ll_admin
2 Email address:
3 Password:
Password (again):
Superuser created successfully.
(ll_env)learning_log$
Ao executar o comando createuperuser, o Django nos pede para
fornecer um nome de usuário para o superusuário 1. Aqui, estou
usando ll_admin, mas é possível inserir qualquer nome de usuário que
quiser. Podemos inserir um endereço de e-mail ou apenas deixar
esse campo em branco 2. Será necessário digitar a senha duas
vezes 3.
NOTA Algumas informações confidenciais podem ser ocultadas dos
administradores de um site. Por exemplo, o Django não
armazena a senha que você digita; ao contrário, armazena
uma string derivada da senha, chamada de hash. Sempre que
digitar sua senha, o Django faz um hash de sua entrada e a
compara com o hash armazenado. Se os dois hashes
corresponderem, você é autenticado. Ao exigir que os hashes
correspondam, o Django garante que, se um hacker obtiver
acesso ao banco de dados de um site, ele conseguirá ler os
hashes armazenados, não as senhas. Quando um site é
adequadamente desenvolvido, é quase impossível obter as
senhas originais dos hashes.
admin.site.register(Topic)
Esse código inicialmente importa o modelo que queremos registrar,
Topic. O ponto na frente de models instrui que o Django procure
models.py no mesmo diretório que admin.py. O código
admin.site.register() informa ao Django para gerenciar nosso modelo por
meio do site admin.
Agora, use a conta de superusuário para acessar o site admin.
Acesse http://localhost:8000/admin/ e insira o nome de usuário e a
senha do superusuário que acabamos de criar. Deveremos ver uma
tela semelhante à mostrada na Figura 18.2. Essa página possibilita
adicionar usuários e grupos novos e mudar os existentes. É possível
também trabalhar com dados relacionados ao modelo Topic que
acabamos de definir.
Adicionando tópicos
Agora que Topic foi registrado no site admin, adicionaremos nosso
primeiro tópico. Clique em Topics para ir para a página Topics, que
está praticamente vazia, pois ainda não temos tópicos para
gerenciar. Clique em Add Topic, e um formulário para adicionar um
tópico novo será exibido. Digite Chess na primeira caixa e clique em
Save. Seremos direcionados de volta para a página admin Topics, e
podemos ver o tópico que acabamos de criar.
Criaremos um segundo tópico para termos mais dados com os quais
trabalhar. Clique Add Topic novamente e insira Rock Climbing. Clique
em Save e, mais uma vez, seremos enviados à página principal
Topic. Agora, podemos ver Chess (Xadrez) e Rock Climbing
(Escalada em Rocha).
class Topic(models.Model):
-- trecho de código omitido --
1 class Entry(models.Model):
"""Algo específico aprendido sobre um tópico"""
2 topic = models.ForeignKey(Topic, on_delete=models.CASCADE)
3 text = models.TextField()
date_added = models.DateTimeField(auto_now_add=True)
4 class Meta:
verbose_name_plural = 'entries'
def __str__(self):
""" Retorna uma string simples representando a entrada"""
5 return f"{self.text[:50]}..."
A classe Entry herda da classe Model base do Django, assim como Topic
fez 1. O primeiro atributo, topic, é uma instância de ForeignKey 2. Uma
chave estrangeira (foreign key) é um termo de banco de dados;
trata-se de uma referência a outro registro no banco de dados. É o
código que relaciona cada entrada a um tópico específico. Cada
tópico recebe uma chave, ou ID, quando é criado. Ao precisar
estabelecer uma conexão entre dois tópicos, o Django usa as chaves
associadas a cada informação. Daqui a pouco, usaremos essas
conexões a fim de acessar todas as entradas associadas a um
determinado tópico. O argumento on_delete= models.CASCADE informa ao
Django que quando um tópico é excluído, todas as entradas
associadas a esse tópico também devem ser excluídas. Isso é
conhecido como exclusão em cascata ou deletar em cascata.
O próximo é um atributo chamado text, uma instância de TextField 3.
Não é necessário limitar o tamanho desse tipo de campo, pois não
queremos restringir o tamanho das entradas individuais. O atributo
date_added nos possibilita apresentar entradas na ordem em que
foram criadas e inserir um timestamp ao lado de cada entrada.
A classe Meta está aninhada dentro da classe Entry 4. A classe Meta
armazena informações extras para gerenciar um modelo; aqui, a
classe nos permite definir um atributo especial informando ao
Django para usar Entries quando for necessário referenciar mais de
uma entrada. Sem isso, o Django referenciaria múltiplas entradas
como Entrys.
O método __str__() informa ao Django qual informação mostrar
quando referenciar a entradas individuais. Como uma entrada pode
ser um corpo extenso de texto, __str__() retorna somente os primeiros
50 caracteres de text 5. Além disso, adicionamos reticências para
elucidar que nem sempre estamos exibindo toda a entrada.
admin.site.register(Topic)
admin.site.register(Entry)
Basta acessar mais uma vez http://localhost/admin/, que veremos as
entradas listadas em Learning_Logs. Clique no link Add de Entries
ou clique em Entries e escolha Add entry. Deveremos ver uma
lista suspensa para selecionar o tópico para o qual estamos criando
uma entrada e uma caixa de texto para adicionar uma entrada.
Selecione Chess na lista suspensa e adicione uma entrada. Vejamos
a primeira entrada que fiz:
A abertura é a primeira parte do jogo, mais ou menos os
primeiros dez movimentos. Na abertura, é uma boa ideia fazer
três coisas – avançar seus bispos e cavalos, tentar controlar o
centro do tabuleiro e mover seu rei com um roque.
Obviamente, não passam de orientações. Será importante
aprender quando seguir essas orientações e quando
desconsiderar essas sugestões.
Ao clicar em Save, retornaremos à página admin principal de
entradas. Nesse momento, você perceberá a vantagem de usar
text[:50] como representação de string para cada entrada; fica mais
fácil trabalhar com múltiplas entradas na interface de administração
se virmos somente a primeira parte de uma entrada, em vez de todo
o texto de cada entrada.
Crie uma segunda entrada para Chess e uma entrada para Rock
Climbing para termos alguns dados iniciais. Vejamos a segunda
entrada para Chess:
Na fase de abertura do jogo, é importante avançar com seus
bispos e seus cavalos. Trata-se de peças poderosas e
manobráveis o suficiente para desempenhar um papel
significativo nos movimentos iniciais de um jogo.
Vejamos a primeira entrada para Rock Climbing:
Na escalada, um dos conceitos primordiais é equilibrar o peso
nos pés o máximo possível. Existe um mito de que os
alpinistas conseguem ficar pendurados com os braços o dia
inteiro. Na realidade, bons alpinistas praticam maneiras
específicas de equilibrar seu peso sobre os pés sempre que
possível.
Essas três entradas nos fornecerão algo com o qual trabalhar à
medida que continuamos a desenvolver o Registro de Aprendizagem.
Shell do Django
Agora que inserimos alguns dados, podemos programaticamente
examiná-los por meio de uma sessão interativa de terminal. Trata-se
de um ambiente interativo chamado de shell do Django, um ótimo
ambiente para testar e solucionar problemas do projeto. Vejamos
um exemplo de uma sessão interativa do shell:
(ll_env)learning_log$ python manage.py shell
1 >>> from learning_logs.models import Topic
>>> Topic.objects.all()
<QuerySet [<Topic: Chess>, <Topic: Rock Climbing>]>
O comando python manage.py shell, executado em um ambiente virtual
ativo, inicia o interpretador Python que podemos utilizar para
explorar os dados armazenados no banco de dados do projeto. Aqui,
importamos o modelo Topic do módulo learning_logs.models 1. Depois,
usamos o método Topic.objects.all() para obter todas as instâncias do
modelo Topic; a lista retornada é chamada de queryset.
É possível percorrer uma queryset com um loop do mesmo jeito que
fazemos com uma lista. Vejamos o ID que foi atribuído a cada objeto
de tópico:
>>> topics = Topic.objects.all()
>>> for topic in topics:
... print(topic.id, topic)
...
1 Chess
2 Rock Climbing
Atribuímos queryset a topics e, em seguida, exibimos o atributo id e a
representação da string de cada tópico. Podemos ver que Chess tem
um ID de 1 e Rock Climbing tem um ID de 2.
Caso saiba o ID de um objeto específico, é possível recorrer ao
método Topic.objects.get() para acessar esse objeto e examinar qualquer
atributo que o objeto tenha. Vejamos os valores text e date_added para
Chess:
>>> t = Topic.objects.get(id=1)
>>> t.text
'Chess'
>>> t.date_added
datetime.datetime(2022, 5, 20, 3, 33, 36, 928759,
tzinfo=datetime.timezone.utc)
É possível ver também as entradas relacionadas a um determinado
tópico. Anteriormente, definimos o atributo topic para o modelo Entry.
Tratava-se de uma ForeignKey, uma conexão entre cada entrada e um
tópico. O Django pode utilizar essa conexão para obter todas as
entradas relacionadas a um determinado tópico, assim.
1 >>> t.entry_set.all()
<QuerySet [<Entry: The opening is the first part of the game, roughly...>, <Entry:
In the opening phase of the game, it's important t...>]>
Para obter dados por meio de um relacionamento de chave
estrangeira, escreva o nome do modelo relacionado com letras
minúsculas, seguido por um underscore e pela palavra set 1. Por
exemplo, digamos que você tenha os modelos Pizza e Topping, e Topping
está relacionado à Pizza por meio de uma chave estrangeira. Caso seu
objeto se chame my_pizza, representando uma única pizza, é possível
obter todos os ingredientes da pizza com o código
my_pizza.topping_set.all().
Mapeando um URL
Como os usuários acessam páginas inserindo URLs em um
navegador e clicando em links, precisamos decidir quais URLs são
necessários. O URL da página inicial é o primeiro: é o URL base que
as pessoas usam para acessar o projeto. No momento, o URL base,
http://localhost:8000/, retorna o site default do Django que nos
informa que o projeto foi devidamente configurado. Vamos alterar
isso mapeando o URL base para a página inicial do Registro de
Aprendizagem.
Na pasta principal ll_project, abra o arquivo urls.py. Devemos ver o
seguinte código:
ll_project/urls.py
1 from django.contrib import admin
from django.urls import path
2 urlpatterns = [
3 path('admin/', admin.site.urls),
]
As duas primeiras linhas importam o módulo admin e uma função
para criar paths de URL 1. O corpo do arquivo define a variável
urlpatterns 2. No arquivo urls.py, que define URLs para o projeto geral,
a variável urlpatterns inclui conjuntos de URLs das aplicações do
projeto. A lista inclui o módulo admin.site.urls, que define todos os URLs
que podem ser requisitados a partir do site admin 3.
Como precisamos incluir os URLs para learning_logs, adicione o
seguinte:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('learning_logs.urls')),
]
Importamos a função include() e também adicionamos uma linha para
o módulo learning_logs.urls.
O urls.py default se encontra na pasta ll_project; agora precisamos
criar um segundo arquivo urls.py na pasta learning_logs. Crie um
arquivo novo Python, salve-o como urls.py em learning_logs e insira
o seguinte código nele:
learning_logs/urls.py
1 """Define padrões de URL para learning_logs"""
4 app_name = 'learning_logs'
5 urlpatterns = [
# Página inicial
6 path('', views.index, name='index'),
]
Para explicitar em qual urls.py estamos trabalhando, adicionamos
uma docstring no início do arquivo 1. Em seguida, importamos a
função path, necessária ao mapear URLs para as views 2. Importamos
também o módulo views 3; o ponto instrui o Python a importar o
módulo views.py do mesmo diretório que o módulo urls.py atual. A
variável app_name ajuda o Django a diferenciar esse arquivo urls.py de
arquivos com o mesmo nome em outras aplicações do projeto 4.
Nesse módulo, a variável urlpatterns é uma lista de páginas individuais
que podem ser requisitadas a partir da aplicação learning_logs 5.
O padrão efetivo de URL é uma chamada para a função path(), que
recebe três argumentos 6. O primeiro argumento é uma string que
ajuda o Django a encaminhar adequadamente a requisição atual. O
Django recebe o URL solicitado e tenta encaminhar a requisição para
uma view. O framework faz isso pesquisando todos os padrões de
URL que definimos para encontrar um que corresponda à requisição
atual. O Django ignora o URL base para o projeto
(http://localhost:8000/), assim a string vazia ('') corresponde ao URL
base. Qualquer outro URL não corresponderá a esse padrão, e o
Django retornará uma página de erro se o URL solicitado não
corresponder a nenhum padrão de URL existente.
O segundo argumento em path() 6 especifica qual função chamar em
views.py. Quando um URL requisitado corresponde ao padrão que
estamos definindo, o Django chama a função index() em views.py.
(Na próxima seção, escreveremos essa função view.) Como o
terceiro argumento fornece o nome index para esse padrão de URL,
podemos referenciá-lo com mais facilidade em outros arquivos
durante todo o projeto. Sempre que quisermos fornecer um link para
a página inicial, usaremos esse nome em vez de escrever um URL.
def index(request):
"""A página inicial para o Registro de Aprendizagem"""
return render(request, 'learning_logs/index.html')
Quando uma requisição de URL corresponde ao padrão que
acabamos de definir, o Django procura uma função chamada index()
no arquivo views.py. Depois, o Django passa o objeto request para
essa função view. Nesse caso, não é necessário processar nenhum
dado para a página, logo o único código na função é uma chamada
para render(). Aqui, a função render() passa dois argumentos: o objeto
request original e um template que pode usar para criar a página.
Agora, vamos escrever esse template.
Escrevendo um template
O template define a aparência da página, e o Django fornece os
dados relevantes sempre que a página é solicitada. Um template
possibilita acessar todos os dados fornecidos pela view. Já que nossa
view para a página inicial não fornece dados, esse template é
relativamente simples.
Dentro da pasta learning_logs, crie uma pasta nova chamada
templates. Dentro da pasta templates, crie outra pasta chamada
learning_logs. Por mais que pareça um tanto redundante (temos
uma pasta chamada learning_logs dentro de uma pasta chamada
templates, que está dentro de uma pasta chamada learning_logs),
isso define uma estrutura que o Django pode interpretar
inequivocamente, mesmo no contexto de um projeto enorme com
muitas aplicações individuais. Dentro da pasta interna learning_logs,
crie um arquivo novo chamado index.html. O path para o arquivo
será ll_project/learning_logs/templates/learning_logs/index.html.
Insira o seguinte código nesse arquivo:
index.html
<p>Learning Log</p>
<p>Learning Log helps you keep track of your learning, for any topic you're
interested in.</p>
Trata-se de um arquivo muito simples. Caso não conheça HTML, as
tags <p></p> significam parágrafos. A tag <p> abre um parágrafo e a
tag </p> fecha um parágrafo. Temos dois parágrafos: o primeiro se
comporta como título, e o segundo descreve o que os usuários
podem fazer com o Registro de Aprendizagem.
Agora, quando acessarmos o URL base do projeto,
http://localhost:8000/, devemos ver a página que acabamos de criar
em vez da página default do Django. O Django verificará se o URL
solicitado corresponde ao padrão ''; em seguida, chamará a função
views.index(), que renderizará a página usando o template contido em
index.html. A Figura 18.3 mostra a página resultante.
Figura 18.3: A página inicial do Registro de Aprendizagem.
Mesmo que, aparentemente, o processo de criar uma página seja
complicado, a separação entre URLs, views e templates funciona até
que bem. Possibilita que pensemos sobre cada aspecto de um
projeto de forma separada. Em projetos maiores possibilita que as
pessoas que trabalham juntas foquem as áreas em que são mais
experientes. Por exemplo, um especialista em banco de dados pode
focar os modelos, um programador pode se encarregar do código da
view e um especialista em front-end se concentrar nos templates.
NOTA Talvez você se depare com a seguinte mensagem de erro:
ModuleNotFoundError: No module named 'learning_logs.urls'
Em caso afirmativo, pare o development server pressionando
CTRL+C na janela de terminal em que executou comando
runserver. Em seguida, execute novamente o comando python
manage.py runserver. Você deve ver a página inicial. Sempre que
se deparar com um erro como esse, tente parar e reiniciar o
servidor.
FAÇA VOCÊ MESMO
18.5 Planejador de cardápios: Imagine um aplicativo que ajude as pessoas a planejar
as refeições ao longo da semana. Crie uma pasta nova chamada meal_planner e inicie
um projeto novo Django dentro dessa pasta. Em seguida, crie uma aplicação nova
chamada meal_plans. Crie uma página inicial simples para esse projeto.
18.6 Página inicial da Pizzaria: Adicione uma página inicial ao projeto Pizzaria que
iniciou no Exercício 18.4 (página 478).
Herança de template
Ao desenvolver um site, alguns elementos precisarão ser repetidos
em cada página. Em vez de escrever esses elementos diretamente
em cada página, é possível criar um template base com os
elementos repetidos e, em seguida, fazer com que cada página
herde esse template. Essa abordagem viabiliza focar o
desenvolvimento dos aspectos específicos de cada página e facilita
muito alterar a aparência geral do projeto.
Template-pai
Criaremos um template chamado base.html no mesmo diretório que
index.html. Esse arquivo conterá os elementos comuns a todas as
páginas; todos os outros templates herdarão de base.html. O único
elemento que queremos repetir em cada página agora é o título na
parte superior. Já que incluiremos esse template em todas as
páginas, faremos com que o título tenha um link para a página
inicial:
base.html
<p>
1 <a href="{% url 'learning_logs:index' %}">Learning Log</a>
</p>
Template-filho
Agora, é necessário reescrevermos index.html para herdar de
base.html. Adicione o seguinte código ao index.html:
index.html
1 {% extends 'learning_logs/base.html' %}
2 {% block content %}
<p>Learning Log helps you keep track of your learning, for any topic you're
interested in.</p>
3 {% endblock content %}
Se compararmos esse arquivo com o index.html original, veremos
que substituímos o título Learning Log pelo código que herda de um
template-pai 1. Um template-filho deve ter uma tag {% extends %} na
primeira linha para informar ao Django de qual template-pai herdar.
Como o arquivo base.html faz parte de learning_logs, incluímos
learning_logs no path para o template-pai. Essa linha extrai tudo o
que está no template base.html e possibilita que index.html defina o
que será inserido no espaço reservado pelo bloco content.
Definimos o bloco de conteúdo inserindo uma tag {% block %} com o
nome content 2. Tudo o que não estamos herdando do template-pai
será inserido dentro do bloco content. Aqui, esse é o parágrafo que
descreve o projeto Registro de Aprendizagem. Indicamos que
terminamos de definir o conteúdo usando uma tag {% endblock content
%} 3. A tag {% endblock %} não exige um nome, mas se um template
aumentar para acomodar múltiplos blocos, ajuda saber exatamente
o final de cada bloco.
Talvez você esteja começando a perceber as vantagens da herança
de template: no template-filho, basta incluir conteúdo específico
dessa página. Isso não apenas simplifica cada template, como
também facilita e muito a alterar o site. Para modificar um elemento
comum a muitas páginas, basta modificar o template-pai. Assim, as
mudanças são transferidas para todas as páginas que herdam desse
template. Em um projeto com dezenas ou centenas de páginas, uma
estrutura como essa facilita e agiliza bastante melhorar nosso site.
Em um projeto grande, é comum ter um template-pai chamado
base.html para todo o site e templates-pai para cada seção principal
do site. Todos os templates de seção herdam de base.html e cada
página no site herda de um template de seção. Dessa forma,
conseguimos modificar facilmente a aparência geral do site, assim
como qualquer seção ou qualquer página individual. Essa
configuração viabiliza um modo mais eficiente de se trabalhar e nos
incentiva a atualizar regularmente nosso projeto a longo prazo.
Página de tópicos
Agora que temos uma abordagem eficaz para criar páginas,
podemos focar nossas próximas duas páginas: a página de tópicos
gerais e a página para exibir entradas para um único tópico. A
página de tópicos mostrará todos os tópicos que os usuários
criaram, sendo a primeira página em que trabalharemos com a
utilização de dados.
def index(request):
-- trecho de código omitido --
2 def topics(request):
"""Mostra todos os tópicos"""
3 topics = Topic.objects.order_by('date_added')
4 context = {'topics': topics}
5 return render(request, 'learning_logs/topics.html', context)
Primeiro, importamos o template associado aos dados de que
precisamos 1. A função topics() precisa de um parâmetro: o objeto
request do Django recebido do servidor 2. Consultamos o banco de
dados solicitando os objetos Topic, ordenados pelo atributo
date_added 3. Atribuímos o queryset resultante a topics.
Template de tópicos
O template para a página de tópicos recebe o dicionário context,
assim consegue usar os dados que topics() fornecem. Crie um arquivo
chamado topics.html no mesmo diretório que index.html. Veja como
podemos exibir os tópicos no template:
topics.html
{% extends 'learning_logs/base.html' %}
{% block content %}
<p>Topics</p>
1 <ul>
2 {% for topic in topics %}
3 <li>{{ topic.text }}</li>
4 {% empty %}
<li>No topics have been added yet.</li>
5 {% endfor %}
6 </ul>
{% endblock content %}
Utilizamos a tag {% extends %} para herdar de base.html, assim como
fizemos na página inicial e, depois, abrimos um bloco content. O corpo
dessa página contém uma lista com marcadores dos tópicos
inseridos. No HTML padrão, uma lista com marcadores se chama
lista não ordenada e é indicada pelas tags <ul></ul>. A tag de
abertura <ul> inicia a lista de tópicos com os marcadores 1.
Em seguida, usamos uma template tag, equivalente a um loop for,
que percorre a lista do topics dicionário context 2. Nos templates, o
código usado difere bastante do código Python. O Python usa
indentação para sinalizar quais linhas de uma instrução for fazem
parte de um loop. Em um template, cada loop for precisa de uma tag
explícita {% endfor %} sinalizando onde o final do loop ocorre. Por isso,
em um template, veremos loops escritos assim:
{% for item in lista %}
faz algo com cada item
{% endfor %}
Dentro do loop, queremos transformar cada tópico em um item na
lista com marcadores. Para exibir uma variável em um template,
insira o nome da variável em chaves duplas. As chaves duplas não
aparecerão na página; apenas indicam ao Django que estamos
usando uma variável de template. Assim, o código {{ topic.text }} 3 será
substituído pelo valor do atributo text do tópico atual em cada
passagem pelo loop. A tag HTML <li></li> indica um item de lista.
Qualquer coisa entre essas tags, dentro de um par de tags <ul></ul>,
aparecerá como um item com marcadores na lista.
Além disso, utilizamos a template tag {% empty %} 4, que informa ao
Django o que fazer se não houver itens na lista. Nesse caso,
exibimos uma mensagem informando ao usuário que nenhum tópico
foi adicionado ainda. As duas últimas linhas fecham o loop for 5 e, em
seguida, encerram a lista com marcadores 6.
Agora, é necessário modificar o template base a fim de incluir um
link para a página de tópicos. Adicione o seguinte código à
base.html:
base.html
<p>
1 <a href="{% url 'learning_logs:index' %}">Learning Log</a> -
2 <a href="{% url 'learning_logs:topics' %}">Topics</a>
</p>
View do tópico
A função topic() precisa obter o tópico e todas as entradas associadas
do banco de dados, assim como fizemos anteriormente no shell do
Django:
views.py
-- trecho de código omitido --
1 def topic(request, topic_id):
"""Mostra um único tópico e todas as suas entradas"""
2 topic = Topic.objects.get(id=topic_id)
3 entries = topic.entry_set.order_by('-date_added')
4 context = {'topic': topic, 'entries': entries}
5 return render(request, 'learning_logs/topic.html', context)
É a primeira função view que requer um parâmetro diferente do
objeto request. A função aceita o valor coletado pela expressão
/<int:topic_id>/ e o atribui a topic_id 1. Em seguida, usamos get() para
acessar o tópico, assim como fizemos no shell do Django 2. Depois,
obtemos todas as entradas associadas a esse tópico e as ordenamos
de acordo com date_added 3. O sinal de menos na frente de date_added
ordena os resultados em ordem inversa, que exibirá as entradas
mais recentes primeiro. Armazenamos o tópico e as entradas no
dicionário contexto 4 e chamamos render() com o objeto request, com o
modelo topic.html e com o dicionário context 5.
NOTA As frases de código em 2 e 3 são chamadas de queries, pois
consultam o banco de dados por meio de queries para obter
informações específicas. Quando escrevemos queries como
esse em nossos projetos, primeiro, recomenda-se testá-las no
shell do Django. No shell, teremos um feedback mais rápido
do que teríamos se criássemos uma view e um template para
verificar os resultados em um navegador.
Template do tópico
O template precisa exibir o nome do tópico e as entradas. É
necessário também informar o usuário se ainda não foram inseridas
entradas para esse tópico.
topic.html
{% extends 'learning_logs/base.html' %}
{% block content %}
<p>Entries:</p>
2 <ul>
3 {% for entry in entries %}
<li>
4 <p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
5 <p>{{ entry.text|linebreaks }}</p>
</li>
6 {% empty %}
<li>There are no entries for this topic yet.</li>
{% endfor %}
</ul>
{% endblock content %}
Incrementamos base.html, como faremos em todas as páginas do
projeto. Depois, mostramos o atributo text do tópico solicitado 1. A
variável topic está disponível porque está incluída no dicionário context.
Assim, iniciamos uma lista com marcadores 2 a fim de mostrar cada
uma das entradas e percorrê-las com um loop 3, como fizemos com
os tópicos anteriores.
Cada marcador enumera duas informações: o timestamp o texto
completo de cada entrada. Para o timestamp 4, exibimos o valor do
atributo date_added. Nos templates Django, uma linha vertical (|)
representa um filtro de template – uma função que modifica o valor
em uma variável de template durante o processo de renderização. O
filtro date:'M d, Y H:i' exibe timestamps no formato January 1, 2022
23:00. A próxima linha exibe o valor do atributo text da entrada
atual. O filtro linebreaks 5 assegura que entradas extensas de texto
incluam quebras de linha em um formato compreendido pelos
navegadores, em vez de mostrar um bloco ininterrupto de texto.
Mais uma vez, recorremos à template tag {% empty %} 6 a fim de
exibir uma mensagem informando ao usuário que nenhuma entrada
foi inserida.
Recapitulando
Neste capítulo, aprendemos como começar a desenvolver aplicações
web simples com o framework Django. Vimos uma breve
especificação do projeto, instalamos o Django em um ambiente
virtual, configuramos um projeto e verificamos se o projeto foi
devidamente configurado. Configuramos uma aplicação e definimos
modelos para representar os dados dessa aplicação. Vimos como os
bancos de dados e como o Django nos ajuda a migrar banco de
dados após alterarmos modelos. Criamos um superusuário para o
site admin e usamos esse site para fornecer alguns dados iniciais.
Além do mais, exploramos o shell do Django, que possibilita
trabalhar com os dados do projeto em uma sessão de terminal.
Aprendemos a definir URLs, criar funções view e escrever templates
para criar páginas de um site. Recorremos também à herança de
templates a fim de simplificar a estrutura de templates individuais e
facilitar a modificação do site à medida que o projeto evolui.
No Capítulo 19, desenvolveremos páginas intuitivas e fáceis de usar
que possibilitam aos usuários adicionar entradas e tópicos novos e
editar entradas existentes sem passar pelo site admin. Veremos
também um sistema de registro de usuário, que possibilita aos
usuários criarem uma conta e seu próprio registro de aprendizagem.
Trata-se do cerne de uma aplicação web – a capacidade de
desenvolver uma aplicação com o qual qualquer número de usuários
consegue interagir.
19
capítulo
Contas de usuário
1 class TopicForm(forms.ModelForm):
class Meta:
2 model = Topic
3 fields = ['text']
4 labels = {'text': ''}
Primeiro, importamos o módulo forms e o modelo com o qual
trabalharemos, Topic. Em seguida, definimos uma classe chamada
TopicForm, que herda de forms.ModelForm 1.
URL new_topic
O URL para uma página nova deve ser breve e descritivo. Quando
quiser adicionar um tópico novo, direcionaremos o usuário para
http://localhost:8000/new_topic/. Vejamos o padrão de URL para a
página new_topic; adicione esse código a learning_logs/urls.py:
learning_logs/urls.py
-- trecho de código omitido --
urlpatterns = [
-- trecho de código omitido --
# Página para adicionar um tópico novo
path('new_topic/', views.new_topic, name='new_topic'),
]
Esse padrão de URL envia requisições para a função view new_topic(),
que escreveremos a seguir.
views.py
from django.shortcuts import render, redirect
Template new_topic
Agora, criaremos um template novo chamado new_topic.html para
exibir o formulário que acabamos de desenvolver:
new_topic.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Add a new topic:</p>
{% endblock content %}
Esse modelo estende base.html, por isso tem a mesma estrutura
base que o restante das páginas no Registro de Aprendizagem.
Usamos as tags <form></form> para definir um formulário HTML 1. O
argumento action informa ao navegador para onde enviar os dados
fornecidos no formulário; nesse caso, nós os reenviamos para a
função view new_topic(). O argumento method instrui o navegador a
enviar os dados como uma requisição POST.
O Django usa a template tag {% csrf_token %} 2 a fim de impedir que
cibercriminosos usem o formulário para obter acesso não autorizado
ao servidor. (Esse tipo de ataque é conhecido como cross-site
request forgery [falsificação de solicitação entre sites].) Em seguida,
exibimos o formulário; aqui, é possível ver como o Django simplifica
determinadas tarefas, como exibir um formulário. Basta incluir a
variável de template {{ form.as_div }} a fim de que o Django crie todos
os campos necessários para exibir automaticamente o formulário 3.
O modificador as_div instrui o Django a renderizar todos os elementos
do formulário como elementos <div></div> HTML ; uma maneira
simples de exibir elegantemente o formulário.
Como o Django não cria um botão de envio para formulários,
definimos um antes de fechar o formulário 4.
topics.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<ul>
-- trecho de código omitido --
</ul>
{% endblock content %}
Insira o link após a lista de tópicos existentes. A Figura 19.1 mostra
o formulário resultante; tente usar o formulário para adicionar
alguns tópicos novos.
Figura 19.1: A página para adicionar um tópico novo.
forms.py
from django import forms
class TopicForm(forms.ModelForm):
-- trecho de código omitido --
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ['text']
1 labels = {'text': ''}
2 widgets = {'text': forms.Textarea(attrs={'cols': 80})}
Atualizamos a instrução import para incluir Entry e Topic. Criamos uma
classe nova chamada EntryForm que herda de forms.ModelForm. A classe
EntryForm tem uma classe Meta aninhada listando o modelo em que se
baseia e o campo a ser incluído no formulário. Mais uma vez,
atribuímos ao campo "text" um rótulo em branco 1.
Incluímos o atributo widgets em EntryForm 2.Um widget é um elemento
de formulário HTML, como uma caixa de texto de linha única, área
de texto de várias linhas ou lista suspensa. Ao incluir o atributo
widgets, podemos substituir as opções default de widget do Django.
Aqui, estamos instruindo o Django a utilizar um elemento a
forms.Textarea com uma largura de 80 colunas, em vez das 40 colunas
default. Com isso, os usuários têm espaço suficiente para fornecer
uma entrada significativa.
URL new_entry
Como entradas novas devem ser associadas a um tópico específico,
precisamos incluir um argumento topic_id no URL para adicionar uma
entrada nova. Vejamos o URL que adicionamos a
learning_logs/urls.py:
learning_logs/urls.py
-- trecho de código omitido --
urlpatterns = [
-- trecho de código omitido --
# Página para adicionar uma entrada nova
path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
]
Esse padrão de URL corresponde a qualquer URL com o formulário
http://localhost:8000/new_entry/id/, em que id é um número
correspondente ao ID do tópico. O código <int:topic_id> coleta um
valor numérico e o atribui à variável topic_id. Quando um URL
correspondente a esse padrão é solicitado, o Django envia a
requisição e o ID do tópico à função view new_entry().
2 if request.method != 'POST':
# Nenhum dado enviado; cria um formulário em branco
3 form = EntryForm()
else:
# Dados POST enviados; processa os dados
4 form = EntryForm(data=request.POST)
if form.is_valid():
5 new_entry = form.save(commit=False)
6 new_entry.topic = topic
new_entry.save()
7 return redirect('learning_logs:topic', topic_id=topic_id)
new_entry.html
{% extends "learning_logs/base.html" %}
{% block content %}
{% endblock content %}
Mostramos o tópico na parte superior da página 1. Assim, o usuário
pode ver em qual tópico está adicionando uma entrada. O tópico
também se comporta como um link que retorna à página principal
desse tópico.
O argumento action do formulário inclui o valor topic.id no URL a fim de
que a função view consiga associar a entrada nova ao tópico
correto 2. Desconsiderando isso, esse template se parece com
new_topic.html.
{% block content %}
<p>Entries:</p>
<p>
<a href="{% url 'learning_logs:new_entry' topic.id %}">Add new entry</a>
</p>
<ul>
-- trecho de código omitido --
</ul>
{% endblock content %}
Inserimos o link para adicionar entradas antes de mostrá-las, porque
adicionar uma entrada nova será a ação mais comum dessa página.
A Figura 19.2 mostra a página new_entry. Agora, os usuários podem
adicionar tópicos novos e quantas entradas quiserem para cada
tópico. Teste a página new_entry: adicione algumas entradas a tópicos
que você criou.
Editando as entradas
Agora, desenvolveremos uma página para que os usuários consigam
editar as entradas que adicionaram.
URL edit_entry
É necessário que o URL da página passe o ID da entrada a ser
editada. Vejamos learning_logs/urls.py:
urls.py
-- trecho de código omitido --
urlpatterns = [
-- trecho de código omitido --
# Página para editar uma entrada
path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
]
Esse padrão de URL corresponde aos URLs como
http://localhost:8000/edit_entry/id/. Em nosso caso, o valor de id é
atribuído ao parâmetro entry_id. O Django envia requisições que
correspondem com esse formato à função view edit_entry().
if request.method != 'POST':
# Requisição inicial; pré-preenche formulário com a entrada atual
2 form = EntryForm(instance=entry)
else:
# Dados POST enviados; processa os dados
3 form = EntryForm(instance=entry, data=request.POST)
if form.is_valid():
4 form.save()
5 return redirect('learning_logs:topic', topic_id=topic.id)
{% block content %}
<p>Edit entry:</p>
{% endblock content %}
O argumento action reenvia o formulário à função edit_entry() para
processamento 1. Já que incluímos entry.id como um argumento na
tag {% url %}, a função view pode modificar o objeto correto de
entrada. Rotulamos o botão enviar como Save changes. Desse modo,
lembramos ao usuário que ele está salvando edições, não criando
uma entrada nova 2.
Aplicação accounts
Começaremos desenvolvendo uma aplicação nova chamada accounts
com o comando startapp:
(ll_env)learning_log$ python manage.py startapp accounts
(ll_env)learning_log$ ls
1 accounts db.sqlite3 learning_logs ll_env ll_project manage.py
(ll_env)learning_log$ ls accounts
2 __init__.py admin.py apps.py migrations models.py tests.py views.py
Como o sistema de autenticação default é criado com base no
conceito de contas de usuário, usar o nome accounts facilita a
integração com o sistema default. O comando startapp mostrado aqui
cria um diretório novo chamado accounts 1 com uma estrutura
idêntica à aplicação learning_logs 2.
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
path('', include('learning_logs.urls')),
]
Adicionamos uma linha para incluir o arquivo urls.py de accounts. Essa
linha corresponderá a qualquer URL que comece com a palavra
accounts, como http://localhost:8000/accounts/login/.
Página de login
Primeiro, implementaremos uma página de login. Como vamos
utilizar a view login default que o Django fornece, o padrão de URL
para essa aplicação será um pouco diferente. Crie um arquivo novo
urls.py no diretório ll_project/accounts/e adicione o seguinte código
dentro dele:
accounts/urls.py
"""Define padrões de URL para contas"""
app_name = 'accounts'
urlpatterns = [
# Inclui URLs de autenticação default
path('', include('django.contrib.auth.urls')),
]
Importamos a função path e, em seguida, importamos a função include
para que possamos incluir alguns URLs de autenticação default
definidos pelo Django. Esses URLs default incluem padrões de URL
nomeados, como 'login' e 'logout'. Definimos a variável app_name como
'accounts' a fim de que o Django consiga distinguir esses URLs dos
URLs pertencentes a outras aplicações. Mesmo os URLs default
fornecidos pelo Django, quando incluídos no arquivo urls.py da
aplicação accounts, serão acessíveis por meio do namespace de
accounts.
Template de login
Quando o usuário solicita a página de login, embora o Django use
uma função view default, ainda precisamos fornecer um template
para a página. Como as views default de autenticação procuram
templates dentro de uma pasta chamada registration, é necessário
criar essa pasta. Dentro do diretório ll_project/accounts/, crie um
diretório chamado templates; dentro dele, crie outro diretório
chamado registration. Vejamos o template login.html, que deve ser
salvo em ll_project/accounts/templates/registration:
login.html
{% extends 'learning_logs/base.html' %}
{% block content %}
1 {% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
{% endblock content %}
Esse template incrementa base.html para garantir que a página de
login tenha a mesma aparência que o restante do site. Perceba que
o template em uma aplicação pode herdar de um template de outra
aplicação.
Caso o atributo errors do formulário esteja definido, exibimos uma
mensagem de erro 1, informando que a combinação de nome e de
usuário e de senha não corresponde a nenhuma informação
armazenada no banco de dados.
Queremos que view de login processe o formulário, assim definimos
o argumento action como URL da página de login 2. A view de login
envia um objeto form para o template, e cabe a nós exibir o
formulário 3 e adicionar um botão de envio 4.
Configurando LOGIN_REDIRECT_URL
Quando um usuário faz login com sucesso, o Django precisa saber
para onde enviar esse usuário. Controlamos esse processo no
arquivo settings.
Adicione o seguinte código ao final de settings.py:
settings.py
-- trecho de código omitido --
# Minhas configurações
LOGIN_REDIRECT_URL = 'learning_logs:index'
Já que temos todas as configurações default em settings.py, é útil
marcar a seção em que estamos adicionando configurações novas. A
primeira configuração nova que adicionaremos é LOGIN_REDIRECT_URL,
que informa ao Django para qual URL redirecionar após uma
tentativa de login bem-sucedida.
Link da página de login
Adicionaremos o link de login à base.html para que apareça em
todas as páginas. Como não queremos que o link seja exibido
quando o usuário já estiver logado, vamos aninhá-lo dentro de uma
tag {% if %}:
base.html
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics' %}">Topics</a> -
1 {% if user.is_authenticated %}
2 Hello, {{ user.username }}.
{% else %}
3 <a href="{% url 'accounts:login' %}">Log in</a>
{% endif %}
</p>
Fazendo logout
Agora, é necessário fornecer um modo de os usuários fazerem
logout. As requisições de logout devem ser enviadas como
requisições POST, logo adicionaremos um pequeno formulário de
logout a base.html. Quando clicam no botão de logout, os usuários
são direcionados para uma página, que confirma que fizeram logout.
{% if user.is_authenticated %}
1 <hr />
2 <form action="{% url 'accounts:logout' %}" method='post'>
{% csrf_token %}
<button name='submit'>Log out</button>
</form>
{% endif %}
O default de URL padrão para logout é 'accounts/logout/'. No entanto, a
requisição deve ser enviada como uma requisição POST; caso
contrário, hackers podem realizar ataques sobrecarregando
facilmente as requisições de logout. Para fazer a requisição de logout
usar POST, definimos um formulário simples.
Inserimos o formulário na parte inferior da página, abaixo de um
elemento de régua horizontal (<hr />) 1. É um modo fácil de sempre
manter o botão de logout em uma posição consistente, abaixo de
qualquer outro conteúdo da página. O formulário propriamente dito
tem o URL de logout como seu argumento action e 'post' como o
método de requisição 2. No Django, cada formulário precisa incluir
{% csrf_token %}, até mesmo um formulário simples como esse. Esse
formulário está vazio, exceto pelo botão enviar.
Configurando LOGOUT_REDIRECT_URL
Quando o usuário clica no botão de logout, o Django precisa saber
para onde enviá-los. Controlamos esse comportamento em
settings.py:
settings.py
-- trecho de código omitido --
# Minhas configurações
LOGIN_REDIRECT_URL = 'learning_logs:index'
LOGOUT_REDIRECT_URL = 'learning_logs:index'
Aqui, a configuração LOGOUT_REDIRECT_URL instrui o Django a
É
redirecionar os usuários deslogados à página inicial. É um modo
simples de confirmar que eles foram desconectados, já que não
devem ver mais o nome de usuário após fazerem o logout.
Página de cadastro
A seguir, criaremos uma página para que usuários novos possam se
cadastrar na aplicação. Utilizaremos o UserCreationForm default do
Django, mas escreveremos nossa própria função view e template.
app_name = accounts
urlpatterns = [
# Inclui URLs de autenticação default
path('', include('django.contrib.auth.urls')),
# Página de cadastro
path('register/', views.register, name='register'),
]
Importamos o módulo views de accounts, isso é necessário porque
estamos escrevendo nossa própria view para a página de cadastro.
O padrão para a página de cadastro corresponde ao URL
http://localhost:8000/accounts/register/e envia requisições para a
função register() que estamos prestes a escrever.
def register(request):
"""Cadastra um usuário novo"""
if request.method != 'POST':
# Exibe o formulário em branco de cadastro
1 form = UserCreationForm()
else:
# Processa o formulário preenchido
2 form = UserCreationForm(data=request.POST)
3 if form.is_valid():
4 new_user = form.save()
# Faz o login do usuário e o redireciona para a página inicial
5 login(request, new_user)
6 return redirect('learning_logs:index')
Template do cadastro
Agora, crie um template para a página de cadastro de usuários, que
será semelhante à página de login. Não se esqueça de salvá-lo no
mesmo diretório que login.html:
register.html
{% extends "learning_logs/base.html" %}
{% block content %}
<button name="submit">Register</button>
</form>
{% endblock content %}
Esse template deve ser parecido com os outros templates baseados
em formulários que estamos desenvolvendo. Mais uma vez,
utilizamos o método as_div para que o Django exiba todos os campos
devidamente no formulário, incluindo quaisquer mensagens de erro
se o formulário não for preenchido de forma correta.
Link para a página de cadastro
Vamos adicionar o código para mostrar o link da página de cadastro
a qualquer usuário que não esteja logado no momento:
base.html
-- trecho de código omitido --
{% if user.is_authenticated %}
Hello, {{ user.username }}.
{% else %}
<a href="{% url 'accounts:register' %}">Register</a> -
<a href="{% url 'accounts:login' %}">Log in</a>
{% endif %}
-- trecho de código omitido --
Agora, os usuários logados veem uma mensagem personalizada e
um botão de logout. Os usuários que não estão logados veem um
link de cadastro e um link de login. Teste a página de cadastro
criando múltiplas contas de usuário com nomes de usuário
diferentes.
Na próxima seção, restringiremos algumas das páginas para que
fiquem disponíveis apenas aos usuários registrados e garantiremos
que cada tópico pertença a um usuário específico.
NOTA O sistema de cadastro que configuramos possibilita que
qualquer pessoa crie diversas contas na aplicação Registro de
Aprendizagem. Alguns sistemas exigem que os usuários
validem sua identidade enviando um e-mail de confirmação
que os usuários devem responder. Com isso, o sistema gera
menos contas de spam do que o sistema simples que estamos
usando aqui. Contudo, como você está aprendendo a
desenvolver aplicações, é perfeitamente adequado praticar
com um sistema simples de cadastro de usuários como o que
estamos usando.
FAÇA VOCÊ MESMO
19.2 Contas do blog: Adicione um sistema de autenticação e cadastro de usuários ao
projeto do Blog iniciado no Exercício 19.1 (página 509). Lembre-se de que os usuários
logados devem ver seu nome de usuário em algum lugar na tela e que os usuários não
cadastrados devem ver um link para a página de registro.
@login_required
def topics(request):
"""Mostra todos os tópicos"""
-- trecho de código omitido --
Primeiro, importamos a função login_required(). Usamos login_required()
como um decorator para a função view topics(), anexando login_required
com o símbolo @. Com isso, o Python sabe como executar o código
em login_required() antes do código em topics().
Em login_required(),
o código verifica se um usuário está logado e, em
topics(), o Django executa o código apenas se um usuário estiver
logado. Se não estiver logado, o usuário será redirecionado para a
página de login.
Para que esse redirecionamento funcione, é necessário modificar
settings.py, assim o Django sabe onde encontrar a página de login.
Adicione o seguinte código ao final de settings.py:
settings.py
-- trecho de código omitido --
# Minhas configurações
LOGIN_REDIRECT_URL = 'learning_logs:index'
LOGOUT_REDIRECT_URL = 'learning_logs:index'
LOGIN_URL = 'accounts:login'
Agora, quando um usuário não autenticado solicita uma página
protegida pelo decorator @login_required, o Django enviará o usuário
para o URL definido por LOGIN_URL em settings.py.
É possível testar essa configuração fazendo logout de qualquer conta
de usuário e indo para a página inicial. Clique no link Topics, que
deve redirecioná-lo para a página de login. Depois, faça login em
qualquer uma de suas contas e, na página inicial, clique mais uma
vez no link Topic. Você deve ser capaz de acessar a página de
tópicos.
É
outras páginas do projeto. É possível corrigir com facilidade o acesso
com restrição excessiva, já que é menos perigoso do que não
restringir páginas confidenciais.
No Registro de Aprendizado, manteremos a página inicial e a página
de registro sem restrições. Restringiremos o acesso às demais
páginas.
Vejamos o learning_logs/views.py com os decorators @login_required
aplicados a todas as views, exceto em index():
learning_logs/views.py
-- trecho de código omitido --
@login_required
def topics(request):
-- trecho de código omitido --
@login_required
def topic(request, topic_id):
-- trecho de código omitido --
@login_required
def new_topic(request):
-- trecho de código omitido --
@login_required
def new_entry(request, topic_id):
-- trecho de código omitido --
@login_required
def edit_entry(request, entry_id):
-- trecho de código omitido --
Tente acessar cada uma dessas páginas quando estiver deslogado;
você deve ser redirecionado à página de login. Não será possível
clicar em links para páginas como new_topic. Mas, caso insira o URL
http://localhost:8000/new_topic/, você será redirecionado para a
página de login. É necessário restringir o acesso a qualquer URL que
seja publicamente acessível, relacionado a dados privados do
usuário.
Vinculando dados a determinados usuários
Logo depois, precisamos vincular os dados ao usuário que os
enviaram. Basta vincular os dados de nível mais alto da hierarquia a
um usuário. Assim, os dados de nível mais baixo também serão
vinculados. No Registro de Aprendizagem, os tópicos estão no nível
mais alto de dados da aplicação e todas as entradas estão
associadas a um tópico. Desde que cada tópico pertença a um
usuário específico, podemos rastrear a propriedade de cada entrada
no banco de dados.
Modificaremos o modelo Topic adicionando uma relação de chave
estrangeira a um usuário. Será necessário migrar o banco de dados.
Por último, modificaremos algumas das views para que mostrem
somente os dados associados ao usuário atualmente conectado.
class Topic(models.Model):
"""Um tópico que o usuário está aprendendo"""
Text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
"""Retorna uma string representando o tópico"""
Return self.text
class Entry(models.Model):
-- trecho de código omitido --
Importamos o modelo user de django.contrib.auth. Depois, adicionamos
um campo owner a Topic, que estabelece uma relação de chave
estrangeira com o modelo User. Se um usuário for excluído, todos os
tópicos associados a ele também serão excluídos.
Identificando usuários existentes
Quando migrarmos o banco de dados, o Django modificará o banco
de dados a fim de que possa armazenar uma conexão entre cada
tópico e um usuário. Para realizar a migração, o Django precisa
saber qual usuário associar a cada tópico existente. A abordagem
mais simples é começar atribuindo todos os tópicos existentes a um
usuário – por exemplo, ao superusuário. Mas, antes, precisamos
saber o ID do usuário.
Vejamos os IDs de todos os usuários criados até agora. Inicie uma
sessão shell do Django e execute os seguintes comandos:
(ll_env)learning_log$ python manage.py shell
1 >>> from django.contrib.auth.models import User
2 >>> User.objects.all()
<QuerySet [<User: ll_admin>, <User: eric>, <User: willie>]>
3 >>> for user in User.objects.all():
... print(user.username, user.id)
...
ll_admin 1
eric 2
willie 3
>>>
Primeiro, importamos o modelo User para a sessão do shell 1. Em
seguida, examinamos todos os usuários criados até agora 2. A saída
mostra três usuários para a minha versão do projeto: ll_admin, eric e
willie.
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
-- trecho de código omitido --
A resposta 404 é uma resposta de erro padrão, retornada quando
um recurso solicitado não existe em um servidor. Aqui, importamos a
exceção Http404 1, que será lançada se o usuário solicitar um tópico
ao qual não deve ter acesso. Após receber a requisição de tópico,
garantimos que o usuário do tópico corresponda ao usuário
atualmente logado antes de renderizar a página. Se o proprietário do
tópico requisitado não for o mesmo que o usuário atual, lançamos a
exceção 2 Http404 e o Django retorna uma página 404-error.
Agora, se tentarmos visualizar as entradas de tópico de outro
usuário, vermos a mensagem “Page Not Found” do Django. No
Capítulo 20, vamos configurar o projeto para que os usuários vejam
uma página de erro adequada em vez de uma página de depuração.
if request.method != 'POST':
-- trecho de código omitido --
Acessamos a entrada e o tópico associado a essa entrada. Em
seguida, verificamos se o proprietário do tópico corresponde ao
usuário atualmente logado; se não corresponderem, criamos uma
exceção Http404.
Recapitulando
Neste capítulo, aprendemos como os formulários possibilitam que os
usuários adicionem entradas e tópicos novos e editem entradas
existentes. Em seguida, aprendemos como implementar contas de
usuário. Viabilizamos aos usuários existentes a capacidade de fazer
login e logout, e recorremos ao UserCreationForm default do Django para
permitir que pessoas criassem contas novas.
Após desenvolvermos um sistema simples de autenticação e registro
de usuários, restringimos o acesso a usuários conectados a
determinadas páginas com o decorator @login_required. Em seguida,
atribuímos dados a usuários específicos por meio de um
relacionamento de chave estrangeira. Além disso, aprendemos a
migrar o banco de dados quando a migração exige que
especifiquemos alguns dados padrão.
Por último, aprendemos como garantir que um usuário só possa ver
os dados que lhe pertencem modificando as funções view.
Acessamos os dados adequados com o método filter() e comparamos
o proprietário dos dados solicitados ao usuário atualmente logado.
Nem sempre fica claro quais dados devemos disponibilizar e quais
dados devemos proteger, pois essa habilidade é adquirida com muita
prática. Neste capítulo, as decisões que tomamos para proteger os
dados de nossos usuários também exemplificam por que trabalhar
com outras pessoas é uma boa ideia ao criar um projeto: quando
outra pessoa analisa seu projeto, a probabilidade de você identificar
áreas vulneráveis é maior.
Agora, temos um projeto que funciona perfeitamente quando
executado em nossa máquina local. No capítulo final, vamos estilizar
o Registro de Aprendizagem para que fique visualmente elegante e
vamos fazer o deploy do projeto em um servidor. Assim, qualquer
pessoa com acesso à internet pode se cadastrar no site e criar uma
conta.
20 capítulo
Aplicação django-bootstrap5
Recorreremos à aplicação django-bootstrap5 para integrar a
Bootstrap ao nosso projeto. Essa aplicação faz o download dos
arquivos necessários da biblioteca Bootstrap, os insere em um local
adequado em seu projeto e disponibiliza as diretivas de estilo nos
templates do projeto.
Para instalar o django-bootstrap5, digite o seguinte comando em um
ambiente virtual ativo:
(ll_env)learning_log$ pip install django-bootstrap5
-- trecho de código omitido --
Successfully installed beautifulsoup4-4.11.1 django-bootstrap5-21.3
soupsieve-2.3.2.post1
Em seguida, precisamos adicionar django-bootstrap5 a INSTALLED_APPS
em settings.py:
settings.py
-- trecho de código omitido --
INSTALLED_APPS = [
# Minhas aplicações
'learning_logs',
'accounts',
# Aplicações de terceiros
'django_bootstrap5',
Modificando base.html
É necessário reescrever base.html usando o template da Bootstrap.
Incrementaremos o novo base.html em seções. Como se trata de um
arquivo grande, talvez você queira copiá-lo a partir dos recursos
online, disponíveis em https://ehmatthes.github.io/pcc_3e. Se copiar o
arquivo, é essencial ler a seção a seguir para entender as mudanças
feitas.
5 {% load django_bootstrap5 %}
{% bootstrap_css %}
{% bootstrap_javascript %}
</head>
Primeiro, declaramos esse arquivo como um documento HTML 1
escrito em inglês 2. Um arquivo HTML é dividido em duas partes
principais: o cabeçalho e o corpo. O cabeçalho do arquivo começa
com a tag de abertura <head> 3. O cabeçalho de um arquivo HTML
não armazena nenhum conteúdo da página; apenas informa ao
navegador o necessário para que exiba corretamente a página.
Incluímos um elemento <title> para a página, que será exibido na
barra de título do navegador sempre que o Registro de
Aprendizagem estiver aberto 4.
Antes de fecharmos a seção do cabeçalho, carregamos a coleção de
template tags disponíveis no django-bootstrap5 5. A template tag
{% bootstrap_css %} é uma tag personalizada do django-bootstrap5;
carrega todos os arquivos CSS necessários para implementar os
estilos Bootstrap. A tag a seguir habilita todo o comportamento
interativo que podemos usar em uma página, como barras de
navegação recolhíveis. A tag de fechamento </head> aparece na
última linha.
Agora, todas as opções de estilo da Bootstrap estão disponíveis em
qualquer template herdado de base.html. Caso queira usar as
template tags personalizadas do django-bootstrap5, cada template
precisará incluir a tag {% load django_bootstrap5 %}.
</body>
</html>
O primeiro elemento novo é a tag de abertura <body>. O corpo de um
arquivo HTML tem o conteúdo que os usuários verão em uma
página. Em seguida, temos um elemento <nav>, que abre o código
para a barra de navegação na parte superior da página 1. Tudo
dentro desse elemento é estilizado segundo as regras de estilo da
Bootstrap, definidas pelos seletores navbar, navbar-expand-md e o
restante que podemos ver aqui. Um seletor estabelece a quais
elementos de uma página uma determinada regra de estilo se aplica.
Os seletores navbar-light e bg-light estilizam a barra de navegação com
background de tema claro. O mb em mb-4 é abreviação de margin-
bottom; esse seletor garante que um pouco de espaço apareça entre
a barra de navegação e o resto da página. O seletor border fornece
uma borda fina ao redor do background claro para diferenciá-lo um
pouco do restante da página.
A tag <div> na próxima linha abre um contêiner redimensionável que
armazenará a barra de navegação geral. O termo div é abreviação
de division; desenvolvemos uma página web dividindo-a em seções
e definindo regras de estilo e comportamento que se aplicam a essa
seção. Quaisquer regras de estilo ou comportamento definidas em
uma tag de abertura <div> afetam tudo o que vemos até sua tag de
fechamento correspondente, escrita como </div>.
Depois, definimos o nome do projeto, Learning Log, para aparecer
como primeiro elemento na barra de navegação 2. Isso também
servirá como link para a página inicial, assim como fizemos na
versão minimamente estilizada do projeto que desenvolvemos nos
dois capítulos anteriores. O seletor navbar-brand estiliza esse link para
que se destaque do resto dos links e ajuda a adicionar uma marcar
ao site.
O template da Bootstrap define um botão que aparece se a janela
do navegador for muito estreita, a fim de exibir horizontalmente a
barra de navegação por completo 3. Quando o usuário clicar no
botão, os elementos de navegação aparecem em uma lista
suspensa. A referência collapse faz com que a barra de navegação
seja recolhida quando o usuário reduz a janela do navegador ou
quando o site é exibido em dispositivos com telas pequenas.
Em seguida, abrimos uma seção nova (<div>) da barra de
navegação 4. Essa é a parte da barra de navegação que pode ser
recolhida dependendo do tamanho da janela do navegador.
A Bootstrap define elementos de navegação como itens em uma lista
não ordenada 5, com regras de estilo que fazem com que essa lista
não se pareça nada com uma. Cada link ou elemento necessário na
barra pode ser incluído como um item em uma lista não ordenada 6.
Aqui, o único item na lista é o nosso link para a página de tópicos 7.
Repare na tag de fechamento </li> no final do link; cada tag de
abertura precisa de uma tag de fechamento correspondente.
O restante das linhas mostradas aqui encerra todas as tags abertas.
Em HTML, escrevemos um comentário assim:
<!-- Um comentário em HTML. -->
Via de regra, as tags de fechamento não têm comentários, mas caso
seja novato em HTML, rotular algumas de suas tags de fechamento
pode ajudar bastante. Uma única tag ausente ou uma tag extra
pode prejudicar o layout de uma página inteira. Incluímos o bloco
content 8 e também as tags de fechamento </body> e as tags </html>.
2 {% if user.is_authenticated %}
<li class="nav-item">
3 <span class="navbar-text me-2">Hello, {{ user.username }}.
</span></li>
4 {% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:register' %}">
Register</a></li>
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:login' %}">
Log in</a></li>
{% endif %}
{% if user.is_authenticated %}
<form action="{% url 'accounts:logout' %}" method='post'>
{% csrf_token %}
1 <button name='submit' class='btn btn-outline-secondary btn-sm'>
Log out</button>
</form>
{% endif %}
1 <main class="container">
2 <div class="pb-2 mb-2 border-bottom">
{% block page_header %}{% endblock page_header %}
</div>
3 <div>
{% block content %}{% endblock content %}
</div>
</main>
</body>
</html>
Primeiro, abrimos uma tag <main> 1. Usamos o elemento main para a
parte mais significativa do corpo de uma página. Nesse caso,
atribuímos o seletor bootstrap container, uma forma simples de
agrupar elementos em uma página. Vamos inserir dois elementos div
nesse contêiner.
O primeiro elemento div contém um bloco page_header 2. Vamos
utilizar esse bloco para fornecer um título a maioria das páginas.
Para destacar essa seção do restante da página, colocamos um
padding abaixo do cabeçalho. Padding se refere ao espaço entre o
conteúdo de um elemento e sua borda. O seletor pb-2 é uma diretiva
da bootstrap que fornece uma quantidade moderada de padding na
parte inferior do elemento estilizado. Uma margem é o espaço entre
a borda de um elemento e outros elementos na página. O seletor mb-
2 fornece uma quantidade moderada de margem na parte inferior
desse div. Como queremos uma borda na parte inferior desse bloco,
usamos o seletor border-bottom, que fornece uma borda fina na parte
inferior do bloco page_header.
1 {% block page_header %}
2 <div class="p-3 mb-4 bg-light border rounded-3">
<div class="container-fluid py-4">
3 <h1 class="display-3">Track your learning.</h1>
4 <p class="lead">Make your own Learning Log, and keep a list of the
topics you're learning about. Whenever you learn something new
about a topic, make an entry summarizing what you've learned.</p>
2 {% block page_header %}
<h2>Log in to your account.</h2>
{% endblock page_header %}
{% block content %}
{% endblock content %}
Primeiro, carregamos as template tags do bootstrap5 nesse template 1.
Em seguida, definimos o bloco page_header, que informa ao usuário o
propósito da página 2. Repare que removemos o bloco
{% if form.errors %} do template; o django-bootstrap5 gerencia
automaticamente os erros de formulário.
Para exibir o formulário, usamos a template tag {% bootstrap_form %} 3;
que substitui o elemento {{ form.as_div }} que estávamos usando no
Capítulo 19. A template tag {% booststrap_form %} insere regras de estilo
da Bootstrap nos elementos individuais do formulário à medida que
o formulário é renderizado. Para gerar o botão de enviar, usamos a
tag {% bootstrap_button %} com argumentos que a definem como um
botão de enviar e lhe atribuímos o rótulo Log in4.
A Figura 20.2 mostra o formulário de login. Agora, a página está
mais limpa, com estilização consistente e um propósito claro. Tente
efetuar login com um nome de usuário ou senha incorretos; você
verá que até mesmo as mensagens de erro são estilizadas de forma
consistente e se integram perfeitamente ao site como um todo.
{% block page_header %}
1 <h1>Topics</h1>
{% endblock page_header %}
{% block content %}
{% endblock content %}
Não precisamos da tag {% load bootstrap5 %}, já que não estamos
usando nenhuma template tag personalizada da bootstrap5 nesse
arquivo. Transferimos o cabeçalho Topics para o bloco page_header e o
tornamos um elemento <h1> em vez de um parágrafo simples 1.
Como o conteúdo principal dessa página é uma lista de tópicos,
utilizamos o componente grupo de lista da Bootstrap para renderizá-
la. Isso aplica um conjunto simples de diretivas de estilização à lista
geral e a cada item da lista. Ao abrirmos a tag <ul>, primeiro
incluímos a classe list-group a fim de aplicar as diretivas de estilização
default à lista 2. Personalizamos ainda mais a lista inserindo uma
borda na parte inferior da lista, um pouco de padding abaixo da
lista (pb-2) e uma margem abaixo da borda inferior (mb-4).
Na lista, cada item precisa da classe list-group-item, e personalizamos a
estilização default removendo a borda ao redor dos itens
individuais 3. A mensagem exibida quando a lista está vazia precisa
dessas mesmas classes 4.
Agora, quando acessamos a página de tópicos, devemos ver uma
página com estilo correspondente à página inicial.
Estilizando as entradas na página de tópico
Na página do tópico, usaremos o componente card da Bootstrap a
fim de destacar cada entrada. Um card é um conjunto aninhável de
divs com estilos flexíveis e predefinidos, perfeitos para exibir as
entradas de um tópico:
topic.html
{% extends 'learning_logs/base.html' %}
1 {% block page_header %}
<h1>{{ topic.text }}</h1>
{% endblock page_header %}
{% block content %}
<p>
<a href="{% url 'learning_logs:new_entry' topic.id %}">Add new entry</a>
</p>
{% endblock content %}
Primeiro, inserimos o tópico no bloco page_header 1. Em seguida,
excluímos a estrutura de lista não ordenada utilizada anteriormente
nesse template. Em vez de fazer de cada entrada um item de lista,
abrimos um elemento div com seletor card 2. Esse card comporta dois
elementos aninhados: um para armazenar o timestamp e o link para
editar a entrada, e outro para armazenar o corpo da entrada. O
seletor card se encarrega da maior parte da estilização de que
precisamos para esse div; personalizamos o card adicionando uma
pequena margem na parte inferior de cada card (mb-3).
O primeiro elemento no card é um cabeçalho, que é um elemento
<h4> com o seletor card-header 3. Esse cabeçalho contém a data em
que a entrada foi feita e um link para editá-la. A tag <small> em torno
do link edit_entry faz com que pareça um pouco menor que o
timestamp 4. O segundo elemento é um div com o seletor card-body 5,
que insere o texto da entrada em uma caixa simples no card.
Perceba que o código Django para incluir as informações na página
está inalterado; mudamos somente os elementos que afetam a
aparência da página. Já que não temos mais uma lista não
ordenada, substituímos as tags de item de lista ao redor da
mensagem da lista vazia por tags de parágrafo simples 6.
A Figura 20.3 mostra a página do tópico com um visual novo. As
funcionalidades do Registro de Aprendizagem não mudaram. No
entanto, agora nosso site parece mais profissional e mais convidativo
aos usuários.
Caso queira utilizar um template da Bootstrap diferente para um
projeto, siga um processo semelhante ao que fizemos até agora
neste capítulo. Copie o template que deseja usar em base.html e
modifique os elementos que têm o conteúdo propriamente dito para
que o template exiba as informações do projeto. Depois, use as
ferramentas individuais de estilização da Bootstrap para estilizar o
conteúdo em cada página.
NOTA O projeto Bootstrap disponibiliza documentação excelente.
Acesse a página inicial em https://getbootstrap.com e clique em Docs
para saber mais sobre o que a Bootstrap oferece.
Figura 20.3: A página de tópico com estilização da Bootstrap.
FAÇA VOCÊ MESMO
20.1 Outros formulários: Aplicamos os estilos da Bootstrap à página login. Faça
alterações parecidas no restante das páginas baseadas em formulários, incluindo
new_topic, new_entry, edit_entry e register.
20.2 Blog estiloso: Use a Bootstrap para estilizar o projeto do Blog que você criou no
Capítulo 19.
Instalando platformshconfig
Precisaremos também instalar um pacote adicional, o platformshconfig.
Esse pacote ajuda a identificar se o projeto está sendo executado
em um sistema local ou em um servidor do Platform.sh. Em um
ambiente virtual ativo, digite o seguinte comando:
(ll_env)learning_log$ pip install platformshconfig
Utilizaremos esse pacote para modificar as configurações do projeto
quando estiver sendo executado no servido ativo.
2 relationships:
database: "db:postgresql"
mkdir logs
4 python manage.py collectstatic
rm -rf logs
5 deploy: |
python manage.py migrate
A seção mount 1 nos permite definir diretórios em que podemos ler e
gravar dados enquanto o projeto está em execução. Essa seção
define um diretório/logs para o projeto implantado.
A seção hooks 2 define as ações que são executadas em diversos
pontos durante o processo de deploy. Na seção build, instalamos
todos os pacotes necessários para atender ao projeto no ambiente
ativo 3. Além disso, executamos collectstatic 4, que coleta todos os
arquivos estáticos necessários para o projeto em um só lugar para
que possam ser atendidos com eficiência.
Por último, na seção deploy 5, especificamos que as migrações devem
ser executadas sempre que o projeto for implantado. Em um projeto
simples, isso não terá efeito quando não houver mudanças.
Os outros dois arquivos de configuração são menores; vamos
escrevê-los agora.
Arquivo de configuração routes.yaml
Uma rota é o caminho que uma requisição segue à medida que é
processada pelo servidor. Quando uma requisição é recebida pelo
Platform.sh, a plataforma precisa saber para onde enviá-la.
Crie uma pasta nova chamada .platform, no mesmo diretório que
manage.py. Não se esqueça de incluir o ponto no início do nome.
Dentro dessa pasta, crie um arquivo chamado routes.yaml e digite o
seguinte:
.platform/routes.yaml
# Cada rota descreve como um URL de entrada será processado pelo Platform.sh.
"https://{default}/":
type: upstream
upstream: "ll_project:http"
"https://www.{default}/":
type: redirect
to: "https://{default}/"
Esse arquivo garante que requisições como https://project_url.com e
www.project_url.com sejam encaminhadas para o mesmo lugar.
db:
type: postgresql:12
disk: 1024
O arquivo define um serviço, um banco de dados Postgres.
Modificando settings.py para Platform.sh
Agora, é necessário adicionarmos uma seção no final de settings.py
para modificar algumas configurações do ambiente do Platform.sh.
Adicione o seguinte código ao final de settings.py:
settings.py
-- trecho de código omitido --
# Configurações do Platform.sh.
1 from platformshconfig import Config
config = Config()
2 if config.is_valid_platform():
3 ALLOWED_HOSTS.append('.platformsh.site')
4 if config.appDir:
STATIC_ROOT = Path(config.appDir) / 'static'
5 if config.projectEntropy:
SECRET_KEY = config.projectEntropy
if not config.in_build():
6 db_settings = config.credentials('database')
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': db_settings['path'],
'USER': db_settings['username'],
'PASSWORD': db_settings['password'],
'HOST': db_settings['host'],
'PORT': db_settings['port'],
},
}
Normalmente, inserimos as instruções import no início de um módulo,
porém, nesse caso, é bom manter todas as configurações específicas
e remotas de controle em uma seção. Aqui, importamos Config de
platformshconfig 1, que ajuda a determinar as configurações no servidor
remoto. Modificamos apenas as configurações se o método
config.is_valid_platform() retornar True 2, sinalizando que as configurações
estão sendo usadas em um servidor Platform.sh.
Modificamos ALLOWED_HOSTS para possibilitar que o projeto seja
atendido por hosts que terminam em .platformsh.site 3. Todos os
projetos implantados no nível gratuito serão atendidos usando esse
mesmo host. Se as configurações estiverem sendo carregadas no
diretório 4 da aplicação implantada, definimos STATIC_ROOT para que
os arquivos estáticos sejam corretamente atendidos. Definimos
também uma SECRET_KEY mais segura no servidor remoto 5.
Para concluir, configuramos o banco de dados de produção 6. O
banco apenas é definido se o processo de build tiver terminado de
ser executado e o projeto estiver sendo atendido. Tudo o que
estamos vendo aqui é necessário, pois é assim que o Django se
comunica com o servidor Postgres que o Platform.sh definiu para o
projeto.
Instalando o Git
Talvez o Git já esteja instalado em seu sistema. Vamos descobrir:
abra uma janela nova de terminal e execute o comando git --version:
(ll_env)learning_log$ git --version
git version 2.30.1 (Apple Git-130)
Caso receba uma mensagem de que o Git não está instalado, confira
as instruções de instalação no Apêndice D.
Configurando o Git
O Git rastreia quem faz alterações em um projeto, mesmo quando
somente uma pessoa está trabalhando no projeto. Para isso, o Git
precisa saber seu nome de usuário e e-mail. Embora seja necessário
um nome de usuário, você pode criar um e-mail apenas para a
prática de seus projetos:
(ll_env)learning_log$ git config --global user.name "eric"
(ll_env)learning_log$ git config --global user.email "[email protected]"
Caso esqueça dessas etapas, o Git solicitará essas informações
quando você fizer o primeiro commit.
Ignorando arquivos
Como não precisamos do Git para rastrear todos os arquivos do
projeto, solicitaremos que ignore alguns arquivos. Crie um arquivo
chamado .gitignore na pasta que contém manage.py. Veja que o
nome desse arquivo começa com um ponto e não tem extensão de
arquivo. Vejamos o código a ser inserido em .gitignore:
.gitignore
ll_env/
__pycache__/
*.sqlite3
Solicitamos que Git ignore todo o diretório ll_env, porque podemos
recriá-lo automaticamente a qualquer momento. Além do mais, não
rastreamos o diretório __pycache__, que contém os arquivos .pyc,
criados automaticamente quando os arquivos .py são executados.
Não rastreamos as alterações no banco de dados local, porque não é
uma prática recomendada: se estivermos usando o SQLite em um
servidor, podemos acidentalmente sobrescrever o banco de dados
ativo com nosso banco de dados de teste local ao enviarmos o
projeto para o servidor. O asterisco em *.sqlite3 instrui o Git a ignorar
qualquer arquivo que termine com a extensão .sqlite3.
NOTA Caso esteja usando o macOS, adicione .DS_Store em seu
arquivo .gitignore. É um arquivo que armazena informações
sobre as configurações de pasta no macOS e não tem nada a
ver com esse projeto.
* Region (--region)
The region where the project will be hosted
--trecho de código--
[us-3.platform.sh] Moses Lake, United States (AZURE) [514 gC02eq/kWh]
2 > us-3.platform.sh
* Plan (--plan)
Default: development
Enter a number to choose:
[0] development
--trecho de código--
3>0
* Environments (--environments)
The number of environments
Default: 3
4>3
* Storage (--storage)
The amount of storage per environment, in GiB
Default: 5
5>5
O primeiro prompt solicita um nome para o projeto 1, logo utilizamos
o nome ll_project. O próximo prompt pergunta em qual região
gostaríamos que o servidor estivesse em 2. Escolha o servidor mais
próximo de sua localização; para mim, é us-3.platform.sh. Para o resto
dos prompts, podemos aceitar os padrões: um servidor
desenvolvimento menor 3, três ambientes para o projeto 4 e 5 GB de
armazenamento para o projeto geral 5.
É necessário respondermos a mais três prompts:
Default branch (--default-branch)
The default Git branch name for the project (the production environment)
Default: main
1 > main
▀▄ ▄▀
█▄█▀███▀█▄█
▀█████████▀
▄▀ ▀▄
___ _ _ __ _
| _ \ |__ _| |_ / _|___ _ _ _ __ __| |_
| _/ / _` | _| _/ _ \ '_| ' \ _(_-< ' \
|_| |_\__,_|\__|_| \___/_| |_|_|_(_)__/_||_|
Welcome to Platform.sh.
1 web@ll_project.0:~$ ls
accounts learning_logs ll_project logs manage.py requirements.txt
requirements_remote.txt static
2 web@ll_project.0:~$ python manage.py createsuperuser
3 Username (leave blank to use 'web'): ll_admin_live
Email address:
Password:
Password (again):
Superuser created successfully.
4 web@ll_project.0:~$ exit
logout
Connection to ssh.us-3.platform.sh closed.
5 (ll_env)learning_log$
Ao executarmos pela primeira vez o comando platform environment:ssh,
podemos obter outro prompt sobre a autenticidade desse host. Se
vir essa mensagem, digite Y e você deve se conectar em uma sessão
de terminal remoto.
Após executarmos o comando ssh, nosso terminal se comporta como
um terminal no servidor remoto. Observe que o prompt foi alterado
para indicar que estamos conectados a uma sessão web, associada
ao projeto chamado ll_project 1. Se executarmos o comando ls,
veremos os arquivos enviados para o servidor do Platform.sh.
Execute o mesmo comando createuperuser que usamos no
Capítulo 18 2. Desta vez, forneci um nome de usuário de
administrador, ll_admin_live, diferente daquele que usei localmente 3.
Quando terminar de trabalhar na sessão de terminal remoto, digite o
comando exit 4. O prompt indicará que você está usando novamente
seu sistema local 5.
Agora, podemos adicionar /admin/ ao final do URL da aplicação e
logarmos no site admin. Se outras pessoas estiverem usando seu
projeto, fique ciente de que terá acesso a todos os dados delas!
Leve essa responsabilidade a sério, e os usuários sempre confiarão
os próprios dados a você.
NOTA Os usuários do Windows usarão os mesmos comandos
mostrados aqui (como ls em vez de dir), porque estamos
executando um terminal Linux por meio de uma conexão
remota.
{% block page_header %}
<h2>The item you requested is not available. (404)</h2>
{% endblock page_header %}
Esse template simples fornece as informações genéricas da página
de erro 404, mas é estilizado para ficar como o restante do site.
Crie outro arquivo chamado 500.html usando o seguinte código:
500.html
{% extends "learning_logs/base.html" %}
{% block page_header %}
<h2>There has been an internal error. (500)</h2>
{% endblock page_header %}
Esses arquivos novos exigem uma pequena alteração em
settings.py.
settings.py
-- trecho de código omitido --
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
-- trecho de código omitido --
},
]
-- trecho de código omitido --
Essa alteração instrui o Django a procurar no diretório root os
templates de página de erro e quaisquer outros que não estejam
associados a uma aplicação específica.
Desenvolvimento contínuo
Talvez você queira desenvolver ainda mais o Registro de
Aprendizagem após o envio inicial para o servidor ativo, ou talvez
queira desenvolver os próprios projetos para deploy. Caso faça isso,
há um processo bastante consistente para atualizar os projetos.
Em primeiro lugar, faça as alterações necessárias em seu projeto
local. Se suas alterações resultarem em arquivos novos, adicione
esses arquivos ao repositório Git com o comando git add . (não se
esqueça de incluir o ponto no final). Qualquer alteração que exija
uma migração de banco de dados precisará desse comando, pois
cada migração gera um arquivo novo.
Em segundo lugar, faça o commit das alterações em seu repositório
usando git commit -am "mensagem de commit". Em seguida, envie suas
alterações para o Platform.sh, com o comando platform push. Visite seu
projeto e verifique se as alterações esperadas surtiram efeito.
Como é fácil cometer erros durante esse processo, não se
surpreenda quando algo der errado. Se o código não funcionar,
revise o que fez e tente identificar o erro. Caso não consiga
identificar o erro ou descobrir como desfazê-lo, confira as sugestões
de ajuda no Apêndice C. Não tenha vergonha de pedir ajuda: todos
os programadores aprenderem a desenvolver projetos perguntando
as mesmas coisas que você provavelmente perguntará. Sempre tem
alguém que ficará feliz em ajudá-lo. Resolver cada problema que
surge o ajuda a desenvolver cada vez mais suas habilidades até que
você esteja criando projetos significativos e confiáveis e
respondendo às perguntas de outros programadores também.
Instalação e solução de
problemas
Python no Windows
As instruções no Capítulo 1 mostram como instalar o Python com o
instalador oficial em https://python.org. Se não conseguiu fazer com que
o Python fosse executado após usar o instalador, as instruções de
solução de problemas desta seção devem ajudá-lo a instalá-lo
corretamente.
Python no macOS
As instruções de instalação no Capítulo 1 usam o instalador oficial do
Python em https://python.org. Embora o instalador oficial do Python
funcione há anos, algumas coisas podem sair errado. Esta seção
ajudará se algo não estiver funcionando.
Python no Linux
Quase todos os sistemas Linux vêm com o Python por padrão. No
entanto, se a versão padrão no seu sistema for anterior ao
É
Python 3.9, é necessário instalar a versão mais recente. É possível
também instalar a versão mais recente, caso queira os recursos mais
recentes, como as mensagens refinadas de erro do Python. As
instruções a seguir devem funcionar para a maioria dos sistemas
baseados em APT.
Configurando o VS Code
É possível alterar as configurações padrão do VS Code de algumas
formas. Podemos efetuar algumas alterações por meio da interface e
outras têm que ser feitas nos arquivos de configuração. Às vezes,
essas alterações afetarão tudo o que fizermos no VS Code, ao passo
que outras afetarão apenas os arquivos dentro da pasta com o
arquivo de configuração.
Por exemplo, se tiver um arquivo de configuração em sua pasta
python_work, essas configurações afetarão somente os arquivos
nessa pasta (e suas subpastas). É um bom recurso, porque significa
que podemos ter configurações específicas do projeto que
substituem nossas configurações globais.
Simplificando a saída
Por padrão, o VS Code mostra a saída de seus programas em uma
janela de terminal incorporada. Essa saída inclui os comandos que
estão sendo usados para executar o arquivo. Em muitas situações, é
o ideal, mas pode causar mais distrações do que você quer,
sobretudo quando está aprendendo Python.
Para simplificar a saída, feche todas as guias abertas e saia do
VS Code. Inicie o VS Code novamente e abra a pasta com os
arquivos Python em que está trabalhando; pode ser apenas a pasta
python_work onde hello_world.py é salvo.
Clique no ícone Executar/Depurar (parecido com um triângulo com
um pequeno inseto) e clique em crie um arquivo launch.json. No
menu suspenso, digite Python entre as opções exibidas. No arquivo
launch.json que é aberto, faça a seguinte alteração:
launch.json
{
-- trecho de código omitido --
"configurations": [
{
-- trecho de código omitido --
"console": "internalConsole",
"justMyCode": true
}
]
}
Aqui, estamos alterando a configuração do console IntegratedTerminal para
internalConsole. Após salvar o arquivo settings, abra um arquivo .py
como hello_world.py e execute-o pressionando CTRL+F5. No painel
de saída do VS Code, clique em Console de Depuração se ainda
não estiver selecionado. Você deve ver apenas a saída do programa,
e a saída deve ser atualizada sempre que executar um programa.
NOTA O Console de Depuração é somente leitura. Não funciona
com arquivos que usam a função input(), utilizada no
Capítulo 7. Quando precisar executar esses programas, é
possível alterar a configuração de console de volta para
IntegratedTerminal ou executar esses programas em uma janela
de terminal separada, conforme descrito em “Executando
programas Python em um terminal” na página 43.
IDLE
O IDLE é um editor de texto incluído no Python. É um pouco menos
intuitivo de programar do que outros editores mais modernos. No
entanto, você verá o IDLE em outros minicursos para iniciantes e
talvez queira testá-lo.
Geany
O Geany é um editor de texto simples que exibe a saída em uma
janela de terminal separada, o que nos ajuda a nos sentir à vontade
usando terminais. Embora o Geany tenha uma interface bastante
minimalista, é poderosa o suficiente para que um número
significativo de programadores experientes ainda o use.
Se achar que o VS Code tem muita distração e muitas
funcionalidade, considere o Geany.
Sublime Text
O Sublime Text é outro editor minimalista que você deve considerar
usar se achar o VS Code poluído visualmente. O Sublime Text tem
uma interface mais limpa e é conhecido por funcionar perfeitamente
mesmo com arquivos muito grandes. É um editor que não
atrapalhará e possibilitará que você se concentre no código que está
escrevendo.
O Sublime Text tem uma avaliação gratuita ilimitada, mas não é
gratuito ou open source. Caso decida usá-lo e goste, e possa se dar
ao luxo de comprar uma licença completa, compre-o. Você compra
somente uma vez, não é uma assinatura de software.
Emacs and Vim
O Emacs e o Vim são dois editores populares, preferidos por muitos
programadores experientes, já que são arquitetados para que
possamos usá-los sem tirar as mãos do teclado. Uma vez que você
aprende como esses editores funcionam, a escrita, a legibilidade e a
modificação do código se tornam mais eficientes. Significa também
que ambos editores têm uma curva de aprendizado bastante
acentuada. O Vim está incluído na maiorias das máquinas com Linux
ou macOS, e tanto o Emacs como o Vim podem ser executados em
um terminal. Por essa razão, costumam ser usados para escrever
código em servidores por meio de sessões de terminal remoto.
Em geral, os programadores experientes recomendam testá-los,
porém muitos deles se esquecem da quantidade de conteúdo que os
programadores iniciantes já estão tentando aprender. É bom
conhecer esses editores, mas lembre-se: só aprenda a usá-los
quando se sentir à vontade programando em um editor mais
amigável, que possibilite que você foque aprender a programar e
não aprender a usar um editor.
PyCharm
O PyCharm é um IDE popular entre os programadores Python
porque foi desenvolvido especificamente para trabalhar com Python.
A versão completa exige assinatura paga, mas uma versão gratuita
chamada PyCharm Community Edition também está disponível, e
muitos desenvolvedores a consideram útil.
Caso teste o PyCharm, fique sabendo que, por padrão, esse editor
define um ambiente isolado para cada um dos projetos. Isso
geralmente é bom, mas pode levar a um comportamento
inesperado, caso você não entenda o que o editor está fazendo.
Jupyter Notebooks
O Jupyter Notebook é um tipo de ferramenta diferente dos editores
de texto ou IDEs tradicionais, pois é uma aplicação web
desenvolvida principalmente por blocos; cada bloco é um bloco de
código ou um bloco de texto. Como os blocos de texto são
renderizados em Markdown, pode-se incluir formatação simples.
Os Jupyter Notebooks foram desenvolvidos para aplicações
científicas com o Python, mas desde então é usado produtivamente
em uma ampla variedade de situações. Em vez de apenas escrever
comentários dentro de um arquivo .py, é possível escrever texto não
criptografado com formatação simples, como cabeçalhos, listas com
marcadores e hiperlinks entre as seções do código. Cada bloco de
código pode ser executado de forma independente, possibilitando
testar pequenos pedaços do programa ou podemos executar todos
os blocos de código de uma só vez. Cada bloco de código tem a
própria área de saída, sendo possível ativar ou desativar as áreas de
saída conforme necessário.
Não raro, os Jupyter Notebooks podem ser confusos por causa das
interações entre diferentes células. Se definir uma função em uma
célula, essa função também estará disponível para outras células. Na
maioria das vezes, isso é vantajoso, mas pode ficar confuso em
notebooks mais extensos e caso você não entenda totalmente como
o ambiente Notebook funciona.
Se estiver desenvolvendo alguma tarefa científica ou seu foco for
dados com Python, certamente verá os Jupyter Notebooks em algum
momento.
C
apêndice
Obtendo ajuda
Primeiros passos
Quando ficar sem saber o que fazer, o primeiro passo é avaliar sua
situação. Antes de pedir ajuda a qualquer outra pessoa, responda
claramente às três perguntas:
• O que você está tentando fazer?
• O que já tentou fazer até agora?
• Quais resultados tem obtido?
Responda da forma mais específica possível. Para a primeira
pergunta, respostas explícitas como “Estou tentando instalar a
versão mais recente do Python no meu novo notebook Windows”
tem detalhes suficientes para que outras pessoas da comunidade
Python o ajudem. Respostas como “Estou tentando instalar o
Python” não fornecem informações suficientes para que outras
pessoas ofereçam ajuda.
A resposta à segunda pergunta deve fornecer detalhes suficientes
para que você não seja aconselhado a repetir o que já tentou fazer:
“Acessei https://python.org/downloads e cliquei no botão Download do
meu sistema. Depois, executei o instalador” é mais útil do que
“Acessei ao site do Python e fiz o download de alguma coisa”.
Para a terceira pergunta, ajuda saber as mensagens exatas de erro
que recebeu. Assim, você consegue pesquisar uma solução ou
fornecê-las quando pedir ajuda.
Às vezes, o fato de responder a essas três perguntas antes de pedir
ajuda possibilita que você identifique algo que deixou passar sem
precisar recorrer a outros recursos. Os programadores até têm um
nome para isso: debug com patinho de borracha. A ideia é que, caso
explique claramente sua situação para um patinho de borracha (ou
para qualquer objeto inanimado) e faça uma pergunta específica,
pode acontecer de você responder à própria pergunta. Algumas
equipes de programação até têm um patinho de borracha para
incentivar as pessoas a “falarem com o pato”.
Pesquisando online
Ao pesquisar online, a probabilidade de que outra pessoa tenha o
mesmo problema que você e tenha pedido ajuda online é grande.
Boas habilidades de pesquisa e perguntas específicas o ajudam a
encontrar recursos existentes para resolver o problema que está
enfrentando. Por exemplo, caso esteja tendo dificuldades para
instalar a versão mais recente do Python em um novo sistema
Windows, pesquisar instalar o Python no Windows e limitar os
resultados aos recursos do ano passado pode direcioná-lo para uma
resposta clara.
Pesquisar a mensagem de erro exata também pode ajudar bastante.
Por exemplo, digamos que você obtenha o seguinte erro ao tentar
executar um programa Python em um terminal de um novo sistema
Windows:
> python hello_world.py
Python was not found; run without arguments to install from the Microsoft
Store...
Pesquisar a frase completa, “Python was not found; run without
arguments to install from the Microsoft Store”, provavelmente
renderá algumas orientações úteis.
Quando começamos a pesquisar tópicos relacionados à
programação, alguns sites aparecerão repetidas vezes. Abordarei
alguns desses sites brevemente, para que você saiba como podem
ser úteis e ajudá-lo.
Stack Overflow
O Stack Overflow (https://stackoverflow.com) é um dos sites de perguntas
e respostas mais populares para programadores e, não raro, aparece
na primeira página de resultados em pesquisas relacionadas ao
Python. Os participantes postam perguntas quando estão com
problemas, e outros participantes tentam ajudar com respostas.
Como os usuários podem votar nas respostas que consideram mais
úteis, as melhores respostas normalmente são as primeiras que você
verá.
No Stack Overflow, muitas perguntas básicas sobre Python
apresentam respostas claras, já que foram refinadas pela
comunidade ao longo do tempo. Os usuários também são
incentivados a postar atualizações, ou seja, as respostas costumam
ser relativamente atualizadas. No momento em que eu escrevia este
livro, quase dois milhões de perguntas relacionadas ao Python foram
respondidas no Stack Overflow.
No entanto, espera-se que você saiba algumas coisas antes de
postar no Stack Overflow. As perguntas devem ser breves e incluir o
tipo de problema que você está enfrentando. Se você postar as
linhas do código (de 5 a 20 linhas) que geram o erro que está
enfrentando e se recorrer às orientações da seção “Primeiros
passos” deste apêndice, na página 583, provavelmente alguém o
ajudará. Agora, caso compartilhe um link para um projeto com
diversos arquivos grandes, as pessoas provavelmente não ajudarão.
Confira este ótimo guia para elaborar uma boa pergunta, acesse
https://stackoverflow.com/help/how-to-ask. É possível usar as sugestões deste
guia para obter ajuda em qualquer comunidade de programadores.
Documentação oficial do Python
A documentação oficial do Python (https://docs.python.org) é um pouco
confusa para iniciantes, já que o objetivo é mais documentar a
linguagem do que fornecer explicações. Na documentação oficial, os
exemplos até funcionam, mas você pode não entender tudo o que é
mostrado. Ainda assim, trata-se de um bom recurso para explorar
quando aparecer em suas pesquisas. À medida que você
compreende cada vez mais o Python, a documentação se tornará
mais útil.
r/learnpython
O Reddit é composto por vários subfóruns chamados subreddits. O
subreddit r/learnpython (https://reddit.com/r/learnpython) é muito ativo e
acolhedor. É possível ler as perguntas dos outros e postar as suas
também. Não raro, você obterá diversas perspectivas sobre as
questões que levantar, o que pode ajudar bastante a desenvolver um
entendimento mais aprofundado do tópico com o qual está
trabalhando.
Postagens em blogs
Muitos programadores têm blogs para compartilharem postagens
sobre as linguagens com as quais estão trabalhando. Procure a data
nas postagens do blog que encontrar, assim é possível ver como as
informações podem ser aplicáveis à versão do Python que você está
usando.
Discord
O Discord é um ambiente de bate-papo online com comunidades
Python onde você pode pedir ajuda e seguir discussões relacionadas
ao Python.
Para conferir, acesse https://pythondiscord.com e clique no link do Discord
no canto superior direito. Se já tiver uma conta no Discord, poderá
fazer login com sua conta existente. Se não tiver uma conta, digite
um nome de usuário e siga as instruções para concluir seu registro
no Discord.
Caso seja a primeira vez em que visita o Discord do Python, é
necessário aceitar as regras da comunidade antes de participar
totalmente. Depois disso, você pode participar de qualquer um dos
canais que lhe interessam. Se estiver procurando por ajuda, lembre-
se de postar em um dos canais de ajuda do Python.
Slack
O Slack é outro ambiente de bate-papo online. É frequentemente
usado para comunicações corporativas internas, mas também há
muitos grupos públicos que você pode participar. Se quiser conferir
os grupos do Python no Slack, comece com https://pyslackers.com. Clique
no link Slack na parte superior da página e digite seu endereço de
e-mail para receber um convite.
Quando você estiver no workspace do Python Developers, verá uma
lista de canais. Clique em Canais e escolha os tópicos que lhe
interessam. Talvez você queira conferir os canais #help e #django.
D apêndice
Instalando o Git
O Git funciona em todos os sistemas operacionais, mas existem
diferentes abordagens para instalá-lo em cada um deles. As seções a
seguir fornecem instruções específicas para cada sistema
operacional.
O Git é incluído em alguns sistemas por padrão e geralmente vem
junto com outros pacotes que você já pode ter instalado. Antes de
tentar instalá-lo, veja se já tem o Git no sistema. Abra uma janela
nova de terminal e execute o comando git --version. Se vir a saída
listando um número de versão específico, o Git será instalado em
seu sistema. Se vir uma mensagem solicitando que instale ou
atualize o Git, siga as instruções na tela.
Se não vir nenhuma instrução na tela e estiver usando o Windows
ou o macOS, poderá fazer o download do instalador em https://git-
scm.com. Caso seja usuário Linux com um sistema compatível APT,
pode instalar o Git com o comando sudo apt install git.
Configurando o Git
O Git acompanha quem efetua alterações em um projeto, mesmo
quando o projeto tem somente uma pessoa. Para fazer isso, o Git
precisa saber seu nome de usuário e e-mail. É necessário fornecer
um nome de usuário, mas você pode criar um endereço de e-mail
falso:
$ git config --global user.name "username"
$ git config --global user.email "[email protected]"
Caso se esqueça essa etapa, o Git solicitará essas informações
quando fizer seu primeiro commit.
Além do mais, é melhor definir o nome padrão para o branch
principal em cada projeto. Um nome bom para esse branch é main:
$ git config --global init.defaultBranch main
Quando fazemos essa configuração, cada projeto novo, em que
usamos o Git para gerenciar, começará com um único branch de
commits chamado main.
Criando um projeto
Criaremos um projeto com o qual trabalhar. Crie uma pasta em
algum lugar do seu sistema chamada git_practice. Dentro dela, crie
um programa Python simples:
hello_git.py
print("Hello Git world!")
Usaremos esse programa para explorar as funcionalidades básicas
do Git.
Ignorando arquivos
Os arquivos com a extensão .pyc são gerados automaticamente a
partir de arquivos .py, ou seja, não é necessário usar o Git para
rastreá-los. Esses arquivos são armazenados em um diretório
chamado __pycache__. Para instruir o Git a ignorar esse diretório,
crie um arquivo especial chamado .gitignore – com um ponto no
início do nome do arquivo e sem extensão de arquivo – e adicione a
seguinte linha:
.gitignore
__pycache__/
Esse arquivo instrui o Git a ignorar qualquer arquivo no diretório
__pycache__. Usar um arquivo .gitignore fará com que seu projeto
fique organizado e mais fácil de trabalhar.
Talvez seja necessário modificar as configurações do navegador de
arquivos para que arquivos ocultos (arquivos cujos nomes começam
com um ponto) sejam exibidos. No Explorador de Arquivos, clique no
menu Exibir e, em seguida, marque a caixa Itens ocultos. No
macOS, pressione ⌘+SHIFT+.(ponto). No Linux, procure uma
configuração chamada Mostrar Arquivos Ocultos.
Verificando o status
Antes de fazer qualquer outra coisa, verificaremos o status do
projeto:
git_practice$ git status
1 On branch main
No commits yet
2 Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
hello_git.py
3 nothing added to commit but untracked files present (use "git add" to track)
git_practice$
No Git, um branch é uma versão do projeto em que estamos
trabalhando; aqui, pode ver que estamos em um branch chamado
main 1. Sempre que verificarmos o status do projeto, devemos ver o
branch main. Pois assim, podemos fazer o commit inicial. Um commit
é um snapshot do projeto em um determinado momento.
O Git nos informa que arquivos não rastreados estão no projeto 2,
pois ainda não informamos quais arquivos rastrear. Em seguida,
somos informados de que não há nada adicionado ao commit atual,
mas há arquivos não rastreados que podemos querer adicionar ao
repositório 3.
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
3 new file: .gitignore
new file: hello_git.py
git_practice$
O comando git add . adiciona ao repositório todos os arquivos dentro
de um projeto que ainda não estão sendo rastreados 1, desde que
não estejam listados no .gitignore. Esse comando não faz o commit
dos arquivos; apenas solicita que o Git comece a prestar atenção
neles. Quando verificamos o status do projeto agora, podemos ver
que o Git reconhece algumas mudanças que precisam de commit 2.
O rótulo new file significa que esses arquivos foram adicionados
recentemente ao repositório 3.
Fazendo um commit
Faremos o primeiro commit:
1 git_practice$ git commit -m "Started project."
2 [main (root-commit) cea13dd] Started project.
3 2 files changed, 5 insertions(+)
create mode 100644 .gitignore
create mode 100644 hello_git.py
4 git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$
Executamos o comando git commit -m "mensagem" 1 para criarmos um
snapshot do projeto. A flag -m instrui o Git a gravar a mensagem que
se segue (Started project.) no log do projeto. A saída mostra que
estamos no branch main 2 e que dois arquivos foram alterados 3.
Quando verificamos o status agora, podemos ver que estamos no
branch main e temos um diretório limpo de trabalho 4. É a mensagem
que você deve ver sempre que efetuar o commit de um estado de
trabalho do projeto. Se receber uma mensagem diferente, leia-a com
atenção; é provável que você tenha esquecido de adicionar um
arquivo antes de fazer um commit.
Verificando o log
O Git mantém um log de todos os commits realizados no projeto.
Vamos verificar o log:
git_practice$ git log
commit cea13ddc51b885d05a410201a54faf20e0d2e246 (HEAD -> main)
Author: eric <[email protected]>
Date: Mon Jun 6 19:37:26 2022 -0800
Started project.
git_practice$
Sempre que fazemos um commit, o Git gera um ID de referência
exclusivo de 40 caracteres. Além disso, registra quem fez o commit,
quando foi realizado e a mensagem registrada. Como nem sempre
precisamos de todas essas informações, o Git oferece a opção para
exibir uma versão mais simples das entradas de log:
git_practice$ git log --pretty=oneline
cea13ddc51b885d05a410201a54faf20e0d2e246 (HEAD -> main) Started project.
git_practice$
A flag --pretty=oneline fornece as duas informações mais importantes:
o ID de referência do commit e a mensagem gravada para o
commit.
Segundo commit
Para conferirmos como o controle de versão é efetivo, precisamos
fazer uma mudança no projeto e comitar essa mudança. Aqui,
vamos apenas adicionar outra linha ao hello_git.py:
hello_git.py
print("Hello Git world!")
print("Hello everyone.")
Quando verificamos o status do projeto, veremos que o Git
identificou o arquivo alterado:
git_practice$ git status
1 On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
2 modified: hello_git.py
3 no changes added to commit (use "git add" and/or "git commit -a")
git_practice$
Vemos o branch em que estamos trabalhando 1, o nome do arquivo
modificado 2 e que nenhuma alteração foi comitada 3. Faremos o
commit dessa alteração e verificaremos o status mais uma vez:
1 git_practice$ git commit -am "Extended greeting."
[main 945fa13] Extended greeting.
1 file changed, 1 insertion(+), 1 deletion(-)
2 git_practice$ git status
On branch main
nothing to commit, working tree clean
3 git_practice$ git log --pretty=oneline
945fa13af128a266d0114eebb7a3276f7d58ecd2 (HEAD -> main) Extended greeting.
cea13ddc51b885d05a410201a54faf20e0d2e246 Started project.
git_practice$
Fizemos um commit novo, passando as flags -am quando usamos o
comando git commit 1. A flag -a instrui o Git a adicionar todos os
arquivos modificados no repositório ao commit atual. (Caso crie
arquivos novos entre commits, execute novamente o comando
git add . para inclui-los no repositório.) A flag -m instrui o Git a gravar
uma mensagem no log para esse commit.
Ao verificarmos o status do projeto, vemos que mais uma vez temos
um diretório limpo de trabalho 2. Finalmente, vemos os dois commits
no log 3.
Revertendo alterações
Agora, veremos como descartar uma alteração e retornar ao estado
de trabalho anterior. Primeiro, adicione uma linha nova a
hello_git.py:
hello_git.py
print("Hello Git world!")
print("Hello everyone.")
1 modified: hello_git.py
no changes added to commit (use "git add" and/or "git commit -a")
git_practice$
O Git sabe que modificamos hello_git.py 1 e podemos efetuar o
commit da alteração se quisermos. Mas desta vez, em vez de fazer o
commit da alteração, voltaremos ao último commit quando nosso
projeto estava funcionando. Não faremos nada em hello_git.py: não
excluiremos a linha nem usaremos o recurso Undo no editor de
texto. Em vez disso, insira os seguintes comandos na sessão do
terminal:
git_practice$ git restore .
git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$
O comando git restore nome_arquivo possibilita descartar todas as
alterações desde o último commit em um arquivo específico. O
comando git restore . descarta todas as alterações feitas em todos os
arquivos desde o último commit; essa ação reverte o projeto para o
último estado de commit.
Quando retornamos ao editor de texto, veremos que hello_git.py
mudou:
print("Hello Git world!")
print("Hello everyone.")
Nesse projeto simples, embora retornar a um estado anterior pareça
comum, se estivéssemos trabalhando em um projeto grande com
dezenas de arquivos modificados, todos os arquivos alterados desde
o último commit seriam restaurados. Esse recurso é extremamente
útil: é possível fazer quantas alterações quisermos ao implementar
uma funcionalidade nova e, caso não funcionarem, poderemos
descartá-las sem afetar o projeto. Não é necessário se lembrar
dessas alterações e desfazê-las manualmente. O Git cuida de tudo
isso.
NOTA Talvez seja necessário atualizar o arquivo no editor para
verificar a versão restaurada.
1 You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -
Excluindo o repositório
Às vezes, o histórico de nosso repositório fica desorganizado e não
sabemos como recuperá-lo. Caso isso ocorra, antes de pedir ajuda,
recorre às abordagens apresentadas no Apêndice C. Se não for bem-
sucedido e estiver trabalhando sozinho em um projeto, é possível
trabalhar com os arquivos, excluindo o diretório .git para descartar o
histórico do projeto. Isso não impactará o estado atual de nenhum
dos arquivos, mas excluirá todos os commits. Ou seja, não será
possível fazer check out de nenhum outro estado do projeto.
Para isso, abra um navegador de arquivos e exclua o repositório .git
ou o exclua a partir da linha de comando. Em seguida, precisaremos
recomeçar com um repositório novo para rastrear as alterações
novamente. Vejamos todo esse processo em uma sessão de
terminal:
1 git_practice$ git status
On branch main
nothing to commit, working directory clean
2 git_practice$ rm -rf .git/
3 git_practice$ git status
fatal: Not a git repository (or any of the parent directories): .git
4 git_practice$ git init
Initialized empty Git repository in git_practice/.git/
5 git_practice$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
hello_git.py
nothing added to commit but untracked files present (use "git add" to track)
6 git_practice$ git add .
git_practice$ git commit -m "Starting over."
[main (root-commit) 14ed9db] Starting over.
2 files changed, 5 insertions(+)
create mode 100644 .gitignore
create mode 100644 hello_git.py
7 git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$
Primeiro, verificamos o status e vemos que temos um diretório de
trabalho limpo 1. Em seguida, usamos o comando rm -rf .git/ para
excluir o diretório .git (del .git no Windows) 2. Quando verificamos o
status após excluir a pasta .git, somos informados de que esse não é
um repositório Git 3. Todas as informações que o Git usa para
rastrear um repositório são armazenadas na pasta .git, portanto,
removê-la exclui todo o repositório.
Assim, podemos usar o git init para iniciar um repositório novo 4.
Verificar o status mostra que retornamos ao estágio inicial,
aguardando o primeiro commit 5. Adicionamos os arquivos e
fazemos o primeiro commit 6. Agora, quando verificamos o status,
vemos um novo branch main, sem nada para comitar 7.
Utilizar o controle de versão exige um pouco de prática, mas uma
vez que começar a usá-lo, você nunca mais vai querer trabalhar sem
ele.
E apêndice
Entendendo os deploys
Ao tentarmos solucionar problemas de uma tentativa de deploy
específico, é importante entender claramente como um típico deploy
funciona. Deploy ou implantação é o processo de pegar um projeto
que funciona em nosso sistema local e copiá-lo para um servidor
remoto, de uma forma que possibilite responder à requisições de
qualquer usuário na internet. O ambiente remoto difere de um típico
sistema local em vários aspectos importantes: provavelmente não é
o mesmo sistema operacional (SO) usado e é mais provável que seja
um dos muitos servidores virtuais em um único servidor físico.
Ao fazer a implantação de um projeto ou o push para o servidor
remoto, é necessário executar as seguintes etapas:
• Criar um servidor virtual em uma máquina física em um
datacenter.
• Estabelecer uma conexão entre o sistema local e o servidor
remoto.
• Copiar o código do projeto para o servidor remoto.
• Identificar todas as dependências do projeto e instalá-las no
servidor remoto.
• Configurar um banco de dados e executar todas as migrações
existentes.
• Copiar arquivos estáticos (CSS, arquivos JavaScript e arquivos de
mídia) para um local em que possam ser atendidos com eficiência.
• Inicializar um servidor para lidar com as requisições recebidas.
• Começar a rotear as requisições recebidas para o projeto, assim
que estiver pronto para lidar com essas requisições.
Ao pensarmos em tudo necessário para um deploy, não é de se
admirar que deploys regularmente falhem. Felizmente, uma vez que
entendemos claramente todo o processo, teremos uma
probabilidade maior de identificar o que saiu de errado. Se conseguir
identificar o que deu errado, talvez consiga identificar uma correção
que fará com que a próxima tentativa de implantação seja bem-
sucedida.
É possível desenvolver localmente em um tipo de sistema
operacional e enviar um projeto para um servidor com um sistema
operacional diferente. É fundamental saber para qual tipo de sistema
estamos enviando o projeto, pois, assim, é mais fácil solucionar
problemas. No momento em que eu escrevia este apêndice, um
servidor remoto básico no Platform.sh usava o Debian Linux; a
maioria dos servidores remotos funcionam com sistemas Linux.
Solução de problemas básicos
Algumas etapas de solução de problemas são específicas de cada
sistema operacional. Em breve, falaremos delas. Primeiro, vejamos
as etapas que todos devem executar ao tentar solucionar problemas
de deploy.
O melhor recurso é a saída gerada durante a tentativa de push.
Talvez essa saída pareça assustadora; caso seja iniciante com
deploys de aplicação, tudo pode parecer extremamente técnico e, de
fato, há muita tecnicidade. Mas, a boa notícia é que não precisamos
entender tudo o que a saída gera. Temos dois objetivos ao examinar
a saída de log: identificar as etapas de deploy que funcionaram e as
etapas que não funcionaram. Se conseguir fazer isso, poderá
identificar o que mudar no projeto ou no processo de implantação
para que o próximo push seja bem-sucedido.
2 [RootNotFoundException]
Project root not found. This can only be run from inside a project
directory.
Git Bash
Podemos recorrer a outra abordagem para criar um ambiente local
em que podemos fazer deploy: usar o Git Bash, ambiente de
terminal compatível com o Bash, porém executado no Windows. O
Git Bash é instalado com o Git quando usamos o instalador em
https://git-scm.com. Essa abordagem pode funcionar, mas não é tão
otimizada quanto o WSL. É necessário usar um terminal Windows
para algumas etapas e um terminal Git Bash para outras.
Primeiro, precisamos instalar o PHP. É possível fazer essa instalação
com o XAMPP, pacote que agrupa o PHP com algumas outras
ferramentas cujo foco é desenvolvedor. Acesse https://apachefriends.org e
clique no botão para fazer o download do XAMPP para Windows.
Abra o instalador e o execute; se vir um aviso sobre restrições de
Controle de Conta de Usuário (UAC), clique em OK. Aceite todos os
default do instalador.
Quando o instalador terminar de executar, precisaremos adicionar o
PHP ao path do sistema; isso informará ao Windows onde procurar
quando quisermos executar o PHP. No menu Iniciar, insira o path e
clique em Editar as Variáveis do Ambiente do Sistema; clique
no botão Variáveis do Ambiente. Devemos ver a variável Path
destacada; clique em Editar. Clique em Novo para adicionar um
novo path à lista atual de paths. Supondo que tenha mantido as
configurações default ao executar o instalador do XAMPP, adicione
C:\xampp\php na caixa exibida e clique em OK. Quando terminar,
feche todas as caixas de diálogo do sistema que ainda estão
abertas.
Com esses requisitos atendidos, podemos instalar a CLI do
Platform.sh. Precisaremos usar um terminal Windows com privilégios
de administrador; insira command no menu Iniciar e, no aplicativo
Prompt de Comando, clique em Executar como administrador.
No terminal que aparece, digite o seguinte comando:
> curl -fsS https://platform.sh/cli/installer | php
Isso instalará a CLI do Platform.sh, conforme descrito
anteriormente.
Finalmente, usaremos o Git Bash. Para abrir um terminal Git Bash,
acesse o menu Iniciar e procure por git bash. Clique no aplicativo
Git Bash que aparece; veremos uma janela de terminal aberta.
Nesse terminal, podemos usar comandos tradicionais Linux como ls e
comandos Windows como dir. Para garantir que a instalação foi bem-
sucedida, execute platform list. Você deve ver uma lista com todos os
comandos da CLI do Platform.sh. Desse ponto em diante, faça todo
o deploy usando a CLI do Platform.sh dentro de uma janela de
terminal do Git Bash.
Deploy a partir do macOS
Apesar de o sistema operacional macOS não ser baseado em Linux,
ambos foram desenvolvidos com princípios semelhantes. Na prática,
significa que muitos dos comandos e fluxos de trabalho usados no
macOS também funcionarão em um ambiente de servidor remoto.
Talvez seja necessário instalar alguns recursos cujo foco seja o
desenvolvedor para que todas as ferramentas sejam disponibilizadas
no ambiente local do macOS. Se receber um prompt para instalar as
ferramentas de desenvolvedor de linha de comando a qualquer
momento, clique em Instalar para aprovar a instalação.
Ao instalar a CLI do Platform.sh, a maior dificuldade é garantir que o
PHP seja instalado. Caso veja uma mensagem de que o comando php
não foi encontrado, precisará instalar o PHP. Uma das maneiras mais
fáceis de instalá-lo é recorrer ao gerenciador de pacotes Homebrew,
que facilita a instalação de uma grande variedade de pacotes dos
quais os programadores dependem. Se ainda não tem o Homebrew
instalado, visite https://brew.sh e siga as instruções para instalá-lo.
Após o Homebrew ser instalado, use o seguinte comando para
instalar o PHP:
$ brew install php
Isso levará um tempo para ser executado, mas, uma vez concluído,
será possível instalar com sucesso a CLI do Platform.sh.