Python3 09 – Classes e Orientação a Objetos

Janeiro 25th, 2018 by Rudson Alves Leave a reply »
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 na biblioteca vetor.py.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/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:

>>> 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

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:

>>> from vetor import vetor
>>> p = vetor(12, 7)
>>> p
12i + 7j
>>> p.modulo()
13.892443989449804
>>> p.angulo()
30.256437163529263

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, grande parte da apresentação girou 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):
    <atributos e métodos da classe>

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:

>>> 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

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.
    >>> f.__dict__
    {}
    >>> from vetor import *
    >>> a = vetor(3, 4)
    >>> a.__dict__
    {'x': 3, 'y': 4}

    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__'.
    >>> f.__module__
    '__main__'
    >>> a.__module__
    'vetor'

    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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/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:

>>> 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

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.

>>> print(a.modulo.__doc__)
 Módulo do vetor 
>>> print(a.__doc__)
 Classe para representar um vetor em 3D

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.

>>> 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'

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.

40
41
42
43
44
    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:

>>> from vetor2 import *
>>> a = vec(1,2,3)
>>> print(a)
1i + 2j + 3k
>>> repr(a)
1i + 2j + 3k

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:

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
    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:

>>> 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

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:

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    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:

>>> 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

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:

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
    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:

>>> 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 "", line 1, in 
    5*a - 3*b*c
TypeError: unsupported operand type(s) for *: 'int' and 'vec'

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:

174
    __rmul__ = __mul__

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

>>> 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

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.

177
178
179
180
181
182
183
184
185
186
187
188
    @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.

>>> from vetor2 import *
>>> vec.test()
0i + 0j + 0k
2i + -41j + -9k
a = 3.7416573867739413   b = 2.449489742783178   c = 7.681145747868608
a > b:  True
b a.alfa = 30.863143184317483    a.theta = 63.43494882292201

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.

>>> a = 12
>>> b = 20
>>> a.__add__(b)
32
>>> a + b
32

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#
# 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:

>>> 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']

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#
# 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:

>>> 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

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#
# 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:

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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:

>>> 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'}

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:

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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.

32
33
34
35
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.

24
25
26
27
28
    @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.


QR Code
  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
Advertisement

Deixe uma resposta

This blog is kept spam free by WP-SpamFree.