Lista 5 - Funções

Funções

  1. Exponenciação:

    a) Escreva uma função que receba uma um número de ponto flutuante $a$ e um inteiro $b$ e devolva a potência $a^b$. Não use bibliotecas como math ou o operador de exponenciação **.

    b) Use a função anterior e crie um programa que leia um númro $n$ do teclado e imprima todas as potências: $2^0, 2^2, \dots, 3^0, 3^1, \dots, n^{n}$.

    c) Enquanto o primeiro item da questão pede uma função, o segundo item pede um programa. Via de regra, uma função deve se preocupar apenas com o subproblema sendo resolvido e não deve realizar leitura e impressão. Assim, ela deve receber a entrada via parâmetro(s) e devolver a saída como retorno(s). A sua função faz alguma chamada para input() ou para print()? Explique porque isso seria uma má prática de programação.

  2. Escreva uma função que computa o fatorial de um número inteiro $n$ passado por parâmetro. Use a função anterior e crie um programa que imprima os valores de $n!$ para $n = 1, 2, \dots, 20$.

  3. Escreva uma função que receba um número inteiro e devolva uma string com o mês correspondente ao número. Por exemplo, 2 corresponde a “fevereiro”. Use uma instrução assert no início da função para se certificar de que a entrada da função é válida.

  4. Escreva uma função que receba um número inteiro positivo menor ou igual a 50 e devolva uma string representando o número romano correspondente. Por exemplo, para entrada 5 a saída desejada é ‘V’. Utilize assert para verificar se a entrada é válida.

  5. Listas invertidas:

    a) Faça uma função que receba uma lista e devolva uma nova lista com os elementos na ordem reversa. Não use a funções prontas, como reversed() ou similares.

    b) Faça uma função que receba uma lista e modifique essa lista, revertendo a ordem dos elementos. Não use a funções prontas, como método reverse() ou similares. Também, não utilize qualquer outra lista além da recebida como parâmetro.

  6. Primos:

    a) Escreva uma função que decida se um número inteiro é primo. Se sim, deve devolver True, caso contrário, False.

    b) Escreva uma função que recebe um número inteiro $n$ passado por parâmetro e devolve o maior número primo que é menor ou igual a $n$.

  7. Casamento de padrões: dadas duas sequências de caracteres, uma chamada texto e outra chamada padrao, faça uma função que verifique se existe uma ocorrência de padrao no texto. Caso exista uma ocorrência, a função retorna a posição inicial do texto onde ocorre o padrão, caso contrário, retorna -1.

  8. Crie uma função que calcule e devolva o número de arranjos de $n$ elementos $p$ a $p$. A fórmula do arranjo é a seguinte:

    $$ A^n_p = \frac{n!}{(n-p)!} $$

    Faça duas funções, uma que utiliza uma função fatorial como sub-rotina e uma que não utiliza. Para você, qual versão é melhor? Por quê?

  9. Faça uma função que calcule a aproximação para a integral:

    $$ \int_0^x e^{-u^2} = x - \frac{x^3}{3 \cdot 1!} + \frac{x^5}{5 \cdot 2!} - \frac{x^7}{7 \cdot 3!} + \dots $$

    Sua função deverá ter a definição

    def aprox_int(x, eps):
    

    onde x é o limite da integral e eps é um número real utilizado como critério de parada da aproximação de forma que sua função utiliza apenas termos cujo valor absoluto é pelo menos eps.

  10. Notas:

    a) Escreva uma função que leia do teclado o número de questões de uma prova e o valor de cada uma das questões. A função deve retornar a nota da prova.

    b) Escreva uma função que receba como parâmetro um número de provas e leia do teclado o número de questões e os valores das questões de cada prova. A função deve retornar a média das provas.

    c) Escreva um programa que leia do teclado um número de provas dadas em um semestre, o número de alunos matriculados e o número de questões e os valores das questões da cada prova de cada aluno. O programa deve imprimir a razão entre a média das notas dos alunos que tiraram pelo menos 5 e a média das notas dos alunos que tiraram abaixo de 5.

  11. O resultado de uma função pode ser utilizado em qualquer expressão, inclusive como parâmetros para outras funções. Analise o código abaixo e faça um teste de mesa, i.e., simule usando papel. Qual o resultado impresso?

    def soma(A, B):
        return A + B
    
    print(soma(1, soma(2, soma(3, 4))))
    
  12. Em Python, é possivel nomear os parâmetros que estamos usando no momemento da chamada. Isso é útil para deixar explícito o que significa cada valor. Veja esse exemplo e indique a saída.

    def dividir(dividendo, divisor):
        return dividendo / divisor
    
    print(dividir(3, 4))
    print(dividir(divisor=3, dividendo=4))
    
  13. Em vários momentos, precisamos retornar vários valores distintos. É possível fazer isso de várias maneiras. Uma delas é usando o que chamamos de tupla, veja o exemplo abaixo. Podemos devolver qualquer tipo de estrutura.

     def funcao():
         texto = "dois"
         numero = 2
         return texto, numero
    
     tupla = funcao()
     print(tupla[1])
    
     texto, numero = funcao()
     print(texto)
    

    Faça uma função que leia uma lista de $n$ inteiros (com repetição) e retorne uma tupla $(a, m)$, em que $a$ é o número com maior número de repetições e $m$, o número de repetições desse elemento.

  14. Produtos de ímpares:

    a) Escreva uma função que decida se um número é produto de dois números ímpares. Se for, a função deverá devolver esses dois números, do contrário, deverá devolver None.

    b) Escreva uma função que decida se um número é produto de quatro números ímpares. Se for, a função deverá devolver esses quatro números, do contrário, deverá devolver None. Tente utilizar a função anterior.

  15. Em Python, uma função é uma variável e pode ser inclusive passada como parâmetro. Veja o exemplo abaixo:

    def media_aritmetica(a, b):
        return (a + b) / 2
    
    def calcular_medias(provas, exercicios, funcao_media):
        finais = []
        n = len(provas)
        for i in len(n):
            nota_final = funcao_media(provas[i], exercicios[i])
            finais.append(nota_final)
        return finais
    
    def main():
        provas = [3.5, 7.5, 10]
        exercicios = [7.0, 6.0, 9.8]
        finais = calcular_medias(provas, exercicios, media_aritmetica)
        print(finais)
    

    Modifique o programa, de forma a utilizar a média geométrica, ao invés da média aritmética.

  16. Escreva uma função chamada raiz que recebe um intervalo $[a,b]$ e uma função real $f$ passada por parâmetro. A função raiz deve devolver uma aproximação da raiz da a equação $f(x) = 0$. Essa função passada recebe um número de ponto flutuante e devolve outro número. Suponha que a função que ela calcula é contínua e que existe uma única raiz para nesse intervalo. Uma aproximação da raiz é qualquer número $x'$ tal que $|x - x'| < \epsilon$ para $\epsilon$ escolhido (digamos, $10^{-6}$).

