Você está na página 1de 200

Machine Translated by Google

Machine Translated by Google

Introdução à Computação e
Programação Usando
Pitão

com Aplicação à Modelagem Computacional


e Entendimento de Dados
Machine Translated by Google

Introdução à Computação e
Programação Usando
Pitão
com Aplicação à Modelagem Computacional
e Entendimento de Dados
terceira edição

John V. Guttag

Imprensa do MIT

Cambridge, Massachusetts
Londres, Inglaterra
Machine Translated by Google

© 2021 Instituto de Tecnologia de Massachusetts

Todos os direitos reservados. Nenhuma parte deste livro pode ser reproduzida de qualquer forma por
qualquer meio eletrônico ou mecânico (incluindo fotocópia, gravação ou armazenamento e recuperação de
informações) sem permissão por escrito do editor.

Este livro foi ambientado no Minion Pro pela New Best-set Typesetters Ltd.

Dados de Catalogação na Publicação da Biblioteca do Congresso

Nomes: Guttag, John, autor.


Título: Introdução à computação e programação usando Python: com aplicação à modelagem computacional
e entendimento de dados / John V. Guttag.
Descrição: Terceira edição. | Cambridge, Massachusetts: The MIT Press, [2021] | Inclui
índice.

Identificadores: LCCN 2020036760 | ISBN 9780262542364 (brochura)


Disciplinas: LCSH: Python (linguagem de programa de computador) — Livros didáticos. | Programação de
computadores — Livros didáticos.
Classificação: LCC QA76.73.P98 G88 2021 | DDC 005.13/3—dc23 LC
disponível em https://lccn.loc.gov/2020036760

10 9 8 7 6 5 4 3 2 1

d_r0
Machine Translated by Google

Para a minha família:

Olga
Davi
Andreia
Michael
Marca
addie
perfurar
Machine Translated by Google

CONTEÚDO

PREFÁCIO

AGRADECIMENTOS

1: COMEÇANDO

2: INTRODUÇÃO AO PYTHON

3: ALGUNS PROGRAMAS NUMÉRICOS SIMPLES

4: FUNÇÕES, ESCOPO E ABSTRAÇÃO

5: TIPOS ESTRUTURADOS E MUTABILIDADE


6: RECURSÃO E VARIÁVEIS GLOBAIS

7: MÓDULOS E ARQUIVOS
8: TESTE E DEBUGAÇÃO

9: EXCEÇÕES E AFIRMAÇÕES
10: AULAS E PROGRAMAÇÃO ORIENTADA A OBJETOS

11: UMA INTRODUÇÃO SIMPLISTA À ALGORITMIA


COMPLEXIDADE

12: ALGUNS ALGORITMOS SIMPLES E ESTRUTURAS DE DADOS

13: TRABALHO E MAIS SOBRE CLASSES

14: PROBLEMAS DE OTIMIZAÇÃO DE MOCHILAS E GRÁFICOS

15: PROGRAMAÇÃO DINÂMICA


16: PASSEIOS ALEATÓRIOS E MAIS SOBRE DADOS
VISUALIZAÇÃO

17: PROGRAMAS ESTOCÁSTICOS, PROBABILIDADE E


DISTRIBUIÇÕES
Machine Translated by Google

18: SIMULAÇÃO DE MONTE CARLO

19: AMOSTRAGEM E CONFIANÇA


20: COMPREENDENDO OS DADOS EXPERIMENTAIS
21: ENSAIOS RANDOMIZADOS E VERIFICAÇÃO DE HIPÓTESES

22: MENTIRAS, MENTIRAS CONDENADAS E ESTATÍSTICAS

23: EXPLORANDO DADOS COM PANDAS


24: UMA OLHADA RÁPIDA NO APRENDIZADO DE MÁQUINA

25: CONJUNTO
26: MÉTODOS DE CLASSIFICAÇÃO

PYTHON 3.8 REFERÊNCIA RÁPIDA


ÍNDICE

Lista de Figuras

Capítulo 1
Figura 1-1 Fluxograma de jantar
Capítulo 2
Figura 2-1 Janela de inicialização do Anaconda
Figura 2-2 Janela do Spyder
Figura 2-3 Operadores nos tipos int e float
Figura 2-4 Vinculação de variáveis a objetos
Figura 2-5 Fluxograma para declaração condicional
Figura 2-6 Fluxograma para iteração
Figura 2-7 Quadrando um número inteiro, da maneira mais difícil

Figura 2-8 Simulação manual de um pequeno programa


Figura 2-9 Usando uma instrução for
Capítulo 3
Machine Translated by Google

Figura 3-1 Usando enumeração exaustiva para encontrar a raiz cúbica


Figura 3-2 Usando enumeração exaustiva para testar a primalidade
Figura 3-3 Um teste de primalidade mais eficiente
Figura 3-4 Aproximando a raiz quadrada usando enumeração exaustiva

Figura 3-5 Usando a pesquisa de bisseção para aproximar a raiz quadrada


Figura 3-6 Usando a pesquisa de bisseção para estimar a base logarítmica 2

Figura 3-7 Implementação do método Newton-Raphson


Capítulo 4
Figura 4-1 Usando a pesquisa de bisseção para aproximar a raiz quadrada
de x

Figura 4-2 Somando uma raiz quadrada e uma raiz cúbica


Figura 4-3 Uma função para encontrar raízes
Figura 4-4 Código para testar find_root
Figura 4-5 Escopos aninhados
Figura 4-6 Estruturas de empilhamento

Figura 4-7 Uma definição de função com uma especificação


Figura 4-8 Dividindo find_root em várias funções
Figura 4-9 Generalizando bisection_solve
Figura 4-10 Usando bisection_solve para aproximar logs
capítulo 5
Figura 5-1 Duas listas
Figura 5-2 Duas listas que parecem ter o mesmo valor, mas
não

Figura 5-3 Demonstração de mutabilidade


Figura 5-4 Métodos comuns associados a listas
Figura 5-5 Aplicando uma função aos elementos de uma lista
Figura 5-6 Operações comuns em tipos de sequência
Machine Translated by Google

Figura 5-7 Comparação de tipos de sequência


Figura 5-8 Alguns métodos em strings
Figura 5-9 Traduzindo texto (mal)
Figura 5-10 Algumas operações comuns em dicts
Capítulo 6
Figura 6-1 Implementações iterativas e recursivas de fatorial
Figura 6-2 Crescimento na população de coelhas
Figura 6-3 Implementação recursiva da sequência de Fibonacci
Figura 6-4 Teste palíndromo
Figura 6-5 Código para visualizar o teste palíndromo
Figura 6-6 Usando uma variável global
Capítulo 7
Figura 7-1 Algum código relacionado a círculos e esferas
Figura 7-2 Funções comuns para acessar arquivos
Capítulo 8
Figura 8-1 Testando condições de contorno
Figura 8-2 Não é o primeiro bug
Figura 8-3 Programa com bugs
Capítulo 9
Figura 9-1 Usando exceções para fluxo de controle
Figura 9-2 Fluxo de controle sem tentativa exceto
Figura 9-3 Obter notas
Capítulo 10
Figura 10-1 Classe Int_set
Figura 10-2 Usando métodos mágicos
Figura 10-3 Pessoa da classe
Figura 10-4 Classe MIT_person
Machine Translated by Google

Figura 10-5 Dois tipos de alunos

Figura 10-6 Notas de classe

Figura 10-7 Gerando um relatório de notas

Figura 10-8 Ocultação de informações nas classes

Figura 10-9 Nova versão de get_students

Figura 10-10 Classe base de hipoteca

Figura 10-11 Subclasses de hipoteca

Capítulo 11

Figura 11-1 Usando enumeração exaustiva para aproximar a raiz quadrada

Figura 11-2 Usando a pesquisa de bisseção para aproximar a raiz quadrada

Figura 11-3 Complexidade assintótica

Figura 11-4 Implementação do teste de subconjunto

Figura 11-5 Implementação da interseção de lista

Figura 11-6 Gerando o conjunto de energia

Figura 11-7 Crescimento constante, logarítmico e linear

Figura 11-8 Crescimento linear, log-linear e quadrático

Figura 11-9 Crescimento quadrático e exponencial

Capítulo 12

Figura 12-1 Listas de implementação

Figura 12-2 Pesquisa linear de uma lista classificada

Figura 12-3 Pesquisa binária recursiva

Figura 12-4 Classificação de seleção

Figura 12-5 Mesclar classificação

Figura 12-6 Classificando uma lista de nomes

Figura 12-7 Implementando dicionários usando hash

Capítulo 13
Machine Translated by Google

Figura 13-1 Um gráfico simples

Figura 13-2 Conteúdo de Figure-Jane.png (esquerda) e Figura Addie.png


(direita)

Figura 13-3 Gráficos de produção mostrando o crescimento composto

Figura 13-4 Gráficos mostrando crescimento composto

Figura 13-5 Outro gráfico de crescimento composto

Figura 13-6 Gráfico de aparência estranha

Figura 13-7 Hipoteca de classe com métodos de plotagem

Figura 13-8 Subclasses de hipoteca

Figura 13-9 Compare hipotecas

Figura 13-10 Gerar parcelas de hipoteca

Figura 13-11 Pagamentos mensais de diferentes tipos de hipotecas

Figura 13-12 Custo ao longo do tempo de diferentes tipos de hipotecas

Figura 13-13 Saldo restante e custo líquido para diferentes tipos de hipotecas

Figura 13-14 Simulação da disseminação de uma doença infecciosa

Figura 13-15 Função para plotar o histórico de infecção

Figura 13-16 Produzir gráfico com um único conjunto de parâmetros

Figura 13-17 Gráfico estático do número de infecções

Figura 13-18 Gráfico interativo com valores iniciais do controle deslizante

Figura 13-19 Gráfico interativo com valores deslizantes alterados

Capítulo 14

Figura 14-1 Tabela de itens

Figura 14-2 Item de classe

Figura 14-3 Implementação de um algoritmo guloso

Figura 14-4 Usando um algoritmo guloso para escolher itens

Figura 14-5 Solução ótima de força bruta para a mochila 0/1


problema
Machine Translated by Google

Figura 14-6 As pontes de Königsberg (à esquerda) e o mapa simplificado


de Euler (à direita)

Figura 14-7 Nós e arestas

Figura 14-8 Grafo e Dígrafo de Classes

Figura 14-9 Algoritmo de caminho mais curto de primeira pesquisa em profundidade

Figura 14-10 Código de pesquisa em profundidade de teste

Figura 14-11 Algoritmo de caminho mais curto de busca em largura primeiro

Capítulo 15

Figura 15-1 Árvore de chamadas para Fibonacci recursivo

Figura 15-2 Implementando Fibonacci usando um memorando

Figura 15-3 Tabela de itens com valores e pesos

Figura 15-4 Árvore de decisão para o problema da mochila

Figura 15-5 Usando uma árvore de decisão para resolver um problema da mochila

Figura 15-6 Testando a implementação baseada em árvore de decisão

Figura 15-7 Solução de programação dinâmica para mochila


problema

Figura 15-8 Desempenho da solução de programação dinâmica

Capítulo 16

Figura 16-1 Um fazendeiro incomum

Figura 16-2 Classes de localização e campo

Figura 16-3 Classes que definem Bêbados

Figura 16-4 O andar do bêbado (com um inseto)

Figura 16-5 Distância do ponto de partida versus passos dados

Figura 16-6 Subclasses da classe base Bêbado

Figura 16-7 Iterando sobre estilos

Figura 16-8 Traçando as caminhadas de diferentes bêbados

Figura 16-9 Distância média para diferentes tipos de bêbados


Machine Translated by Google

Figura 16-10 Plotando os locais finais

Figura 16-11 Onde o bêbado para

Figura 16-12 Traçando caminhadas

Figura 16-13 Trajetória de caminhadas

Figura 16-14 Campos com propriedades estranhas

Figura 16-15 Uma caminhada estranha

Capítulo 17

Figura 17-1 Dados de rolo

Figura 17-2 Jogando uma moeda

Figura 17-3 Regressão à média

Figura 17-4 Ilustração de regressão à média

Figura 17-5 Plotando os resultados de cara ou coroa

Figura 17-6 A lei dos grandes números em ação

Figura 17-7 A lei dos grandes números em ação

Figura 17-8 Variância e desvio padrão

Figura 17-9 Função auxiliar para simulação de cara ou coroa

Figura 17-10 Simulação de cara ou coroa

Figura 17-11 Convergência de proporções cara/coroa

Figura 17-12 Diferenças absolutas

Figura 17-13 Média e desvio padrão de cara - coroa

Figura 17-14 Coeficiente de variação

Figura 17-15 Versão final do flip_plot

Figura 17-16 Coeficiente de variação de caras/coroas e


abs (cara – coroa)

Figura 17-17 Um grande número de tentativas

Figura 17-18 Distribuição de renda na Austrália

Figura 17-19 Código e o histograma que ele gera


Machine Translated by Google

Figura 17-20 Gráfico de histogramas de


lançamentos de moedas Figura 17-21
Histogramas de lançamentos de moedas
Figura 17-22 PDF para random.random Figura
17-23 PDF para distribuição Gaussiana
Figura 17-24 Uma distribuição normal Figura
17-25 Gráfico de valor absoluto de x Figura
17-26 Verificando a regra empírica Figura 17-27
Gráfico de produção com barras de erro
Figura 17-28 Estimativas com barras de erro Figura
17-29 Depuração exponencial de
moléculas Figura 17-30 Decaimento exponencial Figura 17-31 Plotando
o decaimento exponencial com um
logarítmico axis Figura 17-33 Uma distribuição
geométrica Figura 17-32 Produzindo uma
distribuição geométrica Figura 17-34
Simulando uma tabela hash Figura 17-35 Simulação da
World Series
Figura 17-36 Probabilidade de ganhar uma
série de 7 jogos Capítulo 18 Figura
18-1 Verificação Análise de Pascal Figura
18-2 Classe craps_game Figura 18-3 Simulando um jogo de
dados Figura 18-4 Usando a consulta de tabela
para melhorar o desempenho Figura 18-5 Círculo unitário inscrito em um quadrado Figu
Capítulo 19
Figura 19-1 As primeiras linhas em bm_results2012.csv Figura
19-2 Leia os dados e produza o gráfico da Maratona de Boston
Machine Translated by Google

Figura 19-3 Tempos finais da Maratona de Boston Figura


19-4 Amostragem dos tempos finais Figura
19-5 Análise de uma pequena amostra Figura
19-6 Efeito da variância na estimativa da média Figura 19-7
Calcular e plotar médias amostrais Figura 19-8 Médias
amostrais Figura 19-9 Estimando
a média de uma matriz contínua Figura 19-10 Uma ilustração do
CLT Figura 19-11 Gráfico de produção com
barras de erro Figura 19-12 Estimativas de tempos
de acabamento com barras de erro Figura 19-13 Erro padrão da média
Figura 19 -14 Desvio padrão da amostra vs.
população
desvio padrão

Figura 19-15 Desvios padrão de amostra


Figura 19-16 Estimando a média da população 10.000 vezes
Capítulo 20
Figura 20-1 Um experimento clássico
Figura 20-2 Extraindo os dados de um arquivo
Figura 20-3 Plotando os dados
Figura 20-4 Deslocamento da mola
Figura 20-5 Ajustando uma curva aos dados
Figura 20-6 Pontos medidos e modelo linear
Figura 20-7 Ajustes lineares e cúbicos
Figura 20-8 Usando o modelo para fazer uma previsão
Figura 20-9 Um modelo até o limite elástico
Figura 20-10 Dados do experimento de projéteis
Figura 20-11 Traçando a trajetória de um projétil
Machine Translated by Google

Figura 20-12 Gráfico de trajetória


Figura 20-13 Computação R2
Figura 20-14 Calculando a velocidade horizontal de um projétil
Figura 20-15 Ajustando uma curva polinomial a uma distribuição
exponencial

Figura 20-16 Ajustando uma exponencial


Figura 20-17 Um exponencial em um gráfico semilog
Figura 20-18 Usando polyfit para ajustar uma exponencial
Figura 20-19 Um ajuste para uma função exponencial
Capítulo 21
Figura 21-1 Tempos de chegada para ciclistas
Figura 21-2 diferença de temperatura de janeiro de 2020 em relação à
média de 1981-2010 145
Figura 21-3 Plotando uma distribuição t
Figura 21-4 Visualizando a estatística t
Figura 21-5 Calcule e imprima a estatística t e o valor p
Figura 21-6 Código para gerar exemplos de corrida
Figura 21-7 Probabilidade de valores-p
Figura 21-8 Simulação de jogos de Lyndsay
Figura 21-9 Simulação correta de jogos
Figura 21-10 Impacto do tamanho da amostra no valor-p
Figura 21-11 Comparando os tempos médios de finalização para selecionados
países

Figura 21-12 Verificando várias hipóteses Figura


21-13 O sol explodiu?
Capítulo 22
Figura 22-1 Preços de imóveis no meio-oeste dos EUA
Figura 22-2 Gráficos de preços de imóveis
Machine Translated by Google

Figura 22-3 Uma visão diferente dos preços da habitação


Figura 22-4 Preços da habitação relativos a US$ 200.000
Figura 22-5 Comparação do número de seguidores no Instagram
Figura 22-6 Os limões mexicanos salvam vidas?
Figura 22-7 Estatísticas do quarteto de Anscombe
Figura 22-8 Dados do quarteto de Anscombe
Figura 22-9 Bem-estar versus empregos de
tempo integral Figura 22-10 Gelo marinho
no Ártico Figura 22-11 Crescimento do uso da Internet nos EUA
Figura 22-12 Professor intrigado com o arremesso de giz dos alunos
precisão

Figura 22-13 Probabilidade de 48 anoréxicos nascerem em junho


Figura 22-14 Probabilidade de 48 anoréxicos nascerem em alguns
mês

Capítulo 23
Figura 23-1 Um exemplo de Pandas DataFrame vinculado à variável
wwc

Figura 23-2 Um exemplo de arquivo CSV


Figura 23-3 Construindo um dicionário mapeando anos para
dados de temperatura
Figura 23-4 Construindo um DataFrame organizado em anos
Figura 23-5 Produzir gráficos relacionados ao ano com medições
de temperatura

Figura 23-6 Temperaturas médias e mínimas anuais


Figura 23-7 Temperaturas mínimas médias contínuas
Figura 23-8 Temperaturas médias para cidades selecionadas
Figura 23-9 Variação em temperaturas extremas
Figura 23-10 Consumo global de combustíveis fósseis
Machine Translated by Google

Capítulo 24
Figura 24-1 Dois conjuntos de
nomes Figura 24-2 Associando um vetor de características a
cada nome Figura 24-3 Pares de vetores de características/
rótulos para presidentes Figura 24-4 Nome, características e rótulos
para diversos animais Figura 24-5 Visualizando
métricas de distância Figura 24-6
Distância de Minkowski Figura
24-7 Classe Animal Figura 24-8 Construir tabela de distâncias entre
pares de animais Figura 24-9 Distâncias entre três
animais Figura 24-10 Distâncias entre quatro
animais Figura 24-11 Distâncias usando um método diferente
representação
de recursos Capítulo 25 Figura 25-1 Altura, peso
e cor da camisa Figura 25-2
Exemplo de classe Figura
25-3 Grupo de classes Figura 25-4
Agrupamento de K-means Figura 25-5 Encontrando o
melhor agrupamento de k-médios
Figura 25-6 Um teste de k-means Figura 25-7
Exemplos de duas distribuições Figura 25-8 Linhas impressas por uma chamada para
Figura 25-9 Gerando pontos a partir de três distribuições Figura
25-10 Pontos de três gaussianos sobrepostos Figura 25-11
Dentição de mamífero em dentalFormulas.csv Figura 25-12
Leitura e processamento do arquivo CSV
Figura 25-13 Atributos de escala
Figura 25-14 Início do CSV arquivo classificando mamíferos por dieta
Machine Translated by Google

Figura 25-15 Relacionando agrupamento a rótulos

Capítulo 26

Figura 26-1 Gráficos de preferências do eleitor

Figura 26-2 Matrizes de confusão Figura

26-3 Um modelo mais complexo Figura 26-4

Funções para avaliar classificadores Figura 26-5 Primeiras

linhas de bm_results2012. csv Figura 26-6 Crie exemplos e divida

os dados em treinamento e
conjuntos de teste

Figura 26-7 Encontrando os k vizinhos mais próximos

Figura 26-8 Classificador baseado em prevalência

Figura 26-9 Procurando um bom k

Figura 26-10 Escolhendo um valor para k

Figura 26-11 Modelos de regressão linear para homens e mulheres

Figura 26-12 Produzir e plotar modelos de regressão linear

Figura 26-13 Usando regressão linear para construir um classificador

Figura 26-14 Usando sklearn para fazer regressão logística multiclasse

Figura 26-15 Exemplo de regressão logística de duas classes

Figura 26-16 Use a regressão logística para prever o gênero

Figura 26-17 Construir curva ROC e encontrar AUROC

Figura 26-18 Curva ROC e AUROC

Figura 26-19 Classe Passageiro

Figura 26-20 Leia os dados do Titanic e crie uma lista de exemplos 207

Figura 26-21 Modelos de teste para sobrevivência do Titanic

Figura 26-22 Estatísticas de impressão sobre classificadores


Machine Translated by Google

PREFÁCIO

Este livro é baseado em cursos que são oferecidos no MIT desde 2006 e como
“Massive Online Open Courses” (MOOCs) através do edX e MITx desde 2012.
A primeira edição do livro foi baseada em um único curso de um semestre. No
entanto, com o tempo, não resisti em adicionar mais material do que caberia em
um semestre. A edição atual é adequada para uma sequência introdutória de
ciência da computação de dois ou três trimestres.

O livro destina-se a 1) leitores com pouca ou nenhuma experiência anterior


em programação que desejam entender as abordagens computacionais para a
solução de problemas e 2) programadores mais experientes que desejam
aprender como usar a computação para modelar coisas ou explorar dados.

Enfatizamos a amplitude em vez da profundidade. O objetivo é fornecer aos


leitores uma breve introdução a vários tópicos, para que tenham uma ideia do
que é possível fazer quando chegar a hora de pensar em como usar a
computação para atingir um objetivo. Dito isso, este não é um livro de “apreciação
de computação”. É desafiador e rigoroso.
Os leitores que desejam realmente aprender o material terão que gastar muito
tempo e esforço aprendendo a dobrar o computador à sua vontade.
O principal objetivo deste livro é ajudar os leitores a se tornarem habilidosos
no uso produtivo de técnicas computacionais. Eles devem aprender a usar
modos computacionais de pensamento para enquadrar problemas, construir
modelos computacionais e orientar o processo de extração de informações dos
dados. O conhecimento primário que eles tirarão deste livro é a arte da solução
de problemas computacionais.
Optamos por não incluir problemas no final dos capítulos. Em vez disso,
inserimos “exercícios com os dedos” em pontos oportunos dentro dos capítulos.
Alguns são bastante curtos e têm como objetivo permitir que os leitores
confirmem que entenderam o material que acabaram de ler. Alguns são
Machine Translated by Google

mais desafiadores e são adequados para questões de exames. E outros são


desafiadores o suficiente para serem úteis como tarefas de casa.
Os capítulos 1 a 13 contêm o tipo de material normalmente incluído em
um curso introdutório à ciência da computação, mas a apresentação não é
convencional. Trançamos quatro fios de material:

Noções básicas de
programação, linguagem de programação
Python 3, técnicas de resolução de problemas
computacionais e complexidade computacional.

Cobrimos a maioria dos recursos do Python, mas a ênfase está no que


se pode fazer com uma linguagem de programação, não na linguagem em si.
Por exemplo, no final do Capítulo 3, o livro cobriu apenas uma pequena
fração do Python, mas já introduziu as noções de enumeração exaustiva,
algoritmos de adivinhação e verificação, pesquisa de bisseção e algoritmos
de aproximação eficientes. Apresentamos os recursos do Python ao longo do
livro. Da mesma forma, apresentamos aspectos dos métodos de programação
ao longo do livro. A ideia é ajudar os leitores a aprender Python e como ser
um bom programador no contexto do uso da computação para resolver
problemas interessantes. Esses capítulos foram revisados para prosseguir
com mais cuidado e incluir mais exercícios do que os capítulos correspondentes
na segunda edição deste livro.

O Capítulo 13 contém uma introdução à plotagem em Python. Esse


tópico geralmente não é abordado em cursos introdutórios, mas acreditamos
que aprender a produzir visualizações de informações é uma habilidade
importante que deve ser incluída em um curso introdutório de ciência da
computação. Este capítulo inclui material não abordado na segunda edição.

Os capítulos 14 a 26 tratam do uso da computação para ajudar a entender


o mundo real. Eles cobrem o material que achamos que deveria se tornar o
segundo curso usual em um currículo de ciência da computação. Eles
assumem nenhum conhecimento de matemática além da álgebra do ensino
médio, mas assumem que o leitor se sente confortável com o pensamento
rigoroso e não se intimida com os conceitos matemáticos.
Esta parte do livro é dedicada a tópicos não encontrados na maioria dos
textos introdutórios: visualização e análise de dados, análise estocástica
Machine Translated by Google

programas, modelos de simulação, pensamento probabilístico e estatístico e


aprendizado de máquina. Acreditamos que este é um corpo de material muito
mais relevante para a maioria dos alunos do que o que normalmente é abordado
no segundo curso de ciência da computação. Exceto pelo Capítulo 23, o material
desta seção do livro enfoca questões conceituais em vez de programação. O
Capítulo 23 é uma introdução aos Pandas, um tópico não abordado nas edições
anteriores.
O livro tem três temas abrangentes: resolução sistemática de problemas, o
poder da abstração e a computação como uma forma de pensar sobre o mundo.
Ao terminar este livro, você deverá ter:

Aprendeu uma linguagem, Python, para expressar computações,


Aprendeu uma abordagem sistemática para organizar, escrever e
depurar programas de tamanho médio,
Desenvolveu uma compreensão informal da complexidade
computacional,
Desenvolveu algum insight sobre o processo de passar de uma
declaração de problema ambígua para uma declaração computacional
formulação de um método para resolver
o problema, Aprendeu um conjunto útil de técnicas algorítmicas e
de redução
de problemas, Aprendeu a usar aleatoriedade e simulações para lançar luz
sobre problemas que não sucumbem facilmente a soluções de forma
fechada e Aprendeu a usar computação ferramentas (incluindo
estatísticas simples, ferramentas de visualização e aprendizado de
máquina) para modelar e entender os dados.

A programação é uma atividade intrinsecamente difícil. Assim como “não há


estrada real para a geometria”, 1 não há estrada real para a programação.
Se você realmente deseja aprender o material, a leitura do livro não será
suficiente. No mínimo, você deve concluir os exercícios com os dedos que
envolvem codificação. Se você está disposto a tentar tarefas mais ambiciosas,
experimente alguns dos conjuntos de problemas disponíveis em
https://ocw.mit.edu/courses/electrical-engineering-and computer-science/6-0001-
introduction-to-computer-science-
Machine Translated by Google

e-programação-em-python-fall-2016/

e
https://ocw.mit.edu/courses/electrical-engineering-and computer-science/6-0002-
introduction-to-computational thinking-and-data-science-fall-2016/.

1 Essa foi a suposta resposta de Euclides, por volta de 300 aC, ao pedido
do rei Ptolomeu de uma maneira mais fácil de aprender matemática.
Machine Translated by Google

AGRADECIMENTOS

A primeira edição deste livro surgiu de um conjunto de notas de aula que


preparei enquanto lecionava em um curso de graduação no MIT. O curso, e
portanto este livro, beneficiou-se de sugestões de colegas do corpo docente
(especialmente Ana Bell, Eric Grimson, Srinivas Devadas, Fredo Durand, Ron
Rivest e Chris Terman), professores assistentes e alunos que fizeram o curso.
David Guttag superou sua aversão à ciência da computação e revisou vários
capítulos da primeira edição.

Como todos os professores de sucesso, devo muito aos meus alunos de


pós-graduação. Além de fazer uma ótima pesquisa (e me deixar levar parte
do crédito por isso), Guha Balakrishnan, Davis Blalock, Joel Brooks,
Ganeshapillai Gartheeban, Jen Gong, Katie Lewis, Yun Liu, Jose Javier
Gonzalez Ortiz, Anima Singh, Divya Shanmugam , Jenna Wiens e Amy Zhao
forneceram comentários úteis sobre várias versões deste manuscrito.

Tenho uma dívida especial de gratidão com Julie Sussman, PPA, que
editou as duas primeiras edições deste livro, e Lisa Ruffolo, que editou a
edição atual. Tanto Julie quanto Lisa foram colaboradoras que leram o livro
com olhos de estudante e me disseram o que precisava ser feito, o que
deveria ser feito e o que poderia ser feito se eu tivesse tempo e energia para
fazê-lo. Eles me enterraram em “sugestões” boas demais para serem
ignoradas.
Finalmente, agradeço à minha esposa, Olga, por me incentivar a terminar
e por me dispensar de várias tarefas domésticas para que eu pudesse
trabalhar no livro.
Machine Translated by Google

COMEÇANDO

Um computador faz duas coisas, e apenas duas coisas: ele executa cálculos e
lembra os resultados desses cálculos. Mas faz essas duas coisas extremamente
bem. O computador típico que fica em uma mesa ou em uma mochila realiza cerca
de 100 bilhões de cálculos por segundo. É difícil imaginar o quão rápido isso é.
Pense em segurar uma bola um metro acima do chão e soltá-la. No momento em
que chega ao chão, seu computador pode ter executado mais de um bilhão de
instruções. Quanto à memória, um pequeno computador pode ter centenas de
gigabytes de armazenamento. Quão grande é isso? Se um byte (o número de bits,
normalmente oito, necessários para representar um caractere) pesasse um grama
(o que não acontece), 100 gigabytes pesariam 100.000 toneladas métricas. Para
efeito de comparação, esse é aproximadamente o peso combinado de 16.000
elefantes africanos.2 Durante a maior parte da história da humanidade, a computação
foi limitada pela rapidez com que o cérebro
humano podia calcular e pela qualidade com que a mão humana registrava os
resultados computacionais. Isso significava que apenas os menores problemas
poderiam ser atacados computacionalmente. Mesmo com a velocidade dos
computadores modernos, alguns problemas ainda estão além dos modelos
computacionais modernos (por exemplo, entender completamente a mudança
climática), mas cada vez mais problemas estão se mostrando passíveis de solução
computacional. Esperamos que, ao terminar este livro, você se sinta à vontade para
aplicar o pensamento computacional na solução de muitos dos problemas que
encontrar durante seus estudos, trabalho e até mesmo na vida cotidiana.

O que queremos dizer com pensamento computacional?


Todo conhecimento pode ser pensado como declarativo ou imperativo. O
conhecimento declarativo é composto de declarações de fato. Por exemplo, “a raiz
quadrada de x é um número tal que y*y = e
Machine Translated by Google

x” e “é possível viajar de trem de Paris a Roma”. Estas são declarações de fato. Infelizmente, eles
não nos dizem nada sobre como encontrar uma raiz quadrada ou como pegar trens de Paris a
Roma.

O conhecimento imperativo é o conhecimento “como fazer” ou receitas para deduzir


informações. Heron de Alexandria foi o primeiro3 a documentar uma maneira de calcular a raiz
quadrada de um número. Seu método para encontrar a raiz quadrada de um número, chame-o de
x, pode ser resumido como:

1. Comece com um palpite, g.

2. Se g*g estiver próximo o suficiente de x, pare e diga que 3. g é a resposta.

Caso contrário, crie uma nova estimativa calculando a média de g e x/g, ou seja, (g +
x/g)/2.

4. Usando esse novo palpite, que novamente chamamos de g, repita o processo até que g*g
esteja próximo o suficiente de x.

Considere encontrar a raiz quadrada de 25.

1. Defina g para algum valor arbitrário, por exemplo, 3.

2. Decidimos que 3*3 = 9 não é próximo o suficiente de 25.

3. Defina g como (3 + 25/3)/2 = 5,67. 4 4.

Decidimos que 5,67*5,67 = 32,15 ainda não é próximo o suficiente para


25.

5. Defina g como (5,67 + 25/5,67)/2 = 5,04 6.

Decidimos que 5,04*5,04 = 25,4 é próximo o suficiente, então paramos e declaramos


que 5,04 é uma aproximação adequada da raiz quadrada de 25.

Observe que a descrição do método é uma sequência de etapas simples, juntamente com um
fluxo de controle que especifica quando executar cada etapa. Tal descrição é chamada de
algoritmo. 5 O algoritmo que usamos para aproximar a raiz quadrada é um exemplo de adivinhação
Machine Translated by Google

verifique o algoritmo. Baseia-se no fato de que é fácil verificar se um palpite é ou não bom
o suficiente.
Mais formalmente, um algoritmo é uma lista finita de instruções que descrevem um
conjunto de cálculos que, quando executados em um conjunto de entradas, prosseguirão
por uma sequência de estados bem definidos e, eventualmente, produzirão uma saída.

Um algoritmo é como uma receita de um livro de receitas:

1. Leve a mistura de creme ao fogo.

2. Mexa.

3. Mergulhe a colher no creme.

4. Retire a colher e passe o dedo pelas costas da colher.

5. Se deixar um caminho livre, retire o creme do fogo e deixe esfriar.

6. Caso contrário, repita.

A receita inclui alguns testes para decidir quando o processo está completo, bem como
instruções sobre a ordem de execução das instruções, às vezes pulando para uma
instrução específica com base em um teste.

Então, como capturar a ideia de uma receita em um processo mecânico? Uma


maneira é projetar uma máquina especificamente destinada a calcular raízes quadradas.
Por mais estranho que pareça, as primeiras máquinas de computação eram, na verdade,
computadores de programa fixo, o que significa que foram projetados para resolver um
problema matemático específico, por exemplo, calcular a trajetória de um projétil de
artilharia. Um dos primeiros computadores (construído em 1941 por Atanasoff e Berry)
resolvia sistemas de equações lineares, mas não podia fazer mais nada. A máquina bombe
de Alan Turing, desenvolvida durante a Segunda Guerra Mundial, foi projetada para
quebrar os códigos Enigma alemães. Alguns computadores simples ainda usam essa
abordagem. Por exemplo, uma calculadora de quatro funções6 é um computador de
programa fixo. Ele pode fazer aritmética básica, mas não pode ser usado como um
processador de texto ou para rodar videogames. Para alterar o programa de tal máquina,
é preciso substituir o circuito.

O primeiro computador verdadeiramente moderno foi o Manchester Mark 1.7.


Distinguiu-se de seus predecessores por ser um computador de programa armazenado.
Tal computador armazena (e manipula) um
Machine Translated by Google

sequência de instruções e possui componentes que executam qualquer instrução


nessa sequência. O coração desse computador é um interpretador que pode
executar qualquer conjunto legal de instruções e, portanto, pode ser usado para
computar qualquer coisa que possa ser descrita usando essas instruções. O
resultado da computação pode até ser uma nova sequência de instruções, que
podem ser executadas pelo computador que as gerou. Em outras palavras, é
possível que um computador se programe.8 Tanto o programa quanto os dados
que ele manipula residem na
memória.
Normalmente, um contador de programa aponta para um local específico na
memória e a computação começa executando a instrução nesse ponto. Na maioria
das vezes, o interpretador simplesmente vai para a próxima instrução na sequência,
mas nem sempre. Em alguns casos, ele realiza um teste e, com base nesse teste,
a execução pode pular para outro ponto na sequência de instruções. Isso é
chamado de fluxo de controle e é essencial para nos permitir escrever programas
que executam tarefas complexas.

