Para esta unidade, revise as seções que tratam de funções do tutorial Python, começando pela seção 4.6. No texto seguinte, discutiremos alguns exemplos e falaremos um pouco sobre a importância de funções e sobre como utilizá-las efetivamente. No entanto, há diversas formas de utilizar funções em Python que não discutiremos aqui. Para aprender sobre isso, você deve o capítulo 4 do tutorial, particularmente do início da seção 4.7 até 4.7.3. O restante do capítulo também traz alguns detalhes e funcionalidades adicionais, como funções lambda e anotações, mas isso não é conteúdo da disciplina e você pode deixar para aprender depois se quiser.
Vamos retomar o exemplo de nossa calculadora, mas agora vamos adicionar outras operações.
while True:
operador = input()
if operador == "+":
num1 = float(input())
num2 = float(input())
soma = num1 + num2
print(soma)
elif operador == "-":
num1 = float(input())
num2 = float(input())
diferenca = num1 - num2
print(diferenca)
elif operador == "*":
num1 = float(input())
num2 = float(input())
produto = num1 * num2
print(produto)
elif operador == "/":
num1 = float(input())
num2 = float(input())
if num2 == 0:
print("Divisor não pode ser zero")
continue
produto = num1 / num2
print(produto)
elif operador == "F":
break
else:
print("Operação inválida")
Observe que usamos uma nova palavra-chave continue
. Essa instrução
termina a iteração atual do laço, mas ao contrário de break
,
continua na próxima iteração. Assim, imediatamente após executar
continue
, o interpretador irá voltar a verificar a condição do
while
.
Perceba que o programa começa a ficar muito grande e já não cabe em muitas telas, mas ainda é razoavelmente simples e a maioria dos programadores não teria dificuldades em ler esse código. No entanto, à medida em que adicionamos operações, a situação fica um pouco mais complicada.
Queremos que nossa calculadora seja mais útil do que as calculadoras de mesa tradicionais, então vamos adicionar duas operações, uma menos trivial do que a outra: a raiz quadrada e as raízes de uma equação do segundo grau.
Para calcular a raiz quadrada, vamos de novo importar um módulo math
,
então adicionamos no início no arquivo
import math
Com isso, basta basta escolher um nome apropriado para o operador e
adicionar mais algumas linhas no corpo do while
.
# ....
elif operador == "sqrt":
num = float(input())
raiz = math.sqrt(num)
print(raiz)
# ...
A segunda operação — encontrar as raízes de uma equação do segundo grau — diverge das operações anteriores, pois não há função prontamente disponível em Python, e precisamos de um número de instruções um pouco maior. Primeiro, lembramos que uma equação do segundo grau é escrita como
$$ ax^2 + bx + c = 0. $$
Desde muito sabemos como encontrar o valor de $x$. Primeiro, calculamos o valor do discriminante
$$ \Delta = b^2 - 4ac. $$
Com o valor de $\Delta$, podemos determinar quantas e quais são as soluções da equação. A fórmula para isso é
$$ x = \frac{-b \pm \sqrt{\Delta}}{2a}, $$
que é popularmente conhecida como fórmula de Bhaskara. Adicionamos o seguinte ao corpo da função
while True:
# ....
elif operador == "bhaskara":
a = float(input())
b = float(input())
c = float(input())
delta = b*b - 4 * a * c
if delta < 0:
print("Não há raízes reais")
continue
elif delta == 0:
print("Há uma raiz distinta apenas")
else:
print("Há duas raízes distintas")
x1 = (-b + math.sqrt(delta)) / (2 * a)
x2 = (-b - math.sqrt(delta)) / (2 * a)
print(f"As raízes são {x1} e {x2}")
# ...
Se tentarmos olhar para todo o programa, teremos uma surpresa:
while True:
operador = input()
if operador == "+":
num1 = float(input())
num2 = float(input())
soma = num1 + num2
print(soma)
elif operador == "-":
num1 = float(input())
num2 = float(input())
diferenca = num1 - num2
print(diferenca)
elif operador == "*":
num1 = float(input())
num2 = float(input())
produto = num1 * num2
print(produto)
elif operador == "/":
num1 = float(input())
num2 = float(input())
if num2 == 0:
print("Divisor não pode ser zero")
continue
produto = num1 / num2
print(produto)
elif operador == "sqrt":
num = float(input())
raiz = math.sqrt(num)
print(raiz)
elif operador == "bhaskara":
a = float(input())
b = float(input())
c = float(input())
delta = b * b - 4 * a * c
if delta < 0:
print("Não há raízes reais")
continue
elif delta == 0:
print("Há uma raiz distinta apenas")
else:
print("Há duas raízes distintas")
x1 = (-b + math.sqrt(delta)) / (2 * a)
x2 = (-b - math.sqrt(delta)) / (2 * a)
print(f"As raízes são {x1} e {x2}")
elif operador == "F":
break
else:
print("Operação inválida")
Dessa vez, tenho certeza de que todo o código não cabe em uma única tela de seu editor. Mais importante do que isso, é extremamente difícil ler esse código e entender o que está acontecendo! O motivo, além do tamanho, é que para entender o código acima precisamos nos preocupar, ao mesmo tempo, com diversos problemas distintos:
- como controlar um menu de operações ao usuário?
- como ler e mostrar os dados de uma operação de soma, subtração etc.?
- como obter as raízes de uma equação de segundo grau?
Como estamos fazendo tudo isso de maneira intercalada, ao lermos esse
código, ora nos preocumos com o while
, ora com a entrada e saída,
ora com a fórmula de Bhaskara. Pior! Se houver um erro na lógica do
nosso programa, vamos gastar bastante tempo tentando descobrir onde
ele está.
Funções
Para resolver esse problema, vamos criar uma abstração para cada conjunto de instruções dedicadas a resolver uma determinada tarefa. Vamos reescrever ou refatorar o programa. Utilizaremos uma estratégia de resolver os problemas mais gerais primeiro e, depois, os mais específicos (algumas pessoas chamam a isso de estratégia top-down).
Primeiro precisamos descobrir qual a tarefa principal do programa,
isso é, qual é o conjunto de instruções deve ser executado ao iniciar
o programa. Nesse exemplo, a primeira instrução é while
, que é
responsável pelo controle das operações digitadas pelo usuário. Vamos
então nos concentrar nesse problema e esconder todos os demais.
Escrevemos o seguinte:
while True:
operador = input()
if operador == "+":
operacao_soma()
elif operador == "-":
operacao_diferenca()
elif operador == "*":
operacao_multiplicacao()
elif operador == "/":
operacao_divisao()
elif operador == "sqrt":
operacao_raiz()
elif operador == "bhaskara":
operacao_bhaskara()
elif operador == "F":
break
else:
print("Operação inválida")
Agora fica muito mais simples entender o que esse código faz: ele lê
uma linha do teclado e realiza uma operação de acordo com o que o
usuário digitar. É claro que operacao_soma
, operacao_diferenca
etc. não são instruções da linguagem Python. Cada um desses nomes é
uma abstração: aqui, abstrair significa esconder os detalhes de
como realizar um determinada operação.
Se tentarmos executar um código assim iríamos obter uma mensagem de
erro do interpretador dizendo que algum nome não está definido, como
se ele reclamasse, “não sei do que você está falando”. Vamos então
definir cada um desses nomes. Para isso, antes do while
, adicione
def operacao_soma():
pass
def operacao_diferenca():
pass
def operacao_produto():
pass
def operacao_divisao():
pass
def operacao_raiz():
pass
def operacao_bhaskara():
pass
O que estamos fazendo é definir diversas novas funções. Uma função é um conjunto de instruções que realiza determinada tarefa identificada por um nome. As funções acima são só stubs, que são funções incompletas. O motivo para criar stub é poder testar o código do programa principal enquanto ainda estamos desenvolvendo nosso programa.
Depois de terminado o trecho de código do programa principal, devemos
implementar cada uma das funções definidas acima. Implementar uma
função significa escrever o conjunto de instruções que realiza a
tarefa correspondente. Uma das vantagens de definir e usar funções é
que é muito mais fácil implementar tarefas pequenas, uma de cada vez,
do que tentar resolver todo o problema de uma vez. Para a função
operacao_soma
basta copiar as instruções do código original:
def operacao_soma():
num1 = float(input())
num2 = float(input())
soma = num1 + num2
print(soma)
Você deve fazer o mesmo para as operações de diferença e produto. Para a operação de divisão, no entanto, precisamos tomar um cuidado a mais. Copiando o código original, teríamos:
def operacao_divisao():
num1 = float(input())
num2 = float(input())
if num2 == 0:
print("Divisor não pode ser zero")
continue
produto = num1 / num2
print(produto)
O problema do código acima é que a instrução continue
só faz sentido
dentre do corpo de um comando for
ou while
. O que queremos, nesse
caso, é terminar a função e retornar ao menu. Para isso, existe a
instrução return
que termina a função que a contém e continua a
execução imediatamente depois do ponto de chamada.
def operacao_divisao():
num1 = float(input())
num2 = float(input())
if num2 == 0:
print("Divisor não pode ser zero")
return
produto = num1 / num2
print(produto)
Faça o mesmo com operação para obter raízes da equação.
Passando argumentos e devolvendo valores
Vamos olhar como ficaria a função para a operação da raiz quadrada:
def operacao_raiz():
num = float(input())
raiz = math.sqrt(num)
print(raiz)
A expressão math.sqrt(num)
é uma chamada a uma função de nome sqrt
importada com o módulo math
. Repare que o radiando é passado como
parâmetro da função, dentro dos parênteses. Além disso, a chamada da
função é utilizada como se fosse substituído pela raiz da função.
Assim, a função sqrt
funciona, de fato, como uma função matemática,
que tem uma entrada e calcula uma saída.
Embora calcular a raiz quadrada já é uma função própria de uma biblioteca padrão do Python, ela normalmente não é uma instrução elementar dos nossos computadores modernos. Como bons computeiros, devemos ter o espírito curioso de descobrir como uma tal operação complexa pode ser computada a partir de operações elementares (soma, subtração, multiplicação e divisão).
Há vários métodos conhecidos e você deve ter já estudado pelo menos um
na escola. Nesse exemplo, vamos usar o método de Newton. Como essa é
uma operação complicada, primeiro vamos criar um stub para poder
modificar o código de operacao_raiz
. Fazemos o seguinte:
def minha_sqrt(radiando):
raiz = radiando / 2 # TODO: implementar método de Newton
return raiz
# ...
def operacao_raiz():
num = float(input())
raiz = minha_sqrt(num)
print(raiz)
A função minha_sqrt
é só um stub, mas ela ilustra dois conceitos
novos:
-
Uma função pode receber um ou mais parâmetros. Um parâmetro é uma variável que se refere a algum valor passado entre os parênteses quando chamamos a função. Esse pode ser o valor de qualquer expressão, seja uma simples variável como em
minha_sqrt(num)
, mas pode ser também uma expressão mais complicada, como emminha_sqrt(b*b - 4*a*c)
. -
Uma função pode devolver um valor ao retornar ao local de origem. Esse valor é escrito após o
return
. O valor devolvido por uma função tem um tipo determinado e pode ser utilizado em qualquer expressão onde uma variável do mesmo tipo poderia ser utilizada. Por exemplo, o valor pode ser atribuído a uma variável, como emraiz = minha_sqrt(num)
ou utilizado em uma expressão mais complicada, como emx = (-b + minha_sqrt(b*b - 4*a*c)) / (2*a)
.
Já organizamos nosso programa em funções, então podemos nos concentrar
em implementar o método de Newton. Você deve ter percebido que fizemos
um comentário que começa com # TODO: ...
. É comum criar comentários
como esse ou da forma # FIXME: ....
para indicar trechos de código
que merecem atenção posterior. Outra maneira, para projetos grandes, é
anotar um bug
num gerenciador de tarefas (como bugzilla, issues,
etc.). Agora, podemos revisar o
método de Newton
e implementar a função, mas é mais fácil relembrar o
exercício em que calculamos a raiz
quadrada.
Repetição de código
Vamos resolver mais um exercício:
Escreva um programa que recebe um número inteiro e decide se ele é um produto de dois números primos.
Antes de começar, vamos listar as duas pequenas tarefas que devemos fazer:
- controlar a entrada e a saída do programa;
- verificar se um número é produto de dois primos.
Novamente, precisamos identificar qual delas é a tarefa principal, isso é, que será executada primeiro quando o programa começar. Na maioria dos nossos programas, essa vai ser a tarefa de ler os dados de entrada, realizar algumas operações e mostrar os dados de saída. Nesse caso, vamos fazer o seguinte:
def main():
n = int(input("Digite um número inteiro: "))
if eh_produto_dois_primos(n):
print(f"O número {n} é produto de dois primos.")
else:
print(f"O número {n} não é produto de dois primos.")
main()
Observe que definimos uma função chamada main
e que adicionamos uma
chamada a essa função na última linha do programa, que é a única
instrução do programa que não está dentro de uma função. Sempre que
nosso problema não for trivial, vamos preferir organizar nossos
programas dessa maneira. O nome main
é uma convenção e serve para
identificar facilmente qual é a função principal do programa.
Para entender esse programa, precisamos investigar a linha que contém
if eh_produto_dois_primos(n):
. O que está implícito aqui é que
eh_produto_dois_primos
é uma função que devolve um valor booleano
dependendo se o número n
passado como parâmetro é produto de dois
primos. Para podermos deduzir isso foi fundamental que o nome da
função fosse bem representativo do que ela faz e do que ela devolve.
Para poder testar a função main
, faremos um stub.
def eh_produto_dois_primos(n):
"""Devolve True se o argumento n puder
ser escrito como um produto de dois primos"""
return True
A novidade nesse programa é que adicionamos uma string na primeira linha da função. Essa string não é associada a nenhuma variável e não tem nenhum efeito. O motivo para adicioná-la é que ela serve para documentar o que a função faz. Por mais que uma função tenha (e deva ter) um bom nome, nem sempre é claro o que cada função faz, particularmente se voltarmos a ler nosso código depois de alguns dias. Essas strings são denominadas strings de documentação ou documentation strings.
Quando testarmos o programa incompleto com qualquer número, digamos, 100, obteremos sempre uma mensagem como
O número 100 é produto de dois primos.
independentemente do número lido. Para testar o conjunto de instruções
correspondentes ao else
da função main
podemos fazer o nosso stub
devolver False
. Depois, já podemos implementar a função de fato.
Antes, vamos escrever um algoritmo, em alto nível.
- para cada número $q$ em $1, 2, …., n$:
- se $n$ for divisível por $q$:
- faça $r \gets n / q$
- verifique se $q$ é primo
- verifique se $r$ é primo
- se ambos forem primos
- responda SIM
- se $n$ for divisível por $q$:
- se não tiver terminado, responda NÃO
Você pode só acreditar que esse algoritmo está correto, ou pode tentar se convencer de que está. Um bom computeiro tenta entender bem o que um algoritmo faz antes de tentar codificá-lo. Para isso, teste com algumas instâncias pequenas utilizando lápis e papel. Quando estivermos confiantes de que o o algoritmo está correto, podemos passar à implementação.
def eh_produto_dois_primos(n):
"""Devolve True se o argumento n puder
ser escrito como um produto de dois primos"""
produto_primos = False
for q in range(1, n + 1):
if n % q == 0:
r = n // q
r_eh_primo = True
for d in range(2, r):
if r % d == 0:
r_eh_primo = False
break
q_eh_primo = True
for d in range(2, q):
if r % d == 0:
q_eh_primo = False
break
if r_eh_primo and q_eh_primo:
produto_primos = True
break
return produto_primos
Leia com atenção e copie essa função no seu programa. Podemos testar o
programa digitando o número 15
que é um produto de dois primos 3
e
5
. Ao verificar a saída iremos ver que o programa imprimiu
corretamente
O número 15 é produto de dois primos.
Se testarmos com os números 10
ou 4
, o programa também responderá
corretamente. Mas devolver a resposta correta para alguns exemplos não
significa que o programa está correto. Na verdade, não importa qual
número inteiro fornecermos, o programa sempre responderá que ele é
produto de dois primos. Isso não é verdade quando a resposta é não,
como para o número 8
ou o número 20
.
Concluímos duas coisas: primeiro, nosso programa está errado; segundo, é importante testar nossos programas com vários exemplos de entrada, particularmente para entradas que correspondem a saídas diferentes.
Para descobrir onde está o erro do programa, podemos usar diversas estratégias, como simulá-lo com um debugger, ou ler o código lentamente com atenção. Se você ainda não descobriu o erro, pare um pouco e tendo descobri-lo.
Uma vez descoberto o erro, precisamos corrigi-lo. Mais importante,
precisamos entender porque esse erro ocorreu para início de conversa.
O erro é um erro de digitação na segunda ocorrência de
if r % d == 0:
que deveria ser if q % d == 0:
. Como você deve
adivinhar, isso ocorreu porque temos dois trechos de código muito
parecidos e o segundo foi obtido copiando e modificando o primeiro,
mas esquecemos de modificar uma linha.
Esse exemplo descreve o que chamamos de duplicidade de código, que
é uma situação extremamente comum no desenvolvimento de software.
Muitas vezes, cada trecho de código tem papeis similares, mas para
parâmetros diferentes. No exemplo acima, queremos decidir se um dado
número (r
ou q
) é primo. Em situações como essa, devemos refatorar
o código e definir uma função. Reescrevemos assim.
def eh_primo(n):
"""Verifica se n é primo"""
eh_primo = True
for d in range(2, n):
if n % d == 0:
eh_primo = False
break
return eh_primo
def eh_produto_dois_primos(n):
"""Devolve True se o argumento n puder
ser escrito como um produto de dois primos"""
produto_primos = False
for q in range(1, n + 1):
if n % q == 0:
r = n // q
if eh_primo(r) and eh_primo(q):
produto_primos = True
break
return produto_primos
Deve ser evidente que a nova versão é mais simples e muito mais
compacta. Dessa vez, não há repetição de código. Testando para o
número 8
vemos que, agora sim, ele responde corretamente que não é
produto de dois primos.
Infelizmente, como você já pode desconfiar, esse programa ainda não está correto. Estude o programa e tente determinar para que exemplos ele devolve a saída incorreta. Depois, corrija seu programa. Ao terminar esse exercício, você vai descobrir mais uma vantagem de ter refatorado o programa com uma nova função, ao invés de manter as duas cópias praticamente idênticas de um conjunto de instruções.
Criando e organizando seu programa
Vamos resolver mais um problema para nos exercitar.
Crie um programa que leia duas listas do teclado, correspondentes às notas de provas e de exercícios dos estudantes de uma turma, normaliza cada nota dividindo-se a nota pelo máximo da lista correspondente e computa a média final de cada estudante, que é dada pela média geométrica entre a nota de prova e de exercícios.
Você deve tentar fazer todo esse programa por conta própria. Para isso, procure seguir a mesma estratégia que seguimos antes:
- Leia o enunciado e procure entender o que é pedido. Para isso, tente formalizar entrada e saída e crie alguns exemplos. Depois, faça uma lista de todas as tarefas curtas que precisam ser realizadas para se resolver esse problema.
- Identifique a tarefa principal responsável por controlar entrada e
saída e crie uma função
main
utilizando chamadas para funções auxiliares quando quiser abstrair pequenas tarefas. - Crie funções stubs para cada função auxiliar necessária. Certifique-se de que os nomes das funções sejam adequados para as tarefas abstraídas. Não se esqueça de documentar as funções identificando a entrada e a saída sempre que necessário.
- Teste a função principal verificando as instruções que controlam a entrada e a saída da função.
- Implemente cada stub definido. Lembre-se de escrever antes um algoritmo em português, testar e, só depois, traduzi-los à linguagem Python. Procure testar seu programa a cada função implementada.
Em seguida eu mostro como eu resolveria esse problema. Não leia esse código enquanto não tiver terminado o seu próprio programa. Seu programa pode divergir completamente do código abaixo, mas isso não significa que uma maneira é mais correta do que a outra. Procure analisar criticamente as diferenças. Uma peculiaridade da Computação é que, embora seja uma ciência dita exata, os algoritmos são tão distintos quanto seus próprios programadores. Tanto que alguns diriam que programação é uma arte!
"""
Calcula as médias finais dos estudantes.
Entrada:
- uma linha com o número n de estudantes
- n linhas correspondentes às notas de provas
- n linhas correspondentes às notas de exercícios
Saída:
- n linhas correspondentes às medias finais
"""
import math
def ler_lista_numeros(n):
"""Lê uma lista de n números do teclado"""
lista = []
for _ in range(n):
numero = float(input())
lista.append(numero)
return lista
def imprimir_lista_numeros(lista):
"""Imprime cada número de lista em um linha,
com duas casas decimais"""
for valor in lista:
print(f"{valor:.2f}")
def obter_maximo(lista):
"""Devolve o valor máximo da lista"""
assert lista, "Lista não pode ser vazia"
maximo = lista[0]
for numero in lista:
if numero > maximo:
maximo = numero
return maximo
def criar_lista_normalizada(lista_antiga):
"""Devolve uma nova lista com os elementos
de lista_antiga normalizados pelo máximo"""
maximo = obter_maximo(lista_antiga)
lista_nova = []
for valor in lista_antiga:
novo_valor = valor / maximo
lista_nova.append(novo_valor)
return lista_nova
def calcular_medias_finais(notas_provas, notas_exercicios):
"""Devolve uma nova lista com as médias geométricas
dos elementos de notas_provas e notas_exercicios"""
medias_finais = []
assert len(notas_provas) == len(notas_exercicios), \
"As listas de notas devem ter o mesmo tamanho"
n = len(notas_provas)
for i in range(n):
media_final = math.sqrt(notas_provas[i] * notas_exercicios[i])
medias_finais.append(media_final)
return medias_finais
def main():
n = int(input(n))
notas_provas = ler_lista_numeros(n)
notas_exercicios = ler_lista_numeros(n)
notas_provas = criar_lista_normalizada(notas_provas)
notas_exercicios = criar_lista_normalizada(notas_exercicios)
medias_finais = calcular_medias_finais(notas_provas, notas_exercicios)
imprimir_lista_numeros(medias_finais)
main()
Há vários detalhes nesse programa e talvez alguns sejam novos para você. Você deve pesquisar as instruções que não conhecer e descobrir o objetivo de elas estarem ali. O que queremos destacar nesse exemplo, no entanto, é a forma como está organizado. É uma boa prática (embora nem sempre seja seguida no mercado) criar programas bem documentados e com formatação padronizada, como acima. O programa acima está organizado de acordo com algumas convenções:
- O programa começa com um comentário descrevendo o que ele faz. No caso de Python, usamos uma string de múltiplas linhas que é mais convenientes. Repare que a documentação descreve precisamente como utilizar o programa.
- Em seguida, há uma seção de
import
. Por enquanto só conhecemos e precisamos de uma função no módulomath
, mas podemos ter uma sequência bem grande de instruções desse tipos quando tivermos programas mais complexos que precisam de mais bibliotecas. - Logo em seguida há uma sequência de funções auxiliares para se resolver nosso problema, cada uma bem documentada. As funções estão organizadas nessa ordem não por acaso. Nesse nosso exemplo, primeiro estão as funções mais gerais (de entrada e saída) e depois funções mais específicas para nosso problema. Cada desenvolvedor (ou grupo) normalmente adota uma estratégia particular.
- Por último, vem a função principal, denominada
main
, seguida de uma chamada para ela. O programa começa a executar por ali e a maioria dos programadores também começará a ler programas a partir da funçãomain
.
Escopo de variáveis e ciclo de vida de funções
Quando aprendemos a instrução de atribuição, vimos que ela tem a forma
<nome de variável> = <expressão que computa um valor na memória>
Assim, o nome do lado esquerdo deve ser um nome que referencia algum valor armazenado na memória. Essa associação, no jargão de Python é chamada de binding. Uma vez definido o binding, podemos usar o nome da variável em um expressão para representar o valor referenciado. O nome de uma variável, no entanto, só pode ser usado quando satisfeitas determinadas condições:
-
A atribuição correspondente ao binding tiver sido executada antes do uso da variável. Se houver mais de uma atribuição correspondente ao mesmo nome, então o nome corresponderá ao último valor referenciado.
-
O nome da variável ser visível dentro do escopo em que foi criada. Um escopo é um conjunto de nomes de variáveis correspondentes a uma região do código bem definida. Uma atribuição sempre corresponde ao nome do escopo atual.
Vamos estudar dois tipos de escopo.
-
Escopo global: O escopo global é criado quando o interpretador Python inicia a execução de seu programa. São adicionados ao escopo global nome definidos diretamente no programa, que não estejam no corpo de nenhuma função. Essas variáveis são visíveis em qualquer parte do seu programa.
-
Escopo de função: O escopo de uma função é criado somente quando uma função é chamada. São adicionados ao escopo dessa chamada os nomes definidos dentro do corpo da função. Essas variáveis são visíveis apenas dentro do corpo da função.
Para deixar esses conceitos um pouco mais concretos, vejamos um exemplo de código:
PI = 3.141592653589793
def calcular_area_disco(raio):
raio_quadrado = raio ** 2
area = PI * raio_quadrado
def calcular_volume_esfera(raio):
raio_cubo = raio ** 3
volume = 4.0/3.0 * PI * raio_cubo
return volume
def main():
raio = float(input("Digite o raio de uma esfera: "))
peso = float(input("Digite o peso dessa esfera: "))
volume = calcular_volume_esfera(raio)
densidade = peso / volume
print(f"A densidade da esfera á {densidade}")
main()
Existe uma variável global denominada PI
. Essa variável pode ser
utilizada em qualquer ponto do programa que execute após sua
definição. Observe quem ambas as funções calcular_area_disco
e
calcular_area_disco
fazem uso de PI
.
As variáveis locais de calcular_area_disco
são o parâmetro raio
e
as demais variáveis raio_quadrado
e area
. Analogamente, as
variáveis locais de calcular_volume_esfera
são raio
, raio_cubo
e
volume
. Finalmente, as variáveis locais de main
são raio
,
peso
, volume
e densidade
.
Cada função enxerga apenas as variáveis globais e suas variáveis
locais! Assim, o nome raio_cubo
não está no escopo de main
nem de
calcular_area_disco
. Mas e os nomes de variáveis que são comuns a
mais de uma função? Cada função têm suas próprias variáveis locais,
assim há três nomes raio
distintos. Você pode pensar que há o
raio
-da-função-calcular_area_disco
, o
raio
-da-função-calcular_volume_esfera
e o raio
-da-função-main
.
O mesmo acontece para a variável volume
que é comum a
calcular_volume_esfera
e main
.
Para entender melhor, façamos um exercício. O que será impresso pelo programa a seguir?
INCREMENTO = 3
def somar(x):
x = x + INCREMENTO
def main():
x = 10
somar(x)
print(x)
main()
Você deve ter respondido corretamente: 10
. Embora tanto somar
quanto main
tenham uma variável de nome x
, em cada função esse
nome se refere a uma variável distinta.
Fazemos uma modificação. O que será impresso?
INCREMENTO = 3
def somar(x):
x = x + INCREMENTO
return x
def main():
x = 10
soma1 = somar(x)
INCREMENTO = 4
soma2 = somar(x)
print(soma1)
print(soma2)
main()
Agora o valor calculado pela primeira chamada de somar
foi devolvido
e armazenado em um variável referenciada por soma1
e o valor
calculado pela segunda chamada em soma2
. Nesse caso, não é tão
simples descobrir o que será impresso. Se você simular esse programa,
obterá 13
e 13
. Por que não foi impresso 13
e 14
? Sabemos que
somar
depende da variável global INCREMENTO
, mas a função main
faz uma atribuição a uma variável local INCREMENTO
. Lembrem-se de
que atribuições feitas em funções são sempre locais!
Por esse motivo (e por alguns outros que você ainda vai descobrir), nunca use ou faça modificações em variáveis globais! Repetindo: nunca! A única razão para usarmos uma variável global é para dar um nome a um valor que nunca deverá ser mudado durante a execução do algoritmo. Chamamos essas variáveis de constantes. Aliás, é por esse motivo que convencionamos escrever todas as variáveis globais em maiúsculas, para indicar que elas são constantes e não devem ser alteradas.
Para descobrir o que está acontecendo internamente no interpretador Python, precisamos entender o ciclo de vida de uma função. Para isso, vamos fazer mais um exercício.
Crie um programa que leia a lista de notas dos estudantes e normalize as notas de forma que a maior seja 10. Em seguida, determine para cada estudante da lista se ele foi aprovado.
Experimente resolver esse exercício. Para os impacientes, segue o código que eu faria:
NOTA_MINIMA = 5.0
def obter_maximo(lista):
assert lista, "Lista não pode ser vazia"
maximo = lista[0]
for valor in lista:
if maximo < valor:
maximo = valor
# breakpoint()
return maximo
def multiplicar_fator(lista, fator):
n = len(lista)
for i in range(n):
lista[i] = lista[i] * fator
def ler_lista_notas():
n = int(input("Digite o número de estudantes: "))
lista_notas = []
for _ in range(n):
lista_notas.append(float(input()))
return lista_notas
def imprimir_lista_aprovacao(lista_notas):
for nota in lista_notas:
if nota < NOTA_MINIMA:
print("reprovado")
else:
print("aprovado")
def main():
lista_notas = ler_lista_notas()
maximo = obter_maximo(lista_notas)
fator = 10.0 / maximo
multiplicar_fator(lista_notas, fator)
imprimir_lista_aprovacao(lista_notas)
main()
Vamos fazer um desenho representando a memória do computador no
momento imediatamente anterior em que obter_maximo
devolve o
valor máximo da lista.
Há varias coisas a se notar. Nesse momento, a função ler_lista_notas
já foi executada e terminada, então todas as variáveis locais dessa
função já não estão mais disponíveis na função, isso é, não há escopo
para as variáveis dessa função. Do mesmo modo, as funçẽs
multiplicar_fator
e imprimir_lista_aprovacao
também não foram
chamadas e, portanto, suas variáveis ainda não foram criadas.
Há exatamente duas funções sendo executadas nesse momento: a função
main
e a função obter_maximo
, então existem exatamente dois
escopos de função, além do escopo global.
Se você quiser verificar se a figura acima está correta, faça o
seguinte: descomente a linha com breakpoint()
na função
obter_maximo()
e execute o seu programa. Você entrará no mode de
debug com uma mensagem mostrando a próxima instrução. Digite bt
(que
é a abreviatura de backtrace) para mostrar a trajetória do seu
programa até essa instrução e explore a memória investigando os
valores das variáveis, e.g., digite maximo
para ver o valor
associado a esse nome no escopo atual. Se você não gosta de usar o
terminal, então pode fazer o mesmo configurando sua IDE preferida e
adicionado um breakpoint na linha correspondente ao return
.
Digite o número de estudantes: 4
4.8
3.5
8.0
7.5
> /home/user/ra123456/funcoes/notas.py(10)obter_maximo()
-> return maximo
(Pdb) bt
/home/user/ra123456/funcoes/notas.py(40)<module>()
-> main()
/home/user/ra123456/funcoes/notas.py(34)main()
-> maximo = obter_maximo(lista_notas)
> /home/user/ra123456/funcoes/notas.py(10)obter_maximo()
-> return maximo
(Pdb) valor
7.5
(Pdb) maximo
8.0
(Pdb) continue
aprovado
reprovado
aprovado
aprovado
Imediatamente depois que a função obter_maximo
termina, o valor
referenciado na frente de return
é devolvido para a função main
.
Nesse momento, o escopo da função obter_máximo
é destruído. A
próxima instrução de main
é uma atribuição ao nome de variável
máximo
, que recebe o valor devolvido. Podemos olhar para a seguinte
figura:
Com isso, podemos resumir o ciclo de vida de uma função:
-
quando uma função é chamada:
- criamos um novo escopo para essa chamada
- para cada argumento, associamos os valores passados entre parênteses
- continuamos executando a partir da primeira instrução da função
-
quando uma função termina:
- devolvemos o valor após
return
, se houver - destruímos o escopo da chamada de função
- continuamos executando a próxima instrução imediatamente após a chamada
- devolvemos o valor após
O mecanismo de chamadas de função em Python tem uma consequência
especial para listas passadas como argumentos. Quando alteramos uma
lista passada por argumento, essas mudanças ficarão visíveis para a
função que realizou a chamada. Isso acontece quando chamamos a função
multiplicar_fator
. Para ver o motivo disso, repare que tanto a
variável lista_notas
de main
quanto a variável lista
de
multiplicar_fator
são associadas à mesma lista. O desenho a seguir
representa a memória imediatamente após a primeira iteração da linha
lista[i] = lista[i] * fator
.
Módulos
À medida em que nossos projetos ficam maiores e mais complexos, copiar e colar um conjunto de funções em nossos arquivos Python torna-se bastante difícil. Mais dos que isso, pode ser que um conjunto de funções possa ser útil a diferentes programas. A maneira de tratar isso no universo Python é criando-se módulos, que agrupam um conjunto de funções e variáveis relacionadas.
Diversas linguagens de programação têm abstrações similares e implementam módulos de maneiras distintas, mas todas elas têm em comum o objetivo de organizar um programa em partes menores, que podem ser criadas e testadas separadamente. Isso é que permite, por exemplo, que grandes sistemas sejam construídos com a colaboração de várias pessoas. Nesta disciplina, iremos estudar apenas como utilizar e criar módulos simples, mas depois de ler o texto abaixo, você pode estudar a seção 6 do tutorial para uma introdução um pouco mais detalhada sobre módulos em Python.
Um módulo é um arquivo Python que é executado quando executamos
o comando import
. Os módulos podem vir de várias partes,
dependendo de como foram instalados:
- Já vimos um exemplo de módulo quando digitamos
import math
. Esse módulo é um módulo da biblioteca padrão Python, o que significa que está disponível em qualquer instalação Python. - Também há módulos adicionais instalados no sistema, que são responsáveis por tarefas comuns, mas de domínios específicos, como manipulação de imagens, comunicação com a Internet, computação científica, etc.
- Finalmente, há módulos pessoais, que são aqueles criados pelo próprio desenvolvedor para seus próprios projetos.
Por enquanto, só iremos falar em como criar um módulo pessoal. Para isso, vamos ver um exemplo.
Em uma determinada disciplina, há 2 exercícios. A média parcial de um estudante é dada pela média geométrica das notas dos exercícios. Escreva um programa que leia as listas de notas de exercicios e mostre a lista de notas parciais.
Isso é bem parecido com o que já fizemos, então nada melhor do que
copiar e colar algumas funções auxiliares. Copiamos
ler_lista_numeros
e imprimir_lista_numeros
. Para calcular as
médias, já temos uma função que faz isso, calcular_medias_finais
,
mas melhor ajustar os nomes, para não nos confundirmos. Com isso,
escrevemos um programa chamado notas_parciais.py
.
import math
def ler_lista_numeros(n):
"""Lê uma lista de n números do teclado"""
lista = []
for _ in range(n):
numero = float(input())
lista.append(numero)
return lista
def imprimir_lista_numeros(lista):
"""Imprime cada número de lista em um linha,
com duas casas decimais"""
for valor in lista:
print(f"{valor:.2f}")
def calcular_medias_geometricas(lista1, lista2):
"""Devolve uma nova lista com as médias geométricas
dos elementos de lista1 e lista2"""
medias_geometricas = []
assert len(lista1) == len(lista2), "As listas de devem ter o mesmo tamanho"
n = len(lista1)
for i in range(n):
media_geometrica = math.sqrt(lista1[i] * lista2[i])
medias_geometricas.append(media_geometrica)
return medias_geometricas
def main():
n = int(input())
notas_exercicios1 = ler_lista_numeros(n)
notas_exercicios2 = ler_lista_numeros(n)
medias_parciais = calcular_medias_geometricas(notas_exercicios1, notas_exercicios2)
imprimir_lista_numeros(medias_parciais)
main()
Repare que, enquanto nesse programa resolvemos o problema de repetição
de código, afinal, só definimos ler_lista_numeros
uma vez, temos que
escrever exatamente as mesmas instruções que estavam em outro programa
anterior. Nessas situações, é mais conveniente criar um módulo que
possa ser compartilhado entre os dois programas. Primeiro, criamos um
arquivo, no mesmo diretório, chamado utilidades.py
e movemos as
funções utilitárias para lá:
import math
def ler_lista_numeros(n):
"""Lê uma lista de n números do teclado"""
lista = []
for _ in range(n):
numero = float(input())
lista.append(numero)
return lista
def imprimir_lista_numeros(lista):
"""Imprime cada número de lista em um linha,
com duas casas decimais"""
for valor in lista:
print(f"{valor:.2f}")
def calcular_medias_geometricas(lista1, lista2):
"""Devolve uma nova lista com as médias geométricas
dos elementos de lista1 e lista2"""
medias_geometricas = []
assert len(lista1) == len(lista2), "As listas de devem ter o mesmo tamanho"
n = len(lista1)
for i in range(n):
media_geometrica = math.sqrt(lista1[i] * lista2[i])
medias_geometricas.append(media_geometrica)
return medias_geometricas
Com isso podemos modificar nosso arquivo notas_parciais.py
para conter
apenas:
from utilidades import ler_lista_numeros, imprimir_lista_numeros, calcular_medias_geometricas
def main():
n = int(input())
notas_exercicios1 = ler_lista_numeros(n)
notas_exercicios2 = ler_lista_numeros(n)
medias_parciais = calcular_medias_geometricas(notas_exercicios1, notas_exercicios2)
imprimir_lista_numeros(medias_parciais)
main()
A primeira linha desse programa faz o seguinte:
- procura o módulo chamado
utilidades
na sua lista de módulos e encontrar um arquivoutilidades.py
; - executa todas as instruções nesse módulo; nesse caso, há apenas instruções de definição de funções;
- torna disponível no escopo global do programa (
notas_parciais.py
) os nomes das funções selecionadas.
Agora, essas funções podem ser utilizadas em vários programas, mas sem a necessidade de copiar e colar. Por exemplo, suponha que, no final do semestre, tenhamos que escrever outro programa.
Além dos exercícios, há uma prova. A média final da disciplina é dada pela média aritmética entre a média parcial e a nota da prova. Escreva um programa que leia as listas de notas marciais e das provas e mostre a lista de notas finais.
Podemos, agora escrever o seguinte programa, notas_finais.py
import utilidades
def calcular_medias_aritmeticas(lista1, lista2):
"""Devolve uma nova lista com as médias aritméticas
dos elementos de lista1 e lista2"""
medias_aritmeticas = []
assert len(lista1) == len(lista2), "As listas de devem ter o mesmo tamanho"
n = len(lista1)
for i in range(n):
media_aritmetica = (lista1[i] + lista2[i]) / 2
medias_aritmeticas.append(media_aritmetica)
return medias_aritmeticas
def main():
n = int(input())
notas_exercicios1 = utilidades.ler_lista_numeros(n)
notas_exercicios2 = utilidades.ler_lista_numeros(n)
medias_parciais = calcular_medias_aritmeticas(notas_exercicios1, notas_exercicios2)
utilidades.imprimir_lista_numeros(medias_parciais)
main()
Observe que agora utilizamos uma sintaxe alternativa para importar o
módulo. O módulo utilidades.py
continua sendo executado e importado
como antes. A única diferença é que o nome da função
ler_lista_numeros
e demais não estarão disponíveis no escopo global
do nosso programa. Ao invés disso, estará disponível o nome
utilidades
, por onde acessamos as funções do módulo.
Finalmente, repare que a função auxiliar calcular_medias_aritmeticas
que tivemos que criar é suficientemente genérica e pode ser que ela
seja útil em outro programa. Assim, pode ser razoável movê-la para o
módulo de utilidades. Faça isso como exercício. A decisão de quando
uma função deve estar disponível em um módulo para reuso ou mesmo como
organizar os módulos não é uma tarefa trivial. Só com a experiência
você ganhará mais confiança para tomar essas decisões.