Escopo e visibilidade

  1. Lembre-se de sempre verificar se suas funções estão devolvendo algum valor. O que o código abaixo imprime? Verifique sua resposta com um computador e justifique a saída. Experimente fazer modificações.

    a = 1
    b = 2
    c = 0
    
    def soma1(a, b):
        c = a + b
    
    def soma2(a, b):
        return a + b
    
    res1 = soma1(a, b)
    res2 = soma2(a, b)
    
    print(f"c = {c}")
    print(f"res1 = {res1}")
    print(f"res2 = {res2}")
    
  2. É importante entender como funções modificam listas e outras estruturas passados por parâmetro. Veja o código abaixo e determine o que é impresso. Depois, verifique no computador. O resultado foi diferente do que você esperava? Explique.

    def substituir_variavel(variavel):
        variavel = 4
    
    def substituir_lista(lista):
        lista = [5, 5, 5, 5]
    
    def modificar_lista(lista):
        lista.append(6)
    
    variavel = 0
    lista = [1, 2, 3]
    
    substituir_variavel(variavel)
    substituir_lista(lista)
    modificar_lista(lista)
    
    print(f"variavel = {variavel}")
    print(f"lista = {lista}")
    
  3. Considere o código abaixo:

    lista = [1, 2, 3]
    c = 2
    
    def main():
        soma = 0
        produto = 1
        for i in lista:
            soma = somar(soma, i)
            produto = multiplicar(produto, i)
            print(f"soma = {soma}, produto = {produto}")
    
    def somar(a, b):
       c = 3
       return a + b + c
    
    def multiplicar(a, b):
       return a * b * c
    
    main()
    

    a) Determine quais são as variáveis locais e globais deste programa, identificando a que função pertence cada variável local.

    b) Mostre o que será impresso na tela quando este programa for executado. Depois verifique seu programa com um computador.

  4. Para controlar quais funções estão sendo chamadas e quais variáveis estão sendo modificadas, devemos estudar a visibilidade de variáveis, isso é, o que cada uma das funções “enxerga”. Isso é particularmente importante quando trabalhamos com vários arquivos. Para isso, temos alguns modificadores, que são palavras chaves especiais como global (na prática, nunca devemos usar essa palavra-chave). Fazer declarações de funções e variáveis com esses modificadores pode implicar em maneiras diferentes da organização da memória.

    Arquivo lib.py:

    a = 21
    def f1():
        print(a)
        global c
        c = 77;
        print(c)
        return
    
    def f3():
        f2(1,2)
        print(c)
        return
    
    def f2(a, b):
        print(a + b)
        f1()
        print(c)
        return
    
    def f4():
        c = 14
        print(c)
        f3()
        print(c)
        return
    

    Arquivo main.py:

    import lib
    
    def main():
        e = 3
        lib.f4()
        print(lib.c)
        return
    
     main()
    

    a) Para cada função, descreva as variáveis visíveis e funções visíveis. Diga quais variáveis são locais ou não.

    b) Faça um desenho da organização da memória no momento em que a função f2 estiver executando.

    c) Como você faria para contar o número de vezes que uma função é chamada em um programa?

