HomePage RecentChanges

Lesson06

Erros

Quando definimos uma função, queremos em geral emitir um erro se os argumentos não forem do tipo correcto ou se não pertencerem ao domínio correcto. Note-se que nestes casos não queremos apenas utilizar um print, queremos antes interromper a execução da função e não permitir que a mesma continue. Para isso podemos utilizar a instrução raise que gera um erro de execução, designado por excepção em Python. Em BNF,

<instrução raise> ::= raise <nome>(<mensagem>)

<mensagem> ::= <cadeia de caracteres>

Embora tenhamos já definido nomes, o nome acima tem de corresponder à identificação de um dos tipos de erros (ou excepções) conhecidos pelo Python (ou a novos tipos de erros definidos pelo programador). Mais informação na documentação do Python. Alguns exemplos de tipos de erros (ou excepções): AttributeError, IndexError, KeyError, NameError, SyntaxError, ValueError e ZeroDivisionError.

Para tratarmos excepções, podemos utilizar o try.

Exemplo: Potência

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: expoente x nao e\' um numero')

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

    return resultado

Exemplo: Factorial

def factorial(n):

    if n < 0:
        raise ValueError('factorial: nao definido para n negativo')

    resultado = 1
    while n > 1:
        resultado = resultado * n
        n = n - 1

    return resultado

n = int(input('Inteiro: '))

try:
    x = factorial(n)
except ValueError:
    print(n, ' e\' negativo, vamos calcular abs(', n, ')! ...', sep='')
    x = factorial(abs(n))

print(x)

Exemplo: Máximo Divisor Comum (MDC)

Algoritmo de Euclides.

def euclides(n, m):
    
    if n < 0 or m < 0:
        raise ValueError('euclides: argumentos negativos')
    
    while m > 0:
        n, m = m, n%m
    
    return n

def mdc(n, m):
    return euclides(abs(m), abs(n))   

Exemplo: $e^x$

Tendo em conta a série de Taylor, temos que (na vizinhança de $0$): $$ e^x = \sum_{n=0}^\infty \frac{x^n}{n!} = \frac{1}{0!} + \frac{x}{1!} + \frac{x^2}{2!} + ... $$

Vamos aproximar o valor de $e^x$ através do cálculo dos termos desta série. Dizemos aproximado porque para obtermos o valor exacto através da série teríamos de somar todos os termos que são em número infinito. No entanto, dado um erro $\delta$, podemos ir somando termos até obtermos um valor com um erro inferior $\delta$.

Para calcular cada termo da série precisamos de calcular o factorial, mas se observarmos com mais cuidado podemos ver que podemos calcular o próximo termo a partir do anterior sem precisarmos de calcular o factorial para cada termo. O primeiro termo é $1$. Para obter o segundo termo $\frac{x}{1!}$ basta multiplicar o primeiro por $x$. Para calcular o terceiro basta multiplicar o segundo por $x/2$. Em geral, para calcular o $n$-ésimo termo basta multiplicar o termo anterior por $x/(n-1)$. Portanto, se termo_anterior for o valor para o termo anterior, podemos calcular o próximo termo, o $n$-ésimo termo, com a seguinte função:

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

Podemos portanto implementar o nosso programa da seguinte forma:

