Funções e decoradores
No Python tudo é um objeto, inclusive as funções. Isso significa que elas podem ser atribuidas a uma variável ou serem retornadas por outra função. Na programação em geral uma função é considerada um objeto de primeira classe se:
- é uma instância do tipo Object,
- pode pode ser armazenada em uma variável,
- pode ser passada como um parâmetro para outra função,
- pode ser obtida no retorno de outra função,
- pode ser armazenada em estruturas de dados, como listas e dicionários.
No Python funções podem ser atribuídas a variáveis.
# uma variável pode armazenar uma função interna » p = print » p(1234) ↳ 1234 # ou uma do usuário » def funcao(): » print('Tô aqui!') » a = funcao # a é uma função » print(a) ↳ <function __main__.funcao()> # a função é executada com colchetes » a() ↳ Tô aqui! # outra função recebe uma string como parâmetro » def funcao(texto): » print(texto) » a = funcao » a('Meu nome é Enéas!') ↳ 'Meu nome é Enéas!
Funções podem ter outras funções definidas dentro de seu corpo. No caso abaixo temos o cálculo da função composta \(f(x) = \sqrt(x^2+1)\).
» import math » def funcaoComposta(x): » def funcaoInterna(i): » return i**2 + 1 » return math.sqrt(funcaoInterna(x)) » funcaoComposta(7) ↳ 7.0710678118654755
Funções podem ser passadas como argumentos para outras funções. A função digaOla(arg) recebe outras duas funções como argumento.
# funções como argumento de outras funções » def falaAlto(texto): » return texto.upper() » def falaBaixo(texto): » return texto.lower() » def digaOla(func): » # variável oi armazena o retorno (string) das funções no argumento func » oi = func('Olá, texto usado como argumento da função parâmetro!') » print (oi) » digaOla(falaBaixo) ↳ olá, texto passado como argumento da função parâmetro! » digaOla(falaAlto) ↳ OLÁ, TEXTO PASSADO COMO ARGUMENTO DA FUNÇÃO PARÂMETRO!
A função funcOla é chamada de decoradora. A função funcNome, que é passada como argumento para o decorador, é chamada de função decorada.
» # exemplo 1 » def funcOla(varFuncao): » def funcInterna(): » print('Olá ', end='') » varFuncao() » return funcInterna » def funcNome(): » print('Assurbanipal, rei da Assíria') » obj = funcOla(funcNome) » obj() ↳ Olá Assurbanipal, rei da Assíria # exemplo 2 » def func1(txt): » print(txt) » def func2(funcao, txt): » funcao(txt) » func2(func1, 'Libbali-sharrat, esposa de Assurbanipal') ↳ Libbali-sharrat, esposa de Assurbanipal # exemplo 3 » def decoradora(func): » def interna(): » print("Ocorre antes da função parâmetro ser executada.") » func() » print("Ocorre depois da função parâmetro ser executada.") » return interna » def digaUau(): » print("Uau!!!!") » f = decoradora(digaUau) # <---- f é uma função composta » f() # <---- executar a função f ↳ Ocorre antes da função parâmetro ser executada. ↳ Uau!!!! ↳ Ocorre depois da função parâmetro ser executada.
Funções compostas são chamadas de objetos de segunda classe ou funções de ordem superior. Decoradores envolvem uma função, modificando seu comportamento. Quando executamos f = decoradora(digaUau)
estamos executando interna() tendo em seu corpo func=digaUau()
.
O Python fornece uma forma simplificada de usar decoradores, usando o sinal @.
» def funcaoDecoradora(funcaoArg): » def interna(): » # corpo de interna usando funcaoArg() » return interna » @funcaoDecoradora » def funcaoDecorada: » # corpo de decorada # » # essa sintaxe é equivalente à » funcaoComposta = funcaoDecoradora(funcaoDecorada) » # para executá-la » funcaoComposta()
No caso do último exemplo 3 podemos apenas fazer
» def decoradora(func): » def interna(): » print("Ocorre antes da função decorada ser executada.") » func() » print("Ocorre depois da função decorada ser executada.") » return interna » @decoradora » def digaUau(): » print("Uau!!!!") » digaUau() ↳ Ocorre antes da função decorada ser executada. ↳ Uau!!!! ↳ Ocorre depois da função decorada ser executada.
Se a função a ser decorada possuir parâmetros, a função interna (que envolve a decorada) deve possuir os mesmos parâmetros, que devem ser fornecidos quando se invoca a função decorada.
» def produtoDecorado(func): » def interna(a,b): » print('%d x %d = ' % (a,b), end='') » return func(a,b) » » return interna » @produtoDecorado » def produto(a,b): » print(a * b) » produto(55,33) ↳ 55 x 33 = 1815
Vale lembrar que se desejamos passar um número qualquer de parâmetros podemos usar *args e *kwargs, que representam respectivamente um número arbitrário de argumentos e de argumentos com palavras chaves.
» def produtoDecorado(func): » def interna(*args): » print('O produto %s = ' % str(args).replace(', ',' x '), end='') » return func(*args) » » return interna » @produtoDecorado » def produtorio(*args): » prod = 1 » for t in args: » prod *= t » print(prod) » produtorio(1,2,3,4,5,6,7,8,9) ↳ O produto de (1 x 2 x 3 x 4 x 5 x 6 x 7 x 8 x 9) = 362880
Um uso comum para um decorador é o de medir o tempo de execução de um bloco de código qualquer. Isso pode ser útil na otimização de um programa. Para isso usamos o módulo time, e suas funções time.time(), que lê a hora em segundos †, e time.sleep(n), que suspende a execução do código por n segundos.
Para isso envolvemos o bloco de código a ser medido, no caso a função que é decorada, com o contador. O instante inicial é armazenado e comparado com o final, após a execução, a a diferença é exibida.
» # decorador para medir o tempo de execução de um bloco de código » import time » def cronometro(func): » def interna(*arg): » inicio = time.time() » func(*arg) » print('O código levou %s segundos para rodar.' % str(time.time()-inicio)) » return interna » @cronometro » def funcaoTeste(n): » time.sleep(n) » funcaoTeste(1.5) ↳ O código levou 1.5015053749084473 segundos para rodar. » # outro teste, com um laço for » @cronometro » def laco(): » soma = 0 » for t in range(10000): » soma += t » print('soma =',soma) » laco() ↳ soma = 49995000 ↳ O código levou 0.0010344982147216797 segundos para rodar.
Erros, Exceções e tratamento de erros
No Python existem dois tipos de erros que são denominados erros de sintaxe e exceções.
Erros de sintaxe são comandos escritos incorretamente, a ausência ou excesso de parênteses, chaves ou colchetes ((, {, [,), delimitadores incorretos de strings, vírgulas ausentes ou postas em local incorreto, etc. Quando encontra esses erros o interpretador interrompe a execução e retorna uma instrução de onde o erro ocorreu e, em alguns casos, uma sugestão de como consertá-lo. Nessas mensagens um sinal de ^ indica o local onde o erro foi notado. Se o código foi lido em um arquivo.py o nome do arquivo é indicado e a linha do erro é indicada. Essas mensagens são fornecidas pela função Traceback.
» print 'sem parênteses' ↳ File "<ipython-input-6-cfe4fc7e6b4d>", line 1 ↳ print 'sem parênteses' ↳ ^ ↳ SyntaxError: Missing parentheses in call to 'print'. Did you mean print('sem parênteses')? » print('parênteses excessivos')) ↳ File "<ipython-input-7-1c97f0f5b744>", line 1 ↳ print('parênteses excessivos')) ^ ↳ SyntaxError: unmatched ')' » dicionario = {1:'um', 2:'dois' 3:'três'} ↳ File "<ipython-input-12-60359adab8df>", line 1 ↳ dicionario = {1:'um', 2:'dois' 3:'três'} ↳ ^ ↳ SyntaxError: invalid syntax
Esses são, quase sempre, os erros mais fáceis de serem encontrados e corrigidos. Observe que, no Python 2, o comando print 'sem parênteses'
estava correto. No Python 3 print() se tornou uma função e os parênteses passaram a ser obrigatórios.
Vimos que o Python usa indentações (que podem ser espaços ou tabs) para delimitar eus blocos de código. Erros desses tipos são capturados como IndentationError e TabError.
Excessões: Uma exceção é um evento que ocorre durante a execução de um programa que interrompe o fluxo das instruções, além dos erros de sintaxe. Quando o interpretador encontra uma situação com esse tipo de erro ele levanta uma exceção, instanciando uma das classes derivadas da superclasse exception. Exceções levantadas devem ser tratadas para que a execução do código não termine de forma indesejada. Uma lista completa de exceções pode ser encontrada no artigo Python, Resumo.
Um exemplo de exceção é a tentativa de dividir por zero.
» for i in range(4): » v = 10/(2-i) » print(v) ↳ 5.0 ↳ 10.0 ↳ --------------------------------------------------------------------------- ↳ ZeroDivisionError Traceback (most recent call last) ↳ <ipython-input-14-b8aab2286d16> in <module> ↳ 1 for i in range(4): ↳ ----> 2 v = 10/(2-i) ↳ 3 print(v) ↳ ZeroDivisionError: division by zero
No exemplo é claro que quando i = 2 o denominador será nulo e a divisão por 0 não é definida. Por isso ZeroDivisionError foi lançada. Podemos corrigir esse erro simplesmente testando o denomidor e pulando o valor problemático. Mas denominadores nulos podem surgir de forma inesperada de muitas formas, tais como em dados lidos automaticamente ou inseridos pelo usuário. Por isso precisamos de um tratamento de erros. Para esse fim temos os blocos try, except e finally ou else.
- try: verifica se há um erro no bloco seguinte de código,
- except 1: recebe fluxo de execução em caso de exceção 1,
- … : (podem existir várias capturas de exceções),
- except n: recebe fluxo de execução em caso de exceção n,
- else: código executado se nenhum erro for encontrado,
- finally: código executado em ambos os casos.
Portanto, se suspeitamos que há possibilidade de um erro ser lançado envolvemos partes do código nesses blocos.
» for i in range(4): » try: » v = 10/(2-i) » print('i = %d, v = %d' % (i,v)) » except: » print('Erro em i = %d' % i) » # no caso de i=2 o primeiro comando print não é executado ↳ i = 0, v = 5 ↳ i = 1, v = 10 ↳ Erro em i = 2 ↳ i = 3, v = -10
No caso acima except captura qualquer erro que tenha acontecido. Blocos grandes de código podem estar dentro de um try com captura genérica. Isso não é muito bom em muitos casos pois não saberíamos que tipo de de erro foi lançado. Ao invés disso podemos capturar um erro específico.
» # supondo que a variável w não está definida » try: » print(w) » except NameError: » print("A variável w não está definida") » except: » print("Outro erro ocorreu") » A variável w não está definida
O opção else ocorre se nenhuma exceção foi capturada. finally ocorre em ambos os casos e pode ser útil para a execução de alguma finalização ou limpeza.
Suponha que existe o arquivo arquivoTeste.txt na pasta de trabalho atual mas ele está marcado como read only (somente de leitura).
» try: » f = open('arquivoTeste.txt') » f.write('Lorum Ipsum') » except: » print('Aconteceu alguma coisa errada com esse arquivo!') » else: » print('Operação bem sucedida!') » finally: » f.close() » print('* conexão fechada') ↳ Aconteceu alguma coisa errada com esse arquivo! ↳ * conexão fechada
Se o arquivo arquivoTeste2.txt não existe na pasta de trabalho outro erro será lançado:
» try: » f = open('arquivoTeste2.txt') » except FileNotFoundError: » print('Esse arquivo não existe!') » except: » print('Aconteceu alguma coisa errada com esse arquivo!') » finally: » f.close() ↳ Esse arquivo não existe!
Suponha que na atual pasta de trabalho existe uma subpasta dados. Se tentarmos abrir essa pasta como se fosse um arquivo teremos uma exceção.
» try: » arq = 'dados' » f = open(arq) » except FileNotFoundError: » print('Esse arquivo não existe!') » except IsADirectoryError: » print('"%s" é uma pasta e não um arquivo!' % arq) » else: » f.close() ↳ "dados" é uma pasta e não um arquivo!
As exceções FileNotFoundError e IsADirectoryError são ambas subclasses de OSError. As duas exceções são capturadas por essa superclasse.
» try: » arq = 'dados' » f = open(arq) » except OSError: » print('"%s" é uma pasta e não um arquivo!' % arq) ↳ "dados" é uma pasta e não um arquivo! » try: » arq = 'arquivoNaoExistente' » f = open(arq) » except OSError: » print('"%s" não existe!' % arq) ↳ "arquivoNaoExistente" não existe!
Diversos erros podem ser capturados em um bloco.
» try: » lunch() » except SyntaxError: » print('Fix your syntax') » except TypeError: » print('Oh no! A TypeError has occured') » except ValueError: » print('A ValueError occured!') » except ZeroDivisionError: » print('Did by zero?') » else: » print('No exception') » finally: » print('Ok then')
Segue uma lista parcial de erros e sua descrição. Uma lista completa de exceções pode ser encontrada no artigo Python, Resumo.
Exceção | Ocorre quando |
---|---|
AsserationError | na falha de uma instrução assert |
AttributeError | em erro de atribuição de atributo |
FloatingPointError | erro em operação de ponto flutuante |
MemoryError | ocorre falta de memória para realizar a operação |
IndexError | há uma chamada à índice fora do intervalo existente |
NotImplementedError | erro em métodos abstratos |
NameError | não existe uma variável com o nome no escopo local ou global |
KeyError | chave não encontrada no dicionário |
ImportError | tentativa de importar módulo não existente |
ZeroDivisorError | tentativa de divisão por 0 (zero) |
GeneratorExit | um gerador é abandonado antes de seu final |
OverFlowError | uma operação aritmética resulta em número muito grande |
IndentationError | indentação incorreta |
EOFError | uma função como input() ou raw_input() retorna end-of-file (EOF, fim de arquivo) |
SyntaxError | um erro de sintaxe é levantado |
TabError | espaço ou tabulações inconsistentes |
ValueError | uma função recebe um argumento com valor incorreto |
TypeError | tentativa de operação entre tipos incompatíveis |
SystemError | o interpretador detecta erro interno |
É possível capturar o erro lançado com a expressão except Exception as varExcecao:
de forma a exibir a mensagem embutida no objeto.
» x, y = 2, '3' » try: » y + x » except TypeError as t: » print(t) ↳ can only concatenate str (not "int") to str
Vários tipos de exceções podem ser capturadas simultaneamente.
try: <código que pode conter as exceções> ...................... except(Exception1[, Exception2[,...ExceptionN]]]): <tratamento das exceções, caso ocorram> ...................... else: <código executado caso nenhuma das exceções ocorra> ......................
Além das diversas exceções built-in lançadas automaticamente o usuário pode lançar suas próprias exceções. Isso é feito com raise.
» x = 'um' » if not isinstance(x, int): » raise ValueError("Tipo incorreto") » else: » print(34/x) ↳ ValueError: Tipo incorreto
No exemplo acima isinstance(x, int)
testa se x é uma instância de int, ou seja, se x é um inteiro.
O usuário pode definir suas próprias exceções, lembrando que devem ser todas derivadas da classe Exception. No exemplo as classes ValorMuitoBaixoError e ValorMuitoAltoError herdam todos os atributos da superclasse, sem acrescentar nenhuma cacterística própria.
» class ValorMuitoBaixoError(Exception): » """Erro lançado quando a tentativa é um valor muito baixo""" » pass » class ValorMuitoAltoError(Exception): » """Erro lançado quando a tentativa é um valor muito alto""" » pass » # Você deve adivinhar esse número » numero = 10 » # Loop enquanto o número não for correto » while True: » try: » num = int(input("Digite um número: ")) » if num < numero: » raise ValorMuitoBaixoError » elif num > numero: » raise ValorMuitoAltoError » else: » print('Acertou!') » break » except ValorMuitoBaixoError: » print("Valor muito pequeno. Tente de novo!\n") » except ValorMuitoAltoError: » print("Valor muito alto. Tente de novo!\n") » # ao ser executado o código abre um diálogo para input do usuário » # suponha que as tentativas feitas são: 2, 55, 10 ↳ Digite um número: 2 ↳ Valor muito pequeno. Tente de novo! ↳ Digite um número: 55 ↳ Valor muito alto. Tente de novo! ↳ Digite um número: 10 ↳ Acertou!
Além de simplesmente herdar da superclasse as classes de erros customizadas podem fazer o overload de seus métodos para realizar tarefas específicas. No caso abaixo usamos apenas uma classe indicativa de erro e alteramos a propriedade message da classe e da superclasse para informar se o erro foi para mais ou menos. Por default o método __str__ retorna essa mensagem.
No trecho abaixo fazemos o overload também de __str__ para incluir uma mensagem mais completa, mantendo igual todo o restante do código.
» # Você deve adivinhar esse número, entre 0 e 100 » numero = 50 » class ValorIncorretoError(Exception): » """Exceção lançada para erro de valor """ » » def __init__(self, valor): » message='Valor %d é muito %s' % (valor,'baixo' if valor < numero else 'alto') » self.message = message » super().__init__(self.message) » # Loop enquanto o número não for correto » while True: » try: » num = int(input('Digite um número entre 0 e 100: ')) » if num != numero: » raise ValorIncorretoError(num) » else: » print('Acertou!') » break » except ValorIncorretoError as vi: » print('%s. Tente de novo!\n' % str(vi)) ↳ Digite um número entre 0 e 100: 34 ↳ 34. Tente de novo! ↳ Digite um número entre 0 e 100: 89 ↳ 89. Tente de novo! ↳ Digite um número entre 0 e 100: 50 ↳ Acertou!
Assert e AssertionError
A instrução assert fornece um teste de uma condição. Se a condição é verdadeira o código continua normalmente sua execução. Se for falsa a exceção AssertionError é lançada, com uma mensagem de erro opcional. Ela deve ser usada como um auxiliar na depuração do código, informando o desenvolvedor sobre erros irrecuperáveis em um programa. Asserções são autoverificações internas do programa e funcionam através da declaração de condições que não deveriam ocorrer de forma alguma. O lançamento de uma exceção AssertionError deve indicar que há um bug no código e sua ocorrência deve informar qual condição inaceitável foi violada.
» # forçando o levantamento de AssertionError » a, b = 2, 3 » assert a==b ↳ AssertionError
Suponha que uma loja monta um sistema para gerenciar suas vendas. Em algum momento o vendedor pode oferecer um desconto na compra mas o gerente determinou que o desconto não pode ser superior a 50%. Definimos uma função de cálculo do valor final da venda que impede que o preço final seja menor que metade do preço original, o maior que ele.
» def precoComDesconto(preco, desconto): » try: » precoFinal = preco * (1-desconto/100) » assert .5 <= precoFinal/preco <= 1 » except AssertionError: » return 'Desconto inválido!' » else: » return precoFinal » print(precoComDesconto(120,50)) ↳ 60.0 » print(precoComDesconto(120,55)) ↳ Desconto inválido!
O último exemplo mostra que um AssertionError pode ser capturado como qualquer outra exceção lançada.
Exceções do tipo AssertionError não devem ser usadas em produtos finais, no código depois de todos os testes de erros foram executados. Parcialmente porque é possível executar o código desabilitando todas as instruções assert. Suponha que um desenvolvedor quer evitar que um usuário, que não o administrador do sistema, apague registros em um banco de dados.
» # não faça isso! » def apagarRegistros(usr): » assert usr.isAdmin() » < código de apagamento >
Se o sistema for executado com desabilitação de assert qualquer usuário tem acesso ao apagamento de dados!
Erros lógicos
Outro tipo de erro são os erros lógicos que, provavelmente, ocupam a maior parte do tempo de debugging dos desenvolvedores. Eles ocorrem quando o código não tem erros de sintaxe nem exceções de tempo de execução mas foram escritos de forma que o resultado da execução é incorreto. Um exemplo simples seria de uma lista que começa a ler os elementos com índice i=1, o que faz com que o primeiro elemento seja ignorado. Esses erros podem ser complexos e difíceis de serem encontrados e corrigidos pois não causam a interrupção do programa nem lançam mensagens de advertência.
Os três exemplos abaixo mostram casos de erros lógicos.
» # 1) queremos o produto dos 9 primeiros números » produto = 1 » for i in range(10): » produto *= i » print(produto) ↳ 0 » # 2) queremos soma dos 9 primeiros números » num = 0 » for num in range(10): » num += num » print(num) ↳ 18 » # 3) queremos a soma dos quadrados dos 9 primeiros números » soma_quadrados = 0 » for i in range(10): » iquad = i**2 » soma_quadrados += iquad » print(soma_quadrados) ↳ 81
É muito difícil ou impossível escrever um codigo mais complexo sem cometer erros de lógica. Algumas sugestões podem ajudar a minorar esse problema:
- Planeje antes de começar a escrever código:
- Faça diagramas deixando claro quais são os dados de entrada do código e o que se espera obter. Tenha clareza sobre o objetivo de seu projeto.
- Fluxogramas e pseudocódigo ajudam nesse aspecto.
- Comente o código e use docstrings corretos para suas funções e classes:
- Um código bem documentado é mais fácil de ser compreendido não só por outros programadores que talvez trabalhem em seu projeto como para você mesmo, quando tiver que rever um bloco algum tempo após tê-lo idealizado.
- Docstrings podem ser acessados facilmente pelo desenvolvedor e usado por várias IDEs para facilitar seu acesso.
- Escreva primeiro blocos de código de funcionamento geral, depois os detalhes, testando sempre cada etapa.
- Teste o produto final com dados válidos e dados inválidos:
- Faça testes usando valores esperados. É particularmente importante testar o código com valores limítrofes, tais como o mais baixo e o mais alto aceitável. Teste também usando valores incorretos que podem ser, inadvertidamente, pelo usuário final. Um exemplo comum, o usuário pode digitar a letra l ou invés do dígito 1. Valores fora da faixa esperada e de tipos diferentes devem ser experimentados.
- Para aplicativos usados por muitos usuários finais, particularmente os executados na internet, use testadores que não aqueles que desenvolveram o código.
Instrumentos de depuração (debugging)
Uma das formas simples de encontrar erros no código consiste em escrever instruções print() nas partes onde desejamos observar um valor intermediário de alguma variável. Existem IDEs que permitem o acompanhamento em tempo real de cada valor, na medida em que ocorrem na execução do código. E, finalmente, existem programas auxiliares para a localização de erros. Algumas dessas ferramentas verificam a sintaxe do código escrito marcando os erros e sugerindo melhor estilo de programação. Outras nos permitem analisar o programa enquanto ele está em execução.
Pyflakes, pylint, PyChecker e pep8
Descritos na documentação do Python esses quatro utilitários recebem os arquivos *.py como input e analisam o código em busca de erros de sintaxe e alguns de erros de tempo de execução. Ao final eles imprimem avisos sugerindo melhor estilo de codificação e códigos ineficientes e potencialmente incorretos como, por exemplo, variáveis e módulos importados que nunca são usados.
Pyflakes analisa as linhas de código sem importá-lo. Ele detecta menos erros que os demais aplicativos mas é mais seguro pois não há risco de executar código defeituoso. Ele também é mais rápido que as demais ferramentas aqui descritas.
Pylint e PyChecker importam o código e produzem listas mais extensas de erros e advertências. Eles são especialmente importantes quando se considera a funcionalidade de pyflakes muito básica.
Pep8 faz uma análise do código procurando por trechos com estilo de codificação ruim, tendo como padrão o estilo proposto na Pep 8 que é o documento de especificação para um bom estilo de codificação no Python.
Todos eles são usados com os comandos de comando, no prompt do sistema:
> pyflakes meuCodigo.py > pylint meuCodigo.py > pychecker meuCodigo.py > pep8 meuCodigo.py
pdb
pdb é um módulo built-in, usado para depurar código enquanto ele está em execução. É possível usá-lo invocando-o como um script enquanto o código é executado ou importar o módulo e usar suas funções junto com o código do desenvolvedor. pdb permite que o código seja executado uma linha de cada vez, ou em blocos, inspeccionando a cada passo o estado do programa. Ele também emite um relatório de problemas que causam o término da execução por erros.
> import pdb » def funcaoComErro(x): » ideia_ruim = x + '4' » pdb.run('funcaoComErro(3)') ↳ > <string>(1)>module>()
Como script pdb pode ser executado da seguinte forma:
python3 -m pdb myscript.py
.
Uma descrição mais completa de pdb pode ser encontrada em Python Docs: Pdb.
Bibliografia
- Site Kettler, Rafe: A Guide to Python’s Magic Methods, acessado em maio de 2021.
- Site RealPython: Primer on Python Decorators, Introduction, acessado em abril de 2021.
- Site RealPython: Python, Debugging with pdb, acessado em maio de 2021.
- Site Pythonbasics.org: Python Decorators Introduction, acessado em abril de 2021.
- Site Dan Bader: Python Assert Tutorial, acessado em maio de 2021.
Consulte a bibliografia no final do primeiro artigo dessa série.