Python: Classes, métodos especiais


Métodos especiais, ou métodos mágicos, em Python são métodos predefinidos em todos os objetos, com invocação automática sob circunstâncias especiais. Eles normalmente não são chamados diretamente pelo usuário mas podem ser overloaded (sobrescritos e alterados). Seus nomes começam e terminam com sublinhados duplos chamados de dunder (uma expressão derivada de double underscore). As operações abaixo são exemplos de métodos mágicos e como acioná-los, com o operador + e a função len().

» x, y = 34, 45
» x+y
↳ 79
» x.__add__(y)
↳ 79
» 
» l = [4,67,78]
» len(l)
↳ 3
» l.__len__()
↳ 3

Vemos que somar dois números usando o operador + aciona o método __add __ e calcular o comprimento de um objeto usando a função len() equivale a usar seu método __len__().

Uma das principais vantagens de usar métodos mágicos é a possibilidade de elaborar classes com comportamentos similares ou iguais aos de tipos internos.

Atributos especiais das classes

Vimos na seção anterior sobre Classes no Python que a função dir() exibe todos os atributos de uma classe. Muitos deles são built-in, herdados da classe object que é a base de todas as classes, portanto de todos os objetos. Em outras palavras object é uma superclasse para todos os demais objetos.

Podemos listar esses atributos criando uma classe T sem qualquer atributo e examinando o resultado de print(dir(T)).

» class T:
»     pass

» print(dir(T))
↳ ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
↳  '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__',
↳  '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__',
↳  '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

Já vimos e usamos os métodos __init__(), para inicializar objetos, e __str__(), para retornar uma representação de string do objeto, acessada por dir(objeto). Também fizemos o overloading de __eq__() e __add__().

Também usamos a possibilidade de sobrescrever os métodos __len__, que ativa a função len(), __str__, associado às chamadas de print() e __eq__ usado em comparações com ==. Para lembrar esse processo de overload observe os métodos na classe abaixo.

» class Comprimento:
»     def __init__(self, fim = 0):
»         self.minhaLista = list(range(fim))
»     def __len__(self):
»         return len(self.minhaLista)
»     def __eq__(self, outro):
»         return self.minhaLista == outro.minhaLista
»     def __str__(self):
»         return '%s \nlen = %d' % (str(self.minhaLista), len(self.minhaLista))

» comp1 = Comprimento(fim=9)
» comp2 = Comprimento(fim=9)

» print(comp1)
↳ [0, 1, 2, 3, 4, 5, 6, 7, 8] 

» len = 9
» comp1==comp2
↳ True

O método len() se refere à contagem de elementos discretos e só pode ser definido para retornar um número inteiro.

Em outro exemplo definimos na classe Ponto os operadores de comparação __gt__ e __ne__, respectivamente > e != da seguinte forma: consideramos, para esse nosso caso, que um ponto é “maior” que outro se estiver mais afastado da origem de coordenadas, o ponto (0, 0). Para isso definimos o método distancia() que calcula essa distância. O operador __ne__ retorna True se uma ou ambas coordenadas dos dois pontos testados forem diferentes. Para usar a função sqrt(), (raiz quadrada) temos que importar o módulo math.

» from math import sqrt
» class Ponto:
»     def __init__(self, x, y):
»         self.x, self.y = x, y
» 
»     def distancia(self):
»         return sqrt(self.x**2 + self.y**2)
»     
»     def __gt__(self, other):
»         return self.distancia() > other.distancia()
»     
»     def __ne__(self, other):
»         x, y, w, z = self.x, self.y, other.x, other.y
»         return x != w or y != z
»     
»     def __str__(self):
»         return 'Ponto com coordenadas (%d, %d)' % (self.x, self.y)

» p1 = Ponto(4,5)
» p2 = Ponto(1,2)

» p1 != p2
↳ True

» p1 > p2
↳ True

De forma análoga podemos fazer o overload e utilizar os aperadores __eq__, ==, __ge__, >=, __gt__, >, __le__, <=, __lt__, < e __ne__, !=.

Todas as comparações se baseiam no método __cmp __(self, other) que deve retornar um inteiro negativo se self < other, zero se self == other e um inteiro positivo se self > other.

Geralmente é melhor definir cada uma das comparações que serão utilizadas. Mesmo assim a definição do método __cmp__ pode ser uma boa maneira de economizar repetição e melhorar a clareza quando você precisa que todas as comparações sejam implementadas com critérios semelhantes.

Método __init__()

Já vimos na seção anterior o funcionamento do método __init__, e extendemos aqui a descrição de sua funcionalidade. Sabemos que podemos inicializar um objeto sem qualquer referência às propriedades de que ele necessita e inserir mais tarde, dinamicamente, essas propriedades.

» class Area:
»     ''' Área de um retângulo '''
» 
»     def area(self):
»         return self.altura * self.largura
»
» a = Area()
» a.altura = 25
» a.largura = 75
» a.area()
↳ 1875

Uma classe definida dessa forma não deixa claro quais são as propriedades que ele deve usar. Considerando que o método __init__() é acionado internamente, podemos tirar vantagem desse método fazendo seu overload e inicializando as propriedades explicitamente.

» class Area:
»     def __init__(self, altura, largura):
»         self.altura = altura
»         self.largura = largura
» 
»     def area(self):
»         return self.altura * self.largura
» 
» b = Area(123, 90)
» b.area()
↳ 11070

A segunda definição é considerada um melhor design uma vez que torna mais clara a leitura do código e seu uso durante a construção de um aplicativo. A própria definição da classe informa quais são os parâmetros usados pelos objetos dela derivados.

