HomePage RecentChanges

Lesson11

Ainda que já tenhamos visto como definir funções, na realidade ainda não vimos grande parte do que podemos fazer com funções. Este será o objecto de estudo das próximas aulas.

Estruturação de funções

Quando vimos como definir funções foi observado que o corpo de uma função poderia incluir a definição de outras funções. Em particular, vimos o seguinte em BNF:

<definição de função> ::=
    def <nome> (<parâmetros formais>): NEWLINE
    INDENT <corpo> DEDENT

<corpo> ::= <definição de função>* <instruções em função>

Ainda que até agora raramente tenhamos utilizado este aspecto, é importante notar a sua utilidade. Como primeiro exemplo vejamos a função que escrevemos quando aproximámos o valor de $e^x$:

def exponencial_aproximada(x, delta):
    
    def proximo_termo(n, termo_anterior):
        return termo_anterior * x / (n - 1)

    termo = 1
    resultado = 1
    n = 1

    while termo > delta:
        n = n + 1
        termo = proximo_termo(x, n, termo)
        resultado = resultado + termo

    return resultado

Neste caso temos uma função interna que nos permite estruturar melhor a nossa implementação. Nota-se no entanto que a função interna proximo_termo, embora útil no contexto da função exponencial_aproximada, não tem qualquer utilidade fora desta, dependendo em particular do parâmetro x passado à função exponencial_aproximada. Ou seja, a sua definição apenas faz sentido no âmbito da função exponencial_aproximada.

Este tipo de solução baseia-se no conceito de estrutura de blocos. Em linguagens estruturadas em blocos, como o Python, as funções são vistas como blocos dentro dos quais podem existir outros blocos, i.e., funções. Neste âmbito aquilo que é definido dentro de um bloco apenas pode ser utilizado no âmbito do mesmo e dos blocos definidos dentro do mesmo. É fácil perceber porque não pode ser de outra forma, no exemplo anterior a função proximo_termo depende de informação que pode não estar disponível se pudéssemos utilizá-la for do bloco inerente à função exponencial_aproximada, nomeadamente o nome x.

Note-se também a relação entre blocos e a relação entre ambientes vista em aulas anteriores. As funções definidas dentro de outras funções, assim como os nomes definidos em geral dentro de funções, podem ser vistos como nomes privados ou locais, por oposição aos nomes não locais. Os nomes não locais pode ser nomes públicos ou globais definidos no ambiente global, ou nomes livres quando nos referirmos a nomes que não são locais e que não globais, como por exemplos os nomes definidos em ambientes/blocos exteriores aninhados.

O domínio de um nome corresponde ao conjunto de instruções onde o nome pode ser utilizado. Neste sentido, o domínio dos nomes locais é o corpo da função de que são parâmetros formais ou na qual são definidos. O domínio dos nomes globais é o conjunto de todas as instruções num dado programa. Em Python os domínios têm natureza estática na medida em que dependem da estruturação do programa, i.e., da hierarquia de blocos definida, e não são influenciados pelo modo de execução do programa. Em particular, como vimos quando estudámos os ambientes, alterações das associações de nomes locais não são propagadas para nomes não locais mesmo que sejam homónimos (note-se que estamos a falar de associações a nomes, não estamos a falar de alterações em estruturas passadas por referências!). Se quisermos partilhar variáveis não locais entre funções, podemos utilizar a instrução global,

<instrução global>>> ::= global <nomes>

Quando utilizamos esta instrução, os nomes em questão correspondem a nomes partilhados cujas associações podem ser modificadas como se de nomes não locais se tratassem. Notar que a instrução global não pode referir-se a parâmetros formais. Notar também que dever-se-ão utilizar nomes locais sempre que possível, mantendo a independência entre funções, a que chamámos antes abstracção procedimental. Ainda assim, em alguns casos, é importante partilhar nomes entre diferentes blocos num dado programa.

Vejamos outro exemplo. Consideremos a função potencia,

def potencia(x, k):
    if k < 0:
        raise ValueError('potencia: expoente k negativo')
    elif type(k) != int:
        raise ValueError('potencia: expoente k nao inteiro')
    elif type(x) != int and type(x) != float:
        raise ValueError('potencia: base x nao e\' um numero')

    resultado = 1
    while k > 0:
        resultado = resultado * x
        k = k - 1

    return resultado

E se quisermos estender para potencias negativas? Podemos utilizar novamente a noção anterior de estruturação de funções,

def potencia(x, k):
    
    def potencia_aux(k):
        resultado = 1
        while k > 0:
            resultado = resultado * x
            k = k - 1
        return resultado
    
    if type(k) != int:
        raise ValueError('potencia: expoente k nao inteiro')
    elif type(x) != int and type(x) != float:
        raise ValueError('potencia: expoente x nao e\' um numero')
    
    resultado = potencia_aux(abs(k))
    
    if k < 0:
         resultado = 1/resultado
    
    return resultado

Programação funcional

Programação funcional é um paradigma de programação exclusivamente baseado na utilização de funções e que tem a sua origem no cálculo lambda. Até ao momento temos seguido o paradigma de programação imperativa, em que um programa é considerado como um conjunto de instruções dadas ao computador e em que a instrução de atribuição ou de associação de nomes tem um papel preponderante.

Em programação funcional um programa é considerado uma função que calcula ou avalia outras funções e retorna um valor/resultado, evitando alterações de estado e entidades mutáveis. Neste paradigma não existe o conceito de atribuição e não existem ciclos. O conceito de iteração é conseguido através de recursividade, que veremos mais em detalhe na próxima aula e que exemplificamos de seguida com a função potencia,

def potencia(x, k):
    if k < 0:
        raise ValueError('potencia: expoente k negativo')
    elif type(k) != int:
        raise ValueError('potencia: expoente k nao inteiro')
    elif type(x) != int and type(x) != float:
        raise ValueError('potencia: base x nao e\' um numero')

    if k == 0:
         return 1
    else:
        return x * potencia(x, n-1)
    

Ao compararmos com a versão imperativa vista acima, podemos constatar que neste caso não temos iteração explícita ou atribuições. A repetição é conseguida através da recursividade e os valores resultantes da avaliação da função são directamente utilizados nos cálculos subsequentes.

Outro exemplo,

def soma_digitos(n):
    if n == 0:
        return n
    else:
        return n % 10 + soma_digitos(n // 10)