As pessoas às vezes usam fluxogramas para representar o fluxo de controle.


Por convenção, usamos caixas retangulares para representar uma etapa de
processamento, um losango para representar um teste e setas para indicar a
ordem em que as coisas são feitas. A Figura 1-1 contém um fluxograma
descrevendo uma abordagem para conseguir o jantar.

Figura 1-1 Fluxograma de jantar


Machine Translated by Google

Voltando à metáfora da receita, dado um conjunto fixo de ingredientes, um


bom chef pode fazer um número ilimitado de pratos saborosos combinando-os
de diferentes maneiras. Da mesma forma, dado um pequeno conjunto fixo de
recursos primitivos, um bom programador pode produzir um número ilimitado de
programas úteis. Isso é o que torna a programação um empreendimento tão
incrível.
Para criar receitas, ou seqüências de instruções, precisamos de uma
linguagem de programação para descrevê-las, uma forma de dar ao computador
suas ordens de marcha.
Em 1936, o matemático britânico Alan Turing descreveu um dispositivo de
computação hipotético que veio a ser chamado de Máquina de Turing
Universal. A máquina tinha memória ilimitada na forma de uma “fita” na qual se
podia escrever zeros e uns, e um punhado de instruções primitivas simples para
mover, ler e gravar na fita. A tese de Church-Turing afirma que se uma função
é computável, uma máquina de Turing pode ser programada para computá-la.

O “se” na tese de Church-Turing é importante. Nem todos os problemas têm


soluções computacionais. Turing mostrou, por exemplo, que é impossível
escrever um programa que receba um programa arbitrário como entrada e
imprima verdadeiro se e somente se o programa de entrada for executado para
sempre. Isso é conhecido como o problema da parada.
A tese de Church-Turing leva diretamente à noção de completude de
Turing. Uma linguagem de programação é considerada Turing completa se
puder ser usada para simular uma máquina de Turing universal. Todas as
linguagens de programação modernas são Turing completas. Como
consequência, qualquer coisa que possa ser programada em uma linguagem de
programação (por exemplo, Python) pode ser programada em qualquer outra
linguagem de programação (por exemplo, Java). É claro que algumas coisas
podem ser mais fáceis de programar em uma determinada linguagem, mas
todas as linguagens são fundamentalmente iguais em relação ao poder computacional.
Felizmente, nenhum programador precisa construir programas a partir das
instruções primitivas de Turing. Em vez disso, as linguagens de programação
modernas oferecem um conjunto maior e mais conveniente de primitivos. No
entanto, a ideia fundamental de programação como o processo de montagem
de uma sequência de operações permanece central.
Qualquer que seja o conjunto de primitivos que você tenha e quaisquer
métodos que você tenha para montá-los, a melhor e a pior coisa sobre
programação são as mesmas: o computador fará exatamente o que você
Machine Translated by Google

diga a ele para fazer - nada mais, nada menos. Isso é bom porque significa que
você pode fazer o computador fazer todo tipo de coisas divertidas e úteis. É
uma coisa ruim porque quando não faz o que você quer, você geralmente não
tem ninguém para culpar além de si mesmo.
Existem centenas de linguagens de programação no mundo.
Não existe melhor linguagem. Diferentes idiomas são melhores ou piores para
diferentes tipos de aplicativos. O MATLAB, por exemplo, é uma boa linguagem
para manipulação de vetores e matrizes. C é uma boa linguagem para escrever
programas que controlam redes de dados. PHP é uma boa linguagem para
construir websites. E o Python é uma excelente linguagem de uso geral.

Cada linguagem de programação tem um conjunto de construções primitivas,


uma sintaxe, uma semântica estática e uma semântica. Por analogia com uma
linguagem natural, por exemplo, o inglês, as construções primitivas são palavras,
a sintaxe descreve quais sequências de palavras constituem sentenças bem
formadas, a semântica estática define quais sentenças são significativas e a
semântica define o significado dessas sentenças. As construções primitivas em
Python incluem literais (por exemplo, o número 3.2 e a string 'abc') e operadores
infixos (por exemplo, + e /).
A sintaxe de uma linguagem define quais strings de caracteres e símbolos
são bem formados. Por exemplo, em inglês, a string “Cat dog boy.” não é uma
sentença sintaticamente válida, porque a sintaxe do inglês não aceita sentenças
na forma <substantivo> <substantivo> <substantivo>. Em Python, a sequência
de primitivas 3.2 + 3.2 é sintaticamente bem formada, mas a sequência 3.2 3.2
não.
A semântica estática define quais strings sintaticamente válidas têm um
significado. Considere, por exemplo, as strings “Ele corre rápido” e “Eu corro
rápido”. Cada um tem a forma <pronome> <verbo regular> <advérbio>, que é
uma sequência sintaticamente aceitável.
No entanto, nenhum dos dois é válido em inglês, devido à regra bastante
peculiar de que, para um verbo regular, quando o sujeito de uma frase é a
primeira ou a segunda pessoa, o verbo não termina com um “s”, mas quando o
sujeito é a terceira pessoa do singular, sim. Estes são exemplos de erros
semânticos estáticos.
A semântica de uma linguagem associa um significado a cada cadeia de
símbolos sintaticamente correta que não possui erros semânticos estáticos. Em
linguagens naturais, a semântica de uma frase pode ser ambígua. Por exemplo,
a frase “Não posso elogiar muito este aluno” pode ser lisonjeira ou condenatória.
Programação
Machine Translated by Google

linguagens são projetadas para que cada programa legal tenha exatamente um
significado.
Embora os erros de sintaxe sejam o tipo de erro mais comum (especialmente para
aqueles que estão aprendendo uma nova linguagem de programação), eles são o tipo
de erro menos perigoso. Toda linguagem de programação séria detecta todos os erros
sintáticos e não permite que os usuários executem um programa com um único erro
sintático. Além disso, na maioria dos casos, o sistema de linguagem fornece uma
indicação suficientemente clara da localização do erro para que o programador seja
capaz de corrigi-lo sem pensar muito.

Identificar e resolver erros semânticos estáticos é mais complexo.


Algumas linguagens de programação, por exemplo, Java, fazem muitas verificações
semânticas estáticas antes de permitir que um programa seja executado. Outros, por
exemplo, C e Python (infelizmente), fazem relativamente menos verificação semântica
estática antes de um programa ser executado. O Python faz uma quantidade considerável
de verificação semântica durante a execução de um programa.
Se um programa não tem erros sintáticos e nem erros semânticos estáticos, ele
tem um significado, ou seja, tem semântica. Claro, pode não ter a semântica pretendida
por seu criador. Quando um programa significa algo diferente do que seu criador pensa,
coisas ruins podem acontecer.

O que pode acontecer se o programa tiver um erro e se comportar de maneira não


intencional?

Ele pode travar, ou seja, parar de funcionar e produzir uma indicação


óbvia de que o fez. Em um sistema de computação projetado adequadamente,
quando um programa falha, ele não danifica o sistema como um todo. Infelizmente,
alguns sistemas de computador muito populares não têm essa boa propriedade.
Quase todo mundo que usa um computador pessoal já executou um
programa que conseguiu tornar necessário reiniciar todo o sistema.

Pode continuar correndo, correndo, correndo e nunca parando.


Se você não tem ideia de quanto tempo aproximadamente o programa deve
levar para fazer seu trabalho, essa situação pode ser difícil de reconhecer.

Ele pode ser executado até a conclusão e produzir uma resposta que pode ou não
estar correta.
Machine Translated by Google

Cada um desses resultados é ruim, mas o último é certamente o pior. Quando um


programa parece estar fazendo a coisa certa, mas não está, podem ocorrer coisas ruins:
fortunas podem ser perdidas, pacientes podem receber doses fatais de radioterapia, aviões
podem cair.
Sempre que possível, os programas devem ser escritos de modo que, quando não
funcionarem corretamente, isso seja evidente. Discutiremos como fazer isso ao longo do livro.

Exercício com os dedos: os computadores podem ser irritantemente literais. Se você não
disser a eles exatamente o que deseja que eles façam, é provável que eles façam a coisa
errada. Tente escrever um algoritmo para dirigir entre dois destinos. Escreva da maneira que
você faria para uma pessoa e imagine o que aconteceria se essa pessoa fosse tão estúpida
quanto um computador e executasse o algoritmo exatamente como está escrito. (Para uma
ilustração divertida disso, dê uma olhada no vídeo https://www.youtube.com/watch?v=FN2RM-
CHkuI&t=24s.)

1.1 Termos Introduzidos no Capítulo

conhecimento declarativo

conhecimento imperativo

computação

de algoritmo

computador de programa fixo

computador de programa

armazenado intérprete de computador

fluxo de controle do
contador do programa

fluxograma

linguagem de programação

máquina de Turing universal

Tese de Church-Turing
Machine Translated by Google

problema de parada

Literais de completude de
Turing

operadores infixos

sintaxe
semântica estática

semântica

2 Nem tudo é mais caro do que costumava ser. Em 1960, um pouco de memória de
computador custava cerca de US$ 0,64. Hoje custa cerca de $ 0,000000004.

3 Muitos acreditam que Heron não foi o inventor deste método,


e, de fato, há algumas evidências de que era bem conhecido dos antigos babilônios.

4 Para simplificar, estamos arredondando os resultados.

5 A palavra “algoritmo” é derivada do nome do matemático persa Muhammad ibn


Musa al-Khwarizmi.

6 Pode ser difícil para alguns de vocês acreditar, mas antigamente as pessoas não
carregavam telefones que funcionavam como recursos
computacionais. Na verdade, as pessoas carregavam pequenos dispositivos que só
podiam ser usados para cálculos aritméticos.

7 Este computador foi construído na Universidade de Manchester e executou seu primeiro


programa em 1949. Ele implementou ideias previamente descritas por John
von Neumann e foi antecipado pelo conceito teórico da Máquina de Turing
Universal descrita por Alan Turing em 1936.

8 Essa possibilidade serviu de inspiração para uma infinidade de livros e filmes


distópicos.
Machine Translated by Google

INTRODUÇÃO AO PYTHON

Embora cada linguagem de programação seja diferente (embora não tão


diferente quanto seus projetistas querem que acreditemos), elas podem estar
relacionadas em algumas dimensões.

Baixo nível versus alto nível refere-se a se programamos usando


instruções e objetos de dados no nível da máquina (por exemplo,
mover 64 bits de dados deste local para aquele local) ou se programamos
usando operações mais abstratas (por exemplo, pop abrir um menu na
tela) que foram fornecidos pelo designer da linguagem.

Geral versus direcionado a um domínio de aplicativo refere-se a se


as operações primitivas da linguagem de programação são amplamente
aplicáveis ou são ajustadas a um domínio. Por exemplo, o SQL é projetado
para extrair informações de bancos de dados relacionais, mas
você não gostaria de usá-lo para construir um sistema operacional.

Interpretado versus compilado refere-se a se a sequência de instruções


escritas pelo programador, chamada de código-fonte, é executada
diretamente (por um interpretador) ou se é primeiro convertida (por
um compilador) em uma sequência de operações primitivas no
nível da máquina. (Nos primórdios dos computadores, as pessoas tinham
que escrever o código-fonte em uma linguagem próxima ao código da
máquina que pudesse ser diretamente interpretada pelo hardware do computador.)
Há vantagens em ambas as abordagens. Geralmente é mais fácil
depurar programas escritos em linguagens projetadas para serem
interpretadas, porque o interpretador pode produzir mensagens de erro
fáceis de relacionar com o código-fonte. idiomas compilados
Machine Translated by Google

geralmente produzem programas que rodam mais rapidamente e usam menos


espaço.

Neste livro, usamos Python. No entanto, este livro não é sobre Python. Certamente
ajudará você a aprender Python, e isso é bom. O que é muito mais importante, no entanto,
é que você aprenderá algo sobre como escrever programas que resolvem problemas.
Você pode transferir essa habilidade para qualquer linguagem de programação.

Python é uma linguagem de programação de propósito geral que você pode usar
efetivamente para construir quase qualquer tipo de programa que não precise de acesso
direto ao hardware do computador. Python não é ideal para programas que possuem altas
restrições de confiabilidade (devido à sua fraca verificação semântica estática) ou que são
construídos e mantidos por muitas pessoas ou por um longo período de tempo (novamente
devido à fraca verificação semântica estática).

O Python tem várias vantagens sobre muitas outras linguagens.


É uma linguagem relativamente simples e fácil de aprender. Como o Python foi projetado
para ser interpretado, ele pode fornecer o tipo de feedback em tempo de execução que é
especialmente útil para programadores novatos. Um número grande e crescente de
interfaces de bibliotecas disponíveis gratuitamente para Python e fornece funcionalidade
estendida útil. Usamos várias dessas bibliotecas neste livro.

Estamos prontos para apresentar alguns dos elementos básicos do Python.


Estes são comuns a quase todas as linguagens de programação em conceito, embora
não em detalhes.
Este livro não é apenas uma introdução ao Python. Ele usa o Python como um
veículo para apresentar conceitos relacionados à resolução de problemas computacionais
e pensamento. A linguagem é apresentada em dribs e drabs, conforme necessário para
este propósito ulterior. Os recursos do Python que não precisamos para esse propósito
não são apresentados. Sentimo-nos confortáveis em não cobrir todos os detalhes porque
excelentes recursos on-line descrevem todos os aspectos do idioma. Sugerimos que você
use esses recursos on-line gratuitos conforme necessário.

Python é uma linguagem viva. Desde a sua introdução por Guido von Rossum em
1990, passou por muitas mudanças. Durante a primeira década de sua vida, Python foi
uma linguagem pouco conhecida e pouco usada.
Isso mudou com a chegada do Python 2.0 em 2000. Além de incorporar melhorias
importantes à própria linguagem, marcou uma mudança no caminho evolutivo da
linguagem. muitos grupos
Machine Translated by Google

começou a desenvolver bibliotecas que faziam uma interface perfeita com o


Python, e o suporte e desenvolvimento contínuos do ecossistema Python
tornaram-se uma atividade baseada na comunidade.
O Python 3.0 foi lançado no final de 2008. Esta versão do Python eliminou
muitas das inconsistências no design do Python 2. No entanto, o Python 3 não é
compatível com versões anteriores. Isso significa que a maioria dos programas
e bibliotecas escritas para versões anteriores do Python não podem ser
executadas usando implementações do Python 3.
Até agora, todas as importantes bibliotecas Python de domínio público têm
foi portado para o Python 3. Hoje, não há razão para usar o Python 2.

2.1 Instalando Python e Python IDEs

Antigamente, os programadores usavam editores de texto de uso geral para


inserir seus programas. Hoje, a maioria dos programadores prefere usar um
editor de texto que faça parte de um ambiente de desenvolvimento integrado
(IDE).
O primeiro Python IDE, IDLE,9 veio como parte do pacote de instalação
padrão do Python. À medida que a popularidade do Python cresceu, outros IDEs
surgiram. Esses IDEs mais novos geralmente incorporam algumas das bibliotecas
Python mais populares e fornecem recursos não fornecidos pelo IDLE. Anaconda
e Canopy estão entre os mais populares desses IDEs. O código que aparece
neste livro foi criado e testado usando o Anaconda.

IDEs são aplicativos, como qualquer outro aplicativo em seu computador.


Inicie um da mesma forma que iniciaria qualquer outro aplicativo, por exemplo,
clicando duas vezes em um ícone. Todos os IDEs do Python fornecem

Um editor de texto com realce de sintaxe, preenchimento automático


e recuo inteligente,
um shell com realce de sintaxe e um
depurador integrado, que você pode ignorar com segurança por enquanto.

Este seria um bom momento para instalar o Anaconda (ou algum outro
IDE) em seu computador, para que você possa executar os exemplos no
Machine Translated by Google

livro e, mais importante, tente os exercícios de programação com os dedos. Para


instalar o Anaconda, vá para
https://www.anaconda.com/distribution/

e siga as instruções.
Assim que a instalação estiver concluída, inicie o aplicativo Anaconda
Navigator. Uma janela contendo uma coleção de ferramentas Python aparecerá. A
janela se parecerá com a Figura 2-1. 10 Por
enquanto, a única ferramenta que usaremos é o Spyder. Quando você iniciar o
Spyder (clicando no botão Iniciar , entre todas as coisas), uma janela semelhante
à Figura 2-2 será aberta.

Figura 2-1 Janela de inicialização do Anaconda

Figura 2-2 Janela do Spyder


Machine Translated by Google

O painel no canto inferior direito da Figura 2-2 é um console IPython


executando um shell Python interativo . Você pode digitar e executar comandos
Python nesta janela. O painel no canto superior direito é uma janela de ajuda.
Muitas vezes é conveniente fechar essa janela (clicando no x), para que mais
espaço esteja disponível para o console do IPython. O painel à esquerda é uma
janela de edição na qual você pode digitar programas que podem ser salvos e
executados. A barra de ferramentas na parte superior da janela facilita a
execução de várias tarefas, como abrir arquivos e imprimir programas.11 A
documentação do Spyder pode ser encontrada em https://www.spyder-ide.org/.

2.2 Os Elementos Básicos do Python

Um programa Python , às vezes chamado de script, é uma sequência de


definições e comandos. O interpretador Python no shell avalia as definições e
executa os comandos.
Recomendamos que você inicie um shell Python (por exemplo, iniciando o
Spyder) agora e use-o para experimentar os exemplos contidos no restante
deste capítulo. E, aliás, no resto do livro.
Um comando, muitas vezes chamado de instrução, instrui o interpretador
a fazer algo. Por exemplo, a instrução print('Yankees rule!') instrui o interpretador
a chamar a function12 print, que retorna a string Yankees rule! para a janela
associada ao shell.
A sequência de comandos

