Python3 09 – Classes e Orientação a Objetos

Este artigo é a parte 9 de 10 na série Python3

Ao longo de todos os textos apresentados, instâncias de classes foram usadas extensivamente. Embora na maior parte destes o paradigma predominante tenha parecido ser o procedural, orientação a objetos sempre esteve à margem. O Python é uma linguagem multi paradigma, possibilitando ao programador desenvolver seus aplicativos no estilo de sua escolha ou mesmo misturá-los, aproveitando o que há de melhor nos diferentes paradigmas.

Este texto irá cobrir os conceitos e técnicas necessárias para você poder desenvolver aplicativos com Orientação a Objetos em Python, equalizando as terminologias usadas até o momento. Embora não pretenda aprofundar em todos os detalhes da orientação a objetos no Python, os aspectos mais importantes e interessantes para um bom domínio serão abordados.

1. Orientação a Objetos

Em programação procedural, as variáveis são basicamente caixinhas onde uma informação é armazenada, enquanto que todas as demais propriedades que a grandeza possa aceitar, como propriedades de soma, divisão, fatiamento, valor absoluto, … ficam a encargo das funções e procedimentos externos à grandeza. Em Orientação a Objeto, estas variáveis são substituídas por objetos, instâncias de alguma classe que possui atributos como tipo, tamanho, nome, entre outros, e possui rotinas e funções internas que são chamadas de métodos, os quais dizem como estes objetos interagem com outros objetos como nas operações aritméticas, comparações além de fornecer métodos internos como módulo, fatiamento, translado entre outros.

Imagine que busque desenvolver um aplicativo matemático onde empregará a aritmética vetorial, para simplificar, apenas em um plano cartesiano.

Em programação procedural, pode-se usar uma lista como o vetor de duas posições para armazenar suas coordenadas, x e y, sendo a sua hipotenusa e ângulo que o vetor faz com o eixo horizontal calculados por meio de funções. Sua pré implementação seria apenas as duas funções apresentadas no modulo vetor.py, apresentado abaixo.

Para os exemplos a seguir, crie um arquivo vetor.py com o conteúdo abaixo e grave-o na pasta corrente.

#!/bin/env python3
#
# Módulo vetor.py
#

from math import sqrt, atan, degrees

def modulo(x, y):
    return sqrt(x**2 + y**2)

def angulo(x, y):
    return degrees(atan(y/x))

class vetor(object):
    def __init__(self, x = 0, y = 0):
        self.x = x
        self.y = y

    def modulo(self):
        return sqrt(self.x**2 + self.y**2)

    def angulo(self):
        return degrees(atan(self.y/self.x))

    def __str__(self):
        return '{0}i + {1}j'.format(self.x, self.y)

A construção das classes será discutida mais adiante. Neste momento, abstraia este código apenas para o desenvolvimento dos exemplos a seguir.

Sua aplicação seguiria a sequência de comandos abaixo:

[xterm color=’true’ py=’true’]
>>> from vetor import hipotenusa, angulo
>>> p = [12, 7]
>>> p[0]
12
>>> p[1]
7
>>> modulo(p[0], p[1])
13.892443989449804
>>> angulo(p[0], p[1])
30.256437163529263
[/xterm]

Para este primeiro uso, as funções modulo(x, y) e angulo(x, y) são importadas do módulo externo vetor.py, apresentado acima. Os mesmos cálculos no paradigma de Orientação a Objetos seriam mais ao estilo:

[xterm color=’true’ py=’true’]
>>> from vetor import vetor
>>> p = vetor(12, 7)
>>> p
12i + 7j
>>> p.modulo()
13.892443989449804
>>> p.angulo()
30.256437163529263
[/xterm]

Neste exemplo, as coordenadas x e y são os atributos da classe vetor() e as funções modulo() e angulo() são os seus métodos. Observe que, em todos estes textos, sempre foram trabalhados objetos, fossem eles instâncias das classes int, float, str, list, tuple ou dict. Nos diferentes textos apresentados até aqui, grande parte dos conteúdos giraram em torno dos diferentes atributos e métodos destas classes.

1.1. Classe

