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: Classes, variáveis do usuário

Programação Orientada a Objetos

Embora sendo uma linguagem multiparadigma Python tem suporte para a maioria das técnicas empregadas na Programação Orientada a Objetos, POO. Os tipos de dados que encontramos no Python, tais como strings, números inteiros, listas e dicionários são objetos. Eles possuem propriedades e métodos próprios, pré-programados. É frequente, no entanto, que coisas da vida real, sobre as quais queremos usar métodos de computação e análise, exijam uma modelagem mais complexa. Para isso podemos criar objetos que são os tipos definidos pelo usuário, denominados classes. Além de ser uma ferramenta útil para quem cria um programa, essa possibilidade foi explorada por outros programadores que disponibilizam seu código através de módulos disponíveis em um grande número de bibliotecas que expandem o poder do Python.

A programação orientada a objetos faz uso das seguintes técnicas:

  • Encapsulamento de dados : a restrição de que dados e métodos só possam ser acessados de dentro do objeto, com acesso vedado à chamadas externas.
  • Herança : a possibilidade de reutilizar e estender a definição de uma classe, alterando-a para especializações.
  • Polimorfismo : a possibilidade de que várias classes usem os mesmos nomes de métodos gerais.

No Python o encapsulamento de dados não é obrigatório nem automático, mas pode ser implementado. Herança e polimorfismo são partes naturais de sua sintaxe.

Assim como em outras partes desse texto, os exemplos dados são simples e pouco realistas. Casos complexos são compostos de um grande número de partes simples, portanto entender o simples é um grande passo para o gerenciamento dos casos gerais. Além disso nossas classes não são devidamente documentadas para diminuir o tamanho do texto e facilitar a leitura, o que deve ser evitado na prática.

Classes: Tipos de dados criados pelo usuário

Suponha que desejamos elaborar um programa para controle de uma escola. Um elemento básico desse programa seria, por ex., a descrição dos alunos. Um aluno pode ser descrito por uma série de dados de tipos diversos, como uma string para armazenar seu nome, inteiros para sua idade, floats para suas notas, datas para a data de nascimento, etc. É claro que podemos configurar listas ou dicionários complexos que contenham toda essa informação. No entanto temos uma ferramenta mais sofisticada e poderosa: as classes.

Definições:

Uma classe é uma abstração de alguma entidade que se deseja modelar. Um objeto é um caso particular da entidade representada pela classe. Dizemos que o objeto é uma instância da classe. A classe possui atributos que podem ser propriedades ou métodos. Propriedades são o conjunto de valores (dados) que descrevem a entidade. Métodos são funções definidas na classe. Esses métodos contém instruções para as tarefas que esperamos que sejam executadas por aquela entidade.

Os métodos mais frequentes são aqueles que realizam operações CRUD (Create, Read, Update e Delete, ou seja, criar, ler, atualizar e apagar). Mas eles não se retringem a isso e podem fazer operações com os dados armazenados em propriedades, retornar resultados, imprimí-los ou executar qualquer tarefa disponível para a máquina que executa o código. Um objeto instanciado de uma classe herda suas propriedades e métodos.

Uma classe é definida com a palavra chave class seguida de seu nome (por convenção iniciado por maiúscula). A partir da classe objetos são instanciados, ou seja, criados sob o molde da classe. Esses objetos possuem as mesmas propriedades e métodos da classe.

A classe mais simples é aquela que contém sua definição, sem nenhuma propriedade nem método. A palavra chave pass marca a posição, sem executar nenhuma tarefa. Propriedades podem ser inseridas, alteradas e lidas com a notação de ponto, objeto.propridade.

# Uma classe simples
» class Simples:
»     pass

# instanciando um objeto da classe
» s1 = Simples()
# inserindo 2 propriedades
» s1.propriedade1 = 'A solução para a questão da vida, do universo e tudo mais'
» s1.propriedade2 = 42

# examinando o estado das propriedades
» print(s1.propriedade1, s1.propriedade2)
↳ A solução para a questão da vida, do universo e tudo mais 42

# atributos são listados com dir *
» print(dir(s1))
↳ ['__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__',
↳ 'propriedade1', 'propriedade2']

# uma propriedade pode ser removida
» del s1.propriedade2

# a classe (modelo para os objetos) pode ser alterada após sua definição
» Simples.valor = 9
» s2 = Simples()
» s2.valor
↳ 9
» s1.valor
↳ 9

Vemos acima que uma alteração na classe gera alterações nas instâncias. *A função dir lista os atributos da classe ou dos objetos, entre elas as que definimos acrescidas de muitas outras geradas automaticamente. Voltaremos a isso mais tarde.

Propriedades podem ser definidas na classe, sendo herdadas por suas instâncias.

» class Aluno():
»     nome = ''
»     nota = 0

# instanciamos 2 objetos da classe
» aluno1 = Aluno()
» aluno2 = Aluno()

# as propriedades pode ser acessadas para leitura e alteração
» aluno1.nome = 'Ana Anésia'
» aluno1.nota = 79

» print(aluno1.nome, aluno1.nota) 
↳ Ana Anésia  79

# a propridade __class__ indice a que classe pertence esse objeto
» aluno1.__class__
» __main__.Aluno

» aluno2.nome = 'Pedro Paulo Passos'
» aluno2.nota = 45

# propriedades podem ser usadas como variáveis
» print('Média dos alunos: ',  (aluno1.nota + aluno2.nota)/2)
↳ Média dos alunos:  62.0

Uma alteração na classe Aluno altera as propriedades dos objetos que ainda não foram modificados e das novas instâncias. Propriedades nas instâncias alteradas após a inicialização não são afetadas.

# uma alteração na classe    
» Aluno.nota = 50
» aluno3 = Aluno()
» print(aluno3.nota)
↳ 50

» Aluno.nota = 65
» print(aluno3.nota)
↳ 65

# propriedades já alteradas não são afetadas
» print(aluno1.nota)
↳ 79