Para o próximo exemplo suponha um jogo do tipo RPG onde os jogadores são criados com uma determinada quantidade de energia e inteligência e que esses valores são incrementados ou decrementados de acordo com as escolhas feitas pelo jogador. A partir desses valores se calcula vida e poder, significando quanto tempo a personagem tem de vida e quanto poder de destruição ela possui em seus golpes.

Para representar as dois tipos possíveis de personagens no jogo definimos a superclasse Personagem com duas variáveis de classe (energia e inteligência) e dois atributos calculados: vida e poder. Duas subclasses Cientista e Estudante herdam da primeira, todas as variáveis e métodos, inclusive __init__(), mas sobreescrevem os métodos _estado(), usado na inicialização, e __str()__.

» class Personagem:
»     def __init__(self, energia, inteligencia):
»         self.energia = energia
»         self.inteligencia = inteligencia
»         self.vida, self.poder = self._estado()
» 
»     def _estado(self):
»         ''' Retorna uma tupla '''
»         return int(self.energia + self.inteligencia), int(self.energia * 10)
»     
»     def __str__(self):
»         return 'Vida = %d Poder = %d' % (self.energia , self.inteligencia)
» 
» class Cientista(Personagem):
»     def _estado(self):
»         return  int(self.energia + self.inteligencia *10), int(self.energia * 5)
»      
»     def __str__(self):
»         return 'Cientista: Vida = %d Poder = %d' %  (self.vida , self.poder)
» 
» class Estudante(Personagem):
»     def _estado(self):
»         return  int(self.energia + self.inteligencia * 5), int(self.energia * 10)
» 
»     def __str__(self):
»         return 'Estudante: Vida = %d Poder = %d' % (self.vida , self.poder)
» 
» p = Personagem(10,10)
» c = Cientista(10,10)
» e = Estudante(10,10)
» 
» print(p)
↳ Vida = 10 Poder = 10
» 
» print(c)
↳ Cientista: Vida = 110 Poder = 50
» 
» print(e)
↳ Estudante: Vida = 60 Poder = 100

Esse é um exemplo de polimorfismo pois cada subclasse possui seu próprio método _estado(). É importante lembrar que __init__() sempre retorna None pois não possui (nem admite) o comando return. A notação com sublinhado em _estado() sugere que o método é de uso interno na classe e seus objetos.

Exibindo um objeto, __str__, __repr__, __format__

Temos usado o método __str__ que retorna uma string com dados sobre o objeto que é exibida com print(objeto). Usando esse método esperamos obter uma descrição amigável e legível do objeto, contendo todos os dados ou que julgamos mais relevantes.

Outro método, __repr__, também retorna uma representação de string, mais técnica e em geral usando uma expressão completa que pode ser usada para reconstruir o objeto. Ele é acionado pela função repr(objeto). Essa representação, se passada como argumento para a função eval(), retorna um objeto com as mesmas características do original. A função eval() interpreta a string passada como argumento e a interpreta como código, executando esse código como uma linha de programação python. Outros exemplos são dados a seguir.

» # __repr__
» class Bicho:
»     def __init__(self, especie, habitat):
»         self.especie = especie
»         self.habitat = habitat
» 
»     def __repr__(self):
»         return 'Bicho("%s", "%s")' % (self.especie, self.habitat)
» 
»     def __str__(self):
»         return 'O bicho %s com habitat: %s' % (self.especie, self.habitat)
»     
» peixe = Bicho('peixe', 'rios')
» 
» # usando o método __repr__
» print(repr(peixe))
↳ Bicho("peixe", "rios")
» 
» # usando o método __str__
» print(peixe)
↳ O bicho peixe com habitat: rios
» 
» # usando eval()
» bagre = eval(repr(peixe))
» print(bagre)
↳ O bicho peixe com habitat: rios

O retorno de repr(peixe) pode ser usado para reconstruir o objeto peixe. Caso o método __str__ não tenha sido sobrescrito sua execução chama o método __repr__ e a saída é idêntica. A expressão bagre = eval(repr(peixe)) é idêntica à bagre = Bicho("peixe", "rios").

» # outros exemplos de uso de eval()
» txt = 'x+x**x'.replace('x','3')
» eval(txt)
↳ 30
» 
» txt = "'casa da mãe joana'.title()"
» eval(txt)
↳ Casa Da Mãe Joana
» 
» for t in [a + ' * ' + b for a in '67' for b in '89']:
»     print(t, '=', eval(t))
↳ 6 * 8 = 48
↳ 6 * 9 = 54
↳ 7 * 8 = 56
↳ 7 * 9 = 63


A função eval() não deve ser aplicada diretamente a dados digitados pelo usuário devido ao risco de que se digite uma expressão que delete dados ou cause qualquer outro dano ao computador ou rede onde o código é executado.

Função e método format

Já vimos o método de formatação de strings usando format(), que é uma funcionalidade mais moderna e poderosa que a notação de % para inserir campos. Recapitulando e expandindo um pouco vamos listas mais alguns exemplos desse método.