Uma classe é uma estrutura de dados composta de atributos e métodos para melhor representar um objeto. Como no exemplo acima, um vetor possui os atributos x e y, suas coordenadas em um plano cartesiano, os métodos modulo() e angulo(), para calcular algumas das características de um vetor, e por fim o método __repr__(), para criar uma representação escrita do objeto vetor.

Estas ideias de classes se estendem a diversas outras estruturas como, por exemplo, num jogo onde seus protagonistas poderiam ser construídos através de instâncias de uma classe personagem, a qual poderia ter os atributos: nome, força, vitalidade, destreza, profissão, entre outros. Como métodos, a classe personagem poderia ter algo como: alimentar(), descansar(), lutar(), caminhar(), conversar(), comprar_item(), vender_item() entre outros. Desta forma, poderia-se criar um universo de objetos personagens interagindo entre si através de seus métodos, em acordo com seus atributos, e mesmo alterando-os.

Como o objetivo aqui é desenvolver o conceito de classe e objetos, não um jogo, o foco ficará no problema matemático em criar uma biblioteca, chamada de módulo no Python, para incorporar as características de um vetor, bem como sua interação com outros vetores e demais grandezas numéricas. Para o desafio ficar um pouco mais interessante, será trabalhada a representação tridimensional dos vetores.

1.2. Iniciando uma Classe

A sintaxe do comando class é bem simples:

class nome_da_classe(classe_herdada):
    

Se a classe_herdada for omitida, será empregada a classe padrão object. De alguma forma, todas as classes, em algum nível, são herdeiras da classe object.

Os comandos a seguir criam uma classe foo, a qual não faz nada, mas ilustra os pontos acima:

[xterm color=’true’ py=’true’]
>>> class foo(object):
… pass

>>> f = foo()
>>> dir(f)
[‘__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__’]
>>> f.__class__.__name__
‘foo’
>>> a = set(dir(object))
>>> b = set(dir(f))
>>> a.issubset(b)
True
[/xterm]

A linha f = foo() cria uma instância da classe foo e passa seu apontador para f. Todos os atributos e métodos do objeto f são herdados da classe padrão object. Seus métodos e atributos são listados pelo comando dir(f). Em seguida, é criado um conjunto com a lista dos métodos e atributos dos objetos gerados pelas classes object e foo, apontado-os para as variáveis a e b, respectivamente. A ideia é apenas empregar o método issubset() dos conjuntos para mostrar que os métodos e atributos são um subconjunto um do outro, como atestado pelo comando a.issubset(b). Entretanto, alguns atributos novos foram criados:

  • __dict__ um dicionário onde são armazenados os atributos do objeto gerado, bem como seus respectivos valores. No caso da classe foo, este dicionário se encontra vazio, já que a classe não possui atributos.

    [xterm color=’true’ py=’true’]
    >>> f.__dict__
    {}
    >>> from vetor import *
    >>> a = vetor(3, 4)
    >>> a.__dict__
    {‘x’: 3, ‘y’: 4}
    [/xterm]

    Mas os objetos gerados pela classe vetor possuem dois atributos, as coordenadas x e y do vetor no plano cartesiano;

  • __module__ – o nome do módulo em que a classe foi definida. No caso da classe foo, o módulo a que se refere é o '__main__'.

    [xterm color=’true’ py=’true’]
    >>> f.__module__
    ‘__main__’
    >>> a.__module__
    ‘vetor’
    [/xterm]

    Já a classe vetor está definida no módulo externo 'vetor', que se refere ao arquivo vetor.py;

  • __weakref__ – é mais uma flag indicando que o objeto criado é uma referência fraca da classe que o gerou1. O atributo __weakref__ não retorna nada.

Ao criar um objeto no Python, primeiro é invocado um construtor do objeto, o método __new__(), para em seguida os parâmetros serem passados ao método __init__() e o objeto ser iniciado. Em outras linguagens orientadas a objetos, como o C++ e o Java, esta inicialização é feita em uma única etapa, mas no Python estas se mantêm separadas. Em geral, iniciar apenas o método __init__() é suficiente para iniciar uma classe, sendo raras as ocasiões em que o método __new__() padrão tenha de ser sobreposto.

Para ilustrar, crie um novo arquivo vetor2.py para criar uma nova classe vec para o nosso vetor, com o conteúdo abaixo:

#!/bin/env python3
#
# Módulo vetor2.py para a álgebra vetorial em 3D
#