O parâmetro self serve como uma referência para o próprio objeto (como veremos, para cada uma das instâncias da classe) e é usado para acessar os atributos daquele objeto. Qualquer nome de variável pode ser usado, embora seja uma convenção chamá-lo de self. Não é recomendado usar outro nome se queremos que nosso código seja de fácil leitura para outros programadores. Ele deve ser sempre o primeiro parâmetro de todas as funções e mesmo os métodos que não recebem outros parâmetros devem conter self.

Além de propriedades uma classe pode ter métodos que são funções executadas dentro da classe. A classe Calculadora que se segue contém apenas um método e nenhuma propriedade acessada pelo usuário. Ela recebe dois números e o tipo de operação desejada, + – * / e retorna uma string com a operação e seu resultado.

» class Calculadora:
»     def calcular(self, x, y, operacao = '+'):
»         if operacao == '+':   resultado = '%.1f + %.1f = %.1f' % (x, y, x + y)
»         elif operacao == '-': resultado = '%.1f - %.1f = %.1f' % (x, y, x - y)
»         elif operacao == '*': resultado = '%.1f x %.1f = %.1f' % (x, y, x * y)
»         elif operacao == '/':
»             if y == 0:  resultado = 'Erro: divisão por zero!'
»             else:       resultado = '%.1f / %.1f = %.1f' % (x, y, x / y)
»         else:
»             resultado = 'Nenhuma operação conhecida (+ - * /) foi requisitada!'
»         print(resultado)

# instancia um objeto
» calc = Calculadora()
# a operação de soma é default
» calc.calcular(123,123)
↳ 123.0 + 123.0 = 246.0

» calc.calcular(213,432, '-')
↳ 213.0 - 432.0 = -219.0

» calc.calcular(213,43, '*')
↳ 213.0 x 43.0 = 9159.0

» calc.calcular(213,0, '/')
↳ Erro: divisão por zero!

» calc.calcular(213,23, 'r')
↳ Nenhuma operação conhecida foi definida!

# calc é uma variável do tipo
» type(calc)
↳ __main__.Calculadora

Os Métodos __init __() e __str__()

Todas as classes, mesmo que não explicitamente definido, possuem um método chamado __init __ (), que é sempre executado durante sua inicialização. O nome __init__, iniciado e terminado por 2 sublinhados (o que em inglês tem sido chamado de dunder, double underscore), é chamado de construtor da classe.

Ele é usada para fazer as atribuições de valor às propriedades do objeto e executar outras operações que sejam necessárias quando o objeto está sendo criado. É considerada uma boa prática de programação não definir propriedades fora da inicialização, exceto quando propriedades de classe são necessárias. Após a inicialização o método não deve ser usado novamente.

Ao ser instanciado um objeto os parâmetros de __init__ devem ser fornecidos ou um erro é lançado.

» class Aluno:
»     def __init__(self, nome, nota):
»         self.nome = nome
»         self.nota = nota        
    
# Inicializamos um objeto com seus valores iniciais
» aluno1 = Aluno('Joana Paraíba', 67)
» print(a1.nome, a1.nota)
↳ Joana Paraíba  67

# como antes, a propriedade pode ser alterada
» aluno1.nome = 'Joana Pessoa'
» aluno1.nome
↳ 'Joana Pessoa'

# um atributo pode ser removido do objeto
» del aluno1.nome
» print(aluno1.nome)
↳ AttributeError: type object 'a1' has no attribute 'nome'

Após a definição da classe dois objetos foram instanciados: aluno1 e aluno2 têm as mesmas propriedades que a classe Aluno. Essas propriedades podem ser modificadas com a notação de ponto, objeto.propriedade, e acessadas como variáveis comuns, como foi feito no cálculo da média das notas.

Como no caso da calculadora acima, classes contém propriedades e métodos, ou funcionalidades. Métodos do objetos são funções definidas dentro de um objeto. Na nossa classse Aluno, vamos inserir vários métodos: get_sobrenome(), que retorna a última string após partir o nome nos espaços, set_nota() que insere as notas do aluno, get_media() que calcula e retorna a média das 3 notas, e o método especial __str()__ que retorna uma representação de string contendo os dados que julgamos representativos do objeto.

» class Aluno:
»     def __init__(self, nome, nota1=0, nota2=0, nota3=0):
»         self.nome = nome
»         self.nota1 = nota1
»         self.nota2 = nota2
»         self.nota3 = nota3

»     def get_sobrenome(self):
»         return self.nome.split()[-1]

»     def set_nota(self, n, nota):
»         if n==1:
»             self.nota1=nota
»         if n==2:
»             self.nota2=nota
»         if n==3:
»             self.nota3=nota

»     def get_media(self):
»         return (self.nota1 + self.nota2 + self.nota3)/3
        
»     def aprovado(self):
»         return self.get_media() >= 50

»     def __str__(self):
»         txt = 'Nome: ' + self.nome
»         txt += '\nNotas: %2.1f, %2.1f, %2.1f' % (self.nota1, self.nota2, self.nota3) 
»         txt += '\nMédia: %2.1f' % self.get_media()
»         txt +=  '  ** %s **\n' % ('aprovado' if self.aprovado() else 'reprovado')
»         return txt

Acessar um método de instância, objeto.metodo() é o mesmo que chamar o método da classe passando o próprio objeto (a instância) como argumento self.

# instanciando um aluno e suas notas
» a1 = Aluno('Mário Leibniz', 34, 45, 76)
# usar o método de instância
» a1.get_media()
↳ 51.666666666666664

# é o mesmo que chamar o método
» Aluno.get_media(a1)
↳ 51.666666666666664

# 1 aluno inicializado com as notas default    
» pedro = Aluno('Pedro Álvarez')
# pedro tem a propriedade nome
» print(pedro.nome)
↳ Pedro Álvarez

# as notas de pedro podem ser alteradas
» pedro.set_nota(1,84)
» pedro.set_nota(2,89)
» pedro.set_nota(3,97)
# e sua média pode ser calculada
» pedro.get_media()
↳ 90.0

Uma classe pode ter em suas propriedades dados de qualquer tipo. No exemplo seguinte criamos a classe Escola contendo apenas nome e uma lista de alunos, inicialmente por uma lista vazia e depois preenchida com objetos da classe Aluno.

