Você está na página 1de 29

Unidade 5 - Funções

Publicado em 26/04/2022

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

2
ax + bx + c = 0.
Desde muito sabemos como encontrar o valor de x. Primeiro,
calculamos o valor do
discriminante

2
Δ = b − 4ac.

Com o valor de Δ, podemos determinar quantas e quais são as


soluções da
equação. A fórmula para isso é

−b ± √ Δ
x = ,
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 em
minha_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 em
raiz = minha_sqrt(num) ou utilizado em uma expressão
mais
complicada, como em x = (-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:

1. controlar a entrada e a saída do programa;


2. 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.

1. para cada número q em 1, 2, … . , n:


se n for divisível por q:
faça r ← n/q
verifique se q é primo
verifique se r é primo
se ambos forem primos
responda SIM
2. 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:

1. 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.
2. 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.
3. 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.
4. Teste a função principal verificando as instruções que controlam a
entrada e a
saída da função.
5. 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[


medias_finais.append(media_final)

return medias_finais

d f i ()
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_exer


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:

1. 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.
2. Em seguida, há uma seção de import . Por enquanto só conhecemos e
precisamos de uma função no módulo math , mas podemos ter uma
sequência
bem grande de instruções desse tipos quando tivermos
programas mais
complexos que precisam de mais bibliotecas.
3. 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.
4. 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ção main .

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:

1. 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.

2. 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:

1. 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

2. 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

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:

1. 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.
2. 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.
3. 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 me


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
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 me


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, c

def main():

n = int(input())

notas_exercicios1 = ler_lista_numeros(n)

notas_exercicios2 = ler_lista_numeros(n)

medias_parciais = calcular_medias_geometricas(notas_exercicios1
imprimir_lista_numeros(medias_parciais)

main()

A primeira linha desse programa faz o seguinte:


1. procura o módulo chamado utilidades na sua lista de módulos
e encontrar
um arquivo utilidades.py ;
2. executa todas as instruções nesse módulo; nesse caso, há apenas
instruções
de definição de funções;
3. 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 me


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
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.

Você também pode gostar