from math import sqrt, atan, degrees, tan, sin, cos

op_error_str = "'{0}' não suportado entre instâncias de 'vec' e '{1}'"


def className(v):
    ''' Retorna o nome da classe da veriável passada. '''
    return v.__class__.__name__


class vec(object):
    ''' Classe para representar um vetor em 3D '''
    def __init__(self, x = 0, y = 0, z = 0):
        self.x = x
        self.y = y
        self.z = z


    def modulo(self):
        ''' Módulo do vetor '''
        return sqrt(self.x**2 + self.y**2 + self.z**2)


    def theta(self):
        ''' Ângulo que a projeção do vetor no plano xy faz com o eixo x '''
        return degrees(atan(self.y/self.x))


    def alfa(self):
        ''' Ângulo que o vetor faz com o eixo z '''
        rho = sqrt(self.x**2 + self.y**2)
        return degrees(atan(rho/self.modulo()))

Para deixar o código mais limpo, foram deixadas duas linhas em branco entre cada método e, aproveitando o momento, ainda foram adicionados mais três métodos: modulo() – para calcular o módulo do vetor; theta() para calcular o ângulo que a projeção deste no plano xy faz com o eixo x; alfa() – o ângulo que o vetor faz com o eixo z.

Por hora, apenas ignore o texto entre aspas triplas. Observe que o primeiro argumento em todos os métodos da classe vec é a palavrinha self. Este self se refere ao próprio objeto e o primeiro argumento de um método será sempre o próprio objeto. O self não é usado no momento de invocar o método. Portanto, ao método __init__() será passado apenas as componentes x, y e z do vetor que serão armazenados nos atributos x, y e z do objeto vetor.

A string op_error_str será usada em mensagens de erro futuras, e a função className() será usada para retornar o nome da classe. Será útil na comparação de classes futuramente. Por agora, ignore-os.

Execute o comando abaixo no interpretador Python para testar o módulo:

[xterm color=’true’ py=’true’]
>>> from vetor2 import *
>>> a = vec(1,2,3)
>>> a.__dict__
{‘x’: 1, ‘y’: 2, ‘z’: 3}
>>> a.modulo()
3.7416573867739413
>>> a.alfa()
30.863143184317483
>>> a.theta()
63.43494882292201
[/xterm]

1.2.1. DocString

O texto entre aspas triplas é chamado de docstring. Este é apenas uma documentação adicionada ao código, sem qualquer efeito durante a sua execução. Uma docstring pode ser qualquer texto inserido entre aspas triplas, simples ou duplas, com quantas linhas forem necessárias. Esta documentação é acessada através do atributo __doc__ de cada método, função ou classe.

[xterm color=’true’ py=’true’]
>>> print(a.modulo.__doc__)
Módulo do vetor
>>> print(a.__doc__)
Classe para representar um vetor em 3D
[/xterm]

1.3. Sobrecarga

Se executar um comando dir() sobre o vetor criado, vai observar que ele possui diversos métodos herdados da classe básica, object, como já comentado anteriormente.

[xterm color=’true’ py=’true’]
>>> dir(a)
[‘__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__’, ‘alfa’, ‘modulo’, ‘theta’, ‘x’,
‘y’, ‘z’]
>>> b = vec(3,5,7)
>>> a > b
Traceback (most recent call last):
File “<pyshell#33>”, line 1, in <module>
a > b
TypeError: ‘>’ not supported between instances of ‘vec’ and ‘vec’
[/xterm]

Embora estes métodos estejam implementados pela classe padrão, uma boa parte deles são apenas para gerar códigos de erro adequados, informando que a classe não suporta tais operações. Sua implementação é feita por uma sobrecarga ao código do método da classe padrão.

Para fazer a sobrecarga, basta adicionar um método com o nome igual ao método da classe padrão que, então, este código passa a ser executado no lugar do código da classe padrão. O primeiro método sobrecarregado foi o __init__().

Deixando a sobrecarga dos operadores de comparação para mais tarde, primeiro será implementada a saída da representação impressa do vetor, empregando vetores unitários, ou seja, um print() no vetor a deve imprimir a string “1i + 2j + 3k”, ou algo semelhante. Isto é feito criando a sobrecarga do método __str__(). Para isso, adicione as linhas abaixo ao final do módulo vetor2.py.

    def __str__(self):
        return '{0}i + {1}j + {2}k'.format(self.x, self.y, self.z)


    __repr__ = __str__