# usando 2 alunos inicializados
» a1 = Aluno('Mário Leibniz', 34, 45, 76)
» a2 = Aluno('Joana Frida', 10, 28, 16)    
    
# definimos a class Escola
» class Escola:
»     def __init__(self, nome):
»         self.nome = nome
»         self.alunos = []

»     def insere_aluno(self, aluno):
»         self.alunos.append(aluno)

»     def __str__(self):        
»         txt = 'Nome: ' + self.nome
»         txt += '\nTem %d alunos' % len(self.alunos)
»         txt += '\nAlunos:\n'
»         for a in self.alunos:
»             txt += '-' * 38 + '\n'
»             txt += str(a)
»         return txt    

# criamos uma instância da classe
» escola1 = Escola('Caminhos da Luz Albert Einstein')

# inserimos os dois alunos já definidos
» escola1.insere_aluno(a1)
» escola1.insere_aluno(a2)

# usamos o método Escola.__str__ para listar o estado de escola1
» print(escola1.__str__())
↳ Nome: Caminhos da Luz Albert Einstein
↳ Tem 2 alunos
↳ Alunos:
↳ --------------------------------------
↳ Nome: Mário Leibniz
↳ Notas: 34.0, 45.0, 76.0
↳ Média: 51.7   **aprovado**
↳ --------------------------------------
↳ Nome: Joana Frida
↳ Notas: 10.0, 28.0, 16.0
↳ Média: 18.0   ** reprovado**

Embora não obrigatório, é uma convenção útil usar o nome da classe com primeira letra maiúscula. Esse código define a classe Aluno que tem as propriedades nome, nota1, nota2 e nota3, e os métodos set_nota(), que atribue valor a uma das 3 notas; get_sobrenome(), que retorna o último nome registrado; get_media(), que retorna a média das 3 notas; aprovado() que retorna um booleano (False ou True) informando se a aluno foi aprovado. O conjunto das propriedades de um objeto consistem em seu estado. Métodos são as funções que o objeto pode executar, em geral associadas ao seu estado.

Um método retorna um valor de tipo especificado ou None se é terminado por return vazio ou mesmo sem nenhum return.

Nas classes Aluno e Escola usamos também o método especial __str__(self) que retorna uma representação de string do objeto. Ele definido internamente de modo a ser acionado quando fazemos uma chamada a print(objeto). São equivalentes as instruções:
print(objeto.__str__()), print(str(objeto))print(objeto).

» print(pedro)        # ou print(str(pedro)) ou  print(pedro.__str__())
↳ Nome: Pedro Álvarez
↳ Notas: 84.0, 89.0, 97.0
↳ Média: 90.0

# idem
» print(escola1)
↳ Nome: Caminhos da Luz Albert Einstein
↳ Tem 2 alunos
↳ Alunos:
↳ --------------------------------------
↳ Nome: Mário Leibniz
↳ Notas: 34.0, 45.0, 76.0
↳ Média: 51.7   **aprovado**
↳ --------------------------------------
↳ Nome: Joana Frida
↳ Notas: 10.0, 28.0, 16.0
↳ Média: 18.0   ** reprovado**

Vemos, dessa forma, que uma classe é um modelo geral que pode ser usado para a criação de várias casos particulares dos objetos que ela modela. Uma instância da classe, como pedro, tem todas as propriedades e métodos da classe.

Encapsulamento

Uma discussão mais completa de escopo e namespaces pode ser lida na seção Variavéis de Classe e de Instância e no artigo seguinte, neste site, Escopos e namespaces.

Encapsulamento é uma forma de trabalhar com uma variável dentro de um bloco, como um objeto ou função, sem tornar aquela variável disponível para o restante do código. Isso pode ser necessário por motivos de segurança ou para realizar uma verificação antes que uma alteração seja feita a essa variável. Por ex., se um campo de CPF vai ser modificado é sempre bom testar se um número válido está sendo inserido.

Se uma propriedade é definida no método de inicialização com dois sublinhados como __variavel ela não pode ser acessada fora do objeto diretamente por objeto.__variavel. Nesse caso métodos devem ser definidos para o seu acesso. Métodos privados, construídos com a mesma técnica, também só podem ser acessados de dentro da classe.


A classe seguinte representa uma porta que pode estar fechada e trancada. Métodos são definidos para abrir, fechar, trancar, destrancar a porta. Todos eles verificam o parâmetro fornecido em relação ao estado da porta para decidir qual é procedimento correto. Por ex., uma porta trancada não pode ser aberta. (Claro que uma classe mais enxuta poderia ser escrita mas seria, provavelmente, um pouco mais díficil de ler. Da mesmo forma o método __geraTexto() está aí apenas para demonstrar o funcionamento da função privada, pois poderia ser incorporado ao __str__().)

» class Porta:
»     def __init__(self, fechada, trancada):
»         self.__fechada = trancada or fechada
»         self.__trancada = trancada
 
»     def abre(self):
»         if self.__trancada:
»             print('A porta está trancada e não pode ser aberta!')
»         else:
»             print('A porta foi aberta!' if self.__fechada else 'A porta já está aberta!')
»             self.__fechada = False
 
»     def fecha(self):
»         print('A porta já está fechada!' if self.__fechada else 'A porta foi fechada!')
»         self.__fechada = True
 
»     def tranca(self):
»         if not self.__fechada:
»             print('A porta está aberta, não pode ser trancada!')
»         else:
»             print('A porta já está trancada!' if self.__trancada else 'A porta foi trancada!')
»             self.__trancada = True        
 
»     def destranca(self):
»         print('A porta foi destrancada!' if self.__trancada else 'A porta já está destrancada!')
»         self.__trancada = False
 
»     def __geraTexto(self):
»         txt = 'A porta está ' + ('fechada' if self.__fechada else 'aberta')
»         txt += (' e trancada' if self.__trancada else ' mas destrancada') if self.__fechada else ''
»         return txt
     
»     def __str__(self):
»         return self.__geraTexto()