Pythonismo

Python tem algumas convenções, funções e sintaxes particulares que outras linguagens não possuem, ou que são implementadas de maneira diferente. Assim, é comum entrar em fóruns online e ver discussões sobre códigos “pytônicos”, isto é, códigos que foram otimizados para legibilidade e funcionamento com Python. Nesta disciplina, queremos aprender algoritmos, então às vezes evitamos construções como essas, mas nesta seção vamos tentar implementar coisas de jeitos pytônicos.

  1. Considere uma variável lista. Qual a diferença entre for valor in lista e for i in range(len(lista))? Explique com qual a diferença das variáveis de iteração valor e i nos dois fors.

  2. Quando estamos trabalhando com listas, é normal utilizarmos list comprehensions para facilitar o trabalho, particularmente quando queremos filtrar um subconjunto de elementos.

    a) O que o código abaixo imprime?

    A = [-2, 3, -5, 1, 0, - 11]
    B = [i for i in A if i < 0]
    
    print(B)
    

    b) Baseando-se no exemplo acima, cria uma list comprehension que copia em B os elementos múltiplos do primeiro elemento da lista A. Preencha o modelo:

    A = [5, 8, 3, 0, 15, 7, 20, 13, 30, 31]
    B = ...
    
    print(B)
    

    Depois, verifique com o auxílio de um computador.

    c) Se a condição de filtragem for mais elaborada, podemos criar funções para deixar a list comprehension mais simples. O quê o código abaixo imprime?

    def eh_soma_grande(c):
        if c + 3 >= 8:
             return True
        else:
             return False
    
    a = [2, 8, 3, 11, 4, 5, 6, 7]
    b = [i for i in a if eh_soma_grande(i)]
    
    print(b)
    

    d) Baseando-se no exemplo acima, cria uma list comprehension que copia em B os elementos da lista A que são primos.

  3. Na maioria das vezes que queremos percorrer uma lista, estamos interessados no valor dos elementos, mas algumas vezes também estamos interessados nos índices em que esses elementos estão. Para percorrer tanto índices, quanto valores, usamos enumerate(). A função abaixo devolve tanto a posição do maior elemento quanto seu valor.

    def maior(lista):
        maior = lista[0]
        pos_maior = 0
        for i, valor in enumerate(lista):
            if valor > maior:
                maior = valor
                pos_maior = i
         return i, maior
    

    a) A função acima faz uma comparação a mais do que o necessário. Como você pode alterar o código para evitar essa comparação?

    b) Crie uma função que receba uma palavra e devolva uma lista de todos as letras que aparecem nessa palavra imediatamente antes de “p” e “b”. Tente utilizar enumerate().

  4. Funções dentro de funções é algo que você vai encontrar em em diversos códigos em Python. As nested functions, ou funções aninhadas, são utilizadas por uma série de razões.

    a) Quando um trecho de código é repetido várias vezes dentro de uma função, podemos criar uma função interna para evitar repetição de código.

    def operacoes(a, b):
    
        def verificar(x, y, op, res):
            if res == 42:
                print(f"O valor de {x} {op} {y} é {res}.")
                print("Essa é uma operação fundamental!")
    
        verificar(a, b, '+', a + b)
        verificar(a, b, '-', a - b)
        verificar(a, b, '*', a * b)
        verificar(a, b, '/', a / b)
    
        operacoes(6, 7)
    

    Qual a saída desse programa? O que acontece se tentarmos chamar verificar a partir da função principal? Experimente com um computador.

    b) A função interna pode acessar as variáveis do escopo em que está definida. Nesse caso, chamamos essa função de clousure, pois ela mantém junto dela referência a algumas das variáves da função.

    def operacoes(a, b):
    
        fundamentais = []
    
        def verificar(op, res):
            if res == 42:
                print(f"O valor de {a} {op} {b} é {res}.")
                fundamentais.append(op)
    
        verificar("+", a + b)
        verificar("-", a - b)
        verificar("*", a * b)
        verificar("/", a / b)
    
        print(f"As operações fundamentais são {fundamentais}.")
    
    operacoes(6, 7)
    

    Compare este trecho com o trecho anterior. Qual a diferença? Qual a saída do programa?

  5. Por vezes, queremos iterar sobre duas listas simultaneamente. Para esse fim, existe a função zip(), que cria um iterador que agrega duas ou mais sequências (listas, tuplas, etc…). Veja um exemplo:

    lista_a = [1, 2, 3, 4, 5]
    lista_b = ['a', 'b', 'c', 'd', 'e']
    
    for a, b in zip(lista_a, lista_b):
        print(a, b)
    

    Essa função irá imprimir 1 a, 2 b etc. Utilize zip para receber três listas a, b, c, com valores inteiros e criar uma lista d de modo que d[i] == a[i] * b[i] * c[i]. Depois, descubra o que acontece quando as listas não possuem o mesmo tamanho.

  6. Em Python, erros normalmente são tratados por meio das chamadas exceções, que representam condições de erro ou que impedem o fluxo normal de execução. Vamos nos basear em um programa divisao.py que recebe dois números e imprime a divisão.

    def dividir(a, b):
        return a / b
    
    def main()
        print("Este programa divide a por b")
    
        a = int(input("Digite o valor de a: "))
        b = int(input("Digite o valor de b: "))
    
        print(f"O resultado da divisão é {dividir(a, b)}.")
    
    main()
    

    Podemos testar com algumas entradas.

    user@desktop:~/$ python3 divisao.py
    Este programa divide a por b
    Digite o valor de a: 1
    Digite o valor de b: 2
    O resultado da divisão é 0.5.
    user@desktop:~/$ python3 divisao.py
    Este programa divide a por b
    Digite o valor de a: 1
    Digite o valor de b: 0
    Traceback (most recent call last):
    File "divisao.py", line 13, in <module>
        main()
    File "divisao.py", line 11, in main
        print(f"O resultado da divisão é {dividir(a, b)}.")
    File "divisao.py", line 2, in dividir
        return a / b
    ZeroDivisionError: division by zero
    

    Perceba que na segunda divisão, ao tentarmos dividir por zero, o programa termina com uma mensagem de erro. Essa mensagem de erro corresponde a uma condição de exceção não tratada ZeroDivisionError.

    Isso pode indicar um bug no programa, mas também pode acontecer que algumas condições de exceção sejam esperadas. Por exemplo, enquanto não podemos dividir por zero, pode ser que queiramos que o usuário possa digitar qualquer número como divisor.

    Já que não há como continuar a execução normal dividindo por zero, temos que instruir o interpretador Python a o que fazer quando uma condição como essa acontecer. O que queremos fazer é tratar essa exceção para que o programa continue rodando.

    Para isso, podemos usar as construções try e o except. No bloco de try testamos um bloco de código no fluxo normal de execução. Se ocorrer alguma exceção, o fluxo de execução será desviado para o bloco de except correspondente; nesse bloco, devemos tratar o erro. Por exemplo, adaptando o código acima para tratar a divisão por 0, temos o código a seguir.

    def dividir(a, b):
        return a / b
    
    def main():
        print("Este programa divide a por b")
    
        a = int(input("Digite o valor de a: "))
        b = int(input("Digite o valor de b: "))
    
        try:
            print(f"O resultado da divisão é {dividir(a, b)}.")
        except ZeroDivisionError:
            print("Não é possível dividir por zero.")
    
    main()
    

    Dessa vez, a execução do programa continua com o tratamento da exceção.

    user@desktop:~/$ python3 divisao.py
    Este programa divide a por b
    Digite o valor de a: 1
    Digite o valor de b: 0
    Não é possível dividir por zero.
    

    Observe que o nome do erro é colocado após a palavra-chave except. Essa palavra-chave indica qual exceção estamos esperando; pode haver vários blocos. Portanto, para qualquer tipo de erro, ZeroDivisionError, EOFError, entre outros, podemos tratá-lo para o nosso programa continuar executando ao nosso critério.

    Escreva uma função que leia do teclado um número de ponto flutuante. Para converter uma string em número, use a função float. Se a linha digitada não for uma string que possa ser convertida em ponto flutuante, a função deve tentar novamente até que um valor válido seja digitado.

Exercícios criativos

  1. Modificando o comportamento de funções:

    a) Escreva uma função numero_palavras que conta o número de palavras que terminam com “s” em um texto dado.

    b) Escreva uma função eh_plural que devolve True se uma palavra termina com s e False, caso contrário.

    c) Reescreva uma função numero_palavras utilizando a função eh_plural como sub-rotina. Você pode redefinir a assinatura da primeira função para torná-la mais conveniente.

    d) Suponha agora que você esteja interessado em contar outros tipos de palavras de interesse, digamos, palavras que terminal com ão, ou palavras que começam com a. Reescreva a função numero_palavras para que ela conte as palavras de interesse. As palavras de interesse são identificadas por uma função indicadora passada por parâmetro, isso é, uma função que recebe uma palavra e devolve True se for de interesse e False caso contrário.