Aproveitando o momento, foi adicionado o método __repr__() para apontar para o método __str__(). Após isto, será necessário reiniciar o interpretador Python para carregar a nova versão da biblioteca2. Experimente os comandos a seguir:

[xterm color=’true’ py=’true’]
>>> from vetor2 import *
>>> a = vec(1,2,3)
>>> print(a)
1i + 2j + 3k
>>> repr(a)
1i + 2j + 3k
[/xterm]

Para a sobrecarga dos operadores de comparação, será empregado o seguinte critério: as comparações serão efetuadas sempre sobre o módulo dos vetores.

Talvez não seja a melhor opção, no entanto serve para ilustrar a implementação. Portanto, uma comparação entre dois vetores na forma a > b retornará o mesmo que a.modulo() > b.modulo().

Todos estes métodos a serem sobrepostos aqui já foram discutidos em textos anteriores. Suas implementações são apresentadas abaixo:

    def __eq__(self, other):
        if isinstance(other, vec):
            modulo = other.modulo()
        elif isinstance(other, int) or isinstance(other, float):
            modulo = abs(other)
        else:
            raise TypeError(op_error_str.format('==', other.__class__.__name__))

        return self.modulo() == modulo


    def __ge__(self, other):
        if isinstance(other, vec):
            modulo = other.modulo()
        elif isinstance(other, int) or isinstance(other, float):
            modulo = abs(other)
        else:
            raise TypeError(op_error_str.format('>=', other.__class__.__name__))
        
        return self.modulo() >= modulo


    def __gt__(self, other):
        if isinstance(other, vec):
            modulo = other.modulo()
        elif isinstance(other, int) or isinstance(other, float):
            modulo = abs(other)
        else:
            raise TypeError(op_error_str.format('>', other.__class__.__name__))
        
        return self.modulo() > modulo

    def __le__(self, other):
        if isinstance(other, vec):
            modulo = other.modulo()
        elif isinstance(other, int) or isinstance(other, float):
            modulo = abs(other)
        else:
            raise TypeError(op_error_str.format('<=', other.__class__.__name__))
        
        return self.modulo() <= modulo


    def __lt__(self, other):
        if isinstance(other, vec):
            modulo = other.modulo()
        elif isinstance(other, int) or isinstance(other, float):
            modulo = abs(other)
        else:
            raise TypeError(op_error_str.format('<', other.__class__.__name__))
        
        return self.modulo() < modulo


    def __ne__(self, other):
        if isinstance(other, vec):
            modulo = other.modulo()
        elif isinstance(other, int) or isinstance(other, float):
            modulo = abs(other)
        else:
            raise TypeError(op_error_str.format('!=', other.__class__.__name__))
        
        return self.modulo() != modulo

Observe que para cada método é passado o próprio vetor, self, seguido de um segundo nomeado other. O other, no caso, é sempre a segunda grandeza a ser comparada. Por exemplo, o código __ne__(self, other) implementa a comparação self != other.

Em seguida, faça alguns testes no novo módulo:

[xterm color=’true’ py=’true’]
>>> from vetor2 import *
>>> a = vec(1, 2, 3)
>>> b = vec(1, -1, 2)
>>> c = vec(3, -2, -1)
>>> a.modulo(), b.modulo(), c.modulo()
(3.7416573867739413, 2.449489742783178, 3.7416573867739413)
>>> a > b
True
>>> b > a
False
>>> a == b
False
>>> a == c
True
[/xterm]

Neste momento, todos os operadores de comparação estão implementados, empregado sempre o módulo do vetor como elemento de comparação.

1.6. Outras Operações

Para adicionar as operações de soma e subtração, basta definir os métodos __add__() e __sub__(). Suas implementações são apresentadas a seguir:

    def __add__(self, other):
        if not isinstance(other, vec):
            raise TypeError('Não suportada a operação +: vec e outras classes')

        new = vec()
        new.x = self.x + other.x
        new.y = self.y + other.y
        new.z = self.z + other.z

        return new


    def __sub__(self, other):
        if not isinstance(other, vec):
            raise TypeError('Não suportada a operação -: vec e outras classes')

        new = vec()
        new.x = self.x - other.x
        new.y = self.y - other.y
        new.z = self.z - other.z

        return new


    def __neg__(self):
        new = vec()
        new.x = -self.x
        new.y = -self.y
        new.z = -self.z

        return new