» # marcadores nomeados são alimentados por format
» txt1 = '1. Eu me chamo {nome} e tenho {idade} anos.'.format(nome = 'João', idade = 36)
» 
» # os campos podem ser marcados numericamente
» txt2 = '2. Moro em {0}, {1} há {2} anos.'.format('Brasília', 'DF', 20)
» txt3 = '3. Há {2} anos moro em {0}, {1}.'.format('Brasília', 'DF', 20)
» 
» # ou marcados apenas por sua ordem de aparecimento
» txt4 = "4. {} são convertidos em {}. Exemplo: {}.".format('Dígitos', 'strings', 100)
» 
» # controle do número de casas decimais
» txt5 = '5. Esse livro custa R$ {preco:.2f} com desconto!'.format(preco = 49.8)
» 
» print(txt1)
↳ 1. Eu me chamo João e tenho 36 anos.
» 
» print(txt2)
↳ 2. Moro em Brasília, DF há 20 anos.
» 
» print(txt3)
↳ 3. Há 20 anos moro em Brasília, DF.
» 
» print(txt4)
↳ 4. Dígitos são convertidos em strings. Exemplo: 100.
» 
» print(txt5)
↳ 5. Esse livro custa R$ 49.80 com desconto!
» 
» # argumento de format é "unpacking" de sequência
» print('{3}{2}{1}{0}-{3}{0}{1}{2}-{1}{2}{3}{0}-{0}{3}{2} '.format(*'amor'))
↳ roma-ramo-mora-aro
» 
» #indices podem ser repetidos
» print('{0}{1}{0}'.format('abra', 'cad'))
↳ abracadabra

padrao.format(objeto) pode conter um objeto com propriedades que serão lidas em {objeto.propriedade}. Por ex., o módulo sys contém os atributos sys.platform e sys.version.

» import sys
» # sys possui atibutos sys.platform e sys.version
» # no caso abaixo sys é o parâmetro 0
» print ('Platform: {0.platform}\nPython version: {0.version}'.format(sys))
↳ Platform: linux
↳ Python version: 3.8.5 (default, Sep  4 2020, 07:30:14) 
↳ [GCC 7.3.0]

O parâmetro pode ser um objeto construído pelo programador.

» class Nome:
»     nome='Silveirinha'
»     profissao='contador' 
» 
» n = Nome()
» idade = 45
» print('Empregado: {0.nome}\nProfissão: {0.profissao}\nIdade: {1} anos'.format(n, idade))
↳ Empregado: Silveirinha
↳ Profissão: contador
↳ Idade: 45 anos

Uma especificação de formato mais precisa pode ser incluída cim a sintaxe de vírgula seguida da especificação do formato.
{0:30} significa, campo 0 preenchido com 30 espaços, e {1:>4} campo 1 com 4 espaços, alinhado à direita. O alinhamento à esquerda é default.

» # Campo 0: justificado à esquerda (default), preenchendo o campo com 30 caracteres
» # Campo 1: justificado à direita preenchendo o campo com 4 caracteres
» linha = '{0:30} R${1:>4},00'
» 
» print(linha.format('Preço para alunos', 35))
» print(linha.format('Preço para professores', 115))
↳ Preço para alunos              R$  35,00
↳ Preço para professores         R$ 115,00

Os campos usados em format() podem ser aninhados (um campo dentro do outro). No caso abaixo a largura do texto é passada como campo 1, que está dentro do campo 0, como comprimento.

» # campos aninhados. campo 0 é o texto a ser formatado pelo padrão. Campo 1 é a largura do texto
» padrao = '|{0:{1}}|'
» largura = 30
» txt ='conteúdo de uma célula'
» print(padrao.format(txt, largura))
↳ |conteúdo de uma célula        |

Esse processo pode ser muito útil quando se monta um texto com formatação, como em html. No preencimento de texto delimitado por tags um padrão pode incluir trechos de início e fim. Na construção de um tabela, por ex., as tags de abertura e fechamento de uma célula de uma tabela são <td></td>.

» padrao = '{inicio}{0:{1}}{fim}'
» txt ='conteúdo da célula'
» larg = len(txt)
» print(padrao.format(txt, larg, inicio = '', fim = ''))
↳ conteúdo da célula

Lembrando: os parâmetros posicionais 0, 1 devem vir antes dos nomeados.

Outro exemplo é a montagem de uma tabela com valores organizados por colunas. O padrão abaixo estabelece que cada número deve ocupar 6 espaços. Observe que ‘{:6d} {:6d} {:6d} {:6d}’ é o mesmo que ‘{0:6d} {1:6d} {2:6d} {3:6d}’.

» for i in range (3, 8):
»     padrao = '{:6d} {:6d} {:6d} {:6d}'
»     print(padrao.format(i, i ** 2, i ** 3, i ** 4))
↳      3      9     27     81
↳      4     16     64    256
↳      5     25    125    625
↳      6     36    216   1296
↳      7     49    343   2401

Os seguintes sinais podem são usados para alinhamento:

Especificadores de alinhamento
< alinhado à esquerda (default),
> alinhado à direita,
^ centralizado,
= para tipos numéricos, preenchimento após o sinal.

Os seguintes sinais são usados para especificação de formato:

Especificadores de formato
b Binário. Exibe o número na base 2.
c Caracter. Converte inteiros em caractere Unicode.
d Inteiro decimal. Exibe o número na base 10.
o Octal. Exibe o número na base 8.
x Hexadecimal. Exibe número na base 16, usando letras minúsculas para os dígitos acima de 9.
e Expoente. Exibe o número em notação científica usando a letra ‘e’ para o expoente.
g Formato geral. Número com ponto fixo, exceto para números grandes, quando muda para a notação de expoente ‘e’.
n Número. É o mesmo que ‘g’ (para ponto flutuantes) ou ‘d’ (para inteiros), exceto que usa a configuração local para inserir separadores numéricos.
% Porcentagem. Multiplica número por 100 e exibe no formato fixo (‘f’), seguido de sinal %.

A função format() aciona o método __format__() interno ao objeto. Uma classe do programador pode ter esse método overloaded e customizado. Ele deve ser chamado como __format__(self, format_spec) onde format_spec são as especificações das opções de formatação.