Na inicialização a linha self.__fechada = trancada or fechada impede que o estado da porta seja definida como aberta e trancada simultaneamente. Lembrando, foi usada a forma de if else ternário: variavel = valor1 if condicao else valor2, que significa que variavel assume o valor1 se condicao for verdadeira, caso contrário, o valor2.

Observe também que nos métodos, como em destranca(), a mensagem foi gerada antes da troca de valor do estado, pois depende do valor antigo e não no novo.

Agora podemos inicializar um objeto porta e interagir com seus métodos (mas não com seus atributos diretamente). Os atributos privados não podem ser acessados diretamente, nem o método __geraTexto() que só pode ser chamado de dentro da classe, no caso de __str__.

# um atributo privado não pode ser acessado diretamente, nem o método __geraTexto()
» print(porta.__fechada)
↳ AttributeError: 'Porta' object has no attribute '__fechada'

» porta = Porta(True, True)
» print(porta)
↳ A porta está fechada e trancada

» porta.abre()
↳ A porta está trancada e não pode ser aberta!

» porta.destranca()
↳ A porta foi destrancada!

» porta.abre()
↳ A porta foi aberta!

» print(porta)
↳ A porta está aberta

» porta.tranca()
↳ A porta está aberta, não pode ser trancada!

» print(porta)
↳ A porta está aberta

Com encapsulamento conseguimos fazer com que o efeito de um método ou a atribuição de valores às propriedades seja dependente do estado do objeto. É o caso do método porta.abre() que impede que a porta seja aberta se estiver trancada.

A interface de uma classe ou de um módulo são as partes expostas ao usuário do objeto, sejam dados ou métodos.

O encapsulamento favorece a criação de classes que expõe uma interface simples com o código externo (e portanto para o programador), sem a necessidade de exibir possíveis complicações internas. Isso torna esse código mais fácil de reutilizar, em acordo com o princípio DRY do Python (don’t repeat yourself). Ele contribui para a legibilidade do código, pois expõe com clareza o que deve ser fornecido e o que pode ser extraído na classe, impedindo o acesso acidental à informações que não se pretendia tornar acessíveis.

Herdando propriedades e métodos

Além de permitir a criação de objetos como instâncias dessa classe, também é possível construir outras classes herdando e modificando suas propriedades e métodos. Isso é útil quando se quer escrever uma classe que é uma especialização de outra já definida. Quando uma classe herda de outra ela contém todos os métodos e propriedades da primeira classe e pode ter seus atributos alterados e ampliados.

A sintaxe para criar classes derivadas de outra já existente é a seguinte:

# ex. 1: herdando de classe base
» class NomeDaClasseFilha(NomeDaClasseBase):
»     <propriedades e métodos modificados>

# ex. 2: Classe base em outro módulo        
» class NomeDaClasseFilha(Modulo.NomeDaClasseBase):
»     <propriedades e métodos modificados>

Todas as classes herdam de alguma outra. Se NomeDaClasseBase não é especificado a classe herda de object. A classe básica deve estar no mesmo escopo que sua derivada ou, caso contrário, seu módulo deve ser citado, como no exemplo 2.

Na modelagem das pessoas associadas à uma escola encontramos funcionários, professores e alunos. Todos eles partilham características que reuniremos na classe Pessoa.

» class Pessoa(object):
»     def __init__(self, nome, cpf):
»         self.nome=nome
»         self.cpf=cpf
         
»     def __str__(self):
»         return 'Nome: %s; CPF: %s' %(self.nome, self.cpf)
 
» p1 = Pessoa('Ricardo Dalquins','123-456-789-00')
» print(p1)
↳ Nome: Ricardo Dalquins; CPF: 123-456-789-00

As declarações class Pessoa(object):, class Pessoa(): e class Pessoa: são equivalentes.

Os alunos, além de serem pessoas, possuem características especificas como número de matrícula, notas, situação de regularidade quanto às mensalidades, etc. Para demonstrar a criação de uma subclasse usaremos apenas a matrícula.

# definindo uma subclasse    
» class Aluno(Pessoa):
»     def __init__(self, nome, cpf, matricula):
»         super().__init__(nome, cpf)
»         self.matricula = matricula
 
»     def __str__(self):
»         txt =  'Nome: %s \nCPF: %s' %(self.nome, self.cpf)
»         txt += '\nMatricula ' + self.matricula
»         return txt

# inicializando instância da subclasse Aluno
» p2 = Aluno('Uiliam Cheiquispir', '321-654-987-55', '321.654')
 
» print(p2)
↳ Nome: Uiliam Cheiquispir 
↳ CPF: 321-654-987-55
↳ Matricula 321.654

super() é uma função especial que faz a conexão entre a classe mãe e a filha. Ela representa a classe mãe, de onde a classe atual é derivada. Desta forma super().__init__() chama a inicialização de Pessoa e preenche os atribulos nela existentes. O nome super vem da convenção muito usada de chamar a classe mãe de superclasse e a filha de subclasse. A subclasse, por sua vez, pode servir de base para a geração de outra subclasse, abaixo dela. Encontramos também a nomeclatura classe base e classe derivada.

Vamos lembrar que print(p2) aciona o método __str()__ que existe na super e na subclasse. Como ele foi definido na subclasse Aluno este método é executado e o método na superclasse é ignorado. Esse processo, de subreescrever o método da superclasse se chama overriding.

Na prática, quando tentamos modelar um objeto de uso do mundo real, é muito possível que o número de atributos se torne muito grande gerando classes longas e de difícil manuseio. Nesses casos pode ser interessante a criação de classes auxiliares que são usadas como variáveis dentro da classe principal. No caso dos alunos o tratamento de suas notas pode ser gerenciado à parte.

# define uma classe para as notas de um aluno    
» class Nota:
»     def __init__(self, matricula):
»         self.notas = []
»         self.matricula = matricula
»         
»     def insereNota(self,nota):
»         self.notas.append(nota)
»         
»     def getMedia(self):
»         return sum(self.notas)/len(self.notas)
» 
# instancia objeto
» n2 = Nota('321.654')
# insere 3 notas
» n2.insereNota(89)
» n2.insereNota(78)
» n2.insereNota(90)