Aproveitando o momento, também foi implementado o método __neg__(), que dá o suporte para multiplicar o vetor por -1. Seguem alguns testes para estas implementações:

[xterm color=’true’ py=’true’]
>>> from vetor2 import *
>>> a = vec(1, 2, 3)
>>> b = vec(1, -1, 2)
>>> c = vec(3, -2, -1)
>>> a + b
2i + 1j + 5k
>>> a – b
0i + 3j + 1k
>>> -b
-1i + 1j + -2k
[/xterm]

Para implementar produto, são necessários dois operadores, já que vetores suportam dois tipos de produtos: Vetorial e Escalar.

O produto vetorial será implementado pelo método __mul__(), produto normal pelo caractere asterisco. O produto escalar será implementado no método __xor__(), um método que implementa o operador binário (e booliano) ou exclusivo, representado pela operação a ^ b. Como esta operação não é suportada por vetores, este operador será tomado emprestado para representar o produto escalar nesta implementação. Como antes, adicione o código a seguir ao final do módulo vetor2.py:

    def __xor__(self, other):
        if not isinstance(other, vec):
            raise TypeError('Não suportada a operação ^: vec e outras classes')

        return self.x*other.x + self.y*other.y + self.z*other.z


    def __mul__(self, other):
        if isinstance(other, int) or isinstance(other, float):
            new = vec()
            new.x = self.x*other
            new.y = self.y*other
            new.z = self.z*other

            return new
        
        elif isinstance(other, vec):
            new = vec()
            new.x = self.y*other.z - self.z*other.y
            new.y = self.z*other.x - self.x*other.z
            new.z = self.x*other.y - self.y*other.z

            return new

        else:
            raise TypeError('Não suportada a operação *: vec e outras classes')

Seguem os testes das implementações:

[xterm color=’true’ py=’true’]
>>> from vetor2 import *
>>> a = vec(1, 2, 3)
>>> b = vec(1, -1, 2)
>>> c = vec(3, -2, -1)
>>> a*b
7i + 1j + -3k
>>> a^b
5
>>> a*5 – b*c*3
-10i + -11j + 12k
>>> 5*a – 3*b*c
Traceback (most recent call last):
File “<pyshell#87>”, line 1, in
5*a – 3*b*c
TypeError: unsupported operand type(s) for *: ‘int’ and ‘vec’
[/xterm]

Tudo parecia funcionar muito bem até o produto 5*a, que gerou a exceção TypeError acima. Isto aconteceu porque o método __mul__() acrescentou apenas o produto entre um vetor por um inteiro e não o contrário. Esta operação é feita pela implementação do método __rmul__(), que significa multiplicação pela direita (right), ou seja, multiplicar um inteiro ou float por um vetor. Como o código é exatamente o mesmo, basta adicionar uma linha abaixo da classe vec:

    __rmul__ = __mul__

Em seguida, reinicie o interpretador e teste com os comandos a seguir:

[xterm color=’true’ py=’true’]
>>> from vetor2 import *
>>> a = vec(1, 2, 3)
>>> b = vec(1, -1, 2)
>>> c = vec(3, -2, -1)
>>> a*5 – b*c*3
-10i + -11j + 12k
>>> 5*a – 3*b*c
-10i + -11j + 12k
[/xterm]

1.4. Método Estático

Um método estático é qualquer método que não recebe o primeiro argumento self e, por isso, não necessita de instanciar a classe para ser usado. Um método estático no Python é exatamente o mesmo que um método estático em Java, no entanto, diferente do Java, o Python suporta funções externas a uma classe, deixando os métodos estáticos pouco úteis. Geralmente, métodos estáticos acabam sendo pouco usados, preferindo-se o emprego de funções externas a empregá-los.