As seguintes expressões são equivalentes:
format(obj,format_spec) <=> obj.__format__(obj,format_spec) <=> "{:format_spec}".format(obj)

No próximo exemplo construímos uma classe para representar datas com os atributos inteiros dia, mês e ano. O método __format() recebe um padrão e retorna a string de data formatada de acordo com esse padrão. Por exemplo, se padrao = 'dma' ela retorna '{d.dia}/{d.mes}/{d.ano}'.format(d=self). O método __str__() monta uma string diferente usando um dicionário que associa o número ao nome do meses.

» class Data:
»     def __init__(self, dia, mes, ano):
»         self.dia, self.mes, self.ano = dia, mes, ano
»     
»     def __format__(self, padrao):
»         p = '/'.join('{d.dia}' if t=='d' else ('{d.mes}' if t=='m' else '{d.ano}') for t in padrao)
»         return p.format(d=self)
»
»     def __str__(self):
»         mes = {1:'janeiro', 2:'fevereiro', 3:'março', 4:'abril', 5:'maio', 6:'junho',
»          7:'julho', 8:'agosto', 9:'setembro', 10:'outubro', 11:'novembro', 12:'dezembro'}
»         m = mes[self.mes]
»         return '{0} de {1} de {2}'. format(self.dia, m, self.ano)
» 
» d1, d2, d3 = Data(31,12,2019), Data(20,7,2021), Data(1,7,2047)
» 
» print('Usando função: format(objeto, especificacao)')
» print(format(d1,'dma'))
» print(format(d2,'mda'))
» print(format(d3,'amd'))
↳ Usando função: format(objeto, especificacao)
↳ 31/12/2019
↳ 7/20/2021
↳ 2047/7/1
»
» print('\nUsando formatação de string: padrao.format(objeto)')
» print('dia 1 = {:dma}'.format(d1))
» print('dia 2 = {:mda}'.format(d2))
» print('dia 3 = {:amd}'.format(d3))
↳ Usando formatação de string: padrao.format(objeto)
↳ dia 1 = 31/12/2019
↳ dia 2 = 7/20/2021
↳ dia 3 = 2047/7/1
» 
» print('\nUsando método __str__()')
» print(d3)
↳ Usando método __str__()
↳ 1 de julho de 2047

Função property() e decorador @property

Vimos anteriormente que pode ser útil isolar uma propriedade em uma classe e tratá-la como privada. Isso exige a existência de getters e setters, tratados na seção anterior.

No código abaixo definimos uma classe simples com métodos getter, setter e outro para apagar uma propriedade.

» class Pessoa:
»     def __init__(self):
»         self._nome = ''
»     def setNome(self,nome):
»         self._nome = nome.title()
»     def getNome(self):
»         return 'Não fornecido' if not self._nome else self._nome
»     def delNome(self):
»         self._nome = ''
»    
» # inicializamos um objeto da classe e atribuimos um nome
» p1 = Pessoa()
» p1.setNome('juma jurema')
» 
» print(p1.getNome())
↳ Juma Jurema
» 
» # apagando o nome
» p1.delNome()
» print(p1.getNome())
↳ Não fornecido

A função property facilita o uso desse conjunto de métodos. Ela tem a seguinte forma:

property(fget=None, fset=None, fdel=None, doc=None)

onde todos os parâmetros são opcionais. São esses os parâmetros:

  • fget representa o método de leitura,
  • fset método de atribuição,
  • fdel método de apagamento e
  • doc de uma docstring para o atributo. Se doc não for fornecido a função lê o docstring da função getter.

Ela é usada da seguinte forma:

» class Pessoa:
»     def __init__(self, nome=''):
»         self._nome = nome.title()
»
»     def setNome(self, nome):
»         self._nome = nome.title()
»
»     def getNome(self):
»         return 'Não informado' if self._nome=='' else self._nome
»
»     def delNome(self):
»         self._nome = ''
» 
»     nome = property(getNome, setNome, delNome, 'Propriedade de nome')
 
» p = Pessoa('juma jurema')
» print(p.nome)
↳ Juma Jurema
 
» p.nome = 'japira jaciara'
» print(p.nome)
↳ Japira Jaciara

» del p.nome
» print(p.nome)
↳ Não informado

Com a linha nome = property(getNome, setNome, delNome, 'Propriedade de nome') se adiciona um novo atributo nome associada aos métodos getNome, setNome, delNome. Fazendo isso os seguintes comandos são equivalentes:

  • p1.nomep1.getNome(),
  • p1.nome = 'novo nome'p1.setNome('novo nome') e
  • del p1.nomep1.delNome().

Ao invés de usar essa sintaxe também podemos usar o decorador @property.

» class Pessoa:
»     def __init__(self, nome):
»         self._nome = nome.title()
» 
»     @property
»     def nome(self):
»         return 'Não informado' if self._nome=='' else self._nome
» 
»     @nome.setter
»     def nome(self, nome):
»         self._nome = nome.title()
» 
»     @nome.deleter
»     def nome(self):
»         self._nome = ''

» p = Pessoa('adão adâmico')
» print(p.nome)
↳ Adão Adâmico

» p.nome = 'arthur artemis'
» print(p.nome)
↳ Arthur Artemis

» del p.nome
» print(p.nome)
↳ Não informado

Observer que o decorador @property define a propriedade nome e portanto deve aparecer antes de nome.setter e nome.deleter.

Se mais de um atributo deve ser decorado o processo deve ser repetido para cada atributo.