# acessa propriedade de n2
» n2.getMedia()
↳ 85.66666666666667

É claro que na caso real deve haver um tratamento para evitar erros e conflitos. Se o método Nota.getMedia() for chamado antes da inserção de uma nota um erro será lançado. Isso pode ser contornado capturando essa exceção ou com uma modificação do código.

# modificando o método getMedia()
»     def getMedia(self):
»         if not self.notas:
»             return 0
»         else:
»             return sum(self.notas)/len(self.notas) 

if not self.notas retorna True se a lista estiver vazia. É o mesmo que if len(self.notas)==0 ou simplesmente if len(self.notas) uma vez que if 0 retorna False.

Modificamos agora a classe Aluno para conter uma propriedade Aluno.notas que armazena um objeto tipo Nota que contém a matricula do alunos e uma lista com suas notas.

# Altera a classe aluno para usar a classe Nota
» class Aluno(Pessoa):
»     def __init__(self, nome, cpf, matricula):
»         super().__init__(nome, cpf)
»         self.matricula = matricula
»         self.notas = Nota(matricula)

»     def __str__(self):
»         txt =  'Nome: %s \nCPF: %s' %(self.nome, self.cpf)
»         txt += '\nMatricula ' + self.matricula
»         txt += '\nMédia de Notas: %.1f' % self.notas.getMedia()
»         return txt

# cria um aluno específico
» bob = Aluno('Roberto Gepeto', '741-852-963-55', '654.987')
# insere suas notas
» bob.notas.insereNota(86)
» bob.notas.insereNota(88)
» bob.notas.insereNota(87)

# acessa bob.__str__() 
» print(bob)
↳ Nome: Roberto Gepeto 
↳ CPF: 741-852-963-55
↳ Matricula 654.987
↳ Média de Notas: 87.0

Na linha bob.notas.insereNota(86) estamos acessando o objeto bob.notas que é uma instância de Nota e que, portanto, possui o método insereNota().

Polimorfismo e Overloading

A palavra polimorfismo significa literalmente “muitas formas”. No Python polimorfismo significa que uma função pode receber parâmetros de tipos diferentes e ainda assim agir como esperado com esses tipos. A função len() por ex. pode receber parâmetros diversos desde que sejam sequências (string, bytes, tupla, lista or range) ou a coleções (dicionário, conjunto, …). Idem, o operador + pode agir sobre objetos de tipos diferentes, gerando resultados diferentes.

# string
» len('Entrou mudo e saiu calado.')
↳ 26
# lista
» len([12,23,34,45,56,67])
↳ 6
# dicionário
» len({1:'1', 2:'2'})
↳ 2

# operador "+"
» 345 + 543
↳ 888
» 'ama' + 'ciante'
↳ 'amaciante'

Classes diferentes podem ter métodos com o mesmo nome. Ao ser chamado em um objeto o método específico de sua classe é executado.

» class Cavalo:
»     def som(self):
»         print('relincha')
 
» class Galinha:
»     def som(self):
»         print('cacareja')
 
» giselda = Galinha()
» corisco = Cavalo()

» print(giselda.som())
↳ cacareja

» print(corisco.som())
↳ relincha       

Overloading de operadores

Operadores pré-definidos em tipos criados pelo usuário, tal como o operador de soma + podem ser sobrescritos, overloaded (ou sobrecarregados).