print('Regra dos Yankees!') print('Mas não


em Boston!') print('Regra dos Yankees,', 'mas não
em Boston!')

faz com que o interpretador produza a saída


Regra dos ianques!
Mas não em Boston!
Os Yankees governam, mas não em Boston!

Observe que dois valores foram passados para impressão na terceira instrução.
A função print pega um número variável de argumentos separados por vírgulas
e os imprime, separados por um caractere de espaço, na ordem em que
aparecem.
Machine Translated by Google

2.2.1 Objetos, expressões e tipos numéricos Objetos são as


coisas principais que os programas Python manipulam. Cada objeto tem um tipo
que define o que os programas podem fazer com aquele objeto.
Os tipos são escalares ou não escalares. Objetos escalares são indivisíveis.
Pense neles como os átomos da linguagem.13 Objetos não escalares , por
exemplo, strings, têm estrutura interna.
Muitos tipos de objetos podem ser indicados por literais no texto de um
programa. Por exemplo, o texto 2 é um literal representando um número e o texto
'abc' é um literal representando uma string.
Python tem quatro tipos de objetos escalares:

int é usado para representar números inteiros. Literais do tipo int são
escritos da maneira que normalmente denotamos inteiros (por exemplo,
-3 ou 5 ou 10002). float é usado para representar números reais. Os
literais do tipo float sempre incluem um ponto decimal (por exemplo, 3,0 ou
3,17 ou -28,72). (Também é possível escrever literais do tipo float
usando notação científica. Por exemplo, o literal 1.6E3 representa 1.6*103 ,
ou seja, é o mesmo que 1600.0.) Você pode se perguntar por que esse
tipo não é chamado de real. Dentro do computador, os valores do tipo float
são armazenados como números de ponto flutuante. Essa representação,
que é usada por todas as linguagens de programação modernas, tem muitas vantagens.
No entanto, em algumas situações, faz com que a aritmética de
ponto flutuante se comporte de maneiras ligeiramente diferentes da
aritmética em números reais. Discutimos isso na Seção 3.3. bool é
usado para representar os valores booleanos True e False.
None é um tipo com um único valor. Falaremos mais sobre None na Seção
4.

Objetos e operadores podem ser combinados para formar expressões, cada


uma das quais avaliada como um objeto de algum tipo. Isso é chamado de valor
da expressão. Por exemplo, a expressão 3 + 2 denota o objeto 5 do tipo int e a
expressão 3.0 + 2.0 denota o objeto 5.0 do tipo float.

O operador == é usado para testar se duas expressões são avaliadas como


o mesmo valor, e o operador != é usado para testar se duas expressões são
avaliadas como valores diferentes. Um único = significa algo
Machine Translated by Google

bastante diferentes, como veremos na Seção 2.2.2. Esteja avisado - você


cometerá o erro de digitar “=” quando pretendia digitar “==”.
Fique atento a esse erro.
Em um console do Spyder, algo parecido com In [1]: é um prompt de
shell indicando que o interpretador espera que o usuário digite algum código
Python no shell. A linha abaixo do prompt é produzida quando o interpretador
avalia o código Python inserido no prompt, conforme ilustrado pela seguinte
interação com o interpretador:

3
Fora[1]: 3

3+2
Fora [2]: 5

3,0+2,0
Fora[3]: 5.0

3!=2
Out[4]: Verdadeiro

O tipo de função interna do Python pode ser usado para descobrir o tipo
de um objeto:
tipo(3)
Fora[5]: int

tipo(3.0)
Fora[6]: flutuar

Os operadores em objetos do tipo int e float são listados na Figura 2-3.


Os operadores aritméticos têm a precedência usual. Por exemplo, * vincula
mais estreitamente do que +, portanto, a expressão x+y*2 é avaliada primeiro
multiplicando y por 2 e, em seguida, adicionando o resultado a x. A ordem
de avaliação pode ser alterada usando parênteses para agrupar
subexpressões, por exemplo, (x+y)*2 primeiro adiciona
e, x e depois multiplica
o resultado por 2.
Machine Translated by Google

Figura 2-3 Operadores nos tipos int e float

Os operadores primitivos no tipo bool são and, or e not:

a e b é verdadeiro se a e b são verdadeiros e falso caso contrário. a ou

b é verdadeiro se pelo menos um de a ou b for verdadeiro e falso caso contrário.


not a é True se a for False, e False se a for True.

2.2.2 Variáveis e variáveis de atribuição


fornecem uma maneira de associar nomes a objetos. Considere o código

pi = 3
raio = 11 área =
pi * (raio**2) raio = 14

O código primeiro vincula os nomes pi e radius a diferentes objetos do tipo int. 14


Em seguida, vincula a área de nome a um terceiro objeto do tipo int.
Isso é representado no lado esquerdo da Figura 2-4.
Machine Translated by Google

Figura 2-4 Vinculação de variáveis a objetos

Se o programa executar radius = 14, o nome radius é religado para um objeto


diferente do tipo int, conforme mostrado no lado direito da Figura 2-4. Observe que
esta atribuição não tem efeito sobre o valor ao qual a área está vinculada. Ele ainda
está vinculado ao objeto denotado pela expressão 3*(11**2).

Em Python, uma variável é apenas um nome, nada mais. Lembre-se disso - é


importante. Uma instrução de atribuição associa o nome à esquerda do símbolo =
com o objeto denotado pela expressão à direita do símbolo = . Lembre-se disso
também: um objeto pode ter um, mais de um ou nenhum nome associado a ele.

Talvez não devêssemos ter dito “uma variável é apenas um nome”.


Apesar do que Juliet disse,15 nomes importam. As linguagens de programação nos
permitem descrever cálculos para que os computadores possam executá-los. Isso
não significa que apenas computadores leem programas.
Como você logo descobrirá, nem sempre é fácil escrever programas que
funcionem corretamente. Programadores experientes confirmarão que gastam muito
tempo lendo programas na tentativa de entender por que eles se comportam dessa
maneira. Portanto, é de importância crítica escrever programas de modo que sejam
fáceis de ler. A escolha adequada de nomes de variáveis desempenha um papel
importante no aprimoramento da legibilidade.

Considere os dois fragmentos de código


a = 3,14159 pi = 3,14159 diâmetro
b = 11,2 = 11,2
c = a*(b**2) área = pi*(diâmetro**2)
Machine Translated by Google

No que diz respeito ao Python, os fragmentos de código não são diferentes.


Quando executados, eles farão a mesma coisa. Para um leitor humano, no entanto,
eles são bem diferentes. Quando lemos o fragmento à esquerda, não há razão a
priori para suspeitar que algo esteja errado.
No entanto, uma rápida olhada no código à direita deve nos levar a suspeitar de que
algo está errado. A variável deveria ter sido nomeada como raio em vez de diâmetro,
ou o diâmetro deveria ter sido dividido por 2,0 no cálculo da área.

Em Python, os nomes das variáveis podem conter letras maiúsculas e


minúsculas, dígitos (embora não possam começar com um dígito) e o caractere
especial (sublinhado). Os nomes das variáveis do Python diferenciam maiúsculas de
_
minúsculas, por exemplo, Romeo e romeo são nomes diferentes. Por fim, algumas
palavras reservadas (às vezes chamadas de palavras- chave) em Python que
possuem significados integrados e não podem ser usadas como nomes de variáveis.
Versões diferentes do Python têm listas ligeiramente diferentes de palavras
reservadas. As palavras reservadas no Python 3.8 são
e quebrar elif para em não Verdadeiro

como aula outro De é ou tentar

afirmar continuar exceto passagem lambda global enquanto

definição assíncrona Falso se aumento não local com

espere pelo finalmente importar nenhum rendimento de retorno

Outra boa maneira de melhorar a legibilidade do código é adicionar comentários.


O texto após o símbolo # não é interpretado pelo Python. Por exemplo, podemos
escrever

lado = 1 #comprimento dos lados de um quadrado unitário raio = 1 #raio


de um círculo unitário #subtrair a área do círculo unitário da
área do quadrado unitário area_circle = pi*raio**2 area_square = lado*diferença de lados =
area_square – area_circle

Python permite atribuição múltipla. A declaração

x, y = 2, 3

associa x a 2 e y a 3. Todas as expressões no lado direito da atribuição são avaliadas


antes de quaisquer associações serem alteradas.
Machine Translated by Google

Isso é conveniente, pois permite que você use atribuição múltipla para trocar as
ligações de duas variáveis.
Por exemplo, o código

x, y = 2, 3
x, y = y, x print('x
=', x) print('y =', y)

vai imprimir
x=3
y=2

2.3 Programas de ramificação

Os tipos de cálculos que examinamos até agora são chamados de programas em


linha reta. Eles executam uma instrução após a outra na ordem em que aparecem e
param quando ficam sem instruções. Os tipos de cálculos que podemos descrever com
programas de linha reta não são muito interessantes. Na verdade, eles são
absolutamente chatos.

Programas de ramificação são mais interessantes. A instrução de ramificação


mais simples é uma condicional. Conforme mostrado na caixa da Figura 2-5, uma
declaração condicional tem três partes:

Um teste, ou seja, uma expressão avaliada como Verdadeiro ou Falso


Um bloco de código que é executado se o teste for avaliado como True

Um bloco de código opcional que é executado se o teste for avaliado como


Falso

Depois que uma instrução condicional é executada, a execução é retomada em


o código que segue a instrução.
Machine Translated by Google

Figura 2-5 Fluxograma para declaração condicional

Em Python, uma instrução condicional tem a forma

if Expressão booleana: bloco de código if Expressão booleana: bloco de código


ou
outro:
bloco de código

Ao descrever a forma das instruções Python, usamos itálico para identificar


os tipos de código que podem ocorrer naquele ponto de um programa.
Por exemplo, a expressão booleana indica que qualquer expressão avaliada
como True ou False pode seguir a palavra reservada if, e o bloco de código
indica que qualquer sequência de instruções Python pode seguir else:.

Considere o seguinte programa que imprime “Par” se o valor da variável x


for par e “Ímpar” caso contrário:

se x%2 == 0:
print('Par') senão:

print('Ímpar')
print('Feito com condicional')

A expressão x%2 == 0 é avaliada como True quando o resto de x dividido por 2


é 0 e False caso contrário. Lembre-se de que == é usado para comparação,
pois = é reservado para atribuição.
A indentação é semanticamente significativa em Python. Por exemplo, se
a última instrução no código acima for recuada,
Machine Translated by Google

seria parte do bloco de código associado ao else, em vez do bloco de código


que segue a instrução condicional.
Python é incomum em usar indentação dessa maneira. A maioria das
outras linguagens de programação usa símbolos de colchetes para delinear
blocos de código, por exemplo, C inclui blocos entre colchetes, { }. Uma
vantagem da abordagem Python é que ela garante que a estrutura visual de
um programa seja uma representação precisa de sua estrutura semântica.
Como a indentação é semanticamente importante, a noção de linha também
é importante. Uma linha de código muito longa para ser lida facilmente pode
ser dividida em várias linhas na tela terminando cada linha na tela, exceto a
última, com uma barra invertida (\). Por exemplo,
x = 111111111111111111111111111111111 +
222222222222333222222222 +\
3333333333333333333333333333333

Linhas longas também podem ser agrupadas usando a continuação de


linha implícita do Python. Isso é feito com colchetes, ou seja, parênteses,
colchetes e colchetes. Por exemplo,
x = 111111111111111111111111111111111 +
222222222222333222222222 +
333333333333333333333333333333

é interpretado como duas linhas (e, portanto, produz um erro de sintaxe de


"recuo inesperado", enquanto

x = (111111111111111111111111111111111 +
222222222222333222222222 +
3333333333333333333333333333333)

é interpretado como uma única linha por causa dos parênteses. Muitos
programadores Python preferem usar continuações de linha implícitas a usar
uma barra invertida. Mais comumente, os programadores quebram linhas
longas em vírgulas ou operadores.
Voltando aos condicionais, quando o bloco verdadeiro ou o bloco falso
de um condicional contém outro condicional, diz-se que as instruções
condicionais estão aninhadas. O código a seguir contém condicionais
aninhadas em ambas as ramificações da instrução if de nível superior .
se x%2 == 0:
se x%3 == 0:
print('Divisivel por 2 e 3')
Machine Translated by Google

outro:
print('Divisível por 2 e não por 3')
elif x%3 == 0:
print('Divisível por 3 e não por 2')

O elif no código acima significa “else if”.


Muitas vezes é conveniente usar uma expressão booleana composta no
teste de uma condicional, por exemplo,
if x < y e x < z: print('x é o
menor') elif y < z: print('y é o
menor') else:

print('z é o menor')

Exercício de dedo: Escreva um programa que examina três variáveis — e z — e


x, e, imprime o maior número ímpar entre elas. Se nenhum
deles são ímpares, deve imprimir o menor valor dos três.

Você pode atacar este exercício de várias maneiras. Há oito casos separados
a serem considerados: todos são ímpares (um caso), exatamente dois deles são
ímpares (três casos), exatamente um deles é ímpar (três casos) ou nenhum deles
é ímpar (um caso). Portanto, uma solução simples envolveria uma sequência de
oito instruções if , cada uma com uma única instrução print :

se x%2 != 0 e y%2 != 0 e z%2 != 0: print(max(x, y, z)) se


x%2 != 0 e y%2 != 0 e z% 2
== 0: imprimir(max(x, y)) se x%2 != 0 e y%2 == 0 e z%2 !
= 0: imprimir(max(x, z))
se x%2 = = 0 e y%2 != 0 e z%2 != 0: print(max(y, z)) se
x%2 != 0 e y%2 == 0 e
z%2 == 0: print( x) se x%2 == 0 e y%2 != 0 e z%2 == 0:
print(y) se x%2 == 0 e
y%2 == 0 e z%2 != 0: print(z) se x%2 == 0 e y%2 == 0 e
z%2 == 0:
print(min(x, y, z))
Machine Translated by Google

Isso faz o trabalho, mas é bastante complicado. Não são apenas 16 linhas
de código, mas as variáveis são repetidamente testadas quanto à estranheza.
O código a seguir é mais elegante e mais eficiente:
resposta = min(x, y, z) se x%2 !=
0:
resposta = x

se y%2 != 0 e y > resposta: resposta = y se


z%2 != 0 ez >
resposta:
resposta = z

imprimir(resposta)

O código é baseado em um paradigma de programação comum. Ele


começa atribuindo um valor provisório a uma variável (resposta), atualizando-
o quando apropriado e, em seguida, imprimindo o valor final da variável.
Observe que ele testa se cada variável é ímpar exatamente uma vez e contém
apenas uma única instrução de impressão. Esse código é o melhor que
podemos fazer, pois qualquer programa correto deve verificar cada variável
quanto à estranheza e comparar os valores das variáveis ímpares para
encontrar o maior deles.
Python suporta expressões condicionais , bem como
declarações condicionais. Expressões condicionais são da forma
expr1 se condição senão expr2

Se a condição for avaliada como True, o valor da expressão inteira é


expr1; caso contrário, é expr2. Por exemplo, a declaração
x = y se y > z senão z

define x ao máximo de e z. e Uma expressão condicional pode


aparecer em qualquer lugar em que uma expressão comum possa aparecer,
inclusive dentro de expressões condicionais. Assim, por exemplo,

print((x se x > z senão z) se x > y senão (y se y > z senão z))

imprime o máximo de x, e, e z.
Os condicionais nos permitem escrever programas que são mais interessantes do
que os programas de linha reta, mas a classe de programas de ramificação ainda é
bastante limitada. Uma maneira de pensar sobre o poder de uma classe de programas
é em termos de quanto tempo eles podem levar para serem executados. Suponha que
cada linha de código leve uma unidade de tempo para ser executada. Se uma linha reta
Machine Translated by Google

programa tiver n linhas de código, levará n unidades de tempo para ser executado. E
quanto a um programa de ramificação com n linhas de código? Pode levar menos de n
unidades de tempo para ser executado, mas não pode demorar mais, pois cada linha de
código é executada no máximo uma vez.
Diz-se que um programa para o qual o tempo máximo de execução é limitado pela
duração do programa é executado em tempo constante. Isso não significa que cada
vez que o programa é executado, ele executa o mesmo número de etapas. Isso significa
que existe uma constante, k, de modo que o programa não levará mais do que k passos
para ser executado. Isso implica que o tempo de execução não cresce com o tamanho
da entrada do programa.

Os programas de tempo constante são limitados no que podem fazer.


Considere escrever um programa para contar os votos em uma eleição. Seria realmente
surpreendente se alguém pudesse escrever um programa que pudesse fazer isso em
um tempo que fosse independente do número de votos expressos. Na verdade, é
impossível fazê-lo. O estudo da dificuldade intrínseca dos problemas é o tema da
complexidade computacional. Voltaremos a esse tópico várias vezes neste livro.

Felizmente, precisamos apenas de mais uma construção de linguagem de


programação, a iteração, para nos permitir escrever programas de complexidade
arbitrária. Chegamos a isso na Seção 2.5.

2.4 Strings e Entrada

Objetos do tipo str são usados para representar caracteres.16 Literais do tipo str podem
ser escritos usando aspas simples ou duplas, por exemplo, 'abc' ou "abc". O literal '123'
denota uma string de três caracteres, não o número 123.

Tente digitar as seguintes expressões no interpretador Python.

'a'
3*4
3*'a'
3+4
'um'+'um'

Diz-se que o operador + está sobrecarregado porque tem significados diferentes,


dependendo dos tipos de objetos aos quais é aplicado. Por exemplo, o operador +
significa adição quando aplicado
Machine Translated by Google

a dois números e concatenação quando aplicada a duas strings. O operador * também


está sobrecarregado. Significa o que você espera que signifique quando seus
operandos são ambos números. Quando aplicado a um int e um str, é um operador
de repetição — a expressão n*s, onde n é um int e s é um str, resulta em um str com
n repetições de s. Por exemplo, a expressão 2*'John' tem o valor 'JohnJohn'. Existe
uma lógica nisso. Assim como a expressão matemática 3*2 equivale a 2+2+2, a
expressão 3*'a' equivale a 'a'+'a'+'a'.

Agora tente digitar

new_id
'a'*'a'

Cada uma dessas linhas gera uma mensagem de erro. A primeira linha produz a
mensagem

NameError: o nome 'new_id' não está definido

Como new_id não é um literal de nenhum tipo, o interpretador o trata como um nome.
No entanto, como esse nome não está vinculado a nenhum objeto, tentar usá-lo
causa um erro de tempo de execução. O código 'a'*'a' produz a mensagem de erro

TypeError: não é possível multiplicar a sequência por não int do tipo 'str'

Essa verificação de tipo existe é uma coisa boa. Ele transforma erros
descuidados (e às vezes sutis) em erros que interrompem a execução, em vez de
erros que levam os programas a se comportarem de maneiras misteriosas. A
verificação de tipo em Python não é tão forte quanto em algumas outras linguagens
de programação (por exemplo, Java), mas é melhor em Python 3 do que em Python
2. Por exemplo, está claro o que < deve significar quando é usado para comparar
dois strings ou dois números. Mas qual deve ser o valor de '4' < 3 ? De forma bastante
arbitrária, os projetistas do Python 2 decidiram que deveria ser False, porque todos
os valores numéricos deveriam ser menores que todos os valores do tipo str. Os
projetistas do Python 3 e da maioria das outras linguagens modernas decidiram que,
como essas expressões não têm um significado óbvio, elas deveriam gerar uma
mensagem de erro.
Strings são um dos vários tipos de sequência em Python. Eles compartilham as
seguintes operações com todos os tipos de sequência.
Machine Translated by Google

O comprimento de uma string pode ser encontrado usando a função len .


Por exemplo, o valor de len('abc') é 3.
A indexação pode ser usada para extrair caracteres individuais de uma
string. Em Python, toda indexação é baseada em zero. Por exemplo, digitar
'abc'[0] no interpretador fará com que ele exiba a string 'a'.
Digitar 'abc'[3] produzirá a mensagem de erro IndexError: Desde que o
índice de string fora do intervalo. Python usa 0 para indicar o
primeiro elemento de uma string, o último elemento de uma string de
comprimento 3 é acessado usando o índice 2. Números negativos são
usados para indexar a partir do final de uma string. Por exemplo, o valor de 'abc'[ÿ1] é
'c'.

O fatiamento é usado para extrair substrings de comprimento arbitrário. Se


s for uma string, a expressão s[start:end] denota a substring de s que
começa no início do índice e termina no final do índice-1. Por exemplo,
'abc'[1:3] resulta em 'bc'. Por que termina no índice end-1 em vez de
terminar? Assim, expressões como 'abc'[0:len('abc')] têm o valor esperado.
Se o valor antes dos dois pontos for omitido, o padrão será 0. Se o valor
após os dois pontos for omitido, o padrão será o comprimento da string.
Conseqüentemente, a expressão 'abc'[:] é semanticamente equivalente à
mais detalhada 'abc'[0:len('abc')]. Também é possível fornecer
um terceiro argumento para selecionar uma fatia não contígua de
uma string. Por exemplo, o valor da expressão '123456789'[0:8:2]
é a string '1357'.

Geralmente é conveniente converter objetos de outros tipos em strings


usando a função str . Considere, por exemplo, o código

num = 30000000 fração


= 1/2 print(num*fração,
'é', fração*100, '%', 'de', num) print(num*fração, 'é', str(fração*100) + ' %', 'de', num)

que imprime

15000000,0 é 50,0% de 30000000 15000000,0 é 50,0%


de 30000000
Machine Translated by Google

A primeira instrução print insere um espaço entre 50 e % porque o Python insere


automaticamente um espaço entre os argumentos a serem impressos. A
segunda instrução print produz uma saída mais apropriada combinando 50 e %
em um único argumento do tipo
estr.
As conversões de tipo (também chamadas de conversões de tipo) são
usadas com frequência no código Python. Usamos o nome de um tipo para
converter valores para esse tipo. Então, por exemplo, o valor de int('3')*4 é 12.
Quando um float é convertido em um int, o número é truncado (não arredondado),
por exemplo, o valor de int(3.9) é o int 3 .
Voltando à saída de nossas declarações de impressão, você pode estar se
perguntando sobre aquele .0 no final do primeiro número impresso. Isso aparece
porque 1/2 é um número de ponto flutuante e o produto de um int e um float é
um float. Isso pode ser evitado convertendo num*fraction em um int. O código

print(int(num*fração), 'é', str(fração*100) + '%', 'de', num)

imprime 15000000 é 50,0% de 30000000.


O Python 3.6 introduziu uma maneira alternativa e mais compacta de criar
expressões de string. Uma string f consiste no caractere f (ou F) seguido por
um tipo especial de literal de string chamado literal de string formatado. Os
literais de cadeia de caracteres formatados contêm sequências de caracteres
(como outros literais de cadeia de caracteres) e expressões entre colchetes.
Essas expressões são avaliadas em tempo de execução e convertidas
automaticamente em strings. O código
print(f'{int(num*fração)} é {fração*100}% de {num}')

produz a mesma saída que a instrução print anterior, mais detalhada. Se você
quiser incluir uma chave na string denotada por uma string f, use duas chaves.
Por exemplo, print(f'{{{3*5}}}') imprime {15}.
A expressão dentro de uma string f pode conter modificadores que controlam
a aparência da string de saída.17 Esses modificadores são separados da
expressão que indica o valor a ser modificado por dois pontos. Por exemplo, a
string f f'{3.14159:.2f}' é avaliada como a string '3.14' porque o modificador .2f
instrui o Python a truncar a representação de string de um número de ponto
flutuante para dois dígitos após o ponto decimal. E a declaração
Machine Translated by Google

print(f'{num*fração:,.0f} é {fração*100}% de {num:,}')

imprime 15.000.000 é 50,0% de 30.000.000 porque o modificador instrui,o Python a


usar vírgulas como separadores de milhar. Apresentaremos outros modificadores
convenientes posteriormente neste livro.

2.4.1 Entrada
Python 3 tem uma função, entrada, que pode ser usada para obter entrada
diretamente de um usuário. A função de entrada recebe uma string como argumento
e a exibe como um prompt no shell. A função então espera que o usuário digite algo
e pressione a tecla Enter . A linha digitada pelo usuário é tratada como uma string e
se torna o valor retornado pela função.

Executando o código name = input('Enter your name: ') exibirá a linha

Digite seu nome:

na janela do console. Se você digitar George Washington e pressionar enter, a string


'George Washington' será atribuída à variável Se você executar print('Are you really',
nome. name, '?'), o
linha

Você é realmente George Washington?

seria exibido. Observe que a instrução print introduz um espaço antes do “?”. Ele faz
isso porque quando print recebe vários argumentos, ele coloca um espaço entre os
valores associados aos argumentos. O espaço pode ser evitado executando + name
+ '?') ou print(f'Are you really
print('Você é mesmo '
{name}?'), cada um dos quais produz uma única string e passa essa string como o único argumento a
ser impresso.
Agora considere o código

n = input('Digite um int: ') print(type(n))

Este código sempre será impresso


<class 'str'>
Machine Translated by Google

porque input sempre retorna um objeto do tipo str, mesmo que o usuário tenha
digitado algo que se pareça com um número inteiro. Por exemplo, se o usuário
tivesse inserido 3, n seria vinculado ao str '3' e não ao int 3. Portanto, o valor da
expressão n*4 seria '3333' em vez de 12. A boa notícia é que sempre que uma
string é um literal válido de algum tipo, uma conversão de tipo pode ser aplicada a
ela.

Exercício de dedo: Escreva um código que peça ao usuário para inserir seu
aniversário no formato mm/dd/aaaa e, em seguida, imprima uma string no formato
'Você nasceu no ano aaaa'.

2.4.2 Uma digressão sobre codificação de caracteres Por


muitos anos, a maioria das linguagens de programação usou um padrão chamado
ASCII para a representação interna de caracteres. Esse padrão incluía 128
caracteres, o suficiente para representar o conjunto usual de caracteres que
aparecem no texto em inglês — mas não o suficiente para abranger os caracteres
e acentos que aparecem em todos os idiomas do mundo.

O padrão Unicode é um sistema de codificação de caracteres projetado para


suportar o processamento digital e exibição de textos escritos de todos os idiomas.
O padrão contém mais de 120.000 caracteres, abrangendo 129 scripts modernos e
históricos e vários conjuntos de símbolos.
O padrão Unicode pode ser implementado usando diferentes codificações internas
de caracteres. Você pode dizer ao Python qual codificação usar inserindo um
comentário do formulário
-*-
# -*- codificação: nome da codificação

como a primeira ou segunda linha do seu programa. Por exemplo,

# -*- codificação: utf-8 -*-

instrui o Python a usar UTF-8, a codificação de caracteres usada com mais


frequência para páginas da Web.18 Se você não tiver esse comentário em seu
programa, a maioria das implementações do Python será padronizada para UTF-8.
Ao usar UTF-8, você pode, se o editor de texto permitir, inserir diretamente o
código como

print('Mluvíš anglicky?') print('Do you


speak English?')
Machine Translated by Google

que vai imprimir


Você fala inglês?
você fala inglês

Você deve estar se perguntando como consegui digitar a string 'ÿ ÿÿ ÿÿÿÿÿÿ
ÿÿÿÿÿÿ?'. Eu não. Como a maior parte da web usa UTF-8, consegui cortar
a string de uma página da web e colá-la diretamente em meu programa.
Existem maneiras de inserir caracteres Unicode diretamente de um teclado,
mas, a menos que você tenha um teclado especial, todas elas são bastante
complicadas.

2.5 Loops while

Perto do final da Seção 2.3, mencionamos que a maioria das tarefas


computacionais não pode ser realizada usando programas de ramificação.
Considere, por exemplo, escrever um programa que pede o número de
X's. Você pode pensar em escrever algo como
num_x = int(input('Quantas vezes devo imprimir a letra X? ')) to_print = if num_x
== 1:
''
to_print = 'X'
elif num_x == 2:
to_print = 'XX' elif
num_x == 3: to_print
= 'XXX'

#… print(to_print)

Mas rapidamente se tornaria aparente que você precisaria de tantos


condicionais quantos inteiros positivos - e há um número infinito deles. O
que você deseja escrever é um programa parecido com (o seguinte é
pseudocódigo, não Python)
num_x = int(input('Quantas vezes devo imprimir a letra X? ')) to_print = #
''
concatenar
X para to_print num_x vezes print(to_print)
Machine Translated by Google

Quando queremos que um programa faça a mesma coisa várias vezes,


podemos usar a iteração. Um mecanismo genérico de iteração (também
chamado de looping) é mostrado na caixa da Figura 2-6. Como uma
declaração condicional, começa com um teste. Se o teste for avaliado
como True, o programa executará o corpo do loop uma vez e voltará para
reavaliar o teste. Esse processo é repetido até que o teste seja avaliado
como Falso, após o que o controle passa para o código que segue a
instrução de iteração.

Figura 2-6 Fluxograma para iteração

Podemos escrever o tipo de loop representado na Figura 2-6 usando


uma instrução while . Considere o código na Figura 2-7.

Figura 2-7 Quadrando um número inteiro, da maneira mais difícil


Machine Translated by Google

O código começa vinculando a variável x ao inteiro 3. Em seguida, procede ao


quadrado de x usando adição repetitiva. A tabela na Figura 2-8 mostra o valor associado
a cada variável cada vez que o teste no início do loop é alcançado. Construímos a tabela
manualmente simulando o código, ou seja, fingimos ser um interpretador Python e
executamos o programa usando lápis e papel. Usar lápis e papel pode parecer estranho,
mas é uma excelente maneira de entender como um programa se comporta.19

Figura 2-8 Simulação manual de um pequeno programa

Na quarta vez que o teste é alcançado, ele é avaliado como Falso e o fluxo de
controle prossegue para a instrução de impressão após o loop. Para quais valores de x
esse programa terminará? Há três casos a serem considerados: x == 0, x > 0 e x < 0.

Suponha que x == 0. O valor inicial de num_iterations também será 0 e o corpo do


loop nunca será executado.
Suponha que x > 0. O valor inicial de num_iterations será menor que e o corpo do
loop será
x, executado pelo menos uma vez. Cada vez que o corpo do loop é executado, o
valor de num_iterations aumenta exatamente em 1. Isso significa que, como num_iterations
começou antes de um número finito de iterações do loop, num_iterations será igual a x.
x, Nesse ponto, o teste de loop é avaliado como Falso e o controle prossegue para o
código que segue a instrução while .

Suponha que x < 0. Algo muito ruim acontece. O controle entrará no loop e cada
iteração moverá num_iterations para mais longe de x do que para mais perto dele. O
programa, portanto, continuará executando o loop indefinidamente (ou até que algo ruim
ocorra, por exemplo, um erro de estouro). Como poderíamos remover essa falha no
Machine Translated by Google

programa? Alterar o teste para num_iterations < abs(x) quase funciona. O loop
termina, mas imprime um valor negativo. Se a instrução de atribuição dentro do
loop também for alterada, para ans = ans + abs(x), o código funcionará corretamente.

Exercício de dedo: Substitua o comentário no código a seguir por um loop while .

num_x = int(input('Quantas vezes devo imprimir a letra X? ')) to_print = #concatenar X


para
''
to_print num_x
vezes print(to_print)

Às vezes é conveniente sair de um loop sem testar a condição do loop. A


execução de uma instrução break encerra o loop no qual ela está contida e
transfere o controle para o código imediatamente após o loop. Por exemplo, o código

#Encontre um inteiro positivo divisível por 11 e 12 x = 1

enquanto verdadeiro:
se x%11 == 0 e x%12 == 0:
quebrar
x=x+1
print(x, 'é divisível por 11 e 12')

estampas

132 é divisível por 11 e 12

Se uma instrução break for executada dentro de um loop aninhado (um loop
dentro de outro loop), a quebra encerrará o loop interno.

Exercício de dedo: Escreva um programa que peça ao usuário para inserir 10


números inteiros e, em seguida, imprima o maior número ímpar que foi inserido. Se
nenhum número ímpar foi inserido, ele deve imprimir uma mensagem para esse efeito.

2.6 Para loops e alcance

Os loops while que usamos até agora são altamente estilizados, muitas vezes
iterando sobre uma sequência de inteiros. Python fornece uma linguagem
Machine Translated by Google

mecanismo, o loop for , que pode ser usado para simplificar programas contendo
esse tipo de iteração.
A forma geral de uma instrução for é (lembre-se de que as palavras em
itálico são descrições do que pode aparecer, não código real):
para variável em sequência: bloco de
código

A variável seguinte for é vinculada ao primeiro valor na sequência e o bloco de


código é executado. A variável recebe então o segundo valor na sequência e o
bloco de código é executado novamente.
O processo continua até que a sequência se esgote ou uma instrução break seja
executada dentro do bloco de código. Por exemplo, o código
total = 0 para
num in (77, 11, 3): total = total +
num print(total)

imprimirá 91. A expressão (77, 11, 3) é uma tupla. Discutimos as tuplas em detalhes
na Seção 5. Por enquanto, pense apenas em uma tupla como uma sequência de
valores.
A sequência de valores vinculados à variável é mais comumente gerada
usando a função interna que retorna uma sérierecebe de intervalos inteiros. A função
três argumentos inteiros:
faixa start, stop e step. Produz a progressão start, start + step,
start + 2*step, etc. Se step for positivo, o último elemento é o maior inteiro tal que
(start + i*step) é estritamente menor que stop. Se step for negativo, o último
elemento é o menor inteiro tal que (start + i*step) é maior que stop. Por exemplo, a
expressão range(5, 40, 10) produz a sequência 5, 15, 25, 35, e a expressão
range(40, 5, -10) produz a sequência 40, 30, 20, 10.

Se o primeiro argumento para range for omitido, o padrão será 0, e se o último


argumento (o tamanho do passo) for omitido, o padrão será 1. Por exemplo, range(0,
3) e range(3) ambos produzem a sequência 0, 1, 2. Os números na progressão são
gerados “conforme necessário”, portanto, mesmo expressões como range(1000000)
consomem pouca memória. Discutiremos com mais profundidade na Seção 5.2.
faixa
Considere o código
Machine Translated by Google

x=4
para i no intervalo(x): print(i)

imprime
0
1
2
3

O código da Figura 2-9 reimplementa o algoritmo da Figura 2-7 para


elevar ao quadrado um inteiro (corrigido para que funcione com números
negativos). Observe que, ao contrário da implementação do loop while ,
o número de iterações não é controlado por um teste explícito e a
variável de índice num_iterations não é incrementada explicitamente.

Figura 2-9 Usando uma instrução for

Observe que o código na Figura 2-9 não altera o valor de


num_iterations dentro do corpo do loop for . Isso é típico, mas não
necessário, o que levanta a questão do que acontece se a variável de
índice for modificada dentro do loop for . Considerar
para i no intervalo(2): print(i)
i = 0 print(i)

Você acha que ele imprimirá 0, 0, 1, 0 e depois parará? Ou faça


você acha que vai imprimir 0 repetidamente?
A resposta é 0, 0, 1, 0. Antes da primeira iteração do loop for , a
função é avaliada
faixa e o primeiro valor na sequência que ela produz é
atribuído à variável de índice, i. No começo de
Machine Translated by Google

a cada iteração subsequente do loop, i recebe o próximo valor na sequência. Quando


a sequência é esgotada, o loop termina.
O loop for acima é equivalente ao código

índice = 0
last_index = 1 while
index <= last_index: i = index print(i) i =
0 print(i) index
= index + 1

Observe, a propósito, que o código com o loop while é consideravelmente mais


complicado do que o loop for . O loop for é um mecanismo linguístico conveniente.

Agora, o que você acha


x=1
para i no intervalo(x):
imprimir(i)
x=4

impressões? Apenas 0, porque os argumentos para a função faixana linha com for são
avaliados logo antes da primeira iteração do loop e não são reavaliados nas iterações
subsequentes.
Agora, vamos ver com que frequência as coisas são avaliadas quando aninhamos loops.
Considerar

x=4
para j no intervalo (x): para
i no intervalo (x): x = 2

Quantas vezes cada um dos dois loops é executado? Já vimos que o range(x) que
controla o loop externo é avaliado na primeira vez que é atingido e não reavaliado a
cada iteração, portanto, são quatro iterações do loop externo. Isso implica que o loop
for interno é alcançado quatro vezes. A primeira vez que é atingida, a variável x = 4,
então haverá quatro iterações. No entanto, nas próximas três vezes que for atingido,
x = 2, haverá duas iterações de cada vez.

Conseqüentemente, se você executar


Machine Translated by Google

x=3
for j in range(x):
print('Iteração do loop externo') for i in range(x):
print(' x = 2
Iteração do loop interno')

ele imprime

Iteração do loop externo


Iteração do loop interno
Iteração do loop interno
Iteração do loop interno
Iteração do loop externo
Iteração do loop interno
Iteração do loop interno
Iteração do loop externo
Iteração do loop interno
Iteração do loop interno

A instrução for pode ser usada em conjunto com o operador in para iterar
convenientemente os caracteres de uma string. Por exemplo,

total = 0 para
c em '12345678': total = total
+ int(c) print(total)

soma os dígitos na string denotada pelo literal '12345678' e imprime o total.

Exercício de dedo: Escreva um programa que imprima a soma dos números


primos maiores que 2 e menores que 1000. Dica: você provavelmente deseja
usar um loop for que é um teste de primalidade aninhado dentro de um loop
for que itera sobre os inteiros ímpares entre 3 e 999.

2.7 Questões de Estilo

Grande parte deste livro é dedicada a ajudá-lo a aprender uma linguagem de


programação. Mas saber um idioma e saber usar bem um idioma são duas
coisas diferentes. Considere os dois seguintes
frases:
Machine Translated by Google

“Todo mundo sabe que se um homem é solteiro e tem muitos


dinheiro, ele precisa se casar.
“É uma verdade universalmente reconhecida que um único homem em
possuidor de uma boa fortuna, deve estar precisando de uma esposa.”20
Cada uma é uma sentença adequada em inglês e cada uma significa
aproximadamente a mesma coisa. Mas eles não são igualmente atraentes e talvez
não sejam igualmente fáceis de entender. Assim como o estilo é importante ao
escrever em inglês, o estilo é importante ao escrever em Python.
No entanto, embora ter uma voz distinta possa ser um trunfo para um romancista,
não é um trunfo para um programador. Quanto menos tempo os leitores de um
programa tiverem para pensar em coisas irrelevantes para o significado do código,
melhor. É por isso que bons programadores seguem convenções de codificação
projetadas para tornar os programas fáceis de entender, em vez de divertidos de ler.

A maioria dos programadores Python segue as convenções estabelecidas no


guia de estilo PEP 8. 21 Como todos os conjuntos de convenções, algumas de suas
prescrições são arbitrárias. Por exemplo, ele prescreve o uso de quatro espaços para
recuos. Por que quatro espaços e não três ou cinco? Nenhuma razão particularmente
boa. Mas se todos usarem o mesmo número de espaços, é mais fácil ler (e talvez
combinar) códigos escritos por pessoas diferentes. De modo mais geral, se todos
usarem o mesmo conjunto de convenções ao escrever Python, os leitores poderão
se concentrar em entender a semântica do código em vez de desperdiçar ciclos
mentais assimilando decisões estilísticas.

As convenções mais importantes têm a ver com a nomenclatura. Já discutimos


a importância de usar nomes de variáveis que transmitam o significado da variável.
Frases nominais funcionam bem para isso.
Por exemplo, usamos o nome num_iterations para uma variável que denota o número
de iterações. Quando um nome inclui várias palavras, a convenção em Python é usar
um sublinhado (_) para separar as palavras. Novamente, esta convenção é arbitrária.
Alguns programadores preferem usar o que geralmente é chamado de camelCase,
por exemplo, numIterations — argumentando que é mais rápido digitar e usa menos
espaço.
Existem também algumas convenções para nomes de variáveis de um único
caractere. O mais importante é evitar o uso de L minúsculo ou I maiúsculo (que são
facilmente confundidos com o número um) ou O maiúsculo (que é facilmente
confundido com o número zero).
Machine Translated by Google

Chega de convenções por enquanto. Voltaremos ao tópico conforme


apresentamos vários aspectos do Python.
Agora cobrimos praticamente tudo sobre Python que você precisa saber para
começar a escrever programas interessantes que lidam com números e strings. No
próximo capítulo, faremos uma pequena pausa no aprendizado de Python e
usaremos o que você já aprendeu para resolver alguns problemas simples.

2.8 Termos Introduzidos no Capítulo

linguagem de baixo
nível linguagem de alto
nível linguagem
interpretada linguagem
compilada código-fonte
Código da máquina

Ambiente
de desenvolvimento integrado Python (IDE)
anaconda

Spyder
shell do console
IPython

programa (script)
comando (declaração)

objeto
tipo
objeto escalar
objeto não escalar
literal
Machine Translated by Google

bool de ponto
flutuante

Nenhum

valor da

expressão do
operador

variável de
prompt do shell

palavra

reservada de
atribuição vinculativa

comentário (no código)

programa de linha reta

programa de ramificação
condicional

indentação (em Python)


instrução aninhada

expressão composta
tempo constante

complexidade computacional

strings de expressão

condicional sobrecarregado

operador repetição tipo

de operador

verificação

indexação corte
Machine Translated by Google

conversão de tipo (casting)

entrada de expressão de string

formatada
Unicode

pseudocódigo de

iteração

(looping)
durante a simulação de loop manual

quebrar

for loop

tupla

faixa

no operador

PEP 8 guia de estilo

9 Alegadamente, o nome Python foi escolhido como uma homenagem à trupe de


comédia britânica Monty Python. Isso leva a pensar que o nome IDLE é um
trocadilho com Eric Idle, um membro da trupe.

10 Como o Anaconda é atualizado com frequência, quando você ler isto, a aparência
da janela pode ter mudado.

11 Se você não gostar da aparência da janela do Spyder , clique na chave inglesa


na barra de ferramentas para abrir a janela Preferences e altere as configurações
como achar melhor.

12 Funções são discutidas na Seção 4.

13 Sim, os átomos não são verdadeiramente indivisíveis. No entanto, separá-los não


é fácil e isso pode ter consequências nem sempre desejáveis.
Machine Translated by Google

14 Se você acredita que o valor real de ÿ não é 3, você está certo. Nós até demonstramos
esse fato na Seção 18.4.

15 “O que há em um nome? Aquilo que chamamos de rosa por qualquer outro nome
cheiraria tão doce.”

16 Ao contrário de muitas linguagens de programação, Python não tem tipo


correspondente a um personagem. Em vez disso, ele usa strings de comprimento 1.

17 Esses modificadores são os mesmos modificadores usados no formato .format


método associado a strings.

18 Em 2016, mais de 85% das páginas da web foram codificadas usando


UTF-8.

19 Também é possível simular manualmente um programa usando caneta e papel,


ou mesmo um editor de texto.

20 Orgulho e Preconceito, Jane Austen.

21 PEP é um acrônimo que significa “Python Enhancement


Proposta." PEP 8 foi escrito em 2001 por Guido van Rossum, Barry Warsaw e
Nick Coghlan.
Machine Translated by Google

ALGUNS PROGRAMAS NUMÉRICOS SIMPLES

Agora que abordamos algumas construções básicas do Python, é hora de


começar a pensar em como podemos combinar essas construções para
escrever programas simples. Ao longo do caminho, introduziremos mais
construções de linguagem e algumas técnicas algorítmicas.

3.1 Enumeração Exaustiva

O código na Figura 3-1 imprime a raiz cúbica inteira, se existir, de um inteiro.


Se a entrada não for um cubo perfeito, imprime uma mensagem nesse
sentido. O operador != significa diferente.

Figura 3-1 Usando enumeração exaustiva para encontrar a raiz cúbica

O código primeiro tenta definir a variável como a raiz cúbica do valor


absoluto de x. Se for bem-sucedido, definirá ans como -ans se
x for negativo. O trabalho pesado (tal como é) neste código é feito no
Machine Translated by Google

loop enquanto . Sempre que um programa contém um loop, é importante entender o


que faz com que o programa saia desse loop. Para quais valores de x esse loop
while terminará? A resposta é “todos os números inteiros”. Isso pode ser argumentado
de forma bastante simples.

O valor da expressão ans**3 começa em 0 e aumenta a cada vez que passa


pelo loop.
Quando atinge ou excede abs(x), o loop termina.

Como abs(x) é sempre positivo, há apenas um número finito de iterações


antes que o loop termine.

Este argumento é baseado na noção de um decremento


função. Esta é uma função que possui as seguintes propriedades:

Ele mapeia um conjunto de variáveis de programa em um número inteiro.

Quando o loop é inserido, seu valor é não negativo.


Quando seu valor for ÿ 0, o loop termina.
Seu valor é diminuído toda vez que passa pelo loop.

Qual é a função de decremento para o loop while na Figura 3-


1? É abs(x) ÿ ans**3.
Agora, vamos inserir alguns erros e ver o que acontece. Primeiro, tente comentar
a declaração ans = 0. O interpretador Python imprime a mensagem de erro

NameError: o nome 'ans' não está definido

porque o intérprete tenta encontrar o valor ao qual ans está vinculado antes de estar
vinculado a qualquer coisa. Agora, restaure a inicialização de ans, substitua a
instrução ans = ans + 1 por e tente encontrar a raiz cúbica de 8. Depois que você
anos = anos, cansar de esperar, digite “control c” (mantenha pressionada a tecla Ctrl
e a tecla c simultaneamente) . Isso o levará de volta ao prompt do usuário no shell.

Agora, adicione a declaração

print('Valor da função decrescente abs(x) - ans**3 is',

abs(x) - ans**3)
Machine Translated by Google

no início do loop e tente executá-lo novamente. desta vez ele vai imprimir

Valor da função decrescente abs(x) - ans**3 é 8

uma e outra vez.


O programa teria executado para sempre porque o corpo do loop não está
mais reduzindo a distância entre ans**3 e abs(x). Quando confrontados com um
programa que parece não estar terminando, os programadores experientes
geralmente inserem instruções de impressão, como esta aqui, para testar se a
função de decremento está realmente sendo decrementada.

A técnica algorítmica usada neste programa é uma variante de adivinhação


e verificação chamada enumeração exaustiva. Enumeramos todas as
possibilidades até chegarmos à resposta certa ou esgotarmos o espaço de
possibilidades. À primeira vista, isso pode parecer uma maneira incrivelmente
estúpida de resolver um problema. Surpreendentemente, no entanto, algoritmos
de enumeração exaustivos costumam ser a maneira mais prática de resolver um
problema. Eles são geralmente fáceis de implementar e fáceis de entender. E,
em muitos casos, eles correm rápido o suficiente para todos os propósitos
práticos. Remova ou comente a instrução print que você inseriu para depuração
e reinsira a instrução ans = ans + 1. Agora tente encontrar a raiz cúbica de
1957816251. O programa terminará quase instantaneamente. Agora, tente
7406961012236344616.
Como você pode ver, mesmo que sejam necessários milhões de suposições,
o tempo de execução geralmente não é um problema. Os computadores
modernos são incrivelmente rápidos. Leva menos de um nanossegundo – um
bilionésimo de segundo – para executar uma instrução. É difícil avaliar o quão
rápido isso é. Para perspectiva, leva um pouco mais de um nanossegundo para
a luz percorrer um único pé (0,3 metros). Outra maneira de pensar sobre isso é
que, no tempo que leva para o som da sua voz percorrer 30 metros, um
computador moderno pode executar milhões de instruções.
Apenas por diversão, tente executar o código

max_val = int(input('Digite um inteiro positivo: '))


eu = 0
enquanto i < max_val:
eu = eu + 1
imprimir(i)
Machine Translated by Google

Veja o tamanho de um número inteiro que você precisa inserir antes que haja uma
pausa perceptível antes que o resultado seja impresso.
Vejamos outro exemplo de enumeração exaustiva: testar se um
inteiro é um número primo e retornar o menor divisor se não for. Um
número primo é um inteiro maior que 1 que é divisível apenas por si
mesmo e por 1. Por exemplo, 2, 3, 5 e 111.119 são primos e 4, 6, 8 e
62.710.561 não são primos.
A maneira mais simples de descobrir se um inteiro, x, maior que 3
é primo, é dividir x por cada inteiro entre 2 e, x-1. Se o resto de
qualquer uma dessas divisões for 0, x não é primo, caso contrário, x
é primo. O código na Figura 3-2 implementa essa abordagem. Ele
primeiro pede ao usuário para inserir um inteiro, converte a string
retornada em um int e atribui esse inteiro à variável x. Em seguida,
ele configura as condições iniciais para uma enumeração exaustiva
inicializando o palpite como 2 e a variável menor_divisor como None
— indicando que, até prova em contrário, o código assume que x é primo.
A enumeração exaustiva é feita dentro de um loop for . O loop
termina quando todos os divisores inteiros possíveis de x foram
tentados ou quando descobriu um inteiro que é um divisor de x.
Depois de sair do loop, o código verifica o valor de small_divisor e
imprime o texto apropriado. O truque de inicializar uma variável antes
de entrar em um loop e verificar se esse valor foi alterado na saída é
comum.

Figura 3-2 Usando enumeração exaustiva para testar a primalidade


Machine Translated by Google

Exercício de dedo: Altere o código na Figura 3-2 para que ele retorne o
maior em vez do menor divisor. Dica: se y*z = x e y é o menor divisor de x,
z é o maior divisor de x.

O código na Figura 3-2 funciona, mas é desnecessariamente ineficiente.


Por exemplo, não há necessidade de verificar números pares além de 2,
pois se um número inteiro for divisível por qualquer número par, ele será
divisível por 2. O código da Figura 3-3 aproveita esse fato testando primeiro
se x é um numero par. Caso contrário, ele usa um loop para testar se x é
divisível por qualquer número ímpar.
Embora o código da Figura 3-3 seja um pouco mais complexo do que o
da Figura 3-2, ele é consideravelmente mais rápido, pois metade dos
números é verificada dentro do loop. A oportunidade de trocar a
complexidade do código pela eficiência do tempo de execução é um
fenômeno comum. Mas mais rápido nem sempre significa melhor. Há muito
a ser dito sobre um código simples que é obviamente correto e rápido o
suficiente para ser útil.

Figura 3-3 Um teste de primalidade mais eficiente

Exercício de dedo: Escreva um programa que peça ao usuário para inserir


um inteiro e imprima dois inteiros, root e tal que 1 < pwr < 6 e root**pwr
pwr,
seja igual ao inteiro digitado pelo usuário. Se tal par de inteiros não existir,
ele deve imprimir uma mensagem para esse efeito.
Machine Translated by Google

Exercício de dedo: Escreva um programa que imprima a soma dos


números primos maiores que 2 e menores que 1000. Dica: você
provavelmente deseja ter um loop que seja um teste de primalidade
aninhado dentro de um loop que itera sobre os inteiros ímpares entre 3 e 999 .
Machine Translated by Google

3.2 Soluções Aproximadas e Busca da Bisseção

Imagine que alguém lhe peça para escrever um programa que imprima a raiz
quadrada de qualquer número não negativo. O que você deveria fazer?
Você provavelmente deve começar dizendo que precisa de uma declaração
de problema melhor. Por exemplo, o que o programa deve fazer se for solicitado
a encontrar a raiz quadrada de 2? A raiz quadrada de 2 não é um número
racional. Isso significa que não há como representar com precisão seu valor
como uma string finita de dígitos (ou como um ponto flutuante), portanto, o
problema declarado inicialmente não pode ser resolvido.
O que um programa pode fazer é encontrar uma aproximação para a raiz
quadrada - ou seja, uma resposta que seja próxima o suficiente da raiz quadrada
real para ser útil. Voltaremos a essa questão em detalhes consideráveis mais
adiante neste livro. Mas, por enquanto, vamos pensar em “próximo o suficiente”
como uma resposta que está dentro de alguma constante, chame-a de epsilon, do real
responder.

O código na Figura 3-4 implementa um algoritmo que imprime uma


aproximação da raiz quadrada de x.

Figura 3-4 Aproximando a raiz quadrada usando enumeração exaustiva

Mais uma vez, estamos usando enumeração exaustiva. Observe que esse
método para encontrar a raiz quadrada não tem nada em comum com a maneira
de encontrar raízes quadradas usando um lápis que você pode ter aprendido no
ensino médio. Muitas vezes, a melhor maneira de
Machine Translated by Google

resolver um problema com um computador é bem diferente de como alguém abordaria o


problema manualmente.
Se x for 25, o código será impresso

número de palpites = 49990


4,999000000001688 está próximo da raiz quadrada de 25

Devemos ficar desapontados porque o programa não descobriu que 25 é um quadrado


perfeito e imprimiu 5? Não. O programa fez o que pretendia fazer. Embora fosse bom
imprimir 5, isso não é melhor do que imprimir qualquer valor próximo o suficiente de 5.

O que você acha que acontecerá se definirmos x = 0,25? Encontrará uma raiz próxima
de 0,5? Não. Infelizmente, ele irá relatar

número de palpites = 2501 Falha na


raiz quadrada de 0,25

A enumeração exaustiva é uma técnica de pesquisa que funciona apenas se o conjunto


de valores pesquisados incluir a resposta. Neste caso, estamos enumerando os valores
entre 0 e o valor de x. Quando x está entre 0 e 1, a raiz quadrada de x não está neste
intervalo. Uma maneira de corrigir isso é alterar o segundo operando de e na primeira linha
do loop while para obter

enquanto abs(ans**2 - x) >= epsilon e ans*ans <= x:

Quando executamos nosso código após essa alteração, ele informa que

0,48989999999996237 está próximo da raiz quadrada de 0,25

Agora, vamos pensar em quanto tempo o programa levará para ser executado. O
número de iterações depende de quão perto a resposta está do nosso ponto de partida, 0,
e do tamanho das etapas. Grosso modo, o programa executará o loop while no máximo
vezes x/passo .
Vamos tentar o código em algo maior, por exemplo, x = 123456. Ele será executado
por um bom tempo e depois será impresso

número de palpites = 3513631


Falha na raiz quadrada de 123456

O que você acha que aconteceu? Certamente existe um número de ponto flutuante
que aproxima a raiz quadrada de 123456 com precisão de 0,01.
Machine Translated by Google

Por que nosso programa não o encontrou? O problema é que o tamanho do


nosso passo era muito grande e o programa pulou todas as respostas adequadas.
Mais uma vez, estamos buscando exaustivamente um espaço que não contém
solução. Tente igualar o passo a epsilon**3 e executar o programa.
Eventualmente, ele encontrará uma resposta adequada, mas você pode não
ter paciência para esperar que isso aconteça.
Aproximadamente quantos palpites ele terá que fazer? O tamanho do
passo será 0,000001 e a raiz quadrada de 123456 é de cerca de 351,36. Isso
significa que o programa terá que fazer cerca de 351 milhões de suposições
para encontrar uma resposta satisfatória. Poderíamos tentar acelerá-lo
começando mais perto da resposta, mas isso pressupõe que conhecemos a
vizinhança da resposta.
Chegou a hora de buscar uma forma diferente de atacar o problema.
Precisamos escolher um algoritmo melhor em vez de ajustar o atual. Mas antes
de fazer isso, vejamos um problema que, à primeira vista, parece ser
completamente diferente da descoberta da raiz.
Considere o problema de descobrir se uma palavra que começa com uma
determinada sequência de letras aparece em um dicionário impresso22 da
língua inglesa. Uma enumeração exaustiva funcionaria, em princípio. Você
poderia começar na primeira palavra e examinar cada palavra até encontrar
uma palavra começando com a sequência de letras ou ficar sem palavras para
examinar. Se o dicionário contivesse n palavras, levaria, em média, n/2
tentativas para encontrar a palavra. Se a palavra não estivesse no dicionário,
seriam necessárias n sondagens. Claro, aqueles que tiveram o prazer de
procurar uma palavra em um dicionário físico (em vez de on-line) nunca fariam
isso.
Felizmente, as pessoas que publicam dicionários impressos se dão ao
trabalho de colocar as palavras em ordem lexicográfica. Isso nos permite abrir
o livro em uma página onde achamos que a palavra pode estar (por exemplo,
perto do meio para palavras que começam com a letra m). Se a sequência de
letras precede lexicograficamente a primeira palavra na página, sabemos que
devemos retroceder. Se a sequência de letras segue a última palavra da
página, sabemos que devemos seguir em frente. Caso contrário, verificamos
se a sequência de letras corresponde a uma palavra na página.
Agora vamos pegar a mesma ideia e aplicá-la ao problema de encontrar a
raiz quadrada de x. Suponha que saibamos que uma boa aproximação para a
raiz quadrada de x está em algum lugar entre 0 e max. Podemos explorar o
fato de que os números são totalmente ordenados. Que
Machine Translated by Google

é, para qualquer par de números distintos, n1 e n2, ou n1 < n2 ou n1 > n2. Então, podemos
pensar na raiz quadrada de x como estando em algum lugar na linha

0_____________________________________________
_____________max
e comece a pesquisar esse intervalo. Como não sabemos necessariamente por onde
começar a pesquisar, vamos começar pelo meio.
0__________________________suposição______________
____________max
Se essa não for a resposta certa (e não será na maioria das vezes), pergunte se é
muito grande ou muito pequeno. Se for muito grande, sabemos que a resposta deve estar à
esquerda. Se for muito pequeno, sabemos que a resposta deve estar à direita. Em seguida,
repetimos o processo no intervalo menor. A Figura 3-5 contém uma implementação e um
teste desse algoritmo.

Figura 3-5 Usando a pesquisa de bisseção para aproximar a raiz quadrada

Quando executado para x = 25, ele imprime

baixo = 0,0 alto = 25 ans = 12,5 baixo =


0,0 alto = 12,5 ans = 6,25 baixo = 0,0 alto =
6,25 ans = 3,125 baixo = 3,125 alto = 6,25
ans = 4,6875 baixo = 4,6875 alto = 6,25 ans =
5,46875 baixo = 4,687 5 altos = 5,46875 ans =
5,078125 baixo = 4,6875 alto = 5,078125 ans = 4,8828125
Machine Translated by Google

baixo = 4,8828125 alto = 5,078125 ans = 4,98046875 baixo = 4,98046875


alto = 5,078125 ans = 5,029296875 baixo = 4,98046875 alto = 5,029296875
ans = 5,0048828125 baixo = 4,98046875 alto = 5,0048828125 ans = 4,99267578125
baixo = 4,99267578125 alto = 5,0048828125 ans = 4,998779296875 baixo =
4,998779296875 alto = 5,0048828125 ans = 5,0018310546875

numGuesses = 13
5,00030517578125 está próximo da raiz quadrada de 25

Observe que ele encontra uma resposta diferente do nosso algoritmo anterior.
Isso está perfeitamente bem, pois ainda atende à especificação do problema.
Mais importante, observe que a cada iteração do loop, o tamanho do espaço
a ser pesquisado é cortado pela metade. Por esta razão, o algoritmo é chamado
de busca de bisseção. A pesquisa de bisseção é uma grande melhoria em
relação ao nosso algoritmo anterior, que reduzia o espaço de pesquisa em apenas
uma pequena quantidade a cada iteração.
Vamos tentar x = 123456 novamente. Desta vez, o programa leva apenas 30
tentativas para encontrar uma resposta aceitável. Que tal x = 123456789 ? Leva
apenas 45 palpites.
Não há nada de especial em usar esse algoritmo para encontrar raízes
quadradas. Por exemplo, alterando alguns 2s para 3s, podemos usá-lo para
aproximar a raiz cúbica de um número não negativo. No Capítulo 4, apresentamos
um mecanismo de linguagem que nos permite generalizar esse código para
encontrar qualquer raiz.
A pesquisa de bisseção é uma técnica amplamente útil para muitas coisas
além de encontrar raízes. Por exemplo, o código na Figura 3-6 usa a busca de
bisseção para encontrar uma aproximação para o log base 2 de x (ou seja, um
número, ans, tal que 2**ans está próximo de x). Ele é estruturado exatamente
como o código usado para encontrar uma aproximação para uma raiz quadrada.
Ele primeiro encontra um intervalo contendo uma resposta adequada e, em
seguida, usa a pesquisa de bisseção para explorar esse intervalo com eficiência.
Machine Translated by Google

Figura 3-6 Usando a pesquisa de bisseção para estimar a base logarítmica 2

A busca por bisseção é um exemplo de método de aproximação


sucessiva . Esses métodos funcionam fazendo uma sequência de
suposições com a propriedade de que cada suposição esteja mais próxima
de uma resposta correta do que a suposição anterior. Veremos um
importante algoritmo de aproximação sucessiva, o método de Newton, mais adiante neste

Exercício com os dedos: O que o código da Figura 3-5 faria se x = -25?

Exercício com os dedos: o que teria de ser alterado para fazer o código
da Figura 3-5 funcionar para encontrar uma aproximação da raiz cúbica de
números negativos e positivos? Dica: pense em mudar para baixo para
garantir que a resposta esteja dentro da região que está sendo pesquisada.

Exercício com os dedos: O Empire State Building tem 102 andares. Um


homem queria saber o andar mais alto de onde ele poderia deixar cair um
ovo sem que o ovo quebrasse. Ele propôs jogar um ovo do último andar. Se
quebrasse, ele descia um andar e tentava de novo. Ele faria isso até que o
ovo não quebrasse. Na pior das hipóteses, esse método requer 102 ovos.
Implemente um método que, na pior das hipóteses, use sete ovos.

3.3 Algumas palavras sobre o uso de floats


Machine Translated by Google

Na maioria das vezes, números do tipo float fornecem uma aproximação razoavelmente
boa para números reais. Mas “a maior parte do tempo” não é o tempo todo e, quando
não o fazem, pode levar a consequências surpreendentes. Por exemplo, tente
executar o código

x = 0,0 para
i no intervalo (10): x = x + 0,1

se x == 1,0:
print(x, '= 1.0') else:

print(x, 'não é 1.0')

Talvez você, como a maioria das pessoas, ache estranho que imprima,

0,9999999999999999 não é 1,0

Por que chega à cláusula else em primeiro lugar?


Para entender por que isso acontece, precisamos entender como os números de
ponto flutuante são representados no computador durante uma computação. Para
entender isso, precisamos entender os números binários.

Quando você aprendeu sobre números decimais - ou seja, números de base 10


- você aprendeu que qualquer número decimal pode ser representado por uma
sequência de dígitos 0123456789. O dígito mais à direita é a casa 100 , o próximo
dígito à esquerda é a casa 101 , etc. Por exemplo, a sequência de dígitos decimais
302 representa 3*100 + 0*10 + 2*1.
Quantos números diferentes podem ser representados por uma sequência de
comprimento n? Uma sequência de comprimento 1 pode representar qualquer um
dos 10 números (0-9); uma sequência de comprimento 2 pode representar 100
números diferentes (0-99). Mais geralmente, uma sequência de comprimento n pode
representar 10n números diferentes.
Números binários — números de base 2 — funcionam de maneira semelhante.
Um número binário é representado por uma sequência de dígitos, cada um dos quais é
0 ou 1. Esses dígitos geralmente são chamados de bits. O dígito mais à direita é a 0

casa 2 , o próximo dígito à esquerda é a casa 2 1 , etc. Por exemplo, a sequência de


dígitos binários 101 representa 1*4 + 0*2 + 1*1 = 5.
Quantos números diferentes podem ser representados por uma sequência de
comprimento nn? . 2
Machine Translated by Google

Exercício de dedo: Qual é o equivalente decimal do número binário 10011?

Talvez porque a maioria das pessoas tenha dez dedos, gostamos de usar
decimais para representar números. Por outro lado, todos os sistemas de
computador modernos representam números em binário. Isso não ocorre porque
os computadores nascem com dois dedos. É porque é fácil construir switches
de hardware, ou seja, dispositivos que podem estar em apenas um dos dois
estados, ligado ou desligado. O fato de os computadores usarem uma
representação binária e as pessoas uma representação decimal pode levar a
uma dissonância cognitiva ocasional.
Nas linguagens de programação modernas, os números não inteiros são
implementados usando uma representação chamada ponto flutuante. Por
enquanto, vamos supor que a representação interna seja em decimal.
Representaríamos um número como um par de inteiros - os dígitos significativos
do número e um expoente. Por exemplo, o número 1,949 seria representado
como o par (1949, -3), que representa o produto 1949*10-3 .

O número de dígitos significativos determina a precisão com que os


números podem ser representados. Se, por exemplo, houvesse apenas dois
dígitos significativos, o número 1,949 não poderia ser representado com exatidão.
Teria que ser convertido para alguma aproximação de 1,949, neste caso 1,9.
Essa aproximação é chamada de valor arredondado.

Os computadores modernos usam representações binárias, não decimais.


Eles representam os dígitos significativos e os expoentes em binário em vez de
decimal e aumentam 2 em vez de 10 para o expoente. Por exemplo, o número
representado pelos dígitos decimais 0,625 (5/8) seria representado como o par
(101, -11); como 101 é a representação binária do número 5 e -11 é a
representação binária de -3, o par (101, -11) representa 5*2 -3 = 5/8 =

0,625.

E a fração decimal 1/10, que escrevemos em Python como 0,1? O melhor


que podemos fazer com quatro dígitos binários significativos é (0011, -101). Isso
equivale a 3/32, ou seja, 0,09375. Se tivéssemos cinco dígitos binários
significativos, representaríamos 0,1 como (11001, -1000), que equivale a 25/256,
ou seja, 0,09765625. Quantos dígitos significativos precisaríamos para obter
uma representação exata de ponto flutuante de
Machine Translated by Google

0,1? Um número infinito de dígitos! Não existem inteiros sig e tais que sig 2 -exp seja igual a 0,1. Portanto, não
exp *
importa quantos bits o Python (ou qualquer outra linguagem) use para representar números de ponto
flutuante, ele pode representar apenas uma aproximação de 0,1. Na maioria das implementações do Python,
existem 53 bits de precisão disponíveis para números de ponto flutuante, portanto, os dígitos significativos
armazenados para o número decimal 0,1 serão

11001100110011001100110011001100110011001100110011001

Isso é equivalente ao número decimal


0,1000000000000000055511151231257827021181583404541015625

Bem perto de 1/10, mas não exatamente 1/10.


Voltando ao mistério original, por que
x = 0,0 para
i no intervalo (10): x = x + 0,1

se x == 1,0:
print(x, '= 1.0') else:

print(x, 'não é 1.0')

imprimir

0,9999999999999999 não é 1,0

Agora vemos que o teste x == 1,0 produz o resultado Falso porque o valor ao qual x está vinculado não
é exatamente 1,0. Isso explica por que a cláusula else foi executada. Mas por que ele decidiu que x era menor
que 1,0 quando a representação de ponto flutuante de 0,1 é ligeiramente maior que 0,1? Porque durante
alguma iteração do loop, o Python ficou sem dígitos significativos e fez alguns arredondamentos, que por acaso
foram para baixo. O que é impresso se adicionarmos ao final da cláusula else o código print x == 10.0*0.1? Ele
imprime Falso. Não é o que nossos professores do ensino fundamental nos ensinaram, mas somar 0,1 dez
vezes não produz o mesmo valor que multiplicar 0,1 por 10.

A propósito, se você quiser arredondar explicitamente um número


de ponto flutuante, use a função round . A expressão round(x,
num_digits) retorna o número de ponto flutuante equivalente a
Machine Translated by Google

arredondando o valor de x para num_digits dígitos após o ponto decimal.


Por exemplo, print round(2**0.5, 3) imprimirá 1.414 como uma
aproximação da raiz quadrada de 2.
A diferença entre números reais e de ponto flutuante realmente
importa? Na maioria das vezes, felizmente, não. Existem poucas
situações em que a diferença entre 0,9999999999999999, 1,0 e
1,00000000000000001 importa. No entanto, uma coisa com a qual
quase sempre vale a pena se preocupar são os testes de igualdade.
Como vimos, usar == para comparar dois valores de ponto flutuante
pode produzir um resultado surpreendente. Quase sempre é mais
apropriado perguntar se dois valores de ponto flutuante estão próximos
o suficiente um do outro, não se são idênticos. Assim, por exemplo, é
melhor escrever abs(xÿy) < 0,0001 em vez de x == y.
Outra preocupação é o acúmulo de erros de arredondamento. Na
maioria das vezes as coisas funcionam bem, porque às vezes o número
armazenado no computador é um pouco maior do que o pretendido e
às vezes é um pouco menor do que o pretendido. No entanto, em alguns
programas, os erros estarão todos na mesma direção e se acumularão
com o tempo.

3.4 Newton–Raphson23

O algoritmo de aproximação mais comumente usado é geralmente


atribuído a Isaac Newton. Ele é normalmente chamado de método de
Newton, mas às vezes é chamado de método de Newton–Raphson.24
Ele pode ser usado para encontrar as raízes reais de muitas funções,
mas vamos examiná-lo apenas no contexto de encontrar as raízes reais
de uma polinômio com uma variável. A generalização para polinômios
com múltiplas variáveis é direta tanto matematicamente quanto algoritmicamente.
Um polinômio com uma variável (por convenção, escrevemos a
variável como x) é 0 ou a soma de um número finito diferente de zero
constantes, 2 + 2x + 3. Cada termo, por exemplo, 3x2 , consiste em termos
por exemplo, 3x (o coeficiente do termo, 3 neste caso) multiplicado
pela variável (x neste caso) elevada a um expoente inteiro não negativo
(2 neste caso). O expoente em um termo é chamado de grau desse
termo. O grau de um polinômio é o maior grau de qualquer
Machine Translated by Google

2
prazo. Alguns exemplos são 3 (grau 0), 2,5x + 12 (grau 1) e 3x (grau 2).

Se é um polinômio e r um número real, escreveremos p(r) para p


representa o valor do polinômio quando x = r. Uma raiz do
polinômio p é uma solução para a equação p = 0, ou seja, um r tal que p(r) =
0. Assim, por exemplo, o problema de encontrar uma aproximação para a
raiz quadrada de 24 pode ser formulado como encontrar um x tal que x ÿ 24 2
esteja próximo de 0.
Newton provou um teorema que implica que, se um valor, chame-o de
adivinhar, uma aproximação de uma raiz de um polinômio, então é adivinhe -
(palpite)/ '(palpite), onde p' é a primeira derivada de p, um valor melhor.
aproximação que A adivinhar.
primeira derivada de uma função f(x) pode ser pensada como
expressando como o valor de f(x) muda em relação a mudanças em x . Por
exemplo, a primeira derivada de uma constante é 0, porque o valor de uma
, desse
constante não muda. Para qualquer termo c*x p, a primeira derivada
termo é o polinômio c*p*x da forma p-1 . Assim, a primeira derivada de um

Para encontrar a raiz quadrada de um número, digamos k, precisamos encontrar um valor x tal que x ÿ k =
2
0. A primeira derivada desse polinômio é simplesmente 2x. Portanto, sabemos que podemos melhorar o palpite

atual escolhendo como nosso próximo palpite ÿ ( palpite2 ÿ )/2 * palpite. A Figura 3-7 contém um código que ilustra
como usar esse método para encontrar rapidamente uma aproximação para a raiz quadrada.
Machine Translated by Google

Figura 3-7 Implementação do método Newton-Raphson

Exercício de dedo: adicione algum código à implementação de Newton–Raphson que


monitora o número de iterações usadas para encontrar a raiz. Use esse código como
parte de um programa que compara a eficiência de Newton-Raphson e a pesquisa de
bisseção. (Você deve descobrir que Newton-Raphson é muito mais eficiente.)

3.5 Termos Introduzidos no Capítulo

função de decremento

adivinhar e verificar
enumeração exaustiva

Aproximação

Ordenação Total
Bisseção Pesquisa

bits de números binários de

aproximação
sucessiva

trocar

dígitos

significativos de ponto flutuante

expoente
Machine Translated by Google

arredondamento

de precisão

Coeficiente polinomial
de Newton-
Raphson

grau
raiz

22 Se você nunca viu tal coisa, eles ainda podem ser encontrados em
bibliotecas de tijolo e argamassa.

23 Se você ainda não encontrou polinômios ou


derivados, você pode achar que esta breve seção é um trenó lento. É
autocontido, então você deve ser capaz de trabalhar nele.
No entanto, se você optar por ignorá-lo, não será um problema mais adiante
no livro. Faça, pelo menos, tente. É um algoritmo legal.

24 Newton criou uma variante desse algoritmo por volta de 1669. Joseph Raphson
publicou uma variante diferente mais ou menos na mesma época que
Newton. Notavelmente, a variante de Raphson ainda é amplamente usada hoje.
(Talvez ainda mais notável, Stradivari começou a fazer violinos no mesmo
ano, e alguns de seus violinos também estão em uso hoje.)
Machine Translated by Google

FUNÇÕES, ESCOPO E ABSTRAÇÃO

Até agora, introduzimos números, atribuições, entrada/saída, comparações


e construções de loop. Quão poderoso é esse subconjunto do Python?
Em um sentido teórico, é tão poderoso quanto você jamais precisará, ou
seja, é Turing completo. Isso significa que, se um problema pode ser
resolvido usando computação, ele pode ser resolvido usando apenas os
mecanismos linguísticos que você já viu.
Mas só porque algo pode ser feito, não significa que deva ser feito!
Embora qualquer computação possa, em princípio, ser implementada
usando apenas esses mecanismos, fazer isso é extremamente
impraticável. No último capítulo, vimos um algoritmo para encontrar uma
aproximação da raiz quadrada de um número positivo, veja a Figura 4-1.

Figura 4-1 Usando a pesquisa de bisseção para aproximar a raiz quadrada de x


Machine Translated by Google

Este é um trecho de código razoável, mas carece de utilidade geral.


Funciona apenas para os valores atribuídos às variáveis x e epsilon.
Isso significa que, se quisermos reutilizá-lo, precisamos copiar o código,
possivelmente editar os nomes das variáveis e colá-lo onde quisermos.
Não podemos usar facilmente essa computação dentro de alguma outra
computação mais complexa. Além disso, se quisermos calcular raízes
cúbicas em vez de raízes quadradas, teremos que editar o código. Se
quisermos um programa que calcule raízes quadradas e cúbicas (ou,
nesse caso, calcule raízes quadradas em dois locais diferentes), o
programa conterá vários blocos de código quase idêntico.
A Figura 4-2 adapta o código da Figura 4-1 para imprimir a soma da
raiz quadrada de x1 e a raiz cúbica de x2. O código funciona, mas não é
bonito.
Machine Translated by Google

Figura 4-2 Somando uma raiz quadrada e uma raiz cúbica

Quanto mais código um programa contiver, maior a chance de


algo dar errado e mais difícil será a manutenção do código. Imagine,
por exemplo, que houve um erro na implementação inicial da busca
por bisseção, e que o erro veio à tona ao testar o programa. Seria
muito fácil corrigir a implementação em um local e não notar um
código semelhante em outro lugar que precisasse de reparo.
Machine Translated by Google

Felizmente, o Python fornece vários recursos linguísticos que tornam


relativamente fácil generalizar e reutilizar o código. O mais importante é a função.

4.1 Funções e Escopo

Já usamos várias funções internas, por exemplo, max e abs na Figura 4-1. A
capacidade dos programadores de definir e usar suas próprias funções, como se
fossem integradas, é um salto qualitativo em conveniência.

4.1.1 Definições de função Em


Python, cada definição de função tem a forma25

def nome da função (lista de parâmetros formais): corpo da função

Por exemplo, poderíamos definir a função max_val26 pelo código

def max_val(x, y): se x > y:


retorna x

outro:
retornar y

def é uma palavra reservada que informa ao Python que uma função está prestes
a ser definida. O nome da função (max_val neste exemplo) é simplesmente um
nome usado para se referir à função. A convenção PEP 8 é que os nomes das
funções devem estar em letras minúsculas com palavras separadas por
sublinhados para melhorar a legibilidade.
A sequência de nomes entre parênteses após o nome da função (x,y neste
exemplo) são os parâmetros formais da função. Quando a função é usada, os
parâmetros formais são vinculados (como em uma instrução de atribuição) aos
parâmetros reais (frequentemente referidos como argumentos) da invocação
da função (também referida como uma chamada de função). Por exemplo, a
invocação

max_val(3, 4)

liga x a 3 e y a 4.
Machine Translated by Google

O corpo da função é qualquer parte do código Python.27 Existe, entretanto,


uma instrução especial, return, que só pode ser usada dentro do corpo de uma
função.
Uma chamada de função é uma expressão e, como todas as expressões, tem
um valor. Esse valor é retornado pela função invocada. Por exemplo, o valor da
expressão max_val(3,4)*max_val(3,2) é 12, porque a primeira chamada de
max_val retorna o int 4 e a segunda retorna o int 3. Observe que a execução de
uma instrução return termina uma invocação da função.

Para recapitular, quando uma função é chamada

1. As expressões que compõem os parâmetros reais são avaliadas


e os parâmetros formais da função são vinculados aos valores
resultantes. Por exemplo, a invocação max_val(3+4, z) vinculará o
parâmetro formal x a 7 e o parâmetro formal y a qualquer valor que a
variável z tenha quando a invocação for executada.

2. O ponto de execução (a próxima instrução a ser executada) move-se do


ponto de invocação para a primeira instrução no corpo da função.

3. O código no corpo da função é executado até que uma instrução de


retorno seja encontrada, caso em que o valor da expressão após o retorno
torna-se o valor da invocação da função, ou não há mais instruções
para executar, nas quais caso a função retorne o valor None. (Se
nenhuma expressão segue o retorno, o valor da invocação é None.)28

4. O valor da invocação é o valor retornado.


5. O ponto de execução é transferido de volta para o código
imediatamente após a invocação.

Os parâmetros permitem que os programadores escrevam código que acessa


não objetos específicos, mas quaisquer objetos que o chamador da função
escolha usar como parâmetros reais. Isso é chamado de abstração lambda. 29
Machine Translated by Google

A Figura 4-3 contém uma função que possui três parâmetros formais e
retorna um valor, chame-a de resultado, de modo que abs(resultado**potência
– x) >= epsilon.

Figura 4-3 Uma função para encontrar raízes

A Figura 4-4 contém código que pode ser usado para testar se find_root
funciona como pretendido. A função de teste test_find_root tem
aproximadamente o mesmo comprimento que o próprio find_root . Para
programadores inexperientes, escrever funções de teste muitas vezes parece
ser um desperdício de esforço. Programadores experientes sabem, no entanto,
que um investimento na escrita de código de teste costuma render grandes
dividendos. Certamente é melhor do que sentar em um teclado e digitar casos
de teste no shell repetidamente durante a depuração (o processo de descobrir
por que um programa não funciona e corrigi-lo). Observe que, como estamos
invocando test_find_root com três tuplas (isto é, sequências de valores) de
comprimento três, uma chamada verifica 27 combinações de parâmetros.
Finalmente, como test_find_root verifica se find_root está retornando uma
resposta apropriada e relata o resultado, ele salva o programador da tarefa
tediosa e propensa a erros de inspecionar visualmente cada saída e verificar
se está correto. Voltamos ao assunto dos testes no Capítulo 8.
Machine Translated by Google

Exercício com os dedos: Use a função find_root na Figura 4-3 para imprimir a
soma das aproximações para a raiz quadrada de 25, a raiz cúbica de -8 e a
quarta raiz de 16. Use 0,001 como epsilon.

Exercício de dedo: Escreva uma função is_in que aceite duas strings como
argumentos e retorne True se uma string ocorrer em qualquer lugar da outra, e
False caso contrário. Dica: você pode querer usar o operador interno str in.

Exercício de dedo: Escreva uma função para testar is_in.

Figura 4-4 Código para testar find_root

4.1.2 Argumentos de palavra-chave e valores padrão


Em Python, há duas maneiras pelas quais os parâmetros formais são vinculados
aos parâmetros reais. O método mais comum , que é o que usamos até agora,
é chamado de posicional - o primeiro parâmetro formal é vinculado ao primeiro
parâmetro real, o segundo formal ao segundo real etc. são vinculados a valores
reais usando o nome do parâmetro formal. Considere a definição da função
Machine Translated by Google

def print_name(first_name, last_name, reverse): if reverse: print(last_name


+ ', ' else:
+ primeiro_nome)

print(primeiro_nome, sobrenome)

A função print_name assume que first_name e last_name são strings e que reverse é um
booleano. Se reverse == True, imprime last_name, first_name;caso contrário, ele imprime
first_name last_name.
Cada um dos seguintes é uma invocação equivalente de print_name:

print_name('Olga', 'Puchmajerova', False) print_name('Olga',


'Puchmajerova', reverse = False) print_name('Olga', last_name = 'Puchmajerova',
reverse =
False)
print_name(last_name = 'Puchmajerova', first_name = 'Olga', reverse = False)

Embora os argumentos de palavra-chave possam aparecer em qualquer ordem


na lista de parâmetros reais, não é legal seguir um argumento de palavra-chave com
um argumento que não seja de palavra-chave. Portanto, uma mensagem de erro seria
produzida por

print_name('Olga', last_name = 'Puchmajerova', False)

Argumentos de palavra-chave são comumente usados em conjunto com


valores de parâmetro padrão. Podemos, por exemplo, escrever

def print_name(first_name, last_name, reverse = False): if reverse: print(last_name +


', ' else:
+ primeiro_nome)

print(primeiro_nome, sobrenome)

Os valores padrão permitem que os programadores chamem uma função com


menos do que o número especificado de argumentos. Por exemplo,

print_name('Olga', 'Puchmajerova') print_name('Olga',


'Puchmajerova', True) print_name('Olga', 'Puchmajerova',
reverse = True)

vai imprimir

Olga Puchmayerova
Puchmajerova, Olga
Puchmajerova, Olga
Machine Translated by Google

As duas últimas invocações de print_name são semanticamente equivalentes.


O último tem a vantagem de fornecer alguma documentação para o talvez
misterioso argumento Verdadeiro. De maneira mais geral, o uso de argumentos
de palavra-chave reduz o risco de vincular acidentalmente um parâmetro real
ao parâmetro formal errado. A linha de código

print_name(last_name = 'Puchmajerova', first_name = 'Olga')

não deixa ambiguidade sobre a intenção do programador que o escreveu.


Isso é útil porque chamar uma função com os argumentos certos na ordem
errada é um erro comum.
O valor associado a um parâmetro padrão é calculado no momento da
definição da função. Isso pode levar a um comportamento surpreendente do
programa, como discutimos na Seção 5.3.

Exercício de dedo: Escreva uma função mult que aceite um ou dois ints
como argumentos. Se chamada com dois argumentos, a função imprime o
produto dos dois argumentos. Se chamado com um argumento, imprime esse
argumento.

4.1.3 Número Variável de Argumentos


Python tem um número de funções internas que operam em um número
variável de argumentos. Por exemplo,

min(6,4)
min(3,4,1,6)

são legais (e avaliam o que você acha que eles fazem). O Python torna mais
fácil para os programadores definirem suas próprias funções que aceitam um
número variável de argumentos. O operador de descompactação * permite
que uma função aceite um número variável de argumentos posicionais. Por
exemplo,

def média(*args):
# Assume pelo menos um argumento e todos os argumentos são
números
# Retorna a média dos argumentos tot = 0 for a in args:
tot += a
return tot/len(args)
Machine Translated by Google

imprime 1,5 -1,0. Observe que o nome após o * na lista de argumentos não precisa ser
args. Pode ser qualquer nome. Para média, poderia ter sido mais descritivo escrever def
mean(*numbers).

4.1.4 Escopo
Vejamos outro pequeno exemplo:

def f(x): #nome x usado como parâmetro formal


y=1x
= x + y print('x
=', x) return x

x=3y
=2z=
f(x) #valor de x usado como parâmetro atual print('z =', z) print('x =',
x) print('y =', y)

Quando executado, este código imprime

x=4
z=4
x=3a
=2

O que está acontecendo aqui? Na chamada de f, o parâmetro formal x é localmente


vinculado ao valor do parâmetro real x no contexto do corpo da função de f. Embora os
parâmetros reais e formais tenham o mesmo nome, eles não são a mesma variável. Cada
função define um novo espaço de nome, também chamado de escopo. O parâmetro
formal x e a variável local que são usados em f existem apenas dentro do escopo da
definição de f. A instrução de atribuição x = dentro edo corpo da função vincula o nome
local x ao objeto 4.
x+y
As atribuições em f não têm efeito nas ligações dos nomes x e
e que existam fora do escopo de f.
Aqui está uma maneira de pensar sobre isso:

1. No nível superior, ou seja, o nível do shell, uma tabela de símbolos


acompanha todos os nomes definidos nesse nível e suas associações atuais.
Machine Translated by Google

2. Quando uma função é chamada, uma nova tabela de símbolos (muitas


vezes chamada de quadro de pilha) é criada. Esta tabela acompanha
todos os nomes definidos na função (incluindo os parâmetros formais) e
suas ligações atuais. Se uma função for chamada de dentro do corpo da
função, outro quadro de pilha será criado.
3. Quando a função é concluída, seu quadro de pilha desaparece.

Em Python, você sempre pode determinar o escopo de um nome observando o


texto do programa. Isso é chamado de escopo estático ou léxico.
A Figura 4-5 contém um exemplo que ilustra as regras de escopo do Python. A
história dos quadros de pilha associados ao código é representada na Figura 4-6.

Figura 4-5 Escopos aninhados

A primeira coluna na Figura 4-6 contém o conjunto de nomes conhecidos fora do


corpo da função f, ou seja, as variáveis xe e o nome da função f. A primeira instrução
Com,

de atribuição vincula x a 3.
Machine Translated by Google

Figura 4-6 Estruturas de empilhamento

A instrução de atribuição z = f(x) primeiro avalia a expressão f(x) invocando a


função f com o valor ao qual x está vinculado.
Quando f é inserido, um quadro de pilha é criado, conforme mostrado na coluna 2.
Os nomes no quadro de pilha são x (o parâmetro formal x, não o x no contexto de
chamada), g e h. As variáveis eh são vinculadas a objetos g do tipo função. As
propriedades dessas funções são dadas pelas definições de função dentro de f.

Quando h é chamado de dentro de f, outro quadro de pilha é criado, conforme


mostrado na coluna 3. Esse quadro contém apenas a variável local z. Por que
também não contém x? Um nome é adicionado ao escopo associado a uma
função somente se esse nome for um parâmetro formal da função ou uma variável
vinculada a um objeto dentro do corpo da função. No corpo de h, x ocorre apenas
no lado direito de uma instrução de atribuição. O aparecimento de um nome (x
neste caso) que não está vinculado a um objeto em qualquer lugar do corpo da
função (o corpo de h neste caso) faz com que o interpretador procure o quadro de
pilha associado ao escopo dentro do qual a função é definida (o quadro de pilha
associado a f). Se o nome for encontrado (o que é neste caso), o valor ao qual
está vinculado (4) é usado. Se não for encontrado lá, uma mensagem de erro é
produzida.

Quando h retorna, o quadro de pilha associado à invocação de h desaparece


(é removido do topo da pilha), conforme ilustrado na coluna 4. Observe que
nunca removemos quadros do meio do
Machine Translated by Google

pilha, mas apenas o quadro adicionado mais recentemente. Devido a esse


comportamento “último a entrar, primeiro a sair” (LIFO) , nos referimos a ele
como uma pilha. (Imagine cozinhar uma pilha de panquecas. Quando a primeira
sai da chapa, o chef a coloca em um prato de servir. À medida que cada
panqueca sucessiva sai da chapa, ela é empilhada em cima das panquecas que
já estão no prato de servir. Quando chega a hora de comer as panquecas, a
primeira panqueca servida será a que está no topo da pilha, a última adicionada
- deixando a penúltima panqueca adicionada à pilha como a nova panqueca do
topo e a próxima a ser servida.)

Voltando ao nosso exemplo Python, g agora é invocado e uma pilha


o quadro contendo a variável local x de g é adicionado (coluna 5). Quando g
retorna, esse quadro é exibido (coluna 6). Quando f retorna, o quadro de pilha
contendo os nomes associados a f é removido, levando-nos de volta ao quadro
de pilha original (coluna 7).
Observe que quando f retorna, mesmo que a variável g não exista mais, o
objeto do tipo função ao qual esse nome foi associado ainda existe. Isso ocorre
porque as funções são objetos e podem ser retornadas como qualquer outro
tipo de objeto. Assim, z pode ser vinculado ao valor retornado por f, e a chamada
de função z() pode ser usada para invocar a função que foi vinculada ao nome
g dentro de f — mesmo que o não seja conhecidofora do contexto de f. nome g
Machine Translated by Google

Então, o que o código na Figura 4-5 imprime? imprime


x=4
z=4
x = abc
x=4
x=3z
= <função f.<locais>.g em 0x1092a7510> x = abc

A ordem em que ocorrem as referências a um nome não é pertinente. Se


um objeto estiver vinculado a um nome em qualquer lugar no corpo da função
(mesmo que ocorra em uma expressão antes de aparecer como o lado esquerdo
de uma atribuição), ele será tratado como local para essa função.30 Considere
o código

deff():
imprimir(x)
def g():
imprimir(x) x
=1
x = 3 f()
x=
3 g()

Imprime 3 quando f é invocado, mas a mensagem de erro


UnboundLocalError: variável local 'x' referenciada antes da atribuição

é impresso quando a instrução print in é encontrada.g Isso acontece porque a


instrução de atribuição após a instrução print faz com que x seja local para g.
E como x é local para g, ele não tem
valor quando a instrução print é executada.
Confuso ainda? A maioria das pessoas leva um pouco de tempo para
entender as regras de escopo. Não deixe que isso o incomode. Por enquanto,
vá em frente e comece a usar as funções. Na maioria das vezes, você só vai
querer usar variáveis que são locais para uma função, e as sutilezas do escopo
serão irrelevantes. Na verdade, se o seu programa depende de alguma regra
de escopo sutil, você pode considerar reescrever para evitar isso.
Machine Translated by Google

4.2 Especificações

A especificação de uma função define um contrato entre o implementador de


uma função e aqueles que escreverão programas que usam a função. Referimo-
nos aos usuários de uma função como seus clientes. Este contrato pode ser
pensado como contendo duas partes:

Suposições: Descrevem as condições que devem ser atendidas pelos


clientes da função. Normalmente, eles descrevem restrições nos parâmetros
reais. Quase sempre, eles especificam o conjunto aceitável de tipos para
cada parâmetro e, não raramente, algumas restrições sobre o
valor de um ou mais parâmetros. Por exemplo, a especificação de find_root
pode exigir esse número inteiro positivo. poder ser um

Garantias: Descrevem as condições que devem ser atendidas pela função,


desde que tenha sido chamada de forma que satisfaça as suposições.
Por exemplo, a especificação de find_root pode garantir que ele retorne
None se solicitado a encontrar uma raiz que não existe (por exemplo, a raiz
quadrada de um número negativo).

As funções são uma maneira de criar elementos computacionais que podemos


considerar como primitivos. Eles fornecem decomposição e abstração.

A decomposição cria estrutura. Ele nos permite dividir um programa em


partes que são razoavelmente independentes e que podem ser reutilizadas em
diferentes configurações.
A abstração esconde os detalhes. Ela nos permite usar um pedaço de código
como se fosse uma caixa preta - ou seja, algo cujos detalhes internos não
podemos ver, não precisamos ver e nem deveríamos querer ver.31 A essência da
abstração é preservando informações que são relevantes em um determinado
contexto e esquecendo informações que são irrelevantes naquele contexto. A
chave para usar a abstração efetivamente na programação é encontrar uma noção
de relevância que seja apropriada tanto para o construtor de uma abstração
quanto para os clientes potenciais da abstração. Essa é a verdadeira arte da
programação.
A abstração tem tudo a ver com o esquecimento. Existem muitas maneiras de
modelam isso, por exemplo, o aparelho auditivo da maioria dos adolescentes.
Machine Translated by Google

O adolescente diz: Posso pegar o carro emprestado hoje à noite?

O pai diz: Sim, mas volte antes da meia-noite e verifique se o tanque de


gasolina está cheio.
O adolescente ouve: Sim.
O adolescente ignorou todos aqueles detalhes incômodos que considera
irrelevantes. A abstração é um processo muitos-para-um. Se o pai tivesse dito “Sim,
mas volte antes das 2h e certifique-se de que o carro está limpo”, também teria sido
abstraído para Sim.
Por analogia, imagine que você foi solicitado a produzir um curso introdutório à
ciência da computação contendo 25 aulas. Uma forma de fazer isso seria recrutar 25
professores e pedir a cada um deles que preparasse uma palestra de uma hora sobre
seu tema preferido. Embora você possa ter 25 horas maravilhosas, é provável que a
coisa toda pareça uma dramatização de Seis personagens em busca de um autor,
de Pirandello (ou aquele curso de ciência política que você fez com 15 palestrantes
convidados). Se cada professor trabalhasse isoladamente, não teria ideia de como
relacionar o material de sua aula com o material abordado em outras aulas.

De alguma forma, você precisa deixar todo mundo saber o que todo mundo está
fazendo, sem gerar tanto trabalho que ninguém esteja disposto a participar. É aqui
que entra a abstração. Você pode escrever 25 especificações, cada uma dizendo qual
material os alunos devem aprender em cada aula, mas sem dar nenhum detalhe sobre
como esse material deve ser ensinado. O que você obteve pode não ser
pedagogicamente maravilhoso, mas pelo menos pode fazer sentido.

É assim que as organizações usam equipes de programadores para fazer as


coisas. Dada a especificação de um módulo, um programador pode trabalhar na
implementação desse módulo sem se preocupar com o que os outros programadores
da equipe estão fazendo. Além disso, os outros programadores podem usar a
especificação para começar a escrever o código que usa o módulo sem se preocupar
em como o módulo será implementado.

A Figura 4-7 adiciona uma especificação para a implementação de


find_root na Figura 4-3.
Machine Translated by Google

Figura 4-7 Uma definição de função com uma especificação

O texto entre as aspas triplas é chamado de docstring em Python.


Por convenção, os programadores Python usam docstrings para fornecer
especificações de funções. Essas docstrings podem ser acessadas usando
a ajuda da função integrada .
Uma das coisas boas sobre os IDEs do Python é que eles fornecem
uma ferramenta interativa para perguntar sobre os objetos integrados. Se
você quiser saber o que uma função específica faz, basta digitar
help(object) na janela do console. Por exemplo, help(abs) produz o texto
Ajuda sobre a função interna abs nos módulos embutidos:

abs(x, /)
Retorna o valor absoluto do argumento.

Isso nos diz que abs é uma função que mapeia um único argumento para
seu valor absoluto. (O / na lista de argumentos significa que o argumento
deve ser posicional.) Se você inserir help(), uma sessão de ajuda interativa
será iniciada e o interpretador apresentará o prompt help> na janela do
console. Uma vantagem do modo interativo é que você pode obter ajuda
sobre as construções do Python que não são objetos. Por exemplo,
Machine Translated by Google

ajuda> se
A declaração "se"
******************

A instrução "if" é usada para execução condicional:

if_stmt ::= "if" expressão ":" suite ("elif" expressão ":" suite)*
["else" ":" suite]

Ele seleciona exatamente uma das suítes avaliando as expressões uma a uma até
que uma seja
considerada verdadeira (consulte a seção Operações booleanas para a definição de
verdadeiro e
falso); então essa suíte é executada

(e nenhuma outra parte da instrução "if" é executada ou avaliada).

Se todas as expressões forem falsas, o conjunto da cláusula "else", se presente, é


executado.

Tópicos de ajuda relacionados: TRUTHVALUE

A ajuda interativa pode ser encerrada digitando quit.


Se o código da Figura 4-4 tivesse sido carregado em um IDE,
digitar help(find_root) no shell exibiria
find_root(x, potência, epsilon)
Assume x e epsilon int ou float, potencializa um int,
epsilon > 0 & potência >= 1
Retorna float y tal que y**power está dentro de epsilon de
x.
Se tal float não existir, ele retorna None

A especificação de find_root é uma abstração de todas as


possíveis implementações que atendem à especificação. Os clientes
de find_root podem presumir que a implementação atende à
especificação, mas não devem assumir nada além disso. Por
exemplo, os clientes podem assumir que a chamada find_root(4, 2,
0,01) retorna algum valor cujo quadrado está entre 3,99 e 4,01. O
valor retornado pode ser positivo ou negativo e, embora 4 seja um
quadrado perfeito, o valor retornado pode não ser 2 ou -2.
Crucialmente, se as suposições da especificação não forem satisfeitas, nada pod
Machine Translated by Google

efeito de chamar a função. Por exemplo, a chamada find_root(8, 3, 0) poderia


retornar 2. Mas também poderia travar, rodar para sempre ou retornar algum
número nem perto da raiz cúbica de 8.

Exercício de dedo: Usando o algoritmo da Figura 3-6, escreva uma função que
satisfaça a especificação

def log(x, base, epsilon):


"""Assume x e epsilon int ou float, base an int, x > 1, epsilon > 0 & power >= 1

Retorna float y tal que base**y está dentro de epsilon


de x."""

4.3 Usando funções para modularizar código

Até agora, todas as funções que implementamos foram pequenas.


Eles se encaixam perfeitamente em uma única página. À medida que
implementamos funcionalidades mais complicadas, é conveniente dividir as
funções em várias funções, cada uma das quais faz uma coisa simples. Para
ilustrar essa ideia, de forma supérflua, dividimos find_root em três funções
separadas, conforme mostrado na Figura 4-8. Cada uma das funções tem sua
própria especificação e cada uma faz sentido como uma entidade autônoma. A
função find_root_bounds localiza um intervalo no qual a raiz deve estar,
bisection_solve usa a pesquisa de bisseção para pesquisar esse intervalo em
busca de uma aproximação da raiz e find_root simplesmente chama os outros
dois e retorna a raiz.
Esta versão de find_root é mais fácil de entender do que a implementação
monolítica original? Provavelmente não. Uma boa regra geral é que, se uma
função cabe confortavelmente em uma única página, ela provavelmente não
precisa ser subdividida para ser facilmente compreendida.
Machine Translated by Google

Figura 4-8 Dividindo find_root em várias funções

4.4 Funções como Objetos

Em Python, as funções são objetos de primeira classe. Isso significa que eles
podem ser tratados como objetos de qualquer outro tipo, por exemplo, int ou
lista. Eles têm tipos, por exemplo, a expressão type(abs) tem o valor <type
'built in_function_or_method'>; eles podem aparecer em expressões, por
exemplo, como o lado direito de uma instrução de atribuição ou como um
argumento para uma função; eles podem ser retornados por funções; etc.
Machine Translated by Google

O uso de funções como argumentos permite um estilo de codificação


chamado programação de ordem superior. Ele nos permite escrever funções
que são mais úteis em geral. Por exemplo, a função bisection_solve na Figura
4-8 pode ser reescrita para que possa ser aplicada a outras tarefas além da
localização de raízes, conforme mostrado na Figura 4-9.

Figura 4-9 Generalizando bisection_solve

Começamos substituindo o parâmetro inteiro power por uma função,


eval_ans, que mapeia floats para floats. Em seguida, substituímos todas as
instâncias da expressão ans**power pela chamada de função eval_ans(ans).
Se quiséssemos usar o novo bisection_solve para imprimir um
aproximação da raiz quadrada de 99, poderíamos executar o código
def quadrado(ans):
return ans**2
baixo, alto = find_root_bounds(99, 2)
print(bisection_solve(99, quadrado, 0,01, baixo, alto))

Dar-se ao trabalho de definir uma função para fazer algo tão simples
quanto elevar um número ao quadrado parece um pouco bobo. Felizmente, o
Python oferece suporte à criação de funções anônimas (ou seja, funções que
não estão vinculadas a um nome), usando a palavra reservada lambda. A
forma geral de uma expressão lambda é
Machine Translated by Google

sequência lambda de nomes de variáveis : expressão

Por exemplo, a expressão lambda lambda x, y: x*y retorna uma função que
retorna o produto de seus dois argumentos. As expressões lambda são
frequentemente usadas como argumentos para funções de ordem superior. Por
exemplo, poderíamos substituir a chamada acima para bisection_solve por

print(bisection_solve(99, lambda ans: ans**2, 0.01, baixo, alto))

Exercício de dedo: Escreva uma expressão lambda que tenha dois parâmetros
numéricos. Se o segundo argumento for igual a zero, ele deve retornar None. Caso
contrário, deve retornar o valor da divisão do primeiro argumento pelo segundo
argumento. Dica: use uma expressão condicional.

Como as funções são objetos de primeira classe, elas podem ser criadas e
retornadas dentro de funções. Por exemplo, dada a definição da função

def create_eval_ans():
power = input('Digite um inteiro positivo: ') return lambda ans:
ans**int(power)

o código

eval_ans = create_eval_ans()
print(bisection_solve(99, eval_ans, 0,01, baixo, alto))

imprimirá uma aproximação da n-ésima raiz de 99, onde n é um número digitado


por um usuário.
A maneira como generalizamos bisection_solve significa que agora ela pode
ser usada não apenas para procurar aproximações de raízes, mas também para
procurar aproximações de qualquer função monotônica32 que mapeia floats para
floats. Por exemplo, o código na Figura 4-10 usa bisection_solve para encontrar
aproximações para logaritmos.
Machine Translated by Google

Figura 4-10 Usando bisection_solve para aproximar logs

Observe que a implementação de log inclui a definição de uma função


local, find_log_bounds. Essa função poderia ter sido definida fora do log,
mas como não esperamos usá-la em nenhum outro contexto, pareceu
melhor não fazê-lo.

4.5 Métodos, Simplificados

Os métodos são objetos semelhantes a funções. Eles podem ser chamados


com parâmetros, podem retornar valores e podem ter efeitos colaterais.
Eles diferem das funções de algumas maneiras importantes, que
discutiremos no Capítulo 10.
Por enquanto, pense nos métodos como fornecendo uma sintaxe
peculiar para uma chamada de função. Em vez de colocar o primeiro
argumento entre parênteses após o nome da função, usamos a notação
de ponto para colocar esse argumento antes do nome da função.
Apresentamos métodos aqui porque muitas operações úteis em tipos
internos são métodos e, portanto, invocados usando a notação de ponto. s
Por exemplo, se for uma string, o método find pode ser usado para
encontrar o índice da primeira ocorrência de uma substring em s. Portanto,
se s fosse 'abcbc', a invocação s.find('bc') retornaria 1. A tentativa de
tratar find como uma função, por exemplo, invocando find(s,'bc'), produz a mensagem d
nome 'encontrar' não está definido.
Machine Translated by Google

Exercício de dedo: O que s.find(sub) retorna se sub não ocorre em s?

Exercício de dedo: Use find para implementar uma função que satisfaça a especificação

def find_last(s, sub): """se sub


são strings não vazias
Retorna o índice da última ocorrência de sub em s.
Retorna None se sub não ocorrer em s"""

4.6 Termos Introduzidos no Capítulo

definição de função

parâmetro formal

parâmetro real

invocação

de função de argumento (chamada de função)


declaração de retorno

abstração lambda do
ponto de execução

função de teste

depurando

argumento posicional

palavra-chave argumento

valor de parâmetro padrão

operador de descompactação (*)

espaço de nomes

escopo
variável local
Machine Translated by Google

quadro de pilha de

mesa de símbolo

cliente de especificação de

pilha de escopo

estático (lexical)
(LIFO)

suposição

garantir a

abstração da
decomposição

função de

ajuda docstring

objeto de primeira

classe programação de ordem superior

método de expressão
lambda

notação de ponto

25 Lembre-se de que o itálico é usado para denotar conceitos em vez de real


código.

26 Na prática, você provavelmente usaria a função interna em vez de definir sua máximo,

própria função.

27 Como veremos mais adiante, essa noção de função é muito mais geral
do que os matemáticos chamam de função. Foi popularizado pela primeira vez pela
linguagem de programação Fortran 2 no final dos anos 1950.

28 Mais adiante no livro, discutimos dois outros mecanismos para sair de uma função, raise e
yield.
Machine Translated by Google

29 O nome “abstração lambda” é derivado da matemática


desenvolvido por Alonzo Church nas décadas de 1930 e 1940. O nome
é derivado do uso de Church da letra grega lambda (ÿ) para denotar a
abstração da função. Durante sua vida, Church deu diferentes
explicações sobre por que escolheu o símbolo lambda. Meu favorito é
"eeny, meeny, miny, moe".

30 A sabedoria dessa decisão de design de linguagem é discutível.

31 “Onde a ignorância é uma benção, é tolice ser sábio.”—Thomas Gray

32 Uma função de números para números é monotonicamente


crescente (diminuindo) se o valor retornado pela função aumenta
(diminui) conforme o valor de seu argumento aumenta (diminui).
Machine Translated by Google

5
TIPOS ESTRUTURADOS E MUTABILIDADE

Os programas que examinamos até agora lidam com três tipos de objetos: int, float e
str. Os tipos numéricos int e float são tipos escalares. Ou seja, objetos desses tipos
não possuem estrutura interna acessível. Em contraste, str pode ser pensado como
um tipo estruturado ou não escalar. Podemos usar a indexação para extrair caracteres
individuais de uma string e fatiar para extrair substrings.

Neste capítulo, apresentamos quatro tipos estruturados adicionais.


Um, tupla, é uma generalização simples de str. Os outros três – lista e ditado – são
faixa, mais interessantes. Também retornamos ao tópico de programação de ordem
superior com alguns exemplos que ilustram a utilidade de poder tratar funções da
mesma forma que outros tipos de objetos.

5.1 Tuplas

Assim como as strings, as tuplas são sequências ordenadas imutáveis de elementos.


A diferença é que os elementos de uma tupla não precisam ser caracteres.
Os elementos individuais podem ser de qualquer tipo e não precisam ser do mesmo
tipo entre si.
Literais do tipo tupla são escritos colocando uma lista separada por vírgulas de
elementos entre parênteses. Por exemplo, podemos escrever

t1 = () t2 =
(1, 'dois', 3) print(t1) print(t2)

Sem surpresa, as instruções de impressão produzem a saída


Machine Translated by Google

()
(1, 'dois', 3)

Olhando para este exemplo, você pode pensar que a tupla contendo o valor único
1 seria escrita (1). Mas, para citar o RH
Haldeman citando Richard Nixon, “seria errado”. 33 Como os parênteses são usados
para agrupar expressões, (1) é apenas uma maneira detalhada de escrever o inteiro 1.
Para denotar a tupla singleton contendo esse valor, escrevemos (1,). Quase todo
mundo que usa Python, em um momento ou outro, acidentalmente omitiu essa vírgula
irritante.
A repetição pode ser usada em tuplas. Por exemplo, a expressão 3* ('a', 2) resulta em ('a', 2, 'a',
2, 'a', 2).
Assim como as strings, as tuplas podem ser concatenadas, indexadas e divididas.
Considerar

t1 = (1, 'dois', 3) t2 = (t1,


3,25) print(t2) print((t1
+ t2)) print((t1
+ t2)[3]) print((t1 + t2)[2
:5])

A segunda instrução de atribuição vincula o nome t2 a uma tupla que contém a


tupla à qual t1 está vinculado e o número de ponto flutuante 3,25. Isso é possível
porque uma tupla, como tudo em Python, é um objeto, então tuplas podem conter
tuplas. Portanto, a primeira instrução print produz a saída,

((1, 'dois', 3), 3,25)

A segunda instrução print imprime o valor gerado pela concatenação dos valores
vinculados a t1 e t2, que é uma tupla com cinco elementos. Ele produz a saída

(1, 'dois', 3, (1, 'dois', 3), 3,25)

A próxima instrução seleciona e imprime o quarto elemento da tupla concatenada


(como sempre em Python, a indexação começa em 0), e a instrução seguinte cria e
imprime uma fatia dessa tupla, produzindo a saída
Machine Translated by Google

(1, 'dois', 3) (3, (1,


'dois', 3), 3,25)

Uma instrução for pode ser usada para iterar sobre os elementos de
uma tupla. E o operador in pode ser usado para testar se uma tupla contém
um valor específico. Por exemplo, o seguinte código
def intersect(t1, t2):
"""Assume que t1 e t2 são tuplas
Retorna uma tupla contendo elementos que estão em t1 e t2""" result = () for
e in t1:

se e em t2:
resultado += (e,)
resultado de retorno
print(intersect((1, 'a', 2), ('b', 2, 'a')))

imprime ('a', 2).

5.1.1 Atribuição múltipla Se


você souber o comprimento de uma sequência (por exemplo, uma tupla ou
uma string), pode ser conveniente usar a instrução de atribuição múltipla
do Python para extrair os elementos individuais. Por exemplo, a instrução
x, y = (3, 4), vinculará x a 3 e y a 4. Da mesma forma, a instrução a, b, c =
'xyz' vinculará a a 'x', b a 'y ', e c a 'z'.
Esse mecanismo é particularmente conveniente quando usado com
funções que retornam vários valores. Considere a definição da função
def find_extreme_divisors(n1, n2):
"""Assume que n1 e n2 são inteiros positivos
Retorna uma tupla contendo o menor valor comum
divisor > 1 e
o maior divisor comum de n1 & n2. Se não
divisor comum,
diferente de 1, retorna (None, None)"""
min_val, max_val = Nenhum, Nenhum para i no
intervalo(2, min(n1, n2) + 1): se n1%i == 0 e n2%i == 0:
se min_val == Nenhum: min_val = i

max_val = eu
retorno min_val, max_val
Machine Translated by Google

A instrução de atribuição múltipla

min_divisor, max_divisor = find_extreme_divisors(100, 200)

vinculará min_divisor a 2 e max_divisor a 200.

5.2 Intervalos e iteráveis


Conforme discutido na Seção 2.6, a função range produz um objeto do tipo strings e
operações em tuplas, objetos do tipo range são do tipo range. imutável. Todas as
tuplas também estão disponíveis para intervalos, exceto para concatenação e repetição.
Por exemplo, range(10)[2:6][2] resulta em 4. Quando o operador == é usado para
comparar objetos do tipo range, ele retorna True se os dois intervalos representam a
mesma sequência de inteiros. Por exemplo, range(0, 7, 2) == range(0, 8, 2) é avaliado
como True. No entanto, range(0, 7, 2) == range(6, -1, -2) é avaliado como False porque,
embora os dois intervalos contenham os mesmos números inteiros, eles ocorrem em
uma ordem diferente.

Ao contrário dos objetos do tipo tupla, a quantidade de espaço ocupada por um


objeto do tipo range não é proporcional ao seu comprimento. Como um intervalo é
totalmente definido por seus valores de início, parada e etapa, ele pode ser armazenado
em uma pequena quantidade de espaço.
O uso mais comum de intervalo está em loops for , mas objetos do tipo
range pode ser usado em qualquer lugar onde uma sequência de números inteiros pode ser usada.
No Python 3, range é um caso especial de um objeto iterável. Todos os tipos
iteráveis possuem um método,34 __iter__ que retorna um objeto do tipo iterador. O
iterador pode então ser usado em um loop for para retornar uma sequência de objetos,
um de cada vez. Por exemplo, as tuplas são iteráveis e a instrução for

para elem em (1, 'a', 2, (3, 4)):

cria um iterador que retornará os elementos da tupla um por vez. Python tem muitos tipos
iteráveis integrados, incluindo strings, listas e dicionários.

Muitas funções integradas úteis operam em iteráveis. Entre os mais úteis estão sum,
min e max. A função sum pode ser aplicada a iteráveis de números. Retorna a soma dos
elementos. O
Machine Translated by Google

as funções max e min podem ser aplicadas a iteráveis para os quais existe uma
ordenação bem definida dos elementos.

Exercício com os dedos: Escreva uma expressão que resulte na média de uma
tupla de números. Use a função soma.

5.3 Listas e Mutabilidade

Como uma tupla, uma lista é uma sequência ordenada de valores, onde cada valor é
identificado por um índice. A sintaxe para expressar literais do tipo lista é semelhante
àquela usada para tuplas; a diferença é que usamos colchetes em vez de parênteses.
A lista vazia é escrita como [], e as listas de singleton são escritas sem aquela (oh,
tão fácil de esquecer) vírgula antes do colchete de fechamento.

Como as listas são iteráveis, podemos usar uma instrução for para iterar
os elementos da lista. Assim, por exemplo, o código

L = ['Eu fiz tudo', 4, 'amor'] for e in L: print(e)

produz a saída,

eu fiz tudo
4
Amor

Também podemos indexar em listas e dividir listas, assim como podemos para tuplas.
Por exemplo, o código

L1 = [1, 2, 3]
L2 = L1[-1::-1] para i no
intervalo(len(L1)): print(L1[i]*L2[i])

estampas

3
4
3
Machine Translated by Google

O uso de colchetes para três finalidades diferentes (literais do tipo lista,


indexação em iteráveis e fatiamento de iteráveis) pode causar confusão visual. Por
exemplo, a expressão [1,2,3,4][1:3][1], que resulta em 3, usa colchetes de três
maneiras. Isso raramente é um problema na prática, porque na maioria das vezes
as listas são construídas de forma incremental em vez de escritas como literais.

As listas diferem das tuplas de uma maneira extremamente importante: as listas


são mutáveis. Em contraste, tuplas e strings são imutáveis. Muitos operadores
podem ser usados para criar objetos de tipos imutáveis e variáveis podem ser
associadas a objetos desses tipos. Mas objetos de tipos imutáveis não podem ser
modificados depois de criados. Por outro lado, objetos de tipos mutáveis podem ser
modificados depois de criados.

A distinção entre modificar um objeto e atribuir um objeto a uma variável pode,


a princípio, parecer sutil. No entanto, se você continuar repetindo o mantra “Em
Python, uma variável é apenas um nome, ou seja, um rótulo que pode ser anexado
a um objeto”, isso lhe trará clareza. E talvez o seguinte conjunto de exemplos
também ajude.
Quando as declarações

Técnicos = ['MIT', 'Caltech']


Ivys = ['Harvard', 'Yale', 'Marrom']

são executados, o interpretador cria duas novas listas e vincula as variáveis


apropriadas a elas, conforme ilustrado na Figura 5-1.
Machine Translated by Google

Figura 5-1 Duas listas

As declarações de atribuição
Univs = [Techs, Ivys]
Univs1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]

também criar novas listas e vincular variáveis a elas. Os elementos dessas


listas são eles próprios listas. As três declarações de impressão
print('Univs =', Univs) print('Univs1
=', Univs1) print(Univs == Univs1)

produzir a saída
Univs = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]
Univs1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]
Verdadeiro

Parece que Univs e Univs1 estão vinculados ao mesmo valor. Mas as


aparências podem enganar. Como ilustra a Figura 5-2 , Univs e Univs1
estão vinculados a valores bem diferentes.
Machine Translated by Google

Figura 5-2 Duas listas que parecem ter o mesmo valor, mas não

Que Univs e Univs1 estão vinculados a objetos diferentes pode ser verificado usando a função
integrada do Python id, que retorna um identificador inteiro exclusivo para um objeto. Essa função
nos permite testar a igualdade do objeto comparando seu id. Uma maneira mais simples de testar
a igualdade de objeto é usar o operador is . Quando executamos o código

print(Univs == Univs1) #teste a igualdade do valor print(id(Univs)


== id(Univs1)) #teste a igualdade do objeto print(Univs is Univs1) #teste a
igualdade do objeto print('Id of Univs =', id(Univs )) print('Id de Univs1
=', id(Univs1))

ele imprime

Verdadeiro

Falso
Falso
Id da Univs = 4946827936
ID de Univs1 = 4946612464

(Não espere ver os mesmos identificadores exclusivos se você executar este código. A
semântica do Python não diz nada sobre qual identificador está associado a cada objeto; ela apenas
exige que dois objetos não tenham o mesmo identificador.)
Machine Translated by Google

Observe que na Figura 5-2 os elementos de Univs não são cópias das listas
às quais Techs e Ivys estão vinculados, mas sim as próprias listas. Os elementos
de Univs1 são listas que contêm os mesmos elementos que as listas em Univs,
mas não são as mesmas listas. Podemos ver isso executando o código

print('Ids de Univs[0] e Univs[1]', id(Univs[0]), id(Univs[1])) print('Ids de Univs1[0]


e Univs1[1]',
id( Univs1[0]), id(Univs1[1]))

que imprime
IDs de Univs[0] e Univs[1] 4447807688 4456134664
IDs de Univs1[0] e Univs1[1] 4447805768 4447806728

Por que tanto alarido sobre a diferença entre valor e objeto


igualdade? É importante porque as listas são mutáveis. Considere o código
Techs.append('RPI')

O método append para listas tem um efeito colateral. Em vez de criar uma nova
lista, ele altera a lista existente, Techs, adicionando um novo elemento, a string
'RPI' neste exemplo, ao final dela. A Figura 5-3 descreve o estado da computação
após a execução do acréscimo .

Figura 5-3 Demonstração de mutabilidade


Machine Translated by Google

O objeto ao qual o Univs está vinculado ainda contém as mesmas duas


listas, mas o conteúdo de uma dessas listas foi alterado.
Consequentemente, as declarações de impressão

print('Univs =', Univs) print('Univs1 =',


Univs1)

agora produzir a saída


Univs = [['MIT', 'Caltech', 'RPI'], ['Harvard', 'Yale',
'Marrom']]
Univs1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]

O que temos aqui é chamado de aliasing. Existem dois caminhos


distintos para o mesmo objeto de lista. Um caminho é através da variável
Techs e o outro através do primeiro elemento do objeto de lista ao qual Univs
está vinculado. Podemos modificar o objeto através de qualquer um dos
caminhos, e o efeito da mutação será visível através de ambos os caminhos.
Isso pode ser conveniente, mas também pode ser traiçoeiro. Aliasing não
intencional leva a erros de programação que muitas vezes são extremamente
difíceis de rastrear. Por exemplo, o que você acha que é impresso por
L1 = [[]]*2 L2 =
[[], []] para i no
intervalo(len(L1)): L1[i].append(i)

L2[i].append(i) print('L1
=', L1, 'mas', 'L2 =', L2)

Imprime L1 = [[0, 1], [0, 1]] mas L2 = [[0], [1]]. Por que?
Porque a primeira instrução de atribuição cria uma lista com dois elementos,
cada um dos quais é o mesmo objeto, enquanto a segunda instrução de
atribuição cria uma lista com dois objetos diferentes, cada um dos quais é
inicialmente igual a uma lista vazia.

Exercício de dedo: O que o código a seguir imprime?


L = [1, 2, 3]
L.append(L)
imprimir(L é L[-1])

A interação de aliasing e mutabilidade com o parâmetro padrão


valores é algo a se observar. Considere o código
Machine Translated by Google

def append_val(val, list_1 = []): List_1.append(val)


print(list_1)

append_val(3)
append_val(4)

Você pode pensar que a segunda chamada para append_val imprimiria a lista [4]
porque teria anexado 4 à lista vazia. Na verdade, ele imprimirá [3, 4]. Isso ocorre
porque, na hora da definição da função, é criado um novo objeto do tipo lista , com
valor inicial da lista vazia. Cada vez que append_val é chamado sem fornecer um valor
para o parâmetro formal list_1, o objeto criado na definição da função é vinculado a
list_1, modificado e impresso. Portanto, a segunda chamada para append_val muda e,
em seguida, imprime uma lista que já foi alterada pela primeira chamada para essa
função.

Quando anexamos uma lista a outra, por exemplo, Techs.append(Ivys), a estrutura


original é mantida. O resultado é uma lista que contém uma lista. Suponha que não
queremos manter essa estrutura, mas queremos adicionar os elementos de uma lista
em outra lista. Podemos fazer isso usando a concatenação de lista (usando o operador
+ ) ou o método extend , por exemplo,

L1 = [1,2,3]
L2 = [4,5,6]
L3 = L1 + L2
imprimir('L3 =', L3)
L1.extend(L2)
print('L1 =', L1)
L1.append(L2)
print('L1 =', L1)

vai imprimir

L3 = [1, 2, 3, 4, 5, 6]
L1 = [1, 2, 3, 4, 5, 6]
L1 = [1, 2, 3, 4, 5, 6, [4, 5, 6]]

Observe que o operador + não tem efeito colateral. Ele cria uma nova lista e a retorna. Em contraste,
estenda e anexe cada mutação L1.
A Figura 5-4 descreve brevemente alguns dos métodos associados com
listas. Observe que todos eles, exceto count e index, modificam a lista.
Machine Translated by Google

Figura 5-4 Métodos comuns associados a listas

5.3.1 Clonagem
Geralmente é prudente evitar a mutação de uma lista sobre a qual se está
iterando. Considere o código

def remove_dups(L1, L2):


"""Assume que L1 e L2 são listas.
Remove qualquer elemento de L1 que também ocorra em L2""" para e1 em L1: se e1 em L2:

L1.remove(e1)
L1 = [1,2,3,4]
L2 = [1,2,5,6]
Remove_dups(L1, L2) print('L1 =',
L1)

Você pode se surpreender ao descobrir que isso imprime


L1 = [2, 3, 4]

Durante um loop for , o Python rastreia onde está na lista usando um


contador interno que é incrementado no final de cada iteração. Quando o valor
do contador atinge o comprimento atual da lista, o loop termina. Isso funciona
como você pode esperar se a lista
Machine Translated by Google

não sofre mutação dentro do loop, mas pode ter consequências surpreendentes
se a lista sofrer mutação. Nesse caso, o contador oculto começa em 0,
descobre que L1[0] está em L2 e o remove - reduzindo o comprimento de L1
para 3. O contador é então incrementado para 1 e o código procede para
verificar se o valor de L1[1] está em L2. Observe que este não é o valor original
de L1[1] (ou seja, 2), mas sim o valor atual de L1[1] (ou seja, 3). Como você
pode ver, é possível descobrir o que acontece quando a lista é modificada
dentro do loop. No entanto, não é fácil. E o que acontece provavelmente não
é intencional, como neste exemplo.
Uma maneira de evitar esse tipo de problema é usar o slicing para
clonar35 (ou seja, fazer uma cópia) da lista e escrever para e1 em L1[:].
Observe que a escrita

new_L1 = L1 para
e1 em new_L1:

não resolveria o problema. Não criaria uma cópia de L1, mas simplesmente
introduziria um novo nome para a lista existente.
Fatiar não é a única maneira de clonar listas em Python. A expressão
L.copy() tem o mesmo valor que L[:]. Tanto o fatiamento quanto a cópia
executam o que é conhecido como cópia superficial. Uma cópia rasa cria
uma nova lista e insere os objetos (não cópias dos objetos) da lista a ser
copiada na nova lista. O código
L = [2]
L1 = [L]
L2 = L1[:]
L2 = cópia.deepcopy(L1)
L.append(3)
print(f'L1 = {L1}, L2 = {L2}')

imprime L1 = [[2, 3]] L2 = [[2, 3]] porque L1 e L2 contêm o objeto que foi
vinculado a L na primeira instrução de atribuição.

Se a lista a ser copiada contém objetos mutáveis que você também usa
deseja copiar, importe a função do módulo de biblioteca cópia de

padrão copy.deepcopy para fazer uma cópia profunda. O método deepcopy


cria uma nova lista e então insere cópias dos objetos na lista a serem copiados
na nova lista. Se substituirmos a terceira linha no código acima por L2 =
copy.deepcopy(L1), imprimiremos L1 = [[2, 3]], L2 = [[2]], porque L1 não
conteria o objeto ao qual L está vinculado.
Machine Translated by Google

Entender copy.deepcopy é complicado se os elementos de uma lista forem


listas contendo listas (ou qualquer tipo mutável). Considerar
L1 = [2]
L2 = [[L1]]
L3 = copiar. deepcopy(L2)
L1.apêndice(3)

O valor de L3 será [[[2]]] porque copy.deepcopy cria um novo objeto não apenas
para a lista [L1], mas também para a lista L1. Ou seja, faz cópias até o final —
na maioria das vezes. Por que “na maioria das vezes?” O código

L1 = [2]
L1.apêndice(L1)

cria uma lista que contém a si mesma. Uma tentativa de fazer cópias até o fundo
nunca terminaria. Para evitar esse problema, copy.deepcopy faz exatamente
uma cópia de cada objeto e, em seguida, usa essa cópia para cada instância do
objeto. Isso é importante mesmo quando as listas não contêm a si mesmas. Por
exemplo,
L1 = [2]
L2 = [L1, L1]
L3 = copiar. deepcopy(L2)
L3[0].apêndice(3)
imprimir(L3)

imprime [[2, 3], [2, 3]] porque copy.deepcopy faz uma cópia de L1 e a usa nas
duas vezes em que L1 ocorre em L2.

5.3.2 Compreensão de lista A


compreensão de lista fornece uma maneira concisa de aplicar uma operação
aos valores de sequência fornecidos pela iteração sobre um valor iterável. Ele
cria uma nova lista na qual cada elemento é o resultado da aplicação de uma
determinada operação a um valor de um iterável (por exemplo, os elementos de
outra lista). É uma expressão da forma
[expr para elemento em iterável se teste]

Avaliar a expressão é equivalente a invocar a função


Machine Translated by Google

def f(expr, lista_antiga, teste = lambda x: Verdadeiro): lista_nova = []

para e em iterável:
if test(e):
new_list.append(expr(e)) return new_list

Por exemplo, [e**2 for e in range(6)] resulta em [0, 1, 4, 9, 16, 25], [e**2 for e in range(8) if e%2 == 0] é avaliado
como [0, 4, 16, 36], e [x**2 for x in [2, 'a', 3, 4.0] se type(x) == int] é avaliado como [4, 9] .

A compreensão de lista fornece uma maneira conveniente de inicializar listas.


Por exemplo, [[] for in range(10)]
_ gera uma lista contendo 10 listas vazias distintas
(ou seja, sem alias). O nome da variável indica que os valores dessa _variável não
são utilizados na geração dos elementos da lista, ou seja, é apenas um
placeholder. Essa convenção é comum em programas Python.

Python permite várias instruções for dentro de uma lista


compreensão. Considere o código

L = [(x, y) para x
no intervalo(6) se x%2 == 0 para y no intervalo(6)
se y%3 == 0]

O interpretador Python começa avaliando o primeiro for, atribuindo a x a sequência


de valores 0,2,4. Para cada um desses três valores avalia-se o segundo para x,
(o
que gera a sequência de valores 0,3 de cada vez). Em seguida, adiciona à lista
que está sendo gerada a tupla (x, y), produzindo a lista

[(0, 0), (0, 3), (2, 0), (2, 3), (4, 0), (4, 3)]

Claro, podemos produzir a mesma lista sem compreensão de lista, mas o código
é consideravelmente menos compacto:
L = []
para x no intervalo(6): se x%2
== 0: para y no
intervalo(6): se y%3 == 0:
L.append((x, y))
Machine Translated by Google

O código a seguir é um exemplo de aninhamento de uma compreensão de lista


dentro de uma compreensão de lista.
print([[(x,y) para x no intervalo(6) se x%2 == 0]
para y no intervalo(6) se y%3 == 0])

Imprime [[(0, 0), (2, 0), (4, 0)], [(0, 3), (2, 3), (4, 3)]].
É preciso prática para se familiarizar com as compreensões de lista
aninhadas, mas elas podem ser bastante úteis. Vamos usar as compreensões
de lista aninhada para gerar uma lista de todos os números primos menores que
100. A ideia básica é usar uma compreensão para gerar uma lista de todos os
números candidatos (ou seja, de 2 a 99), uma segunda compreensão para gerar
uma lista dos restos da divisão de um primo candidato por cada divisor potencial,
e a função interna all para testar se algum desses restos é 0.

[x for x in range(2, 100) if all(x % y != 0 for y in range(3, x))]

Avaliar a expressão é equivalente a invocar a função


def gen_primes(): primos
= [] for x in
range(2, 100): is_prime = True for
y in range(3, x): if x%y
== 0: is_prime = False if
is_prime:
primes.append( x)

retorna primos

Exercício de dedo: Escreva uma lista de compreensão que gere todos os não
primos entre 2 e 100.

Alguns programadores Python usam compreensão de lista de maneiras


maravilhosas e sutis. Isso nem sempre é uma ótima ideia.
Lembre-se de que outra pessoa pode precisar ler seu código e “sutil” raramente
é uma propriedade desejável para um programa.

5.4 Operações de ordem superior em listas


Machine Translated by Google

Na Seção 4.4, introduzimos a noção de programação de ordem superior.


Pode ser particularmente conveniente com listas, conforme mostrado na
Figura 5-5.

Figura 5-5 Aplicando uma função aos elementos de uma lista

A função apply_to_each é chamada de ordem superior porque tem


um argumento que é uma função. Na primeira vez que é chamado, ele
muda L aplicando a função interna unária abs a cada elemento. Na
segunda vez que é chamado, ele aplica uma conversão de tipo a cada
elemento. E na terceira vez que é chamado, substitui cada elemento
pelo resultado da aplicação de uma função definida por lambda. imprime
L = [1, -2, 3,33]
Aplique abdominais a cada elemento de L.
L = [1, 2, 3,33]
Aplique int a cada elemento de [1, 2, 3.33].
L = [1, 2, 3]
Aplique o quadrado a cada elemento de [1, 2, 3].
L = [1, 4, 9]

Python tem uma função interna de ordem superior, map, que é


semelhante, mas mais geral, à função apply_to_each definida na Figura
5-5. Em sua forma mais simples, o primeiro argumento para mapear é unário
Machine Translated by Google

função (ou seja, uma função que tem apenas um parâmetro) e o segundo
argumento é qualquer coleção ordenada de valores adequados como argumentos
para o primeiro argumento. É freqüentemente usado no lugar de uma
compreensão de lista. Por exemplo, list(map(str, range(10))) é equivalente a
[str(e) for e in range(10)]. A função é freqüentemente
O mapa usada com um loop for . Quando usado em um for se comporta
loop, como a função em que faixaretorna um valor
mapa para cada iteração do loop. Esses valores são gerados aplicando o
primeiro argumento a cada elemento do segundo argumento. Por exemplo, o
código

para i no mapa(lambda x: x**2, [2, 6, 4]): print(i)

estampas

4
36
16

De forma mais geral, o primeiro argumento para map pode ser uma função
de n argumentos, caso em que deve ser seguido por n coleções ordenadas
subsequentes (cada uma com o mesmo tamanho). Por exemplo, o código

L1 = [1, 28, 36]


L2 = [2, 57, 9] para i no
mapa(min, L1, L2): print(i)

estampas

1
28
9

Exercício de dedo: Implemente uma função que satisfaça a seguinte


especificação. Dica: será conveniente usar lambda no corpo da implementação.

def f(L1, L2):


"""Listas L1, L2 com o mesmo comprimento de números retornam
a soma de elevar cada elemento em L1 à potência do elemento no
mesmo índice em L2
Por exemplo, f([1,2], [2,3]) retorna 9"""
Machine Translated by Google

5.5 Strings, tuplas, intervalos e listas

Vimos quatro tipos de sequência iteráveis: str, tupla, intervalo e lista. Eles são semelhantes
porque objetos desses tipos podem ser operados conforme descrito na Figura 5-6. Algumas
de suas outras semelhanças e diferenças estão resumidas na Figura 5-7.

Figura 5-6 Operações comuns em tipos de sequência

Figura 5-7 Comparação de tipos de sequência

Os programadores Python tendem a usar listas com muito mais frequência do que tuplas.
Como as listas são mutáveis, elas podem ser construídas de forma incremental durante uma
computação. Por exemplo, o código a seguir cria incrementalmente uma lista contendo todos
os números pares em outra lista.
Machine Translated by Google

even_elems = [] para
e em L:
se e%2 == 0:
even_elems.append(e)

Como strings podem conter apenas caracteres, elas são


consideravelmente menos versáteis do que tuplas ou listas. Por outro lado,
quando você está trabalhando com uma string de caracteres, existem muitos
métodos integrados úteis. A Figura 5-8 contém descrições curtas de alguns deles.
Lembre-se de que, como as strings são imutáveis, todas elas retornam
valores e não têm efeito colateral.

Figura 5-8 Alguns métodos em strings

Um dos métodos integrados mais úteis é o split, que recebe duas strings
como argumentos. O segundo argumento especifica um separador que é
usado para dividir o primeiro argumento em uma sequência de substrings.
Por exemplo,
Machine Translated by Google

print('Meu professor favorito–John G.–rocks'.split(' ')) print('Meu professor favorito–


John G.–rocks'.split('-')) print('Meu professor favorito–John G .–rocks'.split('–'))

estampas

['Meu', 'favorito', 'professor–John', 'G.–rocks']


['Meu professor favorito', '', 'John G.', '', 'rocks']
['Meu professor favorito', 'John G.', 'rocks']

O segundo argumento é opcional. Se esse argumento for omitido, a primeira string


será dividida usando strings arbitrárias de caracteres de espaço em branco (espaço,
tabulação, nova linha, retorno e feed de formulário).36

5.6 Conjuntos

Conjuntos são outro tipo de tipo de coleção. Eles são semelhantes à noção de um
conjunto em matemática, pois são coleções desordenadas de elementos únicos. Eles são
denotados usando o que os programadores chamam de chaves e os matemáticos
chamam de chaves de conjunto, por exemplo,

baseball_teams = {'Dodgers', 'Giants', 'Padres', 'Rockies'} football_teams = {'Giants', 'Eagles',


'Cardinals',
'Cowboys'}

Como os elementos de um conjunto não estão ordenados, tentar indexar em um


conjunto, por exemplo, avaliar baseball_teams[0], gera um erro de tempo de execução.
Podemos usar uma instrução for para iterar sobre os elementos de um conjunto, mas, ao
contrário dos outros tipos de coleção que vimos, a ordem na qual os elementos são
produzidos é indefinida.
Assim como as listas, os conjuntos são mutáveis. Adicionamos um único elemento a
um conjunto usando o método add . Adicionamos vários elementos a um conjunto
passando uma coleção de elementos (por exemplo, uma lista) para o método update .
Por exemplo, o código

baseball_teams.add('Yankees')
football_teams.update(['Patriots', 'Jets']) print(baseball_teams)
print(football_teams)

estampas
Machine Translated by Google

{'Dodgers', 'Yankees', 'Padres', 'Rockies', 'Giants'}


{'Jets', 'Eagles', 'Patriots', 'Cowboys', 'Cardeais',
'Gigantes'}

(A ordem na qual os elementos aparecem não é definida pelo idioma, portanto, você pode obter
uma saída diferente se executar este exemplo.)

Os elementos podem ser removidos de um conjunto usando o método remove , que gera
um erro se o elemento não estiver no conjunto, ou o método discard , que não gera um erro se o
elemento não estiver no conjunto.
A participação em um conjunto pode ser testada usando o operador in . Por exemplo, 'Rockies' em
baseball_teams retorna True. A união dos métodos binários , a interseção, a diferença e o issubset têm
seus significados matemáticos usuais. Por exemplo,

print(baseball_teams.union({1, 2}))
print(baseball_teams.intersection(football_teams))
print(baseball_teams.difference(football_teams)) print({'Padres',
'Yankees'}.issubset(baseball_teams))

estampas

{'Padres', 'Rockies', 1, 2, 'Giants', 'Dodgers', 'Yankees'}


{'Gigantes'}
{'Padres', 'Rockies', 'Dodgers', 'Yankees'}
Verdadeiro

Uma das coisas boas sobre conjuntos é que existem operadores infixos convenientes para muitos
dos métodos, incluindo | para união, & para interseção, - para diferença, <= para subconjunto e >= para
superconjunto. O uso desses operadores facilita a leitura do código. Compare, por exemplo,

print(baseball_teams | {1, 2}) print(baseball_teams


& football_teams) print(baseball_teams - football_teams)
print({'Padres', 'Yankees'} <= baseball_teams)

ao código apresentado anteriormente, que usa notação de ponto para imprimir os mesmos valores.

Nem todos os tipos de objetos podem ser elementos de conjuntos. Todos os objetos em um
conjunto devem ser passíveis de hash. Um objeto é hashável se tiver
Machine Translated by Google

Um método __hash__ que mapeia o objeto do tipo para um int e o valor retornado
por __hash__ não muda durante o tempo de vida do objeto e

Um __eq__ método que é usado para compará-lo quanto à igualdade com outros
objetos.

Todos os objetos dos tipos imutáveis escalares do Python são passíveis de hash, e
nenhum objeto dos tipos mutáveis integrados do Python é passível de hash. Um objeto de
um tipo imutável não escalar (por exemplo, uma tupla) é hashável se todos os seus
elementos forem hasháveis.

5.7 Dicionários

Objetos do tipo dict (abreviação de dicionário) são como listas, exceto pelo fato de que
os indexamos usando chaves em vez de números inteiros. Qualquer objeto hashable pode
ser usado como uma chave. Pense em um dicionário como um conjunto de pares chave/valor.
Literais do tipo dict são colocados entre chaves e cada elemento é escrito como uma chave
seguida por dois pontos seguidos por um valor. Por exemplo, o código,

números_meses = {'Jan':1, 'Fev':2, 'Mar':3, 'Abr':4,


'5 de maio,
1:'Jan', 2:'Fev', 3:'Mar', 4:'Abr', 5:'Maio'}
print(month_numbers) print('O
terceiro mês é ' + months_numbers[3]) dist = months_numbers['Apr'] -
months_numbers['Jan'] print('Abr e Jan are', dist, 'meses separados')

vai imprimir

{'Jan': 1, 'Fev': 2, 'Mar': 3, 'Abr': 4, 'Maio': 5, 1: 'Jan',


2: 'Fev', 3: 'Mar', 4: 'Abr', 5: 'Maio'}
O terceiro mês é março
abril e janeiro tem 3 meses de diferença

As entradas em um dict não podem ser acessadas usando um índice. É por isso que
months_numbers[1] refere-se inequivocamente à entrada com a chave 1 em vez da
segunda entrada. Se uma chave é definida em um dicionário pode ser testado usando o
operador in .
Machine Translated by Google

Assim como as listas, os dicionários são mutáveis. Podemos adicionar uma


entrada escrevendo, por exemplo, months_numbers['June'] = 6 ou alterar uma entrada
escrevendo, por exemplo, months_numbers['May'] = 'V'.
Os dicionários são uma das grandes vantagens do Python. Eles reduzem muito a
dificuldade de escrever uma variedade de programas. Por exemplo, na Figura 5-9
usamos dicionários para escrever um programa (bastante horrível) para traduzir entre
idiomas.
O código na figura imprime

Eu bebo vinho tinto "bom" e como pão.


Eu bebo vinho tinto.

Lembre-se de que os dicionários são mutáveis. Portanto, tenha cuidado com os


efeitos colaterais. Por exemplo,

FtoE['madeira'] = 'madeira'
print(translate('Eu bebo vinho tinto.', dicts, 'Francês para Inglês'))

vai imprimir
Eu madeira de vinho tinto.
Machine Translated by Google

Figura 5-9 Traduzindo texto (mal)

Muitas linguagens de programação não contêm um tipo interno que forneça


um mapeamento de chaves para valores. Em vez disso, os programadores usam
outros tipos para fornecer funcionalidade semelhante. É, por exemplo,
relativamente fácil implementar um dicionário usando uma lista em que cada
elemento é uma tupla que representa um par chave/valor. Podemos então
escrever uma função simples que faz a recuperação associativa, por exemplo,
Machine Translated by Google

def key_search(L, k): para elem


em L:
if elem[0] == k: return
elem[1] return None

O problema com tal implementação é que ela é computacionalmente


ineficiente. No pior caso, um programa pode ter que examinar cada elemento
da lista para realizar uma única recuperação. Em contraste, a implementação
integrada é rápida. Ele usa uma técnica chamada hashing, descrita no Capítulo
12, para fazer a pesquisa no tempo que é quase independente do tamanho do
dicionário.
Existem várias maneiras de usar uma instrução for para iterar as entradas
em um dicionário. Se d for um dicionário, um loop da forma para k em d itera
sobre as chaves de d. A ordem em que as chaves são escolhidas é a ordem
em que as chaves foram inseridas no dicionário.37 Por exemplo,

maiúsculas = {'França': 'Paris', 'Itália': 'Roma', 'Japão':


'Kyoto'}
para chave em maiúsculas:
print('A capital de', chave, 'é', maiúsculas[chave])

estampas

A capital da França é Paris


A capital da Itália é Roma
A capital do Japão é Quioto

Para iterar sobre os valores em um dicionário, podemos usar o método


values. Por exemplo,
cities = [] for val
in capitals.values(): cities.append(val)
print(cities, 'é uma lista de
capitais')

imprime ['Paris', 'Roma', 'Kyoto'] é uma lista de capitais.


Os valores do método retornam um objeto do tipo dict_values. Este é um
exemplo de um objeto de exibição. Um objeto de exibição é dinâmico porque,
se o objeto ao qual está associado for alterado, a alteração será visível por
meio do objeto de exibição. Por exemplo, o código
Machine Translated by Google

cap_vals = capitais.values() print(cap_vals)


capitais['Japão'] =
'Tóquio' print(cap_vals)

estampas

dict_values(['Paris', 'Roma', 'Kyoto']) dict_values(['Paris',


'Roma', 'Toyko'])

Da mesma forma, as chaves do método retornam um objeto de exibição do tipo dict_keys.


Objetos de exibição podem ser convertidos em listas, por exemplo, list(capitals.values())
retorna uma lista dos valores em maiúsculas.
Para iterar sobre pares chave/valor, usamos o método items. Este método retorna
um objeto de exibição do tipo dict_items. Cada elemento de um objeto do tipo dict_items
é uma tupla de uma chave e seu valor associado.
Por exemplo, o código

para chave, val em maiúsculas.items():


print(val, 'é a capital de', chave)

estampas

Paris é a capital da França


Roma é a capital da Itália
Tóquio é a capital do Japão

Exercício de dedo: implemente uma função que atenda à especificação

def get_min(d): """da


dict mapeando letras para ints
retorna o valor em d com a chave que ocorre primeiro
no
alfabeto. Por exemplo, se d = {x = 11, b = 12}, get_min retorna 12."""

Geralmente é conveniente usar tuplas como chaves. Imagine, por exemplo, usar
uma tupla da forma (flight_number, day) para representar voos de companhias aéreas.
Seria então fácil usar essas tuplas como chaves em um dicionário implementando um
mapeamento de voos para horários de chegada. Uma lista não pode ser usada como
chave, porque objetos do tipo lista não são passíveis de hash.

Como vimos, existem muitos métodos úteis associados aos dicionários, incluindo
alguns para remover elementos. Nós não
Machine Translated by Google

enumere todos eles aqui, mas os usará conforme conveniente em


exemplos mais adiante neste livro. A Figura 5-10 contém algumas das
operações mais úteis em dicionários.

Figura 5-10 Algumas operações comuns em dicts

5.8 Compreensão do Dicionário

A compreensão do dicionário é semelhante à compreensão da lista.


A forma geral é
{chave: valor para id1, id2 em iterável se teste}

A principal diferença (além do uso de colchetes em vez de colchetes)


é que ele usa dois valores para criar cada elemento do dicionário e
permite (mas não exige) que o iterável retorne dois valores por vez.
Considere um dicionário mapeando alguns dígitos decimais para
palavras em inglês:
number_to_word = {1: 'um', 2: 'dois', 3: 'três', 4: 'quatro', 10: 'dez'}
Machine Translated by Google

Podemos facilmente usar a compreensão do dicionário para produzir um dicionário


que mapeie palavras para dígitos com

word_to_number = {w: d para d, w em number_to_word.items()}

Se decidirmos que queremos apenas números de um dígito em word_to_number,


podemos usar a compreensão

word_to_number = {w: d para d, w em number_to_word.items() se d < 10}

Agora, vamos tentar algo mais ambicioso. Uma cifra é um algoritmo que mapeia
um texto simples (um texto que pode ser facilmente lido por um ser humano) para
um texto criptografado. As cifras mais simples são cifras de substituição que
substituem cada caractere no texto simples por uma string única. O mapeamento
dos caracteres originais para a string que os substitui é chamado de chave (por
analogia com o tipo de chave usada para abrir uma fechadura, não o tipo de chave
usada nos dicionários Python). Em Python, os dicionários fornecem uma maneira
conveniente de implementar mapeamentos que podem ser usados para codificar e
decodificar texto.
Uma cifra de livro é uma cifra para a qual a chave é derivada de um livro. Por
exemplo, pode mapear cada caractere no texto simples para o índice numérico da
primeira ocorrência desse caractere no livro (ou em uma página do livro). A suposição
é que o remetente e o destinatário da mensagem codificada concordaram
previamente sobre o livro, mas um adversário que intercepta a mensagem codificada
não sabe qual livro foi usado para codificá-la.

A definição de função a seguir usa compreensão de dicionário para criar um


dicionário que pode ser usado para codificar um texto simples usando uma cifra de
livro.

gen_code_keys = (livro lambda, texto simples:(


{c: str(book.find(c)) para c em texto simples}))

Se texto simples fosse “não é não” e o livro começasse com “Era uma vez, em
uma casa em uma terra distante”, a chamada gen_code_keys(livro, texto_simples)
retornaria

{'n': '1', 'o': '7', ' ': '4', 'i': '13', 's': '26'}

Observe, a propósito, que o é mapeado para sete em vez de zero porque o e O são
caracteres diferentes. Se o livro fosse o texto de Don
Machine Translated by Google

Quixote, 38 a chamada gen_code_keys(book, plain_text) retornaria

{'n': '1', 'o': '13', ' ': '2', 'i': '6', 's': '57'}

Agora que temos nosso dicionário de codificação, podemos usar a


compreensão de lista para definir uma função que a usa para criptografar um texto
simples

encoder = (lambda code_keys, plain_text: ''.join(['*' +


code_keys[c] for c in plain_text])[1:])

Como os caracteres no texto simples podem ser substituídos por vários caracteres
no texto cifrado, usamos * para separar os caracteres no texto cifrado. O
operador .join é usado para transformar a lista de strings em uma única string.

A função criptografar usa gen_code_keys e codificador para criptografar um


texto simples

criptografar = (livro lambda, texto_simples:


encoder(gen_code_keys(livro, texto_simples), texto_simples))

A chamada encrypt(Don_Quixote, 'não é não') retorna

1*13*2*6*57*2*1*13

Antes de podermos decodificar o texto cifrado, precisamos construir um


dicionário de decodificação. A coisa mais fácil de fazer seria inverter o dicionário
de codificação, mas isso seria trapaça. O ponto principal de uma cifra de livro é
que o remetente envia uma mensagem criptografada, mas nenhuma informação
sobre as chaves. A única coisa que o receptor precisa para decodificar a mensagem
é o acesso ao livro que o codificador usou. A definição de função a seguir usa a
compreensão do dicionário para criar uma chave de decodificação do livro e da
mensagem codificada.

gen_decode_keys = (livro lambda, cipher_text:


{s: book[int(s)] for s in cipher_text.split('*')})

A chamada gen_decode_keys(Don_Quixote, '1*13*2*6*57*2*1*13')


produziria a chave de decodificação

{'1': 'n', '13': 'o', '2': ' ', '6': 'i', '57': 's'}
Machine Translated by Google

Se um caractere ocorre no texto simples, mas não no livro, algo ruim acontece. O
dicionário code_keys irá mapear cada um desses caracteres para -1, e decode_keys irá
mapear -1 para qualquer que seja o último caractere do livro.

Exercício com os dedos: Resolva o problema descrito no parágrafo anterior. Dica: uma
maneira simples de fazer isso é criar um novo livro anexando algo ao livro original.

Exercício de dedo: usando o codificador e a criptografia como modelos, implemente as


funções decodificador e descriptografado. Use-os para descriptografar a mensagem

22*13*33*137*59*11*23*11*1*57*6*13*1*2*6*57*2*6*1*22*13*33*1
37*59*11*23*11*1*57*6*173*7*11

que foi criptografado usando a abertura de Dom Quixote.

5.9 Termos Introduzidos no Capítulo

lista

de iteradores de tipo de

objeto iterável de

atribuição
múltipla de tupla

função de id de

tipo imutável de tipo


mutável

efeito colateral de

igualdade de objeto

cópia rasa

de

clonagem de aliasing
Machine Translated by Google

compreensão

da lista de cópia profunda

caractere de espaço em branco

da função de ordem superior


definir

valor das chaves

do dicionário

de
tipo hashável

ver objeto

dicionário compreensão livro cifra

33 Haldeman era chefe de gabinete de Nixon durante o Watergate


audiências. Haldeman afirmou que esta foi a resposta de Nixon a uma proposta
de pagar o silêncio. O registro gravado sugere o contrário.

34 Lembre-se de que, por enquanto, você deve pensar em um método simplesmente como uma
função que é invocada usando a notação de ponto.

35 A clonagem de seres humanos levanta uma série de questões técnicas, éticas e


enigmas espirituais. Felizmente, a clonagem de objetos Python não.

36 Como strings Python suportam Unicode, a lista completa de


caracteres de espaço em branco é muito mais longo
(consulte https://en.wikipedia.org/wiki/Whitespace_character).

37 Até o Python 3.7, a semântica da linguagem não definia


a ordem das chaves.

38 O livro começa assim: “Num povoado de La Mancha, cujo nome


Não tenho nenhum desejo de lembrar, viveu não muito tempo desde que um dos
Machine Translated by Google

aqueles cavalheiros que mantêm uma lança no porta-lanças, um


broquel velho, um traseiro magro e um galgo para correr.
Machine Translated by Google

RECURSÃO E VARIÁVEIS GLOBAIS

Você pode ter ouvido falar de recursão e provavelmente pensa nela como uma
técnica de programação bastante sutil. Essa é uma lenda urbana encantadora
espalhada por cientistas da computação para fazer as pessoas pensarem que
somos mais inteligentes do que realmente somos. A recursão é uma ideia
importante, mas não é tão sutil e é mais do que uma técnica de programação.
Como método descritivo, a recursão é amplamente utilizada, mesmo por
pessoas que nunca sonhariam em escrever um programa. Considere parte do
código legal dos Estados Unidos que define a noção de cidadania por “direito de
primogenitura”. Grosso modo, a definição é a seguinte

Qualquer criança nascida dentro dos Estados


Unidos ou Qualquer criança nascida em casamento fora dos Estados
Unidos, cujo um dos pais seja cidadão dos Estados Unidos.

A primeira parte é simples; se você nasceu nos Estados Unidos, você é um


cidadão natural (como Barack Obama). Se você não nasceu nos EUA, depende
se seus pais eram cidadãos americanos na época de seu nascimento. E se seus
pais eram cidadãos americanos pode depender de seus pais serem cidadãos
americanos, e assim por diante.

Em geral, uma definição recursiva é composta de duas partes. Há pelo menos


um caso base que especifica diretamente o resultado para um caso especial
(caso 1 no exemplo acima) e há pelo menos um caso recursivo (indutivo) (caso
2 no exemplo acima) que define a resposta em termos da resposta à pergunta em
alguma outra entrada, geralmente uma versão mais simples do mesmo problema.
É a presença de um caso base que evita que uma definição recursiva seja uma
definição circular.39
Machine Translated by Google

A definição recursiva mais simples do mundo é provavelmente a função


fatorial (normalmente escrita em matemática usando !) em números
naturais.40 A definição indutiva clássica é

A primeira equação define o caso base. A segunda equação define fatorial


para todos os números naturais, exceto o caso base, em termos do fatorial
do número anterior.
A Figura 6-1 contém uma implementação iterativa (fact_iter) e recursiva
(fact_rec) de fatorial.

Figura 6-1 Implementações iterativas e recursivas de fatorial

Figura 6-1 Implementações iterativas e recursivas de fatorial Esta


função é suficientemente simples para que nenhuma das
implementações seja difícil de seguir. Ainda assim, a segunda é uma
tradução mais direta da definição recursiva original.
Quase parece trapaça implementar fact_rec chamando fact_rec de
dentro do corpo de fact_rec. Funciona pelo mesmo motivo que a
implementação iterativa funciona. Sabemos que a iteração em fact_iter
terminará porque n começa positivo e cada vez que volta ao loop é reduzido
em 1. Isso significa que não pode ser maior que 1 para sempre. Da mesma
forma, se fact_rec for chamado com 1, ele retornará um valor sem fazer
uma chamada recursiva. Quando faz uma chamada recursiva, sempre o
faz com um valor menor que o
Machine Translated by Google

valor com o qual foi chamado. Eventualmente, a recursão termina com a chamada
fact_rec(1).

Exercício do dedo: A soma harmônica de um número inteiro, n > 0, pode ser

calculada usando a fórmula que . Escreva uma função recursiva


calcula isso.

6.1 Números de Fibonacci

A sequência de Fibonacci é outra função matemática comum que geralmente é


definida recursivamente. “Eles se reproduzem como coelhos” é freqüentemente
usado para descrever uma população que o falante acha que está crescendo rápido
demais. No ano de 1202, o matemático italiano Leonardo de Pisa, também
conhecido como Fibonacci, desenvolveu uma fórmula para quantificar essa noção,
embora com algumas suposições não muito realistas.41
Suponha que um par de coelhos recém-nascidos, um macho e uma fêmea,
sejam colocados em um cercado (ou pior, soltos na natureza). Suponha ainda que
os coelhos possam acasalar com a idade de um mês (o que, surpreendentemente,
algumas raças podem) e ter um período de gestação de um mês (o que,
surpreendentemente, algumas raças têm). Finalmente, suponha que esses coelhos
míticos nunca morram (não é uma propriedade de nenhuma raça de coelho
conhecida) e que a fêmea sempre produza um novo par (um macho, uma fêmea)
todos os meses a partir do segundo mês. Quantas coelhas haverá ao final de seis
meses?
No último dia do primeiro mês (chamemos de mês 0), haverá uma fêmea
(pronta para conceber no primeiro dia do próximo mês). No último dia do segundo
mês, ainda haverá apenas uma fêmea (já que ela não dará à luz até o primeiro dia
do próximo mês).
No último dia do próximo mês, haverá duas fêmeas (uma grávida e outra não). No
último dia do próximo mês, haverá três fêmeas (duas grávidas e uma não). E assim
por diante. Vejamos essa progressão em forma de tabela, Figura 6-2.
Machine Translated by Google

Figura 6-2 Crescimento na população de coelhas

Observe que para o mês n > 1, fêmeas(n) = fêmeas(nÿ1) + fêmeas(n-2). Isso


não é um acidente. Cada fêmea que estava viva no mês n-1 ainda estará viva no
mês n. Além disso, cada fêmea viva no mês n-2 produzirá uma nova fêmea no mês
n. As novas fêmeas podem ser adicionadas às fêmeas no mês n-1 para obter o
número de fêmeas no mês n.

Figura 6-2 Crescimento da população de coelhas O


crescimento da população é descrito naturalmente pela recorrência42

fêmeas(0) = 1
fêmeas(1) = 1 fêmeas(n
+ 2) = fêmeas(n+1) + fêmeas(n)

Esta definição é diferente da definição recursiva de fatorial:

Ele tem dois casos básicos, não apenas um. Em geral, podemos ter
quantos casos básicos quisermos.
No caso recursivo, há duas chamadas recursivas, não apenas uma.
Novamente, pode haver quantos quisermos.

A Figura 6-3 contém uma implementação direta da recorrência de Fibonacci,43


juntamente com uma função que pode ser usada para testá-la.
Machine Translated by Google

Figura 6-3 Implementação recursiva da sequência de Fibonacci

Escrever o código é a parte fácil de resolver esse problema. Assim que


passamos da declaração vaga de um problema sobre coelhos para um conjunto
de equações recursivas, o código quase se escreveu sozinho. Encontrar algum
tipo de forma abstrata para expressar uma solução para o problema em questão
geralmente é o passo mais difícil na construção de um programa útil. Falaremos
muito mais sobre isso mais adiante neste livro.
Como você pode imaginar, este não é um modelo perfeito para o crescimento
das populações de coelhos na natureza. Em 1859, Thomas Austin, um fazendeiro
australiano, importou 24 coelhos da Inglaterra para serem usados como alvos
em caçadas. Alguns escaparam. Dez anos depois, aproximadamente dois
milhões de coelhos eram mortos a tiros ou presos a cada ano na Austrália, sem
nenhum impacto perceptível na população. São muitos coelhos, mas nem perto
do 120º número de Fibonacci. 44 Embora a sequência
de Fibonacci não forneça realmente um modelo perfeito do crescimento das
populações de coelhos, ela tem muitas propriedades matemáticas interessantes.
Os números de Fibonacci também são comuns na natureza. Por exemplo, para
a maioria das flores, o número de pétalas é um número de Fibonacci.

Exercício de dedo: Quando a implementação de fib na Figura 6-3 é usada para


calcular fib(5), quantas vezes ela calcula o valor de fib(2) no caminho para
calcular fib(5)?

6.2 Palíndromos
Machine Translated by Google

A recursão também é útil para muitos problemas que não envolvem


números. A Figura 6-4 contém uma função, is_palindrome, que verifica se
uma string é lida da mesma maneira para trás e para frente.

Figura 6-4 Teste palíndromo

A função is_palindrome contém duas funções auxiliares internas.


Isso não deve interessar aos clientes da função, que devem se preocupar
apenas que a implementação de is_palindrome atenda à sua especificação.
Mas você deve se preocupar, porque há coisas a aprender examinando a
implementação.
A função auxiliar to_chars converte todas as letras em minúsculas e
remove todas as não letras. Ele começa usando um método interno em
strings para gerar uma string idêntica a s, exceto que todas as letras
maiúsculas foram convertidas em minúsculas.
A função auxiliar is_pal usa recursão para fazer o trabalho real.
Os dois casos base são cadeias de comprimento zero ou um. Isso significa
que a parte recursiva da implementação é alcançada apenas em cadeias
de comprimento dois ou mais. A conjunção45 na cláusula else é avaliada
da esquerda para a direita. O código primeiro verifica se o primeiro e o último
Machine Translated by Google

caracteres são os mesmos e, se forem, verifica se a string menos esses dois


caracteres é um palíndromo. Que o segundo conjunto não seja avaliado a menos
que o primeiro conjunto seja avaliado como True é semanticamente irrelevante neste
exemplo. No entanto, mais adiante no livro veremos exemplos em que esse tipo de
avaliação de curto-circuito de expressões booleanas é semanticamente relevante.

Essa implementação de is_palindrome é um exemplo de um importante princípio


de solução de problemas conhecido como dividir e conquistar. (Esse princípio é
relacionado, mas ligeiramente diferente, dos algoritmos de divisão e conquista,
discutidos no Capítulo 12.)
O princípio da solução de problemas é vencer um problema difícil dividindo-o em um
conjunto de subproblemas com as propriedades

Os subproblemas são mais fáceis de resolver do que o problema original.


As soluções dos subproblemas podem ser combinadas para resolver o
problema original.

Dividir para conquistar é uma ideia antiga. Júlio César praticava o que os
romanos chamavam de divide et impera (dividir para reinar). Os britânicos
praticaram brilhantemente para controlar o subcontinente indiano.
Benjamin Franklin estava bem ciente da experiência britânica no uso dessa técnica,
o que o levou a dizer na assinatura dos Estados Unidos
Declaração de Independência: “Devemos todos ser enforcados juntos, ou
seguramente todos seremos enforcados separadamente.”
Nesse caso, resolvemos o problema dividindo o problema original em uma
versão mais simples do mesmo problema (verificando se uma string mais curta é um
palíndromo) e algo simples que sabemos fazer (comparando caracteres únicos) e,
em seguida, combinando os soluções com o operador lógico e. A Figura 6-5 contém
algum código que pode ser usado para visualizar como isso funciona.
Machine Translated by Google

Figura 6-5 Código para visualizar o teste palíndromo

Executando o código

print('Tente dogGod')
print(is_palindrome('dogGod')) print('Try
doGood')
print(is_palindrome('doGood'))

estampas

Tente dogGod
is_pal chamado com doggod is_pal
chamado com oggo is_pal chamado
com gg is_pal chamado com

Prestes a retornar True do caso base


Prestes a retornar True para gg
Prestes a retornar True para oggo
Prestes a retornar True para doggod
Verdadeiro

Tente fazer o bem


Machine Translated by Google

is_pal chamado com dogood is_pal


chamado com ogoo is_pal chamado
com go
Prestes a retornar Falso para ir
Prestes a retornar False para ogoo
Prestes a retornar False para dogood
Falso

6.3 Variáveis Globais

Se você tentou chamar fib com um número grande, provavelmente notou


que demorava muito para ser executado. Suponha que queremos saber
quantas chamadas recursivas são feitas. Poderíamos fazer uma análise
cuidadosa do código e descobrir, e no Capítulo 11 falaremos sobre como
fazer isso. Outra abordagem é adicionar algum código que conte o número
de chamadas. Uma maneira de fazer isso usa variáveis globais.
Até agora, todas as funções que escrevemos se comunicam com seu
ambiente apenas por meio de seus parâmetros e valores de retorno.
Na maior parte, isso é exatamente como deveria ser. Normalmente leva a
programas que são relativamente fáceis de ler, testar e depurar. De vez
em quando, no entanto, variáveis globais são úteis. Considere o código na
Figura 6-6.

Figura 6-6 Usando uma variável global


Machine Translated by Google

Em cada função, a linha de código global num_fib_calls informa ao Python


que o nome num_fib_calls deve ser definido fora da função na qual a linha de
código aparece. Se não tivéssemos incluído o código global num_fib_calls, o
nome num_fib_calls teria sido local para cada uma das funções fib e test_fib,
porque num_fib_calls ocorre no lado esquerdo de uma instrução de atribuição
em fib e test_fib. As funções fib e test_fib têm acesso irrestrito ao objeto
referenciado pela variável num_fib_calls. A função test_fib vincula
num_fib_calls a 0 sempre que chama fib e fib incrementa o valor de
num_fib_calls sempre que fib é inserido.

A chamada test_fib(6) produz a saída


fib de 0 = 1
fib ligou 1 vezes.
fib de 1 = 1
fib ligou 1 vezes.
fib de 2 = 2
fib ligou 3 vezes.
fib de 3 = 3
fib ligou 5 vezes.
fibra de 4 = 5
fib ligou 9 vezes.
fibra de 5 = 8
fib ligou 15 vezes.
fib de 6 = 13
fib ligou 25 vezes.

Apresentamos o tópico de variáveis globais com alguma apreensão.


Desde a década de 1970, cientistas da computação de carteirinha têm
investido contra eles, por um bom motivo. O uso indiscriminado de variáveis
globais pode levar a muitos problemas. A chave para tornar os programas
legíveis é a localidade. As pessoas leem os programas uma parte de cada
vez, e quanto menos contexto for necessário para entender cada parte,
melhor. Como as variáveis globais podem ser modificadas ou lidas em uma
ampla variedade de lugares, seu uso descuidado pode destruir a localidade.
No entanto, há algumas vezes em que eles são exatamente o que é
necessário. O uso mais comum de variáveis globais é provavelmente para
definir uma constante global que será usada em muitos lugares. Por
exemplo, alguém escrevendo um programa relacionado à física pode querer
definir a velocidade da luz, C, uma vez, e então usá-la em múltiplas funções.
Machine Translated by Google

6.4 Termos Introduzidos no Capítulo

recursão
caso base

caso recursivo (indutivo)


definição indutiva
recorrência

funções auxiliares
avaliação de curto-circuito

variável global de dividir


e conquistar
constante global

39 Uma definição circular é uma definição que é circular.

40 A definição exata de número “natural” está sujeita a debate.


Alguns o definem como inteiros positivos (argumentando que zero, como
números negativos, é apenas uma abstração matemática
conveniente) e outros como inteiros não negativos (argumentando que,
embora seja impossível ter -5 maçãs, certamente é possível não ter maçãs). .
É por isso que fomos explícitos sobre os possíveis valores de n nas
docstrings da Figura 6-1.

41 O fato de chamarmos isso de sequência de Fibonacci é um exemplo


de interpretação eurocêntrica da história. A grande contribuição
de Fibonacci para a matemática européia foi seu livro Liber Abaci, que
apresentou aos matemáticos europeus muitos conceitos já bem conhecidos
dos estudiosos indianos e árabes. Esses conceitos incluíam numerais indo-
arábicos e o sistema decimal. O que hoje chamamos de sequência de
Fibonacci foi retirado do trabalho do matemático sânscrito Acharya Pingala.
Machine Translated by Google

42 Esta versão da sequência de Fibonacci corresponde à


definição usada no Liber Abaci de Fibonacci. Outras definições da
sequência começam com 0 em vez de 1.

43 Embora obviamente correto, este é um método terrivelmente ineficiente


implementação da função de Fibonacci. Existe uma implementação
iterativa simples que é muito melhor.

44 O dano causado pelos descendentes desses 24 coelhos foi estimado


em US$ 600 milhões por ano, e eles estão comendo muitas plantas
nativas até a extinção.

45 Quando duas expressões de valor booleano são conectadas por “e”,


cada expressão é chamada de conjunto. Se eles estão conectados
por “ou”, eles são chamados de disjuntos.
Machine Translated by Google

MÓDULOS E ARQUIVOS

Até agora, operamos sob as suposições de que 1) todo o nosso programa é


armazenado em um arquivo, 2) nossos programas não dependem de código
escrito anteriormente (além do código que implementa o Python) e 3) nossos
programas não acessam anteriormente coletam os dados nem armazenam seus
resultados de forma que possam ser acessados após o término da execução do
programa.
A primeira suposição é perfeitamente razoável, desde que os programas
sejam pequenos. À medida que os programas ficam maiores, no entanto,
normalmente é mais conveniente armazenar diferentes partes deles em arquivos
diferentes. Imagine, por exemplo, que várias pessoas estejam trabalhando no mesmo programa.
Seria um pesadelo se todos estivessem tentando atualizar o mesmo arquivo. Na
Seção 7.1, discutimos um mecanismo, os módulos Python, que nos permitem
construir facilmente um programa a partir do código em vários arquivos.
A segunda e terceira suposições são razoáveis para exercícios projetados
para ajudar as pessoas a aprender a programar, mas raramente razoáveis ao
escrever programas projetados para realizar algo útil. Na Seção 7.2, mostramos
como aproveitar os módulos de biblioteca que fazem parte da distribuição padrão
do Python. Usamos alguns desses módulos neste capítulo e muitos outros
posteriormente neste livro.
A Seção 7.3 fornece uma breve introdução à leitura e gravação de dados em
arquivos.

7.1 Módulos

Um módulo é um arquivo .py contendo definições e instruções do Python.


Poderíamos criar, por exemplo, um arquivo circle.py contendo o código da Figura
7-1.
Machine Translated by Google

Figura 7-1 Algum código relacionado a círculos e esferas

Um programa obtém acesso a um módulo por meio de uma instrução de


importação. Assim, por exemplo, o código

import circle pi = 3
print(pi)
print(circle.pi)
print(circle.area(3))
print(circle.circumference(3))
print(circle.sphere_surface(3))

vai imprimir

3
3.14159
28.27431
18.849539999999998
113.09724

Os módulos são normalmente armazenados em arquivos individuais. Cada módulo


tem sua própria tabela de símbolos privada. Conseqüentemente, dentro de circle.py,
acessamos objetos (por exemplo, pi e area) da maneira usual. A execução da
importação M cria um vínculo para o módulo M no escopo em que a importação
aparece. Portanto, no contexto de importação, usamos a notação de ponto para indicar
que estamos nos referindo a um nome definido no módulo importado.46 Por exemplo,
fora de circle.py, as referências pi e circle.pi podem (e neste caso fazem) referem-se a
objetos diferentes.
Machine Translated by Google

À primeira vista, o uso da notação de ponto pode parecer complicado.


Por outro lado, quando alguém importa um módulo, geralmente não tem ideia de
quais nomes locais podem ter sido usados na implementação desse módulo. O uso
da notação de ponto para qualificar totalmente os nomes evita a possibilidade de
ser queimado por um conflito acidental de nomes. Por exemplo, executar a atribuição
pi = 3 fora do módulo circle não altera o valor de pi usado dentro do módulo circle .

Como vimos, um módulo pode conter instruções executáveis, bem como


definições de funções. Normalmente, essas instruções são usadas para inicializar o
módulo. Por esse motivo, as instruções em um módulo são executadas apenas na
primeira vez que um módulo é importado para um programa.
Além disso, um módulo é importado apenas uma vez por sessão do interpretador. Se
você iniciar um console, importar um módulo e alterar o conteúdo desse módulo, o
interpretador ainda estará usando a versão original do módulo. Isso pode levar a um
comportamento confuso durante a depuração.
Em caso de dúvida, inicie um novo shell.
Uma variante da instrução de importação que permite ao programa de importação
omitir o nome do módulo ao acessar nomes definidos dentro do módulo importado. A
execução da instrução de M import * cria vinculações no escopo atual para todos os
objetos definidos em M, mas não para o próprio M. Por exemplo, o código

da importação do círculo *
print(pi)
print(circle.pi)

imprimirá primeiro 3,14159 e, em seguida, produzirá a mensagem de erro


NameError: o nome 'círculo' não está definido

Muitos programadores Python desaprovam o uso desse tipo de importação de


“curinga” . Eles acreditam que isso torna o código mais difícil de ler porque não é mais
óbvio onde um nome (por exemplo pi no código acima) é definido.

Uma variante comumente usada da instrução de importação é

importar module_name como new_name

Isso instrui o interpretador a importar o módulo denominado module_name, mas


renomeá-lo para new_name. Isso é útil se
Machine Translated by Google

module_name já está sendo usado para outra coisa no programa de


importação. A razão mais comum pela qual os programadores usam esse
formulário é fornecer uma abreviação para um nome longo.

7.2 Usando Pacotes Predefinidos

Muitos pacotes de módulos úteis vêm como parte da biblioteca Python


padrão; usaremos vários deles mais adiante neste livro.
Além disso, a maioria das distribuições do Python vem com pacotes além
daqueles da biblioteca padrão. A distribuição Anaconda para Python 3.8
vem com mais de 600 pacotes! Usaremos alguns deles mais adiante
neste livro.
Nesta seção, apresentamos dois pacotes padrão, math e calendar, e
damos alguns exemplos simples de seu uso. A propósito, esses pacotes,
como todos os módulos padrão, usam mecanismos do Python que ainda
não abordamos (por exemplo, exceções, abordadas no Capítulo 9).

Nos capítulos anteriores, apresentamos várias maneiras de aproximar


logaritmos. Mas não mostramos a maneira mais fácil. A maneira mais fácil
é simplesmente importar o módulo math. Por exemplo, para imprimir o log
de x base 2, basta escrever
importar
impressão matemática(math.log(x, 2))

Além de conter aproximadamente 50 funções matemáticas úteis, o


módulo math contém várias constantes úteis de ponto flutuante, por
exemplo, math.pi e math.inf (infinito positivo).
Os módulos da biblioteca padrão projetados para suportar a
programação matemática representam uma minoria dos módulos da
biblioteca padrão.
Imagine, por exemplo, que você queira imprimir uma representação
textual dos dias da semana de março de 1949, algo semelhante à figura
ao lado. Você poderia ir online e descobrir como era o calendário naquele
mês e ano. Então, com paciência suficiente e muita tentativa e erro, você
pode conseguir escrever uma declaração de impressão que faria o
trabalho. Alternativamente, você poderia simplesmente escrever
Machine Translated by Google

importar calendário como cal


cal_english = cal.TextCalendar()
print(cal_english.formatmonth(1949, 3))

Ou, se preferir ver o calendário em francês, polonês e


dinamarquês, você poderia escrever

print(cal.LocaleTextCalendar(locale='fr_FR').formatmonth(204 9, 3))

print(cal.LocaleTextCalendar(locale='pl_PL').formatmonth(204 9, 3))

print(cal.LocaleTextCalendar(locale= 'da_dk').formatmonth(204 9, 3))

que iria produzir

Suponha que você queira saber em que dia da semana o Natal


cairá em 2033. A linha
print(cal.day_name[cal.weekday(2033, 12, 25)])

responderá à pergunta. A invocação de cal.weekday retornará um


número inteiro representando o dia da semana,47 que é então usado
para indexar em cal.day_name — uma lista dos dias da semana em inglês.
Agora, suponha que você queira saber em que dia caiu o Dia de
Ação de Graças americano em 2011. O dia da semana é fácil, porque o
Dia de Ação de Graças americano é sempre na quarta quinta-feira de
Machine Translated by Google

Novembro.48 Encontrar a data real é um pouco mais complexo. Primeiro, usamos


cal.monthcalendar para obter uma lista que representa as semanas do mês. Cada elemento
da lista contém sete inteiros, representando o dia do mês. Se o dia não ocorrer naquele mês,
o primeiro elemento da lista da semana será 0. Por exemplo, se um mês com 31 dias
começar em uma terça-feira, o primeiro elemento da lista será a lista [0, 1, 2, 3, 4, 5, 6] e o
último elemento da lista será [30, 31, 0, 0, 0, 0, 0].

Usamos a lista retornada por calendar.monthcalendar para verificar se há uma quinta-


feira na primeira semana. Nesse caso, a quarta quinta-feira está na quarta semana do mês
(que está no índice 3); caso contrário, é na quinta semana.

def find_thanksgiving(ano): mês =


cal.monthcalendar(ano, 11) if mês[0][cal.THURSDAY] !
= 0: ação de graças = mês[3][cal.THURSDAY]

outro:
thanksgiving = mês[4][cal.THURSDAY] return ação de
graças print('In 2011', 'US
Thanksgiving was on November',
find_thanksgiving(2011))

Exercício de dedo: Escreva uma função que atenda à especificação

def shopping_days(ano):
"""ano um número >= 1941
retorna o número de dias entre o Dia de Ação de Graças dos EUA
e
Natal no ano"""

Exercício de dedo: Desde 1958, o Dia de Ação de Graças canadense ocorre na segunda
segunda-feira de outubro. Escreva uma função que receba um ano (>1957) como parâmetro
e retorne o número de dias entre o Dia de Ação de Graças canadense e o Natal.

Por convenção, os programadores Python geralmente

1. Importe um módulo por linha.

2. Coloque todas as importações no início de um programa.

3. Importe os módulos padrão primeiro, seguidos pelos de terceiros


módulos (por exemplo, os módulos fornecidos pelo Anaconda) e
Machine Translated by Google

finalmente, módulos específicos de aplicativos.

Ocasionalmente, colocar todas as importações no início de um programa pode


causar problemas. Uma instrução de importação é uma linha de código executável e o
interpretador Python a executa quando é encontrada. Alguns módulos contêm código que
é executado quando o módulo é importado.
Normalmente, esse código inicializa alguns objetos usados pelo módulo.
Uma vez que parte desse código pode acessar recursos compartilhados (por exemplo, o
sistema de arquivos em seu computador), onde em um programa a importação é
executada pode ser importante. A boa notícia é que é improvável que isso seja um
problema para os módulos que você provavelmente usará.

7.3 Arquivos

Todo sistema de computador usa arquivos para salvar coisas de uma computação para
outra. O Python fornece muitos recursos para criar e acessar arquivos. Aqui ilustramos
alguns dos básicos.
Cada sistema operacional (por exemplo, Windows e macOS) vem com seu próprio
sistema de arquivos para criar e acessar arquivos. O Python alcança a independência do
sistema operacional acessando arquivos por meio de algo chamado de identificador de
arquivo. O código

name_handle = open('crianças', 'w')

instrui o sistema operacional a criar um arquivo com o nome kids e retornar um identificador
de arquivo para esse arquivo. O argumento 'w' para abrir indica que o arquivo deve ser
aberto para escrita. O código a seguir abre um arquivo, usa o método write para escrever
duas linhas. (Em uma string Python, o caractere de escape “\” é usado para indicar que o
próximo caractere deve ser tratado de maneira especial. Neste exemplo, a string '\n' indica
um caractere de nova linha.) Por fim, o código fecha o arquivo. Lembre-se de fechar um
arquivo quando o programa terminar de usá-lo. Caso contrário, existe o risco de algumas
ou todas as gravações não serem salvas.

name_handle = open('kids', 'w') for i in


range(2): name =
input('Enter name: ')
name_handle.write(name + '\n')
name_handle.close()
Machine Translated by Google

Você pode garantir que não se esqueça de fechar um arquivo abrindo-o usando
uma instrução with . Código do formulário

com open(file_name) como name_handle: code_block

abre um arquivo, vincula um nome local a ele que pode ser usado no code_block
e, em seguida, fecha o arquivo quando o code_block é encerrado.
O código a seguir abre um arquivo para leitura (usando o argumento 'r') e
imprime seu conteúdo. Como o Python trata um arquivo como uma sequência de
linhas, podemos usar uma instrução for para iterar sobre o conteúdo do arquivo.

with open('kids', 'r') as name_handle: for line in


name_handle: print(line)

Se digitarmos os nomes David e Andrea, isso irá imprimir


Davi
Andreia

A linha extra entre David e Andrea existe porque print inicia uma nova linha toda
vez que encontra o '\n' no final de cada linha no arquivo. Poderíamos ter evitado
imprimir a linha extra escrevendo print(line[:-1]).

O código

name_handle = open('kids', 'w')


name_handle.write('Michael')
name_handle.write('Mark')
name_handle.close()
name_handle = open('kids', 'r') for line in
name_handle: imprimir(linha)

imprimirá a linha única MichaelMark.


Observe que sobrescrevemos o conteúdo anterior do arquivo kids. Se não
quisermos fazer isso, podemos abrir o arquivo para anexar (em vez de escrever)
usando o argumento 'a'. Por exemplo, se agora executarmos o código

name_handle = open('crianças', 'a')


name_handle = open('crianças', 'a')
name_handle.write('David')
Machine Translated by Google

name_handle.write('Andrea')
name_handle.close()
name_handle = open('kids', 'r') for line in
name_handle: print(line)

ele imprimirá a linha MichaelMarkDavidAndrea.

Exercício de dedo: Escreva um programa que primeiro armazene os dez


primeiros números da sequência de Fibonnaci em um arquivo chamado
fib_file. Cada número deve estar em uma linha separada no arquivo. O
programa deve então ler os números do arquivo e imprimi-los.

Algumas das operações comuns em arquivos estão resumidas na


Figura 7-2.

Figura 7-2 Funções comuns para acessar arquivos


Machine Translated by Google

7.4 Termos Introduzidos no Capítulo

módulo

declaração de

importação nomes

totalmente qualificados biblioteca Python padrão


arquivos

identificador de arquivo

escrevendo e lendo
de arquivos

caractere de nova linha

abrindo e fechando arquivos


com declaração

anexando a arquivos

46 Superficialmente, isso pode parecer não relacionado ao uso da notação de ponto na


invocação do método. No entanto, como veremos no Capítulo 10, existe uma
conexão profunda.

47 A “regra do Juízo Final” de John Conway fornece uma interessante


algoritmo para calcular o dia da semana para uma data. Baseia-se no fato de que, a
cada ano, as datas 4/4, 6/6, 8/8, 10/10, 12/12 e o último dia de fevereiro ocorrem no
mesmo dia da semana. O algoritmo é suficientemente simples para que algumas
pessoas possam executá-lo de cabeça.

48 Nem sempre era a quarta quinta-feira de novembro. Abraão


Lincoln assinou uma proclamação declarando que a última quinta-feira de novembro
deveria ser o Dia de Ação de Graças nacional. Seus sucessores continuaram a
tradição até 1939, quando Franklin Roosevelt declarou que o feriado deveria
ser comemorado no penúltimo dia
Machine Translated by Google

Quinta-feira do mês (para permitir mais tempo para compras entre o


Dia de Ação de Graças e o Natal). Em 1941, o Congresso
aprovou uma lei estabelecendo a data atual. Não foi a coisa mais
importante que fez naquele ano.
Machine Translated by Google

TESTE E DEBUGAÇÃO

Odiamos trazer isso à tona, mas o Dr. Pangloss49 estava errado. Não vivemos no
“melhor dos mundos possíveis”. Há alguns lugares onde chove muito pouco e
outros onde chove muito. Alguns lugares são muito frios, outros muito quentes,
alguns muito quentes no verão e muito frios no inverno. Às vezes, o mercado de
ações cai — muito.
Às vezes, os trapaceiros vencem (consulte Houston Astros). E, irritantemente,
nossos programas nem sempre funcionam corretamente na primeira vez que os
executamos.
Livros foram escritos sobre como lidar com esse último problema, e há muito
a ser aprendido com a leitura desses livros.
No entanto, com o objetivo de fornecer algumas dicas que podem ajudá-lo a
resolver o próximo problema a tempo, este capítulo fornece uma discussão
altamente condensada do tópico. Embora todos os exemplos de programação
estejam em Python, os princípios gerais se aplicam ao funcionamento de qualquer
sistema complexo.
O teste é o processo de execução de um programa para tentar verificar se ele
funciona como pretendido. A depuração é o processo de tentar corrigir um
programa que você já sabe que não funciona conforme o esperado.
Teste e depuração não são processos nos quais você deve começar a pensar
depois que um programa foi criado. Bons programadores projetam seus programas
de forma a torná-los mais fáceis de testar e depurar. A chave para fazer isso é
dividir o programa em componentes separados que podem ser implementados,
testados e depurados independentemente de outros componentes. Neste ponto
do livro, discutimos apenas um mecanismo para modularizar programas, a função.
Portanto, por enquanto, todos os nossos exemplos serão baseados em funções.
Quando chegarmos a outros mecanismos, em particular as classes, voltaremos a
alguns dos tópicos abordados neste capítulo.
Machine Translated by Google

A primeira etapa para fazer um programa funcionar é fazer com que o


sistema de linguagem concorde em executá-lo - ou seja, eliminar erros de
sintaxe e erros semânticos estáticos que podem ser detectados sem a execução
do programa. Se você ainda não passou desse ponto em sua programação, não
está pronto para este capítulo. Passe um pouco mais de tempo trabalhando em
pequenos programas e depois volte.

8.1 Teste
O objetivo do teste é mostrar que existem bugs, não mostrar que um programa
está livre de bugs. Para citar Edsger Dijkstra, “o teste de programa pode ser
usado para mostrar a presença de bugs, mas nunca para mostrar sua ausência!”
50 Ou, como Albert Einstein supostamente disse: “Nenhuma quantidade de
experimentação pode provar que estou certo; um único experimento pode
provar que estou errado.”
Porque isto é assim? Mesmo o mais simples dos programas tem bilhões de
entradas possíveis. Considere, por exemplo, um programa que pretende atender
à especificação

def é_menor(x, y):


"""Assume que x e y são inteiros
Retorna True se x for menor que y e False
de outra forma."""

Executá-lo em todos os pares de números inteiros seria, para dizer o


mínimo, tedioso. O melhor que podemos fazer é executá-lo em pares de inteiros
que tenham uma probabilidade razoável de produzir a resposta errada se
houver um bug no programa.
A chave para o teste é encontrar uma coleção de entradas, chamada de
conjunto de testes, que tenha uma alta probabilidade de revelar erros, mas
que não demore muito para ser executada. A chave para fazer isso é particionar
o espaço de todas as entradas possíveis em subconjuntos que forneçam
informações equivalentes sobre a correção do programa e, em seguida, construir
um conjunto de testes que contenha pelo menos uma entrada de cada partição.
(Normalmente, a construção de tal suíte de teste não é realmente possível.
Pense nisso como um ideal inatingível.)
Uma partição de um conjunto divide esse conjunto em uma coleção de
subconjuntos de modo que cada elemento do conjunto original pertença a
exatamente um dos subconjuntos. Considere, por exemplo, is_smaller(x, y). O conjunto de
Machine Translated by Google

as entradas possíveis são todas as combinações de pares de números inteiros. Uma maneira
de particionar esse conjunto é nesses nove subconjuntos:

x positivo, y positivo, x < yx positivo, y


positivo, y < xx negativo, y negativo, x < yx
negativo, y negativo, y < xx negativo, y
positivo x positivo, y negativo x = 0, y = 0 x
= 0, y ÿ 0 x ÿ 0, y = 0

Se testássemos a implementação em pelo menos um valor de cada um desses subconjuntos,


teríamos uma boa chance (mas não garantia) de expor um bug, se houver.

Para a maioria dos programas, encontrar um bom particionamento das entradas é muito
mais fácil falar do que fazer. Normalmente, as pessoas confiam em heurísticas baseadas na
exploração de diferentes caminhos por meio de alguma combinação de código e especificações.
A heurística baseada na exploração de caminhos através do código se enquadra em uma
classe chamada teste de caixa de vidro (ou caixa branca) .
Heurísticas baseadas na exploração de caminhos através da especificação se enquadram em
uma classe chamada teste de caixa preta.

8.1.1 Teste de caixa-preta Em


princípio, os testes de caixa-preta são construídos sem olhar para o código a ser testado. O
teste de caixa preta permite que testadores e implementadores sejam selecionados de
populações separadas. Quando aqueles de nós que ensinam cursos de programação geram
casos de teste para os conjuntos de problemas que atribuímos aos alunos, estamos
desenvolvendo suítes de teste caixa-preta. Os desenvolvedores de software comercial
geralmente têm grupos de garantia de qualidade que são amplamente independentes dos
grupos de desenvolvimento. Eles também desenvolvem suítes de teste de caixa preta.

Essa independência reduz a probabilidade de gerar suítes de teste que exibem erros
correlacionados com erros no código. Suponha, por exemplo, que o autor de um programa
tenha feito a suposição implícita, mas inválida, de que uma função nunca seria chamada com
um número negativo. Se a mesma pessoa construísse o conjunto de testes para o programa,
provavelmente repetiria o erro e não testaria a função com um argumento negativo.
Machine Translated by Google

Outra característica positiva do teste caixa-preta é que ele é robusto em relação a


mudanças de implementação. Como os dados de teste são gerados sem conhecimento da
implementação, os testes não precisam ser alterados quando a implementação é alterada.

Como dissemos anteriormente, uma boa maneira de gerar dados de teste de caixa preta é
para explorar caminhos através de uma especificação. Considere, a especificação

def sqrt(x, epsilon):


"""Assume x, epsilon flutua x >= 0

épsilon > 0
Retorna o resultado tal que
x-epsilon <= resultado*resultado <= x+epsilon"""

Parece haver apenas dois caminhos distintos através desta especificação: um


correspondente a x = 0 e outro correspondente a x > 0. No entanto, o senso comum nos diz
que, embora seja necessário testar esses dois casos, dificilmente é suficiente.

As condições de contorno também devem ser testadas. Observar um argumento do tipo


lista geralmente significa examinar a lista vazia, uma lista com exatamente um elemento, uma
lista com elementos imutáveis, uma lista com elementos mutáveis e uma lista contendo listas.
Ao lidar com números, isso normalmente significa observar valores muito pequenos e muito
grandes, bem como valores “típicos”. Para sqrt, por exemplo, pode fazer sentido tentar valores
de x e epsilon semelhantes aos da Figura 8-1.

Figura 8-1 Testando condições de contorno


Machine Translated by Google

As primeiras quatro linhas destinam-se a representar casos típicos. Observe


que os valores de x incluem um quadrado perfeito, um número menor que um e um
número com raiz quadrada irracional. Se algum desses testes falhar, há um bug no
programa que precisa ser corrigido.
As linhas restantes testam valores extremamente grandes e pequenos de x e
epsilon. Se algum desses testes falhar, algo precisa ser corrigido.
Talvez haja um bug no código que precise ser corrigido, ou talvez a especificação
precise ser alterada para que seja mais fácil de atender. Pode, por exemplo, não
ser razoável esperar encontrar uma aproximação de uma raiz quadrada quando o
epsilon é ridiculamente pequeno.
Outra condição de limite importante a ser considerada é o aliasing.
Considere o código

cópia def(L1, L2):


"""Assume que L1, L2 são listas
Muta L2 para ser uma cópia de L1""" enquanto
len(L2) > 0: #remove todos os elementos de L2
L2.pop() #remove o último elemento de L2
para e em L1: #acrescentar os elementos de L1 para inicialmente esvaziar L2
L2.apêndice(e)

Funcionará na maioria das vezes, mas não quando L1 e L2 se referirem à mesma


lista. Qualquer suíte de teste que não incluísse uma chamada do formulário copy(L,
L), não revelaria o bug.

8.1.2 Teste de caixa de vidro


O teste de caixa preta nunca deve ser ignorado, mas raramente é suficiente.
Sem examinar a estrutura interna do código, é impossível saber quais casos de
teste provavelmente fornecerão novas informações.
Considere o exemplo trivial:

def is_prime(x):
"""Assume que x é um int não negativo
Retorna True se x for primo; Falso caso contrário"""
se x <= 2:
retorna falso
para i no intervalo(2, x): se x%i
== 0:
retorna falso
retornar Verdadeiro
Machine Translated by Google

Observando o código, podemos ver que por causa do teste se x <= 2, os


valores 0, 1 e 2 são tratados como casos especiais e, portanto, precisam ser
testados. Sem olhar para o código, pode-se não testar is_prime(2) e, portanto, não
descobrir que a chamada da função is_prime(2) retorna False, indicando
erroneamente que 2 não é primo.

Os conjuntos de teste caixa-de-vidro são geralmente muito mais fáceis de


construir do que os conjuntos de teste caixa-preta. As especificações, incluindo
muitas neste livro, geralmente são incompletas e muitas vezes bastante descuidadas,
tornando um desafio estimar o nível de profundidade com que um conjunto de testes
caixa-preta explora o espaço de entradas interessantes. Em contraste, a noção de
um caminho através do código é bem definida e é relativamente fácil avaliar o nível
de exploração do espaço. Existem, de fato, ferramentas comerciais que podem ser
usadas para medir objetivamente a completude dos testes de caixa de vidro.
Um conjunto de testes de caixa de vidro é path-complete se exercitar cada
caminho potencial através do programa. Normalmente, isso é impossível de
conseguir, porque depende do número de vezes que cada loop é executado e da
profundidade de cada recursão. Por exemplo, uma implementação recursiva de
fatorial segue um caminho diferente para cada entrada possível (porque o número
de níveis de recursão será diferente).
Além disso, mesmo um conjunto de testes com caminho completo não garante
que todos os bugs serão expostos. Considerar:

def abs(x):
"""Assume que x é um int
Retorna x se x>=0 e –x caso contrário"""
se x < -1:
retornar -x
outro:
retornar x

A especificação sugere que há dois casos possíveis: x é negativo ou não é.


Isso sugere que o conjunto de entradas {2, -2} é suficiente para explorar todos os
caminhos na especificação. Este conjunto de testes tem a propriedade adicional de
forçar o programa por todos os seus caminhos, de modo que também se parece
com um conjunto de caixa de vidro completo. O único problema é que este conjunto
de testes não exporá o fato de que abs(-1) retornará -1.

Apesar das limitações do teste de caixa de vidro, geralmente vale a pena seguir
algumas regras práticas:
Machine Translated by Google

Exercite ambas as ramificações de todas as instruções if .

Certifique-se de que cada cláusula exceto (consulte o Capítulo 9) seja executada.

Para cada loop for , tenha casos de teste nos quais

• O loop não foi inserido (por exemplo, se o loop estiver iterando sobre os
elementos de uma lista, verifique se ele foi testado na lista vazia).

• O corpo do loop é executado exatamente uma vez. • O corpo

do loop é executado mais de uma vez.

Para cada loop while

• Observe os mesmos tipos de casos ao lidar com


rotações.

• Incluir casos de teste correspondentes a todas as formas possíveis de


sair do loop. Por exemplo, para um loop começando com

enquanto len(L) > 0 e não L[i] == e

encontre casos onde o loop sai porque len(L) é maior que zero e casos
onde sai porque L[i] == e.

Para funções recursivas, inclua casos de teste que façam com que a função retorne
sem chamadas recursivas, exatamente uma chamada recursiva e mais de uma
chamada recursiva.

8.1.3 Conduzindo testes Os


testes geralmente ocorrem em duas fases. Deve-se sempre começar com testes de
unidade. Durante esta fase, os testadores constroem e executam testes projetados para
verificar se unidades individuais de código (por exemplo, funções) funcionam corretamente.
Isso é seguido pelo teste de integração, que é projetado para verificar se os grupos de
unidades funcionam adequadamente quando combinados. Finalmente, o teste funcional é
usado para verificar se o programa como um todo se comporta como pretendido. Na prática,
os testadores percorrem essas fases, pois falhas durante a integração ou teste funcional
levam a alterações em unidades individuais.

O teste funcional é quase sempre a fase mais desafiadora.


O comportamento pretendido de um programa inteiro é consideravelmente mais difícil de
Machine Translated by Google

caracterizam do que o comportamento pretendido de cada uma de suas partes. Por


exemplo, caracterizar o comportamento pretendido de um processador de texto é
consideravelmente mais desafiador do que caracterizar o comportamento do subsistema
que conta o número de caracteres em um documento.
Problemas de escala também podem dificultar o teste funcional. Não é incomum que os
testes funcionais levem horas ou mesmo dias para serem executados.
Muitas organizações de desenvolvimento de software industrial têm um grupo de
garantia de qualidade de software (SQA) que é separado do grupo encarregado de
implementar o software. A missão de um grupo de SQA é garantir que, antes do lançamento
do software, ele seja adequado para o fim a que se destina. Em algumas organizações, o
grupo de desenvolvimento é responsável pelos testes de unidade e o grupo de controle de
qualidade pela integração e testes funcionais.

Na indústria, o processo de teste geralmente é altamente automatizado.


Testers51 não se sentam em terminais digitando entradas e verificando saídas.
Em vez disso, eles usam drivers de teste que autonomamente

Configure o ambiente necessário para invocar o programa (ou unidades) para testar.

Chame o programa (ou unidades) para testar com uma sequência de


entradas predefinida ou gerada automaticamente.
Salve os resultados dessas invocações.

Verifique a aceitabilidade dos resultados do teste.

Prepare um relatório apropriado.

Durante o teste de unidade, geralmente precisamos criar stubs e drivers. Os drivers


simulam partes do programa que usam a unidade que está sendo testada, enquanto os
stubs simulam partes do programa usadas pela unidade que está sendo testada. Os stubs
são úteis porque permitem que as pessoas testem unidades que dependem de software
ou, às vezes, até de hardware que ainda não existe. Isso permite que equipes de
programadores desenvolvam e testem simultaneamente várias partes de um sistema.

Idealmente, um stub deve

Verifique a razoabilidade do ambiente e dos argumentos fornecidos pelo


chamador (chamar uma função com argumentos inadequados é um erro
comum).
Machine Translated by Google

Modifique argumentos e variáveis globais de maneira consistente


com a especificação.
Retornar valores consistentes com a especificação.

Construir stubs adequados costuma ser um desafio. Se a unidade que


o stub substitui destina-se a executar alguma tarefa complexa, construir
um stub que execute ações consistentes com a especificação pode ser
equivalente a escrever o programa que o stub foi projetado para substituir.
Uma maneira de superar esse problema é limitar o conjunto de argumentos
aceitos pelo stub e criar uma tabela que contenha os valores a serem
retornados para cada combinação de argumentos a serem usados no
conjunto de testes.
Uma atração de automatizar o processo de teste é que ele facilita o
teste de regressão. Conforme os programadores tentam depurar um
programa, é muito comum instalar uma “correção” que quebra algo, ou
talvez muitas coisas, que costumavam funcionar. Sempre que qualquer
alteração for feita, por menor que seja, você deve verificar se o programa
ainda passa em todos os testes que costumava passar.

8.2 Depuração

Existe uma lenda urbana encantadora sobre como o processo de correção


de falhas no software passou a ser conhecido como depuração. A foto na
Figura 8-2 é de uma página, datada de 9 de setembro de 1947, de um livro
de laboratório do grupo que trabalhava no Mark II Aiken Relay Calculator
da Universidade de Harvard. Observe a mariposa colada na página e a
frase “Primeiro caso real de bug sendo encontrado” abaixo dela.
Machine Translated by Google

Figura 8-2 Não é o primeiro bug

Alguns alegaram que a descoberta daquela infeliz mariposa presa no


Mark II levou ao uso da frase depuração.
No entanto, a redação, “Primeiro caso real de um bug sendo encontrado”,
sugere que uma interpretação menos literal da frase já era comum. Grace
Murray Hopper,52 líder do projeto Mark II, deixou claro que o termo “bug”
já era amplamente utilizado para descrever problemas com sistemas
eletrônicos durante a Segunda Guerra Mundial. E bem antes disso, o Novo
Catecismo de Eletricidade de Hawkins, um manual elétrico de 1896,
incluía a entrada: “O termo 'bug' é usado de forma limitada para designar
qualquer falha ou problema nas conexões ou no funcionamento de
aparelhos elétricos”. No uso do inglês, a palavra “bugbear” significa
“qualquer coisa que cause medo ou ansiedade aparentemente
desnecessário ou excessivo”. 53 Shakespeare parece ter encurtado isso
para “bug” quando ele fez Hamlet kvetch sobre “bugs and goblins in my life”.
O uso da palavra “bug” às vezes leva as pessoas a ignorar o fato
fundamental de que se você escreveu um programa e ele tem um “bug”,
você estragou tudo. Bugs não rastejam espontaneamente em programas
sem falhas. Se o seu programa tem um bug, é porque você o colocou lá. insetos não
Machine Translated by Google

raça em programas. Se o seu programa tem vários bugs, é porque você cometeu
vários erros.
Os bugs de tempo de execução podem ser categorizados em duas dimensões:

Overt ÿ covert: Um bug aberto tem uma manifestação óbvia, por


exemplo, o programa trava ou leva muito mais tempo (talvez para
sempre) para ser executado do que deveria. Um bug oculto não tem manifestação óbvia.
O programa pode ser executado até a conclusão sem nenhum problema -
além de fornecer uma resposta incorreta. Muitos bugs estão entre os
dois extremos, e se o bug é evidente pode depender de quão
cuidadosamente você examina o comportamento do programa.
Persistente ÿ intermitente: Um bug persistente ocorre toda vez que o
programa é executado com as mesmas entradas. Um bug intermitente
ocorre apenas algumas vezes, mesmo quando o programa é executado
nas mesmas entradas e aparentemente sob as mesmas condições.
Quando chegarmos ao Capítulo 16, veremos programas que modelam
situações nas quais a aleatoriedade desempenha um papel. Em programas
desse tipo, bugs intermitentes são comuns.

O melhor tipo de bug é o aberto e persistente.


Os desenvolvedores não podem ter ilusões sobre a conveniência de implantar
o programa. E se alguém for tolo o suficiente para tentar usá-lo, descobrirá
rapidamente sua loucura. Talvez o programa faça algo horrível antes de travar,
por exemplo, exclua arquivos, mas pelo menos o usuário terá motivos para se
preocupar (se não entrar em pânico).
Bons programadores tentam escrever seus programas de forma que os erros
de programação levem a erros que são evidentes e persistentes. Isso geralmente
é chamado de programação defensiva.
O próximo passo para o poço da indesejabilidade são os bugs que são
evidentes, mas intermitentes. Um sistema de controle de tráfego aéreo que
calcula a localização correta dos aviões quase o tempo todo seria muito mais
perigoso do que um que comete erros óbvios o tempo todo. Pode-se viver no
paraíso dos tolos por um período de tempo e talvez chegar ao ponto de implantar
um sistema incorporando o programa defeituoso, mas mais cedo ou mais tarde
o bug se manifestará. Se as condições que levam o bug a se manifestar forem
facilmente reproduzíveis, geralmente é relativamente fácil rastrear e reparar o
problema. Se as condições que provocam o bug não são claras, a vida é muito
mais difícil.
Machine Translated by Google

Programas que falham de forma encoberta geralmente são altamente perigosos.


Como não são aparentemente problemáticos, as pessoas os usam e confiam neles para fazer
a coisa certa. Cada vez mais, a sociedade depende de software para realizar cálculos críticos
que estão além da capacidade dos humanos de realizar ou mesmo verificar se estão corretos.
Portanto, um programa pode fornecer uma resposta falaciosa não detectada por longos
períodos de tempo. Esses programas podem, e têm causado, muitos danos.54 Um programa
que avalia o risco de uma carteira de títulos hipotecários e dá com confiança a resposta errada
pode colocar um banco (e talvez toda a sociedade) em muitos problemas. O software em um
computador de gerenciamento de vôo pode fazer a diferença entre uma aeronave permanecer
no ar ou não.55 Uma máquina de radioterapia que fornece um pouco mais ou um pouco
menos de radiação do que o pretendido pode ser a diferença entre a vida e a morte de uma
pessoa com câncer. Um programa que comete um erro encoberto apenas ocasionalmente
pode ou não causar menos estragos do que um que sempre comete tal erro. Bugs que são
ocultos e intermitentes são quase sempre os mais difíceis de encontrar e corrigir.

8.2.1 Aprendendo a depurar


Depurar é uma habilidade aprendida. Ninguém o faz bem instintivamente. A boa notícia é que
não é difícil de aprender e é uma habilidade transferível.
As mesmas habilidades usadas para depurar software podem ser usadas para descobrir o
que há de errado com outros sistemas complexos, por exemplo, experimentos de laboratório
ou humanos doentes.
Por pelo menos quatro décadas, as pessoas criaram ferramentas chamadas depuradores,
e as ferramentas de depuração são incorporadas a todos os IDEs populares do Python. (Se
ainda não o fez, experimente a ferramenta de depuração do Spyder.) Essas ferramentas
podem ajudar. Mas o que é muito mais importante é como você aborda o problema. Muitos
programadores experientes nem se preocupam com as ferramentas de depuração, confiando
na instrução print .

A depuração começa quando o teste demonstra que o programa se comporta de maneira


indesejável. A depuração é o processo de busca de uma explicação desse comportamento. A
chave para ser consistentemente bom na depuração é ser sistemático na condução dessa
pesquisa.
Machine Translated by Google

Comece estudando os dados disponíveis. Isso inclui os resultados do


teste e o texto do programa. Estude todos os resultados do teste. Examine
não apenas os testes que revelaram a presença de um problema, mas
também os testes que pareciam funcionar perfeitamente. Tentar entender
por que um teste funcionou e outro não costuma ser esclarecedor. Ao olhar
para o texto do programa, tenha em mente que você não o entende
completamente. Se o fizesse, provavelmente não haveria um bug.
Em seguida, forme uma hipótese que você acredita ser consistente com
todos os dados. A hipótese pode ser tão restrita quanto “se eu mudar a linha
403 de x < y para x <= y, o problema desaparecerá” ou tão ampla quanto
“meu programa não está funcionando porque esqueci a possibilidade de
aliasing em vários lugares .”
Em seguida, projete e execute um experimento repetível com potencial
para refutar a hipótese. Por exemplo, você pode colocar uma instrução print
antes e depois de cada loop. Se eles estiverem sempre emparelhados, a
hipótese de que um loop está causando a não terminação foi refutada.
Decida antes de executar o experimento como você interpretaria vários
resultados possíveis. Todos os seres humanos estão sujeitos ao que os
psicólogos chamam de viés de confirmação – interpretamos as informações
de uma forma que reforça aquilo em que queremos acreditar. Se você
esperar até depois de executar o experimento para pensar sobre quais
devem ser os resultados, é mais provável que seja vítima de pensamentos positivos.
Finalmente, mantenha um registro de quais experimentos você tentou.
Quando você gasta muitas horas alterando seu código tentando rastrear um
bug indescritível, é fácil esquecer o que você já tentou. Se você não for
cuidadoso, poderá perder muitas horas tentando o mesmo experimento (ou
mais provavelmente um experimento que parece diferente, mas fornecerá
as mesmas informações) repetidas vezes. Lembre-se, como muitos já
disseram, “insanidade é fazer sempre a mesma coisa, mas esperar resultados
diferentes”. 56

8.2.2 Projetando o experimento


Pense na depuração como um processo de pesquisa e em cada experimento
como uma tentativa de reduzir o tamanho do espaço de pesquisa. Uma
maneira de reduzir o tamanho do espaço de busca é projetar um experimento
que possa ser usado para decidir se uma região específica do código é
responsável por um problema descoberto durante o teste. Outra forma de reduzir a procur
Machine Translated by Google

espaço é reduzir a quantidade de dados de teste necessários para provocar a


manifestação de um bug.
Vejamos um exemplo artificial para ver como você pode depurá-lo. Imagine
que você escreveu o código de verificação do palíndromo na Figura 8-3.

Figura 8-3 Programa com bugs

Agora, imagine que você está tão confiante em suas habilidades de


programação que colocou esse código na web — sem testá-lo. Suponha ainda
que você receba um e-mail dizendo: “Eu testei o seu !!!!!! programa inserindo
as 3.116.480 letras da Bíblia, e seu programa imprimiu Sim. No entanto,
qualquer tolo pode ver que a Bíblia não é um palíndromo.
Consertá-lo! (Seu programa, não a Bíblia.)”
Você poderia tentar e testá-lo na Bíblia. Mas pode ser mais sensato
começar experimentando em algo menor. Na verdade, faria sentido testá-lo em
um não palíndromo mínimo, por exemplo,

>>> bobo(2)
Digite o elemento: a
Digite o elemento: b
Machine Translated by Google

A boa notícia é que ele falha até mesmo neste teste simples, então você não precisa
digitar milhões de caracteres. A má notícia é que você não tem ideia de por que falhou.

Nesse caso, o código é pequeno o suficiente para que você provavelmente consiga
olhar para ele e encontrar o bug (ou bugs). No entanto, vamos fingir que é muito grande
para fazer isso e começar a reduzir sistematicamente o espaço de busca.
Freqüentemente, a melhor maneira de fazer isso é conduzir uma pesquisa de bisseção.
Encontre algum ponto na metade do código e elabore um experimento que permitirá que
você decida se há um problema antes desse ponto que possa estar relacionado ao sintoma.
(Claro, pode haver problemas depois desse ponto também, mas geralmente é melhor
procurar um problema de cada vez.) Ao escolher esse ponto, procure um local onde alguns
valores intermediários facilmente examinados forneçam informações úteis. Se um valor
intermediário não for o que você esperava, provavelmente há um problema que ocorreu
antes desse ponto no código. Se todos os valores intermediários parecerem corretos, o bug
provavelmente está em algum lugar posterior no código. Esse processo pode ser repetido
até que você restrinja a região em que um problema está localizado a algumas linhas de
código ou a algumas unidades, se estiver testando um sistema grande.

Olhando bobamente, o ponto intermediário está em torno da linha if is_pal(result). A


coisa óbvia a verificar é se o resultado tem o valor esperado, ['a', 'b']. Verificamos isso
inserindo a instrução print(result) antes da instrução if em nonsense. Quando o experimento
é executado, o programa imprime ['b'], sugerindo que algo já deu errado. A próxima etapa é
imprimir o resultado do valor aproximadamente na metade do loop. Isso revela rapidamente
que result nunca tem mais de um elemento de comprimento, sugerindo que a inicialização
de result precisa ser movida para fora do loop for .

O código “corrigido” para bobo é

def bobo(n):
"""Assume que n é um int > 0
Obtém n entradas do usuário
Imprime 'Sim' se a sequência de entradas forma um palíndromo;

'Não' caso contrário"""


result = [] for i in
range(n): elem = input('Enter
element: ')
Machine Translated by Google

result.append(elem)
print(result) if
is_pal(result): print('Yes')

outro:
print('Não')

Vamos tentar isso e ver se result tem o valor correto após o loop for .
Sim, mas infelizmente o programa ainda imprime Sim. Agora, temos
motivos para acreditar que um segundo bug está abaixo da instrução
print . Então, vamos ver is_pal. Insira a linha
print(temperatura, x)

antes da declaração de retorno . Quando executamos o código, vemos


que temp tem o valor esperado, mas x não. Subindo o código, inserimos
uma instrução print após a linha de código temp = x e descobrimos que
ambos temp e x têm o valor ['a', 'b']. Uma rápida inspeção do código
revela que em is_pal escrevemos temp.reverse em vez de temp.reverse()
— a avaliação de temp.reverse retorna o método reverse integrado para
listas, mas não o invoca.
Executamos o teste novamente e agora parece que tanto temp
quanto x têm o valor ['b','a']. Agora reduzimos o bug a uma linha. Parece
que temp.reverse() mudou inesperadamente o valor de x.
Um bug de aliasing nos mordeu: temp e x são nomes para a mesma
lista, antes e depois da lista ser invertida. Uma maneira de corrigir o bug
é substituir a primeira instrução de atribuição em is_pal por temp = x[:],
que faz uma cópia de x.
A versão corrigida de is_pal é
def is_pal(x):
"""Assume que x é uma lista
Retorna True se a lista for um palíndromo; Falso
de outra forma"""
temp = x[:]
temp.reverse() return
temp == x

8.2.3 Quando as coisas ficam difíceis


Joseph P. Kennedy, pai do presidente dos Estados Unidos, John F.
Kennedy, supostamente instruiu seus filhos: “Quando as coisas ficam difíceis, o
Machine Translated by Google

difícil começar. 57 Mas ele nunca depurou um software. Esta subseção contém algumas
dicas pragmáticas sobre o que fazer quando a depuração fica difícil.

Procure os suspeitos de sempre. Você já

• Passou argumentos para uma função na ordem errada? • Escreveu

um nome errado, por exemplo, digitou uma letra minúscula quando deveria
ter digitado uma maiúscula?
• Falha ao reinicializar uma variável?

• Testou se os valores de dois pontos flutuantes são iguais (==) em vez de


quase iguais (lembre-se de que a aritmética de ponto flutuante não é igual
à aritmética que você aprendeu na escola)? • Testou a igualdade de

valor (por exemplo, comparou duas listas escrevendo a expressão L1 == L2)


quando pretendia testar a igualdade de objeto (por exemplo, id(L1) ==
id(L2))?

• Esqueceu que alguma função integrada tem um efeito colateral? •

Esqueceu o () que transforma uma referência a um objeto do tipo função


em uma chamada de função?

• Criou um alias não intencional?

• Cometeu algum outro erro típico de você?

Pare de se perguntar por que o programa não está fazendo o que você
deseja. Em vez disso, pergunte a si mesmo por que ele está fazendo o que
está fazendo. Essa deve ser uma pergunta mais fácil de responder e
provavelmente será um bom primeiro passo para descobrir como consertar o programa.

Lembre-se de que o bug provavelmente não está onde você pensa.


Se fosse, você o teria encontrado há muito tempo. Uma maneira prática de decidir
onde procurar é perguntar onde o bug não pode estar. Como Sherlock
Holmes disse: “Elimine todos os outros fatores, e o que resta deve ser a
verdade”. 58

Tente explicar o problema para outra pessoa. Todos nós desenvolvemos


pontos cegos. A simples tentativa de explicar o problema a alguém
muitas vezes o levará a ver coisas que você perdeu. Você também pode tentar
explicar por que o bug não pode estar em determinados lugares.
Machine Translated by Google

Não acredite em tudo que você lê.59 Em particular, não acredite na


documentação. O código pode não estar fazendo o que os
comentários sugerem.
Pare de depurar e comece a escrever a documentação. Isso ajudará você a
abordar o problema de uma perspectiva diferente.
Vá embora e tente novamente amanhã. Isso pode significar que o bug foi
corrigido mais tarde do que se você tivesse continuado com ele, mas
provavelmente você gastará menos tempo procurando por ele. Ou seja, é
possível trocar latência por eficiência. (Estudantes, esta é uma excelente razão
para começar a trabalhar em conjuntos de problemas de programação mais
cedo do que tarde!)

8.2.4 Quando você encontra “o” bug Quando você


acha que encontrou um bug em seu código, a tentação de começar a codificar e
testar uma correção é quase irresistível. Muitas vezes é melhor, no entanto, fazer
uma pausa. Lembre-se de que o objetivo não é corrigir um bug, mas avançar rápida
e eficientemente para um programa livre de bugs.
Pergunte a si mesmo se esse bug explica todos os sintomas observados ou se é
apenas a ponta do iceberg. Nesse último caso, pode ser melhor cuidar do bug em
conjunto com outras alterações. Suponha, por exemplo, que você descobriu que o
bug é resultado de uma mutação acidental de uma lista. Você pode contornar o
problema localmente, talvez fazendo uma cópia da lista. Como alternativa, você pode
considerar o uso de uma tupla em vez de uma lista (já que as tuplas são imutáveis),
talvez eliminando bugs semelhantes em outras partes do código.

Antes de fazer qualquer alteração, tente entender a ramificação do “conserto”


proposto. Será que vai quebrar alguma outra coisa? Introduz complexidade excessiva?
Ele oferece a oportunidade de arrumar outras partes do código?

Sempre certifique-se de que você pode voltar para onde você está.
Nada é mais frustrante do que perceber que uma longa série de mudanças o deixou
mais longe do objetivo do que quando começou e não ter como voltar ao ponto de
partida. O espaço em disco geralmente é abundante. Use-o para armazenar versões
antigas do seu programa.
Finalmente, se houver muitos erros inexplicáveis, você pode considerar se
encontrar e corrigir um bug de cada vez é a abordagem correta. Talvez seja melhor
você pensar em uma maneira melhor
Machine Translated by Google

para organizar seu programa ou talvez um algoritmo mais simples que será mais fácil de implementar
corretamente.

8.3 Termos Introduzidos no Capítulo

testando

depuração

conjunto de testes

partição de entradas teste

de caixa de vidro teste

de caixa preta teste de

caminho completo teste de

unidade teste de

integração teste funcional

driver de teste de garantia

de qualidade de software (SQA)

canhoto de teste

bug de teste de regressão

bug

aberto bug

oculto bug

persistente bug

intermitente programação

defensiva depuradores viés de

confirmação
Machine Translated by Google

pesquisa de bisseção

49 O Dr. Pangloss aparece em Cândido, de Voltaire, como professor de


metafísico-teológico-cosmo-lonigologia. A visão otimista de Pangloss sobre o mundo
é o limite da tese central dos Ensaios de Teodicéia de Gottfried Leibniz sobre
a Bondade de Deus, a Liberdade do Homem e a Origem do Mal.

50 “Notes On Structured Programming,” Technical University Eindhoven, TH Report


70-WSK-03, April 1970.

51 Ou, por falar nisso, aqueles que dão notas a conjuntos de problemas muito grandes
cursos de programação.

52 Grace Murray Hopper desempenhou um papel de destaque no início


desenvolvimento de linguagens de programação e processadores de linguagem.
Apesar de nunca ter se envolvido em combate, ela ascendeu ao posto de contra-
almirante da Marinha dos Estados Unidos.

53 Dicionário Webster's New World College.

54 Em 1º de agosto de 2012, a Knight Capital Group, Inc. implantou um novo software


de negociação de ações. Em 45 minutos, um bug naquele software fez a empresa
perder $ 440.000.000. No dia seguinte, o CEO da Knight comentou que o bug fazia
com que o software inserisse “uma tonelada de pedidos, todos errados”. Nenhum
humano poderia ter cometido tantos erros tão rapidamente.

55 O software inadequado foi um fator que contribuiu para a perda de 346 vidas em dois
acidentes aéreos comerciais entre outubro de 2018 e março de 2019.

56 Esta citação foi extraída de Sudden Death, de Rita Mae Brown .


No entanto, tem sido atribuído de várias maneiras a muitas outras fontes - incluindo
Albert Einstein.

57 Ele também teria dito a JFK: “Não compre um único voto a mais do que o necessário.
Eu serei amaldiçoado se vou pagar por um deslizamento de terra.”
Machine Translated by Google

58 Arthur Conan Doyle, “O Signo dos Quatro.”


59 Esta sugestão é de utilidade geral e deve ser estendida a
coisas que você vê na televisão ou ouve no rádio.
Machine Translated by Google

EXCEÇÕES E AFIRMAÇÕES

Uma “exceção” é geralmente definida como “algo que não está de


acordo com a norma” e, portanto, é um tanto rara. Não há nada de
raro nas exceções em Python. Eles estão em toda parte.
Praticamente todos os módulos da biblioteca padrão do Python os
usam, e o próprio Python os criará em muitas circunstâncias. Você
já viu algumas exceções.
Abra um shell Python e digite
teste = [1,2,3] teste[3]

e o intérprete responderá com algo como


IndexError: índice de lista fora do intervalo

IndexError é o tipo de exceção que o Python gera quando um


programa tenta acessar um elemento que está fora dos limites de
um tipo indexável. A string após IndexError fornece informações
adicionais sobre o que causou a ocorrência da exceção.
A maioria das exceções internas do Python lidam com situações
em que um programa tentou executar uma instrução sem semântica
apropriada. (Trataremos das exceções excepcionais — aquelas que
não lidam com erros — mais adiante neste capítulo.)
Os leitores (todos vocês, esperamos) que tentaram escrever e
executar programas em Python já encontraram muitos deles.
Entre os tipos mais comuns de exceções estão TypeError, IndexError,
NameError e ValueError.
Machine Translated by Google

9.1 Tratamento de Exceções

Até agora, tratamos as exceções como eventos terminais. Quando uma exceção é
levantada, o programa termina (crashes pode ser uma palavra mais apropriada
neste caso), e voltamos ao nosso código e tentamos descobrir o que deu errado.
Quando surge uma exceção que causa o encerramento do programa, dizemos que
uma exceção não tratada foi gerada.

Uma exceção não precisa levar ao encerramento do programa.


Exceções, quando levantadas, podem e devem ser tratadas pelo programa. Às
vezes, uma exceção é levantada porque há um bug no programa (como acessar
uma variável que não existe), mas muitas vezes, uma exceção é algo que o
programador pode e deve antecipar. Um programa pode tentar abrir um arquivo que
não existe. Se um programa interativo solicitar uma entrada do usuário, o usuário
poderá inserir algo inapropriado.

Python fornece um mecanismo conveniente, try-except, para


captura e tratamento de exceções. A forma geral é

tente
bloco de código
exceto (lista de nomes de exceção): bloco de código

outro:
bloco de código

Se você sabe que uma linha de código pode gerar uma exceção quando
executada, você deve lidar com a exceção. Em um programa bem escrito, exceções
não tratadas devem ser a exceção.
Considere o código

success_failure_ratio = num_successes/num_failures print('A taxa de sucesso/


falha é', success_failure_ratio)

Na maioria das vezes, esse código funcionará bem, mas falhará se num_failures for
zero. A tentativa de dividir por zero fará com que o sistema de tempo de execução
do Python gere uma exceção ZeroDivisionError e a instrução de impressão nunca
será alcançada.
É melhor escrever algo ao longo das linhas de
Machine Translated by Google

try:
success_failure_ratio = num_successes/num_failures print('A taxa de sucesso/falha
é', success_failure_ratio) exceto ZeroDivisionError:

print('Sem falhas, então a taxa de sucesso/falha é


indefinido.')

Ao entrar no bloco try , o interpretador tenta avaliar a expressão


num_successes/num_failures. Se a avaliação da expressão for bem-
sucedida, o programa atribuirá o valor da expressão à variável
success_failure_ratio, executará a instrução print no final do bloco try e
executará o código que segue o bloco try-except . Se, no entanto, uma
exceção ZeroDivisionError for gerada durante a avaliação da expressão, o
controle pula imediatamente para o bloco except (pulando a atribuição e a
instrução print no bloco try ), a instrução print no bloco except é executada
e, em seguida, a execução continua seguindo o bloco try-except .

Exercício de dedo: Implemente uma função que atenda à especificação


abaixo. Use um bloco try-except . Dica: antes de começar a codificar, você
pode digitar algo como 1 + 'a' no shell para ver que tipo de exceção é gerada.

def soma_dígitos:
"""Assume que s é uma string
Retorna a soma dos dígitos decimais em s
Por exemplo, se s é 'a2b3c' ele retorna 5"""

Se for possível para um bloco de código de programa gerar mais de um


tipo de exceção, a palavra reservada except pode ser seguida por uma tupla
de exceções, por exemplo,
except(ValueError, TypeError):

nesse caso, o bloco except será inserido se qualquer uma das exceções
listadas for levantada dentro do bloco try .
Como alternativa, podemos escrever um bloco except separado para
cada tipo de exceção, o que permite ao programa escolher uma ação com
base na qual a exceção foi levantada. Se o programador escrever
Machine Translated by Google

exceto:

o bloco except será inserido se qualquer tipo de exceção for levantada


dentro do bloco try . Considere a definição de função na Figura 9-1.

Figura 9-1 Usando exceções para fluxo de controle

Existem dois blocos except associados ao bloco try . Se uma exceção


for gerada dentro do bloco try , o Python primeiro verifica se é um
ZeroDivisionError. Nesse caso, ele acrescenta um valor especial, nan, do
tipo float às proporções. (O valor nan significa "não é um número". Não há
literal para ele, mas pode ser denotado convertendo a string 'nan' ou a string
'NaN' para o tipo float. Quando nan é usado como um operando em um
expressão do tipo float, o valor dessa expressão também é nan.)
Se a exceção for diferente de ZeroDivisionError, o código executará o
segundo bloco except , que gera uma exceção ValueError com uma
string associada.
Em princípio, o segundo bloco exceto nunca deve ser inserido,
porque o código que invoca get_ratios deve respeitar as suposições
na especificação de get_ratios. No entanto, como verificar essas
suposições impõe apenas uma carga computacional insignificante,
provavelmente vale a pena praticar programação defensiva e verificar
de qualquer maneira.
O código a seguir ilustra como um programa pode usar get_ratios.
O nome msg na linha , exceto ValueError como msg: é
Machine Translated by Google

vinculado ao argumento (uma string neste caso) associado a ValueError quando ele
foi levantado. Quando o código

tente: print(get_ratios([1, 2, 7, 6], [1, 2, 0, 3])) print(get_ratios([], []))


print(get_ratios([1, 2], [3 ])) exceto
ValueError como msg: print(msg)

é executado ele imprime

[1.0, 1.0, nan, 2.0] []

get_ratios chamado com argumentos incorretos

Para comparação, a Figura 9-2 contém uma implementação da mesma


especificação, mas sem usar um try-except. O código na Figura 9-2 é mais longo e
mais difícil de ler do que o código na Figura 9-1. Também é menos eficiente. (O código
na Figura 9-2 poderia ser reduzido eliminando as variáveis locais vect1_elem e
vect2_elem, mas apenas ao custo de introduzir ainda mais ineficiência pela indexação
nas listas repetidamente.)

Figura 9-2 Fluxo de controle sem tentativa exceto


Machine Translated by Google

Vejamos outro exemplo. Considere o código

val = int(input('Digite um inteiro: ')) print('O quadrado do


número que você digitou é', val**2)

Se o usuário gentilmente digitar uma string que pode ser convertida em um


número inteiro, tudo ficará bem. Mas suponha que o usuário digite abc?
A execução da linha de código fará com que o sistema de tempo de execução
do Python gere uma exceção ValueError e a instrução de impressão nunca será
alcançada.
O que o programador deveria ter escrito seria algo como

enquanto verdadeiro:
val = input('Digite um número inteiro: ') try: val =
int(val)
print('O quadrado do
número digitado é',
val**2)
break #para sair do loop while exceto ValueError:
print(val, 'não é um número
inteiro')

Depois de entrar no loop, o programa solicitará que o usuário insira um


número inteiro. Depois que o usuário digita algo, o programa executa o bloco try
—except . Se nenhuma das duas primeiras instruções no bloco try causar uma
exceção ValueError , a instrução break será executada e o loop while será
encerrado. No entanto, se a execução do código no bloco try gerar uma exceção
ValueError , o controle será imediatamente transferido para o código no bloco
except . Portanto, se o usuário inserir uma string que não represente um número
inteiro, o programa solicitará que o usuário tente novamente. Não importa qual
texto o usuário insira, isso não causará uma exceção não tratada.

A desvantagem dessa mudança é que o texto do programa cresceu de duas


linhas para oito. Se houver muitos lugares onde o usuário é solicitado a inserir
um número inteiro, isso pode ser problemático. Claro, esse problema pode ser
resolvido introduzindo uma função:

def read_int(): while


True:
val = input('Digite um inteiro: ') try: return(int(val))

#converte str para int antes


Machine Translated by Google

retornando
except ValueError: print(val,
'não é um número inteiro')

Melhor ainda, essa função pode ser generalizada para solicitar qualquer tipo de
entrada:

def read_val(val_type, request_msg, error_msg):


enquanto verdadeiro:

val = input(request_msg + try: ' ')

return(val_type(val)) #converte str para val_type exceto ValueError: print(val,


error_msg)

A função read_val é polimórfica, ou seja, funciona para argumentos de


vários tipos diferentes. Essas funções são fáceis de escrever em Python, pois os
tipos são objetos de primeira classe. Agora podemos pedir um número inteiro
usando o código

val = read_val(int, 'Digite um inteiro:', 'não é um inteiro')

As exceções podem parecer hostis (afinal, se não forem tratadas, uma


exceção fará com que o programa trave), mas considere a alternativa. O que a
conversão de tipo int deve fazer, por exemplo, quando solicitado a converter a
string 'abc' em um objeto do tipo int? Ele poderia retornar um número inteiro
correspondente aos bits usados para codificar a string, mas é improvável que
isso tenha qualquer relação com a intenção do programador. Como alternativa,
ele pode retornar o valor especial None. Se fizesse isso, o programador precisaria
inserir código para verificar se a conversão de tipo havia retornado None. Um
programador que esquecesse essa verificação correria o risco de receber algum
erro estranho durante a execução do programa.

Com exceções, o programador ainda precisa incluir o código que lida com a
exceção. No entanto, se o programador esquecer de incluir esse código e a
exceção for levantada, o programa será interrompido imediatamente. Isto é uma
coisa boa. Ele alerta o usuário do programa que algo problemático aconteceu.
(E, como discutimos no Capítulo 8, bugs evidentes são muito melhores do que
bugs encobertos.) Além disso, dá a alguém que está depurando o programa uma
indicação clara de onde as coisas deram errado.
Machine Translated by Google

9.2 Exceções como Mecanismo de Fluxo de Controle

Não pense em exceções apenas como erros. Eles são um mecanismo de fluxo
de controle conveniente que pode ser usado para simplificar programas.
Em muitas linguagens de programação, a abordagem padrão para lidar
com erros é fazer com que as funções retornem um valor (geralmente algo
análogo ao None do Python) indicando que algo está errado. Cada chamada
de função deve verificar se esse valor foi retornado. Em Python, é mais comum
que uma função gere uma exceção quando não pode produzir um resultado
consistente com a especificação da função.

A instrução raise do Python força a ocorrência de uma exceção


especificada. A forma de uma declaração de aumento é

levantar exceçãoNome(argumentos)

O exceptionName geralmente é uma das exceções internas, por exemplo,


ValueError. No entanto, os programadores podem definir novas exceções
criando uma subclasse (consulte o Capítulo 10) da classe interna Exception.
Diferentes tipos de exceções podem ter diferentes tipos de argumentos, mas
na maioria das vezes o argumento é uma única string, que é usada para
descrever o motivo pelo qual a exceção está sendo gerada.

Exercício de dedo: Implemente uma função que satisfaça a especificação

def find_an_even(L):
"""Assume que L é uma lista de inteiros
Retorna o primeiro número par em L
Aumenta ValueError se L não contiver um par
número"""

Vejamos mais um exemplo, Figura 9-3. A função get_grades retorna um


valor ou gera uma exceção com a qual ela associou um valor. Ele gera uma
exceção ValueError se a chamada para gerar um IOError. Ele poderia ter
abrir ignorado o IOError e deixado a parte do programa que chama get_grades
lidar com isso, mas isso teria fornecido menos informações ao código de
chamada sobre o que deu errado. O código que chama get_grades usa o valor
retornado
Machine Translated by Google

para calcular outro valor ou manipula a exceção e imprime uma mensagem


de erro informativa.

Figura 9-3 Obter notas

9.3 Afirmações

A instrução assert do Python fornece aos programadores uma maneira


simples de confirmar se o estado de uma computação é o esperado. Uma
instrução assert pode assumir uma de duas formas:
afirmar expressão booleana

ou
afirmar expressão booleana, argumento

Quando uma instrução assert é encontrada, a expressão booleana é


avaliada. Se for avaliado como True, a execução prossegue normalmente.
Se for avaliado como False, uma exceção AssertionError é lançada.
Machine Translated by Google

Asserções são uma ferramenta útil de programação defensiva. Eles podem ser
usados para confirmar que os argumentos para uma função são de tipos apropriados.
Eles também são uma ferramenta de depuração útil. Eles podem ser usados, por
exemplo, para confirmar que os valores intermediários têm os valores esperados ou
que uma função retorna um valor aceitável.

9.4 Termos Introduzidos no Capítulo

exceções
levantando uma exceção
exceção não tratada
exceção tratada
construção try-except
catch (uma exceção)
funções polimórficas objetos
de primeira classe
declaração de aumento

afirmações
Machine Translated by Google

10
AULAS E ORIENTADAS A OBJETOS
PROGRAMAÇÃO

Agora voltamos nossa atenção para nosso último tópico importante relacionado
à programação em Python: usar classes para organizar programas em torno de
abstrações de dados.
As classes podem ser usadas de várias maneiras diferentes. Neste livro,
enfatizamos seu uso no contexto da programação orientada a objetos. A
chave para a programação orientada a objetos é pensar nos objetos como
coleções de dados e métodos que operam nesses dados.

As ideias subjacentes à programação orientada a objetos têm cerca de 50


anos e têm sido amplamente aceitas e praticadas nos últimos 30 anos. Em
meados da década de 1970, as pessoas começaram a escrever artigos
explicando os benefícios dessa abordagem de programação. Quase ao mesmo
tempo, as linguagens de programação SmallTalk (no Xerox PARC) e CLU (no
MIT) forneceram suporte linguístico para as ideias. Mas não foi até a chegada
de C++ e Java que a programação orientada a objetos realmente decolou na
prática.
Contamos implicitamente com a programação orientada a objetos durante a
maior parte deste livro. Na Seção 2.2.1, dissemos “Objetos são as coisas
principais que os programas Python manipulam. Todo objeto tem um tipo que
define os tipos de coisas que os programas podem fazer com esse objeto.”
Desde o Capítulo 2, contamos com tipos integrados, como float e str , e com os
métodos associados a esses tipos. Mas assim como os projetistas de uma
linguagem de programação podem construir apenas uma pequena fração das
funções úteis, eles podem construir apenas uma pequena fração dos tipos úteis.
Já examinamos um mecanismo

Você também pode gostar