» class Pessoa:
»     def __init__(self):
»         self._nome = ''
»         self._cpf = ''
» 
»     @property
»     def nome(self):
»         return self._nome
» 
»     @property
»     def cpf(self):
»         return self._cpf
»  
»     @nome.setter
»     def nome(self, nome):
»         self._nome = nome
»         
»     @cpf.setter
»     def cpf(self, cpf):
»         self._cpf = cpf
        
» p = Pessoa()
» p.nome = 'Albert'
» p.nome
↳ 'Albert'

» p.cpf = '133.551.052-12'
» p.cpf
↳ '133.551.052-12'

O decorador @property permite que um método seja acessado como se fosse um atributo. Ele é particularmente útil quando já existe código usando um acesso direto à propriedade na forma de objeto.atributo. Se a classe é modificada para usar getters e setters o uso de @property dispensa que todo o restante do código seja alterado..

Método __getattr__

A função interna getattr é usada para ler o valor de um atributo dentro de um objeto. Além de realizar essa leitura ela permite que se retorne um valor especificado caso o atributo não exista. Ela tem a seguinte sintaxe:

getattr(objeto, atributo[, default])

onde os parâmetros são:

  • objeto (obrigatório), um objeto qualquer;
  • atributo (obrigatório), um atributo do objeto;
  • default (opcional), o valor a retornar caso o atributo não exista.

Ela retorna o valor de objeto.atributo.
Com funcionalidades associadas temos as seguintes funções, exemplificadas abaixo:

  • hasattr(objeto, atributo), que verifica se existe o atributo,
  • setattr(objeto, atributo), que insere um valor nesse atributo,
  • delattr(objeto, atributo), que remove o atributo desse objeto.
» class Pessoa:
»     nome = 'Einar Tandberg-Hanssen'
»     idade = 91
»     pais = 'Norway'

» p = Pessoa
» p.pais='Noruega'
» print(getattr(p, 'nome'))
↳ Einar Tandberg-Hanssen

» print(getattr(p, 'idade'))
↳ 91

» print(getattr(p, 'pais'))
↳ Noruega

» print(getattr(p,'profissao','não encontrado'))
↳ não encontrado

» # funções hasattr, setattr e delattr
» hasattr(p,'pais')
↳ True

» hasattr(p,'profissao')
↳ False

» setattr(p, 'idade', 28)
» p.idade
↳ 28

» delattr(p,'idade')
» hasattr(p,'idade')
↳ False

O método __getattr__ permite um overload da função getattr. Ele é particularmente útil quando se deseja retornar muitos atributos derivados dos dados fornecidos, seja por cálculo, por composição ou modificação.

» class Pessoa:
»     def __init__(self, nome, sobrenome):
»         self._nome = nome
»         self._sobrenome = sobrenome
» 
»     def __getattr__(self, atributo):
»         if atributo=='nomecompleto':
»             return '%s %s' % (self._nome, self._sobrenome)
»         elif atributo=='nomeinvertido':
»             return '%s, %s' % (self._sobrenome, self._nome)
»         elif atributo=='comprimento':
»             return len(self._nome) + len(self._sobrenome)
»         else:
»             return 'Não definido'

» p = Pessoa('Albert','Einsten')

» p.nomecompleto
↳ 'Albert Einsten'

» p.nomeinvertido
↳ 'Einsten, Albert'

» p.comprimento
↳ 13

» p.idade
↳ 'Não definido'

Função e método hash

O método __hash__ é invocado quando se aciona a função hash().

chave = hash(objeto)

Hash usa um algoritmo que transforma o objeto em um número inteiro único que identifica o objeto. Esses inteiros são gerados de forma aleatória (tanto quanto possível) de forma que diversos objetos no código não repitam o mesmo código. O hash é mantido até o fim da execução do código (ou se o objeto for reinicializado). Só existem hashes de objetos imutáveis, como inteiros, booleanos, strings e tuplas e essa propriedade pode ser usada para testar se um objeto é ou não imutável.

Essa chave serve como índice que agiliza a localização de objetos nas coleções e é usada internamente pelo Python em dicionários e conjuntos.

» # o hash de um inteiro é o  próprio inteiro
» hash(12345)
↳ 12345

» hash('12345')
↳ -918245046130431123

» # listas não possuem hashes
» hash([1,2,3,4,5])
↳ TypeError: unhashable type: 'list'

» # o hash de uma função
» def func(fim):
»     for t in range(fim):
»         print(t, end='')
» a = func
» print(hash(a))
↳ 8743614090882

» # todos os elementos de uma tupla devem ser imutáveis
» print(hash((1, 2, [1, 2])))
↳ TypeError: unhashable type: 'list'

Em uma classe podemos customizar __hash__ e __eq__ de forma a alterar o teste de igualdade, ou seja, definimos nosso próprio critério de comparação.

As implementações default de __eq__ e __hash__ nas classes usam id() para fazer comparações e calcular valores de hash, respectivamente. A regra principal para a implementação customizada de métodos __hash__ é que dois objetos iguais devem ter o mesmo valor de hash. Por isso se __eq__ for alterada para um teste diferente de igualdade, o que pode ser o caso dependendo de sua aplicação, o método __hash__ também deve ser alterado para ficar em acordo com essa escolha.

Métodos id, hash e operadores == e is

Três conceitos são necessários para entender id, hash e os operadores == e is que são, respectivamente: identidade, valor do objeto e valor de hash. Nem todos os objetos possuem os três.

Objetos têm uma identidade única, retornada pela função id(). Se dois objetos têm o mesmo id eles são duas referências ao mesmo objeto. O operador is compara itens por identidade: a is b é equivalente a id(a) == id(b).