No exemplo seguinte, definimos uma classe para um “vetor” com dois componentes, (x, y) e a soma de dois vetores, compatível com a soma matemática de vetores, que consiste em somar os componentes x, y, separadamente de cada vetor, e retornar outro vetor (que é instanciado dentro do método __add__().
Para definir essa soma sobreescrevemos o método __add__() que é internamente usado quando se opera com +. Observe que na definição de __add__() nos referimos ao segundo operando da soma como other (outro). Se esta definição não for feita explicitamente a soma entre dois vetores não estará definida.

Também inserimos o overloading do método __eq__ que define como se testa dois objetos para a igualdade, de forma que dois vetores sejam considerados iguais se seus dois componentes são iguais.

# nova definição de vetor
» class Vetor:
»     def __init__(self, x, y):
»         self.x = x
»         self.y = y
     
»     def __add__(self, other):
»         return Vetor(self.x + other.x, self.y + other.y)
 
»     def __eq__(self, other):
»         return self.x == other.x and self.y == other.y
 
»     def __str__(self):
»         return 'Objeto da Classe Vetor: (%d, %d)' % (self.x, self.y)
 
# instanciamos 2 vetores
» v1 = Vetor(34,74)
» v2 = Vetor(16, 26)

# somamos e exibimos a soma
» v3 = v1 + v2
» print(v3)
↳ Objeto da Classe Vetor: (50, 100)

# v3 é uma instância de Vetor
» isinstance(v3, Vetor)
↳ True

# para verificar o teste de igualdade
» v3 = Vetor(3,7)
» v4 = Vetor(3,7)
» print(v3 == v4)
↳ True

A classe Vetor, definida dessa forma, mostra que um objeto pode retornar outro objeto de seu próprio tipo. De fato funções e métodos podem receber e retornar parâmetros que são objetos de qualquer tipo, inclusive os tipos de usuário. A função interna isinstance(objeto, classe) retorna True se objeto é membro da classe, caso contrário retorna False.

Um outro exemplo mostra o overloading de len().

# controle de compras online
» class Compra:
»     def __init__(self, cesta, usuario):
»         self.cesta = cesta
»         self.usuario = usuario
        
»     def __len__(self):
»         return len(self.cesta)

» compra = Compra(['sapato', 'camisa', 'gravata'], 'Pedro Paulo Pizo')
» print('%s possui %d itens em sua cesta' % (compra.usuario, len(compra)))
↳ Pedro Paulo Pizo possui 3 itens em sua cesta

Observe que o “comprimento” de um objeto de Compra não estaria definido se não inseríssemos a definição de __len__() na classe.

Para fazer overloading em uma classe (ou função) definida pelo usuário é necessário testar que tipo de argumento foi passado para a função. No caso de class Ola o método trata diferentemente argumentos passados como None (o que ocorre se o parâmtro for omitido) ou passados como string.

# overloading na classe
» class Ola:
»     def digaOla(self, nome=None):
»         if nome is None:
»             print('Insira o seu nome ')
»         else:
»             print('Olá ' + nome)

» o1 = Ola()
» o1.digaOla()
↳ Insira o seu nome 

» o1.digaOla('Olavo')
↳ Olá Olavo

isinstance() foi usada na classe MontaTexto que espera receber uma string com itens separados por ; ou uma lista. Ela trata cada uma delas de modo diferente. Naturalmente este requisito deveria estar documentado junto à definição da classe.

» class MontaTexto:
»     def montaTexto(self, obj):
»         conta = 0
»         txt = ''
»         if isinstance(obj, str):
»             partes = obj.split(';')
»         elif isinstance(obj, list):
»             partes = obj
»         for item in partes:
»             conta +=1
»             txt += 'Item %d: %s\n' % (conta, item)
»         print(txt)            
    
» txt = ['abóbora', 'brócolis', 'couve', 'alface']
» texto = MontaTexto()
» texto.montaTexto(txt)
↳ Item 1: abóbora
↳ Item 2: brócolis
↳ Item 3: couve
↳ Item 4: alface

» txt = 'lâmpada;fio;alicate;parafuso'
» texto.montaTexto(txt)
↳ Item 1: lâmpada
↳ Item 2: fio
↳ Item 3: alicate
↳ Item 4: parafuso

Polimorfismo


Em termos de classes, polimorfismo signica que podemos, em uma subclasse, usar o mesmo nome de método que já existe na superclasse, alterando seu comportamento. Uma subclasse é sempre um caso particular da superclasse e, portanto, possui características comuns com ela (ou não seria boa ideia herdar da classe mãe). No entanto ela pode modificar as faixas válidas de valores das propriedades e o resultado da atuação de métodos, além de inserir novos atributos. Essa tipo de herança com customização favorece a reutilização de código pronto, alterando classes prontas para especializá-las ao caso desejado.

Considerando as classes herdadas de superclasses, polimorfismo signica que ao chamar um atributo em um objeto da classe 3 (na figura x) esse atributo será procurado primeiro em (3), depois em (2) e finalmente em (1), e ler ou executar o que encontrar primeiro.

Uma classe pode herdar de mais de uma classe, carregando os atributos de ambas.

# define classes C1 e C2
» class C1:
»     c0='C1.c0'
»     c1='C1.c1'
    
» class C2:
»     c0='C2.c0'
»     c2='C2.c2'
»     c3='C2.c3'
   
# define classe C3 que herda de C1 e C2
» class C3(C1, C2):    
»     c3='C3.c3'
 
# instancia objeto de C3 e imprime suas propriedades
» obj3 = C3()
» print('%s - %s - %s - %s' % (obj3.c0, obj3.c1, obj3.c2, obj3.c3))
↳ C1.c0 - C1.c1 - C2.c2 - C3.c3

Como se vê no resultado impresso, a variável c0, que não existe em C3 foi lida em C1 pois essa classe está inserida antes (à esquerda) de C2.

No código abaixo queremos modelar as pessoas que trabalham em uma empresa. A classe mais geral, Pessoa, possui atributos que todas as pessoas na empresa (ou fora dela) possuem. O único método de nosso exemplo é __str__().

Dessa classe se deriva Funcionario que tem a propriedade extra, salario, e um método, dar_aumento(porcento). Funcionario.__str__() busca a representação de string de super() e acrescenta a informação do salário.

A última classe é um caso especial de funcionário que são os gerentes. A classe Gerente, além de receber os aumentos regulares de todos os funcionários recebe um bônus. O método dar_aumento(porcento, bonus) sobreescreve o método de mesmo nome na superclasse para adicionar um bônus. Para isso ele primeiro dá o aumento regular de todos os funcionários, super().dar_aumento(porcento) (que altera o self.salario) para depois somar a ele o bônus.

# define uma classe geral
» class Pessoa:
»     def __init__(self, nome, cpf):
»         self.nome = nome
»         self.cpf = cpf
»     def __str__(self):
»         return 'Nome: %s\nCPF: %s' % (self.nome, self.cpf)

» p1 = Pessoa('Maria Quiri', '555-444-888-99')

» print(p1)
↳ Nome: Maria Quiri
↳ CPF: 555-444-888-99
 
# define classe especializada de Pessoa
» class Funcionario(Pessoa):
»     def __init__(self, nome, cpf, salario):
»         super().__init__(nome, cpf)
»         self.salario = salario
»     def dar_aumento(self, porcento):
»         self.salario *= (1 + porcento/100)

»     def __str__(self):
»         return '%s \nSalário: %.2f '  % (super().__str__(), self.salario)

» f1 = Funcionario('Joana Darcos', '111-222-333-44', 1000)

» print(f1)
↳ Nome: Joana Darcos
↳ CPF: 111-222-333-44 
↳ Salário: 1000.00

» f1.dar_aumento(10)
» print(f1)
↳ Nome: Joana Darcos
↳ CPF: 111-222-333-44 
↳ Salário: 1100.00 

# define um tipo especial de Funcionario
» class Gerente(Funcionario):
»     def __init__(self, nome, cpf, salario):
»         super().__init__(nome, cpf, salario)
     
»     def dar_aumento(self, porcento, bonus):
»         super().dar_aumento(porcento)
»         self.salario += bonus

» g1 = Gerente('Isaque Nilton','999-888-777-22',2000)

» print(g1)
↳ Nome: Isaque Nilton
↳ CPF: 999-888-777-22 
↳ Salário: 2000.00 

» g1.dar_aumento(10, 800)
» print(g1)
↳ Nome: Isaque Nilton
↳ CPF: 999-888-777-22 
↳ Salário: 3000.00 

Como vemos, classes podem herdar atributos de uma superclasse, alterá-los para seu funcionamento específico ou inserir novos atributos.

Frequentemente esses dados, no caso de uma empresa real, estão gravados em bancos de dados que são lidos e armazenados nas propriedades dessas classes. Também existem formas de gravar o estado de um objeto em disco para reutilização posterior, como veremos.

Importando Classes

Uma vez que as classes estão bem definidas e funcionando adequadamente, temos a opção de armazená-las em módulos e utilizá-las como fazemos com qualquer outra classe importada do Python. Podemos gravar um arquivo com o nome escola.py que contém as definições das classes Pessoa, Aluno e Nota já definidas.

# arquivo escola.py    
» class Pessoa:
»     def __init__(self, nome, cpf):
»         self.nome=nome
»         self.cpf=cpf
        
»     def __str__(self):
»         return 'Nome: %s; CPF: %s' %(self.nome, self.cpf)

# classe aluno
» class Aluno(Pessoa):
»     def __init__(self, nome, cpf, matricula):
»         super().__init__(nome, cpf)
»         self.matricula = matricula
»         self.notas = Nota(matricula)
        
»     def __str__(self):
»         txt =  'Nome: %s \nCPF: %s' %(self.nome, self.cpf)
»         txt += '\nMatricula ' + self.matricula
»         txt += '\nMédia de Notas: %.1f' % self.notas.getMedia()
»         return txt

# classe nota
» class Nota:
»     def __init__(self, matricula):
»         self.notas = []
»         self.matricula = matricula
        
»     def insereNota(self,nota):
»         self.notas.append(nota)

»     def getMedia(self):    
»         if not self.notas:
»             return 0
»         else:
»             return sum(self.notas)/len(self.notas)         

Para usar essas classes temos que importar o módulo e as classes necessárias. Para a importação temos que fornecer o caminho completo de onde está o módulo, caso ele não esteja na pasta em uso ou no PATH do Python.

# importa módulo e classes
from escola import Aluno
# inicializa aluno
» novoAluno = Aluno('Homero Poeta', '234-456-656-56', '346.345')
#insere suas notas
» novoAluno.notas.insereNota(34)
» novoAluno.notas.insereNota(45)
» novoAluno.notas.insereNota(41)

» print(novoAluno)
↳ Nome: Homero Poeta 
↳ CPF: 234-456-656-56
↳ Matricula 346.345
↳ Média de Notas: 40.0

A importação de Aluno tornou disponível Pessoa e Nota que são por ela utilizados, uma vez que estão todas no mesmo escopo, de modo que não é necessário importar as 3 classes.

Você pode importar todos as classes de um módulo simultaneamente de duas formas.

» from escola import *
» import escola
» novoAluno = escola.Aluno('Homero Poeta', '234-456-656-56', '346.345')

O primeiro método apenas estabelece um caminho de busca para as classes usadas, e não onera o código em termos de memória, durante sua execução. No entanto ele não é recomendado pois é útil poder ver nas primeiras linhas do código as classes que serão usadas. Também, ele pode gerar confusão com os nomes do código e os do módulo caso você importe outro módulo com nomes coincidentes. Isso pode gerar erros difíceis de serem encontrados e corrigidos.

A segunda abordagem é mais interessante: o módulo é importado sem menção às classes ou funções usadas. Em seguida classes e funções são acessadas como componentes desse módulo.

# importamos o modulo que contém classe nomeDaClasse e função fc()
» import modulo

# classes e funções são chamadas
» objeto = modulo.nomeDaClasse
» h = modulo.fc(parametro)


Desta forma os nomes de classes não estarão listados no topo do código mas aparecem claramente no corpo do programa, onde as classes e funções são usadas.

Se o número de classes for muito grande elas podem ser gravadas em arquivos diferentes e importadas dentro do módulo que necessita de outro módulo. Suponha que gravamos Aluno e Pessoa nos arquivos aluno.py e pessoa.py. Como a classe Aluno necessita de Pessoa para sua definição ela deve conter em seu cabeçalho a linha import pessoa from Pessoa.

Gravar os módulos e suas classes em arquivos separados é uma maneira eficaz de manter um código limpo e mais fácil de gerenciar. Cada classe carrega a lógica relativa aquele objeto e o programa central fica encarregado de juntar as partes e executar a lógica principal.


No Jupyter Notebook podemos reinicializar o kernel limpando todas as definições prévias. Para isso selecione menu | Kernel | Restart ou pressione 0, 0 na célula, modo de controle.Além disso podemos escrever o nome na variável seguido de um ponto, e apertar tab. Isso faz com com métodos e propriedades sejam exibidos, como mostrado na figura.

Variavéis de Classe e de Instância

Getters e Setters

Métodos que visam ler o estado de alguma propriedade do objeto, sem realizar alterações, são denominados getters (assessors ou leitores). Já os que alteram propriedades são setters, (mutators ou definidores).

Diferente de outras linguagens POO, variáveis ou métodos no Python não podem ser declaradas como públicas ou privadas, que são aqueles que só podem ser acessados de dentro da classe. Depois de definido em objeto instância de Aluno (da classe já definida) podemos acessar suas propriedades e métodos livremente no objeto. Podemos definir ana.nome='Ana Marta' e recuperar esse nome através de print(ana.nome).

Apesar disso há uma convenção seguida pela maioria dos programadores: um nome (de variável ou método) prefixado com um sublinhado (como _variavel) deve ser tratado como parte não pública daquele bloco de código que pode ser uma função, um método ou uma classe. Além disso um duplo sublinhado (como __variavel) tem o efeito de tornar privada aquela variável. Sublinhados possuem signficados especiais no Python, que já exploraremos.

Como vimos, existe no Python o chamado ocultamento de nome (name muting) que permite a definição de nomes válidos para membros de uma classe privada, o que é útil para evitar conflitos de nomes com outros nomes definidos em subclasses (classes que se baseiam nesta para sua construção). Um identificador (nome de variável ou função) com dois sublinhados iniciais (pode ter um sublinhado final), como por ex. __nome é substituído por _nomeDaClasse.__nome, onde nomeDaClasse é o nome da classe onde está o objeto, com um sublinhado removido. Esse ocultamento é útil para permitir que as subclasses sobrescrevam (override) os métodos sem impedir que os métodos da classe mãe continuem funcionando.

Vejamos o exemplo seguinte para entender como esse duplo sublinhado funciona:

» class Publico:
»     __privadaDeClasse = 66
»     def __init__(self):
»         self.__privadaDeInstancia = 17;

»     def __metodoPrivado(self):
»         print('Dentro do método privado')

»     def metodoPublico(self):
»         print('Essa classe tem uma variável privada com valor: ', self.__privadaDeInstancia)

»     def getPrivada(self):
»         print(self.__privadaDeClasse)

»     def setPrivada(self, k):
»         self.__privadaDeClasse = k

# instancia um objeto da classe
» caso = Publico()

# usando um método público
» caso.metodoPublico()
↳ Essa classe tem uma variável privada com valor:  17

# tentativa de usar um método privado
» caso.metodoPrivado()
↳ AttributeError: 'Publico' object has no attribute 'metodoPrivado'

# tentativa de ler diretamente uma varável privada
» caso.__privadaDeInstancia
↳ AttributeError: 'Publico' object has no attribute '__privadaDeInstancia'

# a variável privada só pode ser acessada de dentro do objeto
» caso.getPrivada()
↳ 66

# para alterar a variável privada usamos método público
» caso.setPrivada(9)
» caso.getPrivada()
↳ 9

A propriedade __privadaDeClasse, além se ser privada, é chamada de variável de classe. Embora ela possa ser alterada após a criação todos os objetos instanciados terão esse valor inicial. Da mesma forma métodos privados só podem ser chamados de dentro do objeto. Dessa forma se impede que partes do código não sejam expostas ao módulo mais geral. Essas partes não aparecem nos chamados à help(), nem nas caixas dropdown do Jupyter Notebook. Ao atribuir um valor a uma variável se pode, por ex., fazer testes de verificação se um tipo correto foi enviado e se o valor está dentro da faixa esperada, enviando mensagens apropriados de erro, quando for o caso.

A classe a seguir define uma variável quantos que pertence à definição da classe. Ela pode ser alterada e todo objeto instanciado a partir dela terá essa mesma propriedade. Já a variável id pertence à cada objeto individual e não é compartilhada com outros membros da classe.

Essa classe está definida da seguinte forma: a cada nova inicialização, que corresponde à inserção de um novo funcionário, a variável quantos é incrementada de 1, e a variável id copia esse valor. Na criação de novo Funcionario o atributo de classe quantos é alterado para todos os funcionários, enquanto seu id permanece o mesmo.

Observe que, dentro de __init__, a referência foi feita à Funcionario.quantos e self.id respectivamente. Lembrando, self é uma forma de referenciar o objeto instância da classe.

# declare uma classe
» class Funcionario:
»     # atributo de classe
»     quantos = 0
    
»     def __init__(self):
»         Funcionario.quantos += 1
»         self.id = Funcionario.quantos
    
»     def __str__(self):
»         return 'Funcionário %d de %d' % (self.id, Funcionario.quantos) 

A variável quantos é de classe, enquanto id é uma variável de instância.

O método __self__() retorna uma string com o id do funcionário e o número de cadastrados.

# cria um Funcionario
» f1 = Funcionario()
# usa a função exibir() para ver atributos de instância e de classe
» print(f1) 
↳ Funcionário  1 de 1

# novos funcionários
» f2 = Funcionario()
» f3 = Funcionario()

# agora existem 3 funcionários
» print(f2) 
↳ Funcionário  2 de 3

» print(f3) 
↳ Funcionário  3 de 3

# o estado de f1 foi alterado
» print(f1)
↳ Funcionário  1 de 3

# o estado da classe também foi alterado
» Funcionario.quantos
↳ 3

Esse exemplo também ilustra o fato de que uma variável de classe pode ser acessada e modificada de dentro de cada instância. A modificação da classe geradora modifica suas instâncias, o que mostra que a execução de cada instância acessa o codigo da classe.

Iteradores

Já vimos que objetos que são sequências e coleções podem ser lidos iterativamente com o uso do operador for.

» lista = ['Aa', 'Bb', 'Cc', 'Dd', 'Ee', 'Ff']
» for t in lista:
»     print(t, end=' < ')
↳ Aa < Bb < Cc < Dd < Ee < Ff <

Laços desse tipo são claros e concisos. Por trás desse resultado simples, a instrução for chama a função iter() na sequência (ou coleção) que retorna um objeto iterador. Dentro do iterador existe o método __next__() que acessa os elementos no contêiner um de cada vez. Ao final da iteração, quando se extinguem os elementos, __next__() levanta uma exceção StopIteration que é reconhecida pelo laço como o fim da iteração. O método __next__() pode ser chamado através da função interna next(), como mostra o exemplo:

» iterador = iter('OMS')
» print(next(iterador))
» print(next(iterador))
» print(next(iterador))
» print(next(iterador))
↳ O
↳ M
↳ S
↳ StopIteration:

Podemos tornar uma classe iterável inserindo nela os métodos __iter__() e next(). No caso abaixo a classe simplesmente retorna a sequência invertida, do último elemento para o primeiro

» class Inverter:
»     def __init__(self, data):
»         self.data = data
»         self.index = len(data)

»     def __iter__(self):
»         return self

»     def __next__(self):
»         if self.index == 0:
»             raise StopIteration
»         self.index -= 1
»         return self.data[self.index]

# inv é Inverter usando uma lista como argumento
» inv = Inverter(['Aa', 'Bb', 'Cc', 'Dd', 'Ee', 'Ff'])

» for t in inv:
»     print(t, end=' < ')
↳ Ff < Ee < Dd < Cc < Bb < Aa <

# s é Inverter usando uma string como argumento
» s = 'Joazeiro'
» seq = Inverter(s)
» for t in seq:
»     print(t, end=' < ')
↳ o < r < i < e < z < a < o < J <     
🔺Início do artigo

Bibliografia

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