Sua definição é feita com o decorador @staticmethod, seguido da definição do método. Para testar na classe vec, adicione o método test() abaixo.

    @staticmethod
    def test():
        a = vec(1, 2, 3)
        b = vec(1, -1, 2)
        c = a*b

        print((a*b)*c)
        print(5*a - 3*b*c)
        print('a = {0}   b = {1}   c = {2}'.format(a.modulo(), b.modulo(), c.modulo()))
        print('a > b: ', a > b)
        print('b <= c: ', b <= c)
        print('a.alfa = {0}    a.theta = {1}'.format(a.alfa(), a.theta()))

As linhas abaixo mostram o método estático em ação.

[xterm color=’true’ py=’true’]
>>> from vetor2 import *
>>> vec.test()
0i + 0j + 0k
2i + -41j + -9k
a = 3.7416573867739413 b = 2.449489742783178 c = 7.681145747868608
a > b: True
b <= c: True
a.alfa = 30.863143184317483 a.theta = 63.43494882292201
[/xterm]

Observe que a classe vec não foi instanciada para chamar o método estático test(), sendo este invocado diretamente através da classe.

1.5. Métodos/Atributos Privados e a Classe Property

Os atributos e métodos considerados privados em uma classe, no Python, são aqueles cujos nomes são iniciados com um ou dois underline. Por exemplo, o método __add__() é um método privado da classe para implementar a adição entre objetos. No entanto, este método pode ser acessado diretamente sem qualquer problema no Python.

[xterm color=’true’ py=’true’]
>>> a = 12
>>> b = 20
>>> a.__add__(b)
32
>>> a + b
32
[/xterm]

Isto vale para todos os métodos e atributos de uma classe. Por convenção, no Python, todos os métodos e atributos possuem visibilidade pública, deixando a obrigação de diferenciar o que é privado ou público em uma classe a encargo do programador. O programador, por sua vez, respeitará estas convenções por duas questões básicas:

  1. o permite abstrair os detalhes da classe, tornando seu papel no desenvolvimento do programa mais simples e rápido;
  2. evita erros de entrada impróprias nos atributos. Se a abstração está implementada no código, isto pode significar que o criador da classe teve seus motivos para a empregar, como a implementação de alguma pré análise aos dados de entrada.

A ausência de métodos getters e setters em um programa geralmente denota inexperiência no código, direcionando o programador a interagir diretamente com dados, em alguns casos, sensíveis para a classe. Por isto, é sempre aconselhável criar alguma camada de abstração entre o que é público e privado em uma classe. Por este motivo, é uma boa prática de programação empregar métodos getters e setters para encapsular os membros privados de uma classe.

Como exemplo, tome uma classe pessoa, a qual deve possuir dois atributos para armazenarem o nome e a idade da pessoa. Os atributos privados escolhidos serão: _nome e _idade.

Crie e salve o módulo cidadao.py no diretório corrente com o conteúdo abaixo:

#
# Módulo cidadao
#

class pessoa(object):
    def __init__(self, nome = '', idade = 0):
        self.setName(nome)
        self.setIdade(idade)

    def setNome(self, nome):
        self._nome = nome.title()

    def getNome(self):
        return self._nome

    def setIdade(self, idade):
        self._idade = int(idade)

    def getIdade(self):
        return self._idade

O uso adequado deste módulo seria algo como:

[xterm color=’true’ py=’true’]
>>> from cidadao import *
>>> a = pessoa(‘alberto santos dumont’, 25.75)
>>> a.getNome()
‘Alberto Santos Dumont’
>>> a.getIdade()
25
>>> dir(a)
[‘__class__’, …, ‘_idade’, ‘_nome’, ‘getIdade’, ‘getName’,
‘setIdade’, ‘setNome’]
[/xterm]

Embora seja um código muito básico, ilustra bem o ponto. Observe que algum tratamento é feito sobre os valores passados para o nome e idade a serem armazenados para a pessoa a ser criada, de forma que, em uma inserção direta dos atributos _nome e _idade, tal tratamento não necessariamente ocorreria.

Para facilitar este processo, existe a classe property, que simplifica esta abstração levando a uma sintaxe mais natural ao acesso por parte do programador. Sua sintaxe é apresentada a seguir:

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

onde

  • fget é o método getter, pega o atributo;
  • fset é o método setter, altera o atributo;
  • fdel é o método deleter, destrutor do atributo;
  • doc é uma docstring para a propriedade.

No exemplo anterior, é necessário criar uma abstração para cada um dos atributos da classe pessoa. Altere o código anterior, adicionando as linhas porperty conforme o código abaixo:

#
# Módulo cidadao
#

class pessoa(object):
    def __init__(self, nome = '', idade = 0):
        self.nome = nome
        self.idade = idade

    def setNome(self, nome):
        self._nome = nome.title()

    def getNome(self):
        return self._nome

    nome = property(getNome, setNome)

    def setIdade(self, idade):
        self._idade = int(idade)

    def getIdade(self):
        return self._idade

    idade = property(getIdade, setIdade)

    def __repr__(self):
        return 'Nome: {0}     Idade: {1}'.format(self.nome, self.idade)
 
    __str__ = __repr__

O uso da classe, após estas alterações, permite algumas mudanças sintáticas:

[xterm color=’true’ py=’true’]
>>> from cidadao import *
>>> a = pessoa(‘alberto santos dumont’, 25.75)
>>> a.nome
‘Alberto Santos Dumont’
>>> a.idade
25
>>> a.nome = ‘ALBERT EINSTEIN’
>>> a.idade = 45.23
>>> a.nome, a.idade
(‘Albert Einstein’, 45)
>>> a.__dict__
{‘_nome’: ‘Albert Einstein’, ‘_idade’: 45}
>>> a
Nome: Albert Einstein Idade: 45
[/xterm]

Nas linhas de teste acima, observe que o acesso aos atributos _nome e _idade ficam abstraídos pela classe property, invocando os métodos getNome() e getIdade() no momento de consultar os atributos, e invocando os métodos setNome() e setIdade() no momento de atribuir novos valores a estes atributos. A consulta ao dicionário interno do objeto a, uma instância da classe pessoa, mostra que esta ainda possui apenas os atributos _nome e _idade, como era o esperado.

Observe também que, mesmo no código da classe pessoa, os acessos aos atributos _nome e _idade foram mascarados no método __init__(), nas linhas 7 e 8, e no método __repr__(), linha 27. Estas mudanças no código não são necessárias, mas mostram como fica mais intuitivo o acesso aos atributos da classe.

Para terminar, a classe property ainda pode ser usada como o decorador @property, seguido pela definição do método getter, setter e, por fim, o método deleter. Veja o código exemplo abaixo:

class valor():
    def __init__(self, x):
        self.x = x

    @property
    def x(self):
        ''' Esta é a docstrig '''
        return self._x

    @x.setter
    def x(self, x):
        self._x = x

    @x.deleter
    def x(self):
        del self._x

Aplicando a classe property na forma de decorador à classe pessoa, esta ficará assim:

#
# Módulo cidadao
#

class pessoa(object):
    def __init__(self, nome = '', idade = 0):
        self.nome = nome
        self.idade = idade

    @property
    def nome(self):
        return self._nome

    @nome.setter
    def nome(self, nome):
        self._nome = nome.title()

    @property
    def idade(self):
        return self._idade

    @idade.setter
    def idade(self, idade):
        self._idade = int(idade)

    def __repr__(self):
        return 'Nome: {0}     Idade: {1}'.format(self.nome, self.idade)
 
    __str__ = __repr__

Pessoalmente, acho a sintaxe anterior mais limpa, mas fica a gosto do programador. Para as implementações seguintes, será considerado o código anterior.

1.6. Herança, Polimorfismo e o Comando super()

Uma das grandes vantagens em se usar Orientação a Objetos é a possibilidade da reutilização do código. Isto permite que uma classe herde os atributos e métodos de uma classe base, sem que estes tenham de ser reescritos. É disto que trata a herança entre classes.

Para ilustrar, considere a adição da classe pessoaFisica ao final do módulo cidadao.py, com o código abaixo:

class pessoaFisica(pessoa):
    def __init__(self, nome = '', idade = 0, cpf = '0'):
        pessoa.__init__(self, nome, idade)
        self.cpf = cpf

    def setCPF(self, cpf):
        self._cpf = str(cpf)

    def getCPF(self):
        return self._cpf

    cpf = property(getCPF, setCPF)

    def __repr__(self):
        return 'Nome: {0}\nIdade: {1}\nCPF: {2}'.format(self.nome, self.idade, self.cpf)

    def __str__(self):
        return '{0}   CPF: {1}'.format(pessoa.__str__(self), self.cpf)