Objetos também têm um valor: dois objetos a e b têm o mesmo valor se a igualdade pode ser testada e a == b. Objetos container (como listas) têm valor definido por seu conteúdo. Objetos do usuário tem valores baseados em seus atributos. Objetos de diferentes tipos podem ter os mesmos valores, como acontece com os números: 0 == 0.0 == 0j == decimal.Decimal ("0") == fraction.Fraction(0) == False.

Se o método __eq__ não estiver definido em uma classe (para implementar o operador ==), seus objetos herdarão da superclasse padrão e a comparação será feita entre seus ids.

Objetos distintos podem ter o mesmo hash mas objetos iguais devem ter o mesmo hash. Armazenar objetos com o mesmo hash em um dicionário é muito menos eficiente do que armazenar objetos com hashes distintos pois a colisão de hashes exige processamento extra. Objetos do usuário são “hashable” por padrão pois seu hash é seu id. Se um método __eq__ for overloaded em uma classe personalizada, o hash padrão será desativado e deve também ser overloaded se existe intenção de manter a classe imutável. Por outro lado se você deseja forçar uma classe a gerar objetos mutáveis (sem valor de hash) você pode definir seu método __hash__ para retornar None.

Exibiremos dois exemplos para demonstrar esses conceitos. Primeiro definimos uma classe com sua implementação default de __equal__ e __hash__. Em seguida alteramos hash e __eq__ fazendo o teste de igualdade concordar com o teste de hash. Observe que em nenhum caso dois objetos diferentes satisfazem b1 is b2 já que o id é fixo e não pode ser alterado pelo usuário.

» # ----------- Exemplo 1 ---------------    
» class Bar1:
»     def __init__(self, numero, lista):
»         self.numero = numero
»         self.lista = lista

» b1 = Bar1(123, [1,2,3])
» b2 = Bar1(123, [1,2,3])
» b3 = b1

» b1 == b2                      # (o mesmo que b1 is b2)
↳ False
» b1 == b3                      # (o mesmo que b1 is b3)
↳ True
» id(b1) == id(b2)
↳ False
» hash(b1) == hash(b2)
↳ False

» # ----------- Exemplo 2 ---------------
» class Bar2:
»     def __init__(self, numero, lista):
»         self.numero = numero
»         self.lista = lista
»     def __hash__(self):
»         return hash(self.numero)
»     def __eq__(self, other):
»         return self.numero == other.numero and self.lista == other.lista
        
» b1 = Bar2(123, [1,2,3])
» b2 = Bar2(123, [1,2,3])
» b3 = Bar2(123, [4,5,6])

» b1 == b2
↳ True
» id(b1) == id(b2)
↳ False
» hash(b1) == hash(b2)
↳ True
» b1 == b3
↳ False
» hash(b1) == hash(b3)
↳ True

No segundo caso a alteração de __hash__ faz com que dois objetos diferentes, de acordo com o teste is, possam ter o mesmo código hash, o que pode ser útil, dependendo da aplicação.

Uma tabela que associa objetos usando o hash de uma coluna como índice, agilizando a busca de cada par é chamada de hashtable. No Python os dicionários são exemplos dessas hashtables.

O processo de hashing é usado em encriptação e verificação de autenticidade. O Python oferece diversos módulos para isso, como hashlib, instalado por padrão, e cryptohash disponível em Pypi.org.

Métodos __new__ e __del__

Vimos que o método __init__ é adicionado automaticamente quando uma classe é instanciada e que podemos passar valores em seus parâmetros para inicializar o objeto. Antes dele outro método é acionado automaticamente, o método __new__, acessado durante a criação do objeto. Ambos são acionados automaticamente.

Além dele o método __del__ é ativado quando o objeto é destruído (quando a última referência é desfeita e o objeto fica disponível para ser coletado pela lixeira).

O parâmetro cls representa a superclasse que será instanciada e seu valor é provido pelo interpretador. Ele não tem acesso a nenhum dos atributos do objeto definidos em seguida.

» class A:
»     def __new__(cls):
»         print("Criando novo objeto") 
»         return super().__new__(cls)
»     def __init__(self): 
»         print("Dentro de __init__ ")
»     def __del__(self):
»         print("Dentro de __del__ ")

» # instanciando um objeto
» a = A()
↳ Criando novo objeto
↳ Dentro de __init__ 

» # finalizando a referência ao objeto
» del a
↳ Dentro de __del__ 

A função super(), que veremos como mais detalhes, é usada para acessar a superclasse de A, que no caso é object, a classe default da qual herdam todos os demais objetos. Nessa linha se retorna o método __new__ da superclasse e, sem ela, não teríamos acesso à A.__init__.

Como não há uma garantia de que o coletor de lixo destruirá o objeto imediatamente após a referência ser cortada (veja a seção sobre o coletor de lixo) método __del__ tem utilidade reduzida. Ele pode ser útil, no entanto e por ex., para desfazer outra referência que pode existir para o mesmo objeto. Também podem ser usadas em conjunto __new__ e __del__ para abir e fechar, respectivamente, uma conexão com um arquivo ou com um banco de dados.

Se __init__ demanda por valores de parâmetros esses valores devem ser passados para __new__. O exemplo abaixo demostra o uso de __new__ para decidir se cria ou não um objeto da classe Aluno. Se a nota < 5 nenhum objeto será criado.

