quinta-feira, 29 de janeiro de 2009

Propriedades e acessores

Em Java e em C++ há um bom motivo para que os atributos sejam sempre privados: preservar a interface das classes.

Por exemplo, imagine que temos uma classe que representa o registro de uma pessoa. Nela temos a idade. Sendo um número inteiro, poderia ser um atributo público:
Person person;
person.age = 23;
std::cout << person.age << std::endl;


Faz sentido. Agora imagine que mais tarde descobrimos que é preciso fazer um tratamento do valor recebido – para estar numa faixa aceitável, por exemplo…

Sem alterar a interface – e efetuar uma refatoração total de todo código onde ocorre instância – é impossível.

Então, em Java e C++ é importante usar métodos acessores e manter os atributos privados. Dessa forma qualquer alteração futura pode ser feita na classe sem alterar o restante do código:
Person person;
person.setAge(23);
std::cout << person.getAge() << std::endl;

Propriedades

Já em Python isso não faz sentido!

Em Python, além de atributos e métodos, há as propriedades. Então atributos podem ser públicos e, caso seja necessário adicionar alguma lógica na leitura ou gravação do atributo, basta substituí-lo por uma propriedade, sem prejudicar a interface.
class Person(object):

    def __init__(self):
        self.name = ""
        self.age = 0


Substituindo por propriedades:
class Person(object):

    def __init__(self):
        self.name = ""
        self.__age = 0

    def _get_age(self):
        return self.__age

    def _set_age(self, age):
        if 0 <= age < 150:
            self.__age = age

    age = property(_get_age, _set_age)


E a interface permanece imutável!

Aviso: não use propriedade quando não for necessário! Use atributo público. Um pouco de desempenho não faz mal a ninguém, mesmo em scripts.
Aviso²: testei esse código no prompt e não funciona, mas funciona em script. Se alguém puder descobrir o que está acontecendo, agradeço.

[update 2009-08-26]Usando IPython funcionou corretamente, portanto não sei o que causou o erro. =P

Assim assim, valeu Thiago Coutinho por tê-lo identificado (acho que ainda ocorre durante a interpretação em linha do CPython tradicional).[/update]

Usando metaclasse

Uma forma de implementar automaticamente autopropriedades foi sugerida por Guido van Rossum em Unificando Tipos e Classes (original).

Usaremos uma metaclasse:
class autoprop(type):

    def __init__(cls, name, base, dict):
        super(autoprop, cls).__init__(name, base, dict)
        props = {}
        for method in dict:
            if method.startswith("_get_") \
                or method.startswith("_set_") \
                or method.startswith("_del_"):
                props[method[5:]] = True
        for prop in props:
            fget = getattr(cls, "_get_" + prop, None)
            fset = getattr(cls, "_set_" + prop, None)
            fdel = getattr(cls, "_del_" + prop, None)
            setattr(cls, prop, property(fget, fset, fdel))

A ideia por trás dessa metaclasse é identificar métodos iniciados por _get_, _set_ ou _del_ e criar propriedades a partir daí. O método iniciado por _get_ representa a leitura, _set_ representa a escrita e _del_ representa a remoção.

Nossa classe então ficaria assim:
class Person(object):
    __metaclass__ = autoprop

    def __init__(self):
        self.name = ""
        self.__age = 0

    def _get_age(self):
        return self.__age

    def _set_age(self, age):
        if 0 <= age < 150:
            self.__age = age

Usando decoradores

Mas realmente, ao contrário da metaclasse para singleton, usar metaclasse para autopropriedade é como usar uma escopeta para espantar uma mosca.

Vamos usar decoradores!

O decorador @property pode ser usado para criar propriedades. Funciona muito bem e deixa o código bastante limpo:
class Person(object):

    def __init__(self):
        self.name = ""
        self.__age = 0

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, age):
        if 0 <= age < 150:
            self.__age = age

A função deve ter o nome da propriedade. O método de escrita deve chamar-se fset, o de leitura fget e o de remoção fdel. A parte mais feia desse código é o retorno. =)

**

Fica aqui a dica!

[]'s
Cacilhas, La Batalema
blog comments powered by Disqus