Desta forma, a classe pessoaFisica herda os atributos e propriedades da classe pessoa. Um novo atributo _cpf é adicionado a esta nova classe, além dos já herdados da classe pessoa, _nome e _idade.

Em seguida, execute os comandos a seguir em um terminal:

[xterm color=’true’ py=’true’]
>>> from cidadao import *
>>> b = pessoaFisica(‘beto carneiro’, 200, ‘000.000.001-00’)
>>> b
Nome: Beto Carneiro
Idade: 1200
CPF: 000.000.001-00
>>> str(b)
‘Nome: Beto Carneiro Idade: 200 CPF: 000.000.001-00’
>>> b.nome
Beto Carneiro
>>> b.idade
Idade:200
>>> b.cpf
‘000.000.001-00’
>>> b.__dict__
{‘_nome’: ‘Beto Carneiro’, ‘_idade’: 200, ‘_cpf’: ‘000.000.001-00’}
[/xterm]

Alguns métodos são sobrescritos na nova classe pessoaFisica, como no método __init__(), onde a inicialização da classe pessoa é empregada para iniciar os atributos _nome e _idade, e um novo código é adicionado para dar suporte ao novo atributo _cpf. Esta modificação, empregando as habilidades anteriores do método da classe base e adicionando novas propriedades, é chamada de polimorfismo.

Algum polimorfismo também é empregado no método __str__(), alterando a sua saída impressa, enquanto que o método __repr__() fora completamente substituído.

Para simplificar o processo de invocar um método da classe base, existe a classe super. Com o super, não tem de adicionar o nome da classe base para acessá-la, ou mesmo passar o objeto corrente, self, como argumento. O código da classe derivada pessoaFisica, empregando a classe super, é apresentado a seguir:

class pessoaFisica(pessoa):
    def __init__(self, nome = '', idade = 0, cpf = '0'):
        super().__init__(nome, idade)
        self.cpf = cpf

    def setCPF(self, cpf):
        self._cpf = str(cpf)

    def getCPF(self):
        return self._cpf

    cpf = property(getCPF, setCPF)

    def __repr__(self):
        return 'Nome: {0}\nIdade: {1}\nCPF: {2}'.format(self.nome, self.idade, self.cpf)

    def __str__(self):
        return '{0}   CPF: {1}'.format(super().__str__(), self.cpf)

As mudanças ocorreram apenas nas linhas 34 e 40, substituindo as chamadas da classe pessoa. Além de não ter de verificar o nome da classe base a cada chamada de um método da classe base, o uso da classe super facilita no caso de uma futura mudança da classe base, já que o nome seu nome aparece apenas na declaração da classe derivada.

class pessoaFisica(pessoa):
    def __init__(self, nome = '', idade = 0, cpf = '0'):
        super().__init__(nome, idade)
...

2. Conclusão

De fato, devo ter deixado passar alguns detalhes da Orientação a Objetos no Python, mas acredito ter detalhado características suficientes sobre o assunto para desenvolver um bom código em Python neste paradigma.

De certo existem diversos trunques e procedimentos interessantes, muitos dos quais desconheço e outros que poderiam deixar este texto duas vezes maior do que já está.

Um dos truques interessantes, muito empregados em código Python, é o emprego da classe property com apenas o método getter, criando uma espécie de atributo readonly, apenas leitura.

Como por exemplo, métodos como modulo(), theta() e alfa() do módulo vetor.py, classe vec, poderiam ser acessados como atributos readonly, usando o decorador @property com apenas o getter.

    @property
    def modulo(self):
        ''' Módulo do vetor '''
        return sqrt(self.x**2 + self.y**2 + self.z**2)
...

Desta forma, estes “atributos” poderiam ser acessados apenas por declarações como a.modulo, a.alfa e a.theta, sem a necessidade dos parenteses.

No entanto, acredito que o conteúdo já seja suficiente para iniciar com Orientação a Objetos no Python. Caso encontre algo realmente interessante, adiciono em textos futuros, ou mesmo reedito este.

  1. Existe uma discussão sobre as weak references na página StackOverflow e mais na documentação oficial do Python aqui e slots.
  2. No interpretador idle3, basta clicar no menu superior Shell -> Restart Shell

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.