def exponencial_aproximada(x, delta):
    
    def proximo_termo(x, 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

Exemplos de uso:

print(exponencial_aproximada(0, 0.001))
print(exponencial_aproximada(1, 0.001))
print(exponencial_aproximada(2, 0.001))

Módulos

Muitas vezes vamos querer utilizar módulos ou bibliotecas de funções já existentes. Para isso temos de utilizar a instrução de importação, em BNF:

<intrução de importação> ::=
    import <módulo> NEWLINE |
    from <módulo> import <nomes a importar> NEWLINE

<módulo> ::= <nome>

<nomes a importar> ::= * | <nomes>

<nomes> ::= <nome> | <nome>, <nomes>

Podemos portanto importar de duas formas. Utilizando apenas import estamos a pedir para importar todas as funções, mas estas ficam no "espaço de nomes" do módulo, veremos de seguida como as utilizar. Utilizando também from importamos todas as funções (*) ou apenas algumas para o nosso "espaço de nomes", podendo ser utilizadas directamente através do seu nome.

Para aceder às funções no "espaço de nomes" de um módulo, por exemplo após importar um módulo com apenas import, temos de utilizar nomes compostos, em BNF

<nome composto> ::= <nome simples>.<nome simples>

Exemplo:

>>> import math
>>> math.e
2.718281828459045
>>> math.pi
3.141592653589793
>>> math.sin(math.pi/2)
1.0
>>>

Se importarmos as funções que precisamos utilizando também o from, podemos fazer antes:

>>> from math import e, pi, sin
>>> e
2.718281828459045
>>> pi
3.141592653589793
>>> sin(pi/2)
1.0
>>>

Embora possamos ficar tentados a utilizar sempre o from, em particular com o *, em geral isso pode causar conflitos com as nossas funções, ou com outros módulos que estejamos a utilizar, e pode ser mais conveniente utilizar nomes compostos por forma a evitar conflitos.

Para criarmos novos módulos basta guardar as nossas funções num ficheiro .py. Se guardarmos a nossa função exponencial_aproximada no ficheiro exponencial.py podemos fazer a seguinte experiência:

>>> import exponencial
>>> import math
>>> abs(exponencial.exponencial_aproximada(0, 0.001) - math.e**0)
0.0
>>> abs(exponencial.exponencial_aproximada(1, 0.001) - math.e**1)
2.7860205076724043e-05
>>> abs(exponencial.exponencial_aproximada(2, 0.001) - math.e**2)
6.138993594184683e-05
>>>

A lista de módulos disponíveis por omissão em Python encontra-se também na documentação do Python. Podemos também ver na documentação como publicar e instalar outros módulos.

Tuplos

Um tuplo é uma sequência de elementos. Em Python podemos pensar num tuplo como um vector em Matemática e os seus elementos são referenciados de forma análoga, ou seja, através do seu índice ou posição. A representação externa de um tuplo em Python defini-se da seguinte forma:

<tuplo> ::= () | (<elemento>, <elementos>)

<elementos> ::= <nada> | <elemento> | <elemento>, <elementos>

<elemento> ::= <expressão> | <tuplo> | <lista> | <dicionário>

<nada> ::=

Alguns exemplos de tuplos (e de coisas que não são tuplos):

>>> type(())
<class 'tuple'>
>>> type((2))
<class 'int'>
>>> type((2,))
<class 'tuple'>
>>> type((2,4,5))
<class 'tuple'>
>>> type((2,4,5,))
<class 'tuple'>
>>> type((2,4,5,'ola'))
<class 'tuple'>
>>> type((2,4,5,'ola',(8,9,)))
<class 'tuple'>
>>> type((2,4,(False,5),True,(8,9,)))
<class 'tuple'>
>>>

Para nos referirmos aos elementos num tuplo utilizamos nomes indexados,

<nome indexado> ::= <nome>[<expressão>]

em que <nome> corresponde ao nome do tuplo e <expressão>, do tipo inteiro, corresponde a uma posição.

Podemos aceder aos elementos de um tuplo segundo o seguinte esquema:

                             <------
  -7   -6   -5   -4   -3   -2   -1
+----+----+----+----+----+----+----+
| 12 | 10 | 15 | 11 | 14 | 18 | 17 |
+----+----+----+----+----+----+----+
   0    1    2    3    4    5    6
------>

Exemplo:

>>> notas = (12, 10, 15, 11, 14, 18, 17)
>>> notas[0]
12
>>> notas[3]
11
>>> notas[-1]
17
>>> notas[-2]
18
>>> notas[3+1]
14
>>> notas[9]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: tuple index out of range
>>>
>>> v = (12, 10, (15, 11, 14), 18, 17)
>>> v[2][1]
11
>>>
>>> v[2] = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>>

Note-se que nestes dois últimos casos podemos ver como aceder a um tuplo dentro de outro tuplo, e podemos ver também que um tuplo é imutável, i.e., não podemos modificá-lo.

Operações sobre tuplos (note-se a sobrecarga dos operadores + e *):

>>> a = (2, 1, 3, 3, 5)
>>> b = (8, 2, 4, 7)
>>> a + b
(2, 1, 3, 3, 5, 8, 2, 4, 7)
>>> c = a + b
>>> c[3:5]
(3, 5)
>>> c[3:6]
(3, 5, 8)
>>> a * b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't multiply sequence by non-int of type 'tuple'
>>> a * 2
(2, 1, 3, 3, 5, 2, 1, 3, 3, 5)
>>> 1 in a
True
>>> 1 in b
False
>>> len(a)
5
>>> a[:3]
(2, 1, 3)
>>> a[4:]
(5,)
>>> a[:]
(2, 1, 3, 3, 5)
>>> tuple(8)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>

O último exemplo só funcionará se o argumento for um iterável, como por exemplo listas ou dicionários, que estudaremos mais tarde.

Como os tuplos são imutáveis, para efectuarmos transformações sobre tuplos temos de aplicar as operações acima e construir novos tuplos. Consideremos por exemplo a seguinte função que recebe um tuplo, uma posição e um elemento, e que devolve um tuplo idêntico ao primeiro mas em que o elemento surge na posição indicada.

def substitui(t, p, e):
    if not (0 <= p < len(t)):
        raise IndexError('substitui: no tuplo dado como primeiro argumento')
    return t[:p] + (e,) +t[p+1:]

Exemplo:

>>> a = (2, 1, 3, 3, 5)
>>> substitui(a, 2, 'a')
(2, 1, 'a', 3, 5)
>>> substitui(a, 4, 'a')
(2, 1, 3, 3, 'a')
>>> a = substitui(a, 0, 'a')
>>> a = substitui(a, 5, 'a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in substitui
IndexError: substitui: no tuplo dado como primeiro argumento
>>> a
('a', 1, 3, 3, 5)
>>>

Note-se que quando atribuímos o resultado à variável a não estamos a modificar o tuplo, estamos sim a mudar a entidade associada a a.

Uma das operações sobre tuplos mais comuns consiste em processar a sequência de elementos iterando sobre os mesmos. Exemplo:

def soma(t):
    resultado = 0
    i = 0
    while i < len(t):
        resultado = resultado + t[i]
        i = i +1
    
    return resultado

Note-se na condição de guarda do ciclo estamos a calcular sempre o comprimento do tuplo, o que pode ser ineficiente dependendo da implementação da função len. Alternativamente podemos guardar esse valor numa variável e utilizar a mesma na condição de guarda.

Como último exemplo consideremos a função alisa que dado um tuplo, que pode conter tuplos, devolve um tuplo em que os elementos que eram tuplos no argumento são desmantelados e os seus elementos passam a fazer parte do resultado. Notar que, se algum dos elementos dos tuplos internos for um tuplo, então também devem ser desmantelados. Para isso precisamos de identificar os elementos que são tuplos. Vimos antes que o podemos fazer com a função type, mas vejamos como utilizar a função isinstance,

>>> isinstance(3, int)
True
>>> isinstance(3, (int, bool))
True
>>> isinstance(True, (int, bool))
True
>>> isinstance(5.6, (int, bool))
False
>>> isinstance('a', (int, bool))
False
>>> isinstance('a', (int, bool, str))
True
>>> isinstance((8,), tuple)
True
>>>

Proposta de solução para a função alisa:

def alisa(t):
    i = 0
    
    while i < len(t):
        if isinstance(t[i], tuple):
            t = t[:i] + t[i] + t[i+1:]
        else:
            i = i + 1
    
    return t

Vejamos o resultado:

>>> alisa((2, 4, (8, (9, (7, ), 3, 4), 7), 6, (5, (7, (8, )))))
(2, 4, 8, 9, 7, 3, 4, 7, 6, 5, 7, 8)
>>>