» class Aluno:
»     def __new__(cls, nome, nota):
»         if nota >= 5:
»             return object.__new__(cls)
»         else:
»             return None
» 
»     def __init__(self, nome, nota):
»         self.nome = nome
»         self.nota = nota
» 
»     def getConceito(self):
»         return 'A' if self.nota >= 8.5 else 'B' if self.nota >= 7.5 else 'C'
» 
»     def __str__(self):
»         return 'Classe = %s\nDicionário(%s) Conceito: %s %s' % (self.__class__.__name__,
»                                                    str(self.__dict__),
»                                                    self.getConceito(),
»                                                    '-'*60 )
                                                   
» a1, a2, a3 = Aluno('Isaac Newton', 9), Aluno('Forrest Gump',4), Aluno('Mary Mediana',7)

» print(a1)
» print(a2, '<---- Objeto não criado' + '-'*60)
» print(a3)

↳ Classe = Aluno
↳ Dicionário({'nome': 'Isaac Newton', 'nota': 9}) Conceito: A
↳ ------------------------------------------------------------
↳ None <---- Objeto não criado
↳ ------------------------------------------------------------
↳ Classe = Aluno
↳ Dicionário({'nome': 'Mary Mediana', 'nota': 7}) Conceito: C
↳ ------------------------------------------------------------

A classe Aluno usa no método __str__ dois atributos das classes: __class__ e __dict__. __class__ retorna um objeto descritor, que é de leitura apenas (não pode ser modificado) que contém dados sobre a superclasse, entre eles o atributo __name__, o nome da classe.

» a1.__class__==Aluno
↳ True

» print(a1.__class__.__name__)
↳ Aluno

» a1.__dict__
↳ {'nome': 'Isaac Newton', 'nota': 9}

» print(dir(a1))
↳ ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
↳ '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
↳  '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
↳ '__weakref__', 'getConceito', 'nome', 'nota']

A função dir retorna todos os seus métodos e propriedades. Como todos os objetos do Python, a classe Aluno possui o atributo __dict__ que é um dicionário contendo suas propriedades. As propriedades podem ser apagadas, editadas ou inseridas diretamente nesse dicionário.

» # inserindo campo 'nascimento'
» a1.__dict__['nascimento'] = '21/03/2001'
» a1.nascimento
↳ '21/03/2001'

» # apagando campo 'nota'
» del(a1.__dict__)['nota']
» a1.nota
↳ AttributeError: 'Aluno' object has no attribute 'nota

Funções e Classes Fábrica (Factory Classes, Functions)


Na programação orientada a objetos uma fábrica é um objeto usado para a criação de outros objetos. Ela pode ser uma função ou método que retorna objetos derivados de um protótipo ou de uma classe e suas subclasses, à partir de parâmetros que permitem a decisão sobre qual objeto retornar.

Isso é particularmente útil quando um grande número de objetos devem ser criados. Esse construtor generalizado pode ter um processo de decisão sobre que subclasse usar. No exemplo seguinte usamos a classe Personagem e suas subclasses Cientista e Estudante definidas anteriormente para construir um exemplo que gera alguns objetos.

» # uma função "factory"
» def criaPersonagem(qual, energia, inteligencia):
»     if qual == 0:
»         return Personagem(energia, inteligencia)
»     elif qual ==1:
»         return Cientista(energia, inteligencia)
»     elif qual ==2:
»         return Estudante(energia, inteligencia)
»     else:
»         print('Personagem = 0, 1, 2')

» # Constroi 3 personagens de tipos diferentes e os armazena em um dicionário
» personagens = {}
» for t in range(3):
»     personagens[t] = criaPersonagem(t, 100, 100)

» # temos um dicionário com os personagens
» for t in range(3):
»     print(personagens[t])
↳ Vida = 100 Poder = 100
↳ Cientista: Vida = 1100 Poder = 500
↳ Estudante: Vida = 600 Poder = 1000

Um design interessante para evitar um código repleto de ifs e, ao mesmo tempo, forçar uma padronização da informação consiste em usar códigos associados ao dado que se quer informar. Para o exemplo seguinte suponha que estamos interessados em classificar livros em uma biblioteca. Para isso criamos um dicionário que associa um código a cada gênero de livros. Com isso obrigamos o preenchimento de dados a se conformar com a inserção correta do código e evitar erros tais como escrever de modo diferente o mesmo gênero (como Poesias e Poemas).

Digamos que em uma biblioteca existem livros dos seguintes gêneros:

  • Romance
  • Poesia
  • Ficção Científica
  • Divulgação Científica
  • Política
  • Biografias

Claro que em um caso mais realista teríamos muito mais gêneros e subgêneros, tipicamente armazenados por código em um banco de dados.

Iniciamos por criar um dicionário (categoria) usando código do gênero como chave. A definição da classe Livro testa na inicialização se o código fornecido existe e, caso afirmativo, substitui self.genero com o gênero correspondente. Se o código não existe fazemos self.genero = "Não Informado".

» # armazenamos a relação codigo - gênero
» categoria = {1 : 'Romance',
»              2 : 'Poesia',
»              3 : 'Ficção Científica',
»              4 : 'Divulgação Científica',
»              5 : 'Política',
»              6 : 'Biografias'
»              }
» 
» # definimos a classe livro
» class Livro:
»     def __init__(self, codigoGenero, titulo, autor):
»         if codigoGenero in categoria.keys():
»             self.genero = categoria[codigoGenero]
»         else:
»             self.genero = 'Não informado'
»         self.titulo = titulo
»         self.autor = autor
»         
»     def __str__(self):
»         txt = 'Gênero: %s\n' % self.genero
»         txt += 'Título: %s\n' % self.titulo
»         txt += 'Autor: %s\n' % self.autor
»         return txt

» # Inicializamos livro com código existente
» livro1 = Livro(1, 'Anna Karenina','Leo Tolstoy')
» print(livro1)
↳ Gênero: Romance
↳ Título: Anna Karenina
↳ Autor: Leo Tolstoy

» # Inicializamos livro com código inexistente
» livro2 = Livro(9, 'Cosmos','Carl Sagan')
» print(livro2)
↳ Gênero: Não informado
↳ Título: Cosmos
↳ Autor: Carl Sagan

Outra abordagem seria armazenar o próprio código transformando-o em texto apenas no momento de uma consulta ou impressão. Esse tipo de design é particularmente útil quando a quantidade de dados mapeados é grande e a consulta a eles é frequente.

Já vimos que uma subclasse pode fazer poucas modificações na classe base, aproveitando quase todo o seu conteúdo mas customizando alguns atributos.

» class Biografia(Livro):
»     def __init__(self, titulo, autor, biografado):
»         self.biografado = biografado
»         super().__init__(6, titulo, autor)
»     def __str__(self):
»         return '%sBiografado: %s' % (super().__str__(), self.biografado)

» bio1 = Biografia('Um Estranho ao Meu Lado','Ann Rule','Ted Bundy')
» print(bio1)
↳ Gênero: Biografias
↳ Título: Um Estranho ao Meu Lado
↳ Autor: Ann Rule
↳ Biografado: Ted Bundy

Suponha que tenhamos criado subclasses especializadas para cada gênero: Biografia, Politica, Poesia, …, etc. Uma factory de classes pode usar um dicionário que associa diretamente um código à classe. No exemplo temos, além da classe Biografia, criamos outras duas apenas para efeito de demonstração.

» class Politica(Livro):
»     pass
»
» class Poesia(Livro):
»     pass
    
» # uma factory de classe (retorna a classe apropriada)
» class FactoryLivro:
»     def __init__(self, i):
»         classe = {6: Biografia, 5: Politica, 2: Poesia}
»         self.essaClasse = classe.get(i,Livro)
»     def getClasse(self):
»         return self.essaClasse

» # Inicializa com livro do gênero 6 (biografia, único que defimos corretamente)
» fact = FactoryLivro(6)
» # classBio vai receber a classe Biografia
» ClassBio = fact.getClasse()

» # instancia um objeto de ClassBio
» bio2 = ClassBio('Vivendo na Pré-história','Ugah Bugah','Fred Flistone')
» print(bio2)
↳ Gênero: Biografias
↳ Título: Vivendo na Pré-história
↳ Autor: Ugah Bugah
↳ Biografado: Fred Flistone

Relembrando, usamos acima o método de dicionários dict.get(key, default), que retorna o valor correspondente à key se ela existe, ou default, se não existe.

Claro que, ao invés de criar subclasses para cada gênero, também poderíamos ampliar a generalidade da própria superclasse Livro inserindo campos flexíveis capazes de armazenar as especificidades de cada gênero, tal como as propriedades Livro.nomeDoCampo e Livro.valorDoCampo.

Classes servidoras de dados: Podemos definir uma classe que não recebe dados do usuário e apenas é usada para preparar e retornar dados em alguma forma específica. A classe Baralho abaixo representa um baralho construído na inicialização, juntando naipes com números de 2 até 10, adicionados de A, J, Q, K. Um coringa C é adicionado a cada baralho. Um objeto da classe tem acesso ao método Baralho.distribuir(n) que seleciona e retorna aleatoriamente n cartas. Para embaralhar as cartas usamos o método random.shuffle que mistura elementos de uma coleção (pseudo) aleatoriamente.

» import random
» class Baralho:
»     def __init__(self):
»         naipes = ['♣', '♠', '♥','♦']
»         numeros = list(range(1, 14))
»         numeros = ['A' if i==1
»                    else 'K' if i==13
»                    else 'Q' if i==12
»                    else 'J' if i==11
»                    else str(i) for i in numeros
»                   ]
»         deck = [a + b for a in numeros for b in naipes]
»         deck += ['C']
»         random.shuffle(deck)
»         self.deck = deck
» 
»     def distribuir(self, quantas):
»         if quantas > len(self.deck):
»             return 'Só restam %d cartas!' % len(self.deck)
»         mao = ''
»         for t in range(quantas):
»             mao += '[%s]' % self.deck.pop()
»         return mao

» jogo = Baralho()
» player1 = jogo.distribuir(11)
» player2 = jogo.distribuir(11)

» print(player1)
↳ [7♥][5♠][6♥][3♦][6♠][8♣][4♥][3♣][9♠][Q♦][7♣]

» print(player2)
↳ [4♦][A♦][J♣][J♥][8♠][9♦][8♦][10♦][10♥][5♥][3♥]


O método lista.pop() retorna o último elemento da lista, removendo o elemento retornado, como uma carta retirada de um baralho.

Para tornar esse processo ainda mais interessante poderíamos criar a classe Carta que gera objetos contendo separadamente seu naipe e valor para facilitar as interações com outras cartas durante o jogo. Esses objetos poderiam, inclusive, armazenar o endereço de imagens de cada carta do baralho, gerando melhores efeitos visuais. Com isso o método Baralho.distribuir() poderia retornar uma coleção de cartas e não apenas strings com uma represntação simples das cartas do baralho.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.

Python: Funções, Decoradores e Exceções


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
: time.time() retorna a hora em segundos, como um número de ponto flutuante, lida no relógio interno do computador. Ela é o número de segundos decorridos desde a época, 1 de Janeiro de 1970, 00:00:00 (UTC), também chamada de (Unix time).

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.

🔺Início do artigo

Bibliografia

Consulte a bibliografia no final do primeiro artigo dessa série.