15 de dezembro de 2008

Getters e Setters em Python como em Python


Depois de sermos apresentados aos Getters e Setters, entender sobre Atributos Privados em Python e como criar Getters e Setters em Python, como em Java, chegou a hora de conhecermos o Python-way-of-doing-it.

Se todos os métodos get e set possuem assinaturas semelhantes (apenas o nome do método muda, de acordo com o atributo), por que não concentrar todos eles em um único método? Imagino que o time de desenvolvimento de Python tenha pensado assim quando criaram os métodos __getattr__(name) e __setattr__(name, value).

Estes dois métodos funcionam de forma semelhante aos getters e setters tradicionais. A diferença é que, em vez de termos um getter e um setter para cada atributo que necessida de tal método, tudo é implementado no corpo de __getattr__(name) e __setattr__(name, value). Então o leitor que nunca viu isso pode estar pensando: Como o método vai saber qual atributo está sendo processado? Simples. O nome do atributo é passado ao método pela variável name. Então, dentro do método, o programador utiliza ifs, para saber qual o atributo foi requisitado para processamento. Vamos a um exemplo prático.
class Clock:
    def __init__(self):
        self.hour = 0
        self.minute = 0
        self.second = 0

    def __getattr__(self, name):
        print "--- Accessing %s in __getattr__()..." % name

        if name == "hour":
            return self._hour

        elif name == "minute":
            return self._minute

        elif name == "second":
            return self._second

        else:
            raise AttributeError, name

    def __setattr__(self, name, value):
        print "+++ Accessing %s in __setattr__()..." % name

        if name == "hour":
            if 0 <= value <= 23:
                self.__dict__["_hour"] = value

            else:
                raise ValueError, "Invalid %s value: %s" % (name, value)

        elif name == "minute":
            if 0 <= value <= 59:
                self.__dict__["_minute"] = value

            else:
                raise ValueError, "Invalid %s value: %s" % (name, value)

        elif name == "second":
            if 0 <= value <= 59:
                self.__dict__["_second"] = value

            else:
                raise ValueError, "Invalid %s value: %s" % (name, value)

        else:
            raise AttributeError, name

# Main

clock = Clock()

print "=== State of clock object is: %d:%d:%d." % (clock.hour, clock.minute,
                                                   clock.second)

clock.hour = 17
clock.minute = 25
clock.second = 23

print "=== State of clock object is: %d:%d:%d." % (clock.hour, clock.minute,
                                                   clock.second)

# Access object's attributes without using __getattr__().
print ">>> State of clock object is: %d:%d:%d." % (clock._hour, clock._minute,
                                                   clock._second)
Listagem 1. Exemplo de utilização de __getattr__(name) e __setattr__(name, value).

Da linha 1 à 47 é declarada a classe Clock. Entre as linhas 7 e 20, temos a declaração de __getattr__(name). Reparem que a primeira linha deste método (8) apenas imprime na tela uma indicação de que o método foi acessado. Na prática esta linha não existiria, mas eu a coloquei para que possamos analisar os acessos ao método. A seguir, dentro do mesmo método, temos um conjunto de if-elifs, usados para saber qual o atributo que deve ser retornado. Ao final, temos um else, que gera uma excessão, no caso de um nome inválido de atributo ser passado.

Entre as linhas 22 e 47 nós temos a implementação de __setattr__(name, value). Reparem que a primeira linha dele, assim como no método anterior, serve apenas para visualizarmos quando e porque o método foi acessado, não possuindo aplicação prática. A seguir, mais uma estrutura de if-elifs, para saber qual o atributo foi requisitado. Dentro de cada possibilidade é implementado o tratamento de erro, para evitar que valores errados sejam passados aos atributos. Caso o valor passado seja inválido para o atributo ou caso o nome do atributo seja não esteja previsto, uma excessão é gerada.

A seguir, temos a instanciação da classe, na linha 51. Logo depois realizamos várias operações com o objeto criado. Para melhor analizarmos, vejamos a saída gerada.
+++ Accessing hour in __setattr__()...
+++ Accessing minute in __setattr__()...
+++ Accessing second in __setattr__()...
--- Accessing hour in __getattr__()...
--- Accessing minute in __getattr__()...
--- Accessing second in __getattr__()...
=== State of clock object is: 0:0:0.
+++ Accessing hour in __setattr__()...
+++ Accessing minute in __setattr__()...
+++ Accessing second in __setattr__()...
--- Accessing hour in __getattr__()...
--- Accessing minute in __getattr__()...
--- Accessing second in __getattr__()...
=== State of clock object is: 17:25:23.
>>> State of clock object is: 17:25:23.
Listagem 2. Saída do código apresentadao na Listagem 1.

A três primeiras linhas da listagem 2 mostram houveram chamadas implícitas para o setter do objeto. Mas como? É simples: toda e qualquer alteração nos atributos do objeto que possui implementado um método __setattr__(name, value), passam por este método, mesmo que implicitamente. No caso, as três primeiras linhas da listagem 2 são geradas na instanciação do objeto, quando o construtor da classe (__init__()) é executado. Na listagem 2, as linhas 4, 5 e 6, mostram que o getter foi acessado. Isto ocorreu nas linhas 53 e 54 da listagem 1 e exemplifica uma característica interessante deste tipo de implementação.

Reparem que nas linhas 27, 34 e 41 da listagem 1, as atribuições de valores aos atributos são feitas de forma pouco comum. Em vez de indicarmos self.attribute = value, fazemos self.__dict__["_attribute"] = value. Acontece que __dict__ é um dicionário,que corresponde ao namespace do objeto. Se fizermos uma atribuição comum, o interpretador criará uma nova entrada neste dicionário, associando o atributo ao valor passado. Da maneira como fazemos, nós forçamos a criação de um atributo com nome igual ao passado no parâmetro name, mas com um underscore no início. Desta forma, parece que o nosso objeto tem os atributos hour, minute e second, quando, na verdade, os nomes destes atributos são, respectivamente, _hour, _minute e _second.

Agora, voltando às linhas 53 e 54 da primeira listagem, notem que as chamadas para os atributos indicam atributos que não existem (lembrem-se que nossos atributos foram gravados em __dict__ com underscores no início). Contudo, os nomes dos atributos são iguais àqueles previstos no parâmetro name de __getattr__(name). Sempre que o usuário de um objeto da classe tenta acessar um atributo que não está em __dict__, o interpretador executa o método __getattr__(name), passando o nome do atributo que não foi encontrado em name. Dessa forma o programador pode chamar os atributos como exemplificado nas linhas 53 e 54, com a garantia de que o getter será executado automaticamente pelo interpretador.

Nas linhas 56, 57 e 58 da listagem 1, temos valores sendo passados a atributos inexistentes na classe. De forma semelhante às linhas 53 e 54 da mesma listagem, o interpretador faz uma chamada automática ao setter da classe quando encontra uma atribuição, passando o atributo no parâmetro name e o valor atribuído no parâmetro value. Estas 3 linhas correspondem às linhas 8, 9 e 10 da listagem 2.

As linhas 60 e 61 da listagem 1 correspondem às linhas 11, 12, 13 e 14 da segunda listagem e têm funcionamento igual ao das linhas 53 e 54. Já as linhas 64 e 65 trazem uma novidade: o acesso direto aos atributos do objeto (notem os underscores no início do nome de cada atributo). Como os atributos passados constam no __dict__ do objeto, o getter não é acessado, fazendo com que os valores associados a cada um deles sejam retornados. Assim, essas linhas corresponde apenas à linha 15 da listagem 2.

A grande diferença [não óbvia] entre __getattr__(name) e __setattr__(name, value), é que o primeiro é executado apenas no caso do atributo que se tentou acessar não constar no __dict__ do objeto. Já o segundo é executado em todas as tentativas de modificação dos valores dos atributos.

Alguns podem ficar temerosos quanto à perda de desempenho dos programas que usarem classes com getters e setters como os aqui apresentados e tal preocupação é até compreensível, visto que chamadas adicionais a métodos podem aumentar o tempo de resposta do programa. Contudo, eu acredito que caso os acessos a estes atributos sejam bem definidos (i.e., prover acesso apenas a atributos que necessitarem) e a implementação destes métodos sejam bem feitas, sem código desnecessário, o programador pode conseguir uma boa relação performance x segurança.

Série Orientação a Objetos: Acessos a Atributos

Leia Também

9 comentários:

  1. Olá... eu adorei descobrir o Versão Própria... ainda mais agora que sou uma novata no Mac...

    gostei tanto que até indiquei seu blog para os novatos na área!!!!

    obrigada pelos posts valiosos!!!

    ResponderExcluir
  2. Oláaaa... obrigada pela visita ao Dois caminhos! e como eu não tenho desconfiômetro, eu vou sempre vir aqui quando tiver alguma dúvida com o Mac rsrsrs

    bom final de semana!!!

    ResponderExcluir
  3. Fala brow!!!

    To de volta, mas aquela cidade é foda viu!!!!!

    Mas vamos ao que interessa, a parte referente ao encapsulamento dos metodos set e get foi bem bacana, mas principalmente nos set quando a validação for um pouco mais abragente tem-se que ficar modolarizando o set em vários pedaços, não sei se seria legal. Agora para os métodos gets foi a melhor que já vi, e nunca tinha pensado nisso, parabéns.

    Só mais uma coisa, o método __dict__ recebe os espaços de nomes refenciados pelo módulo(me refiro ao nome como um arquivo, independente de ele ter ou não classe). Pelo que sei deste método, ao voce por exemplo, module__dict__.keys(), será apresentado suas classes que está no arquivo de nível superior do seu módulo. O mesmo imagino para o objeto que está sendo passado para o metodo __setattr__. Então pelo que entendi se usarmos o acesso convencional instanciamos um novo objeto?

    E sábado tem minha postagem para o CR não esqueci não hein.

    Abração brow, e OO e foda.

    ResponderExcluir
  4. @TwoWays: Volte sempre, hohoho!
    @maurim: Bem vindo de volta! Agradeça ao Deitel. Li sobre isso lá! ;) Cara, primeiro que __dict__ não é um método. É uma variável especial, pois é um dicionário. ;) Achei sua pergunta confusa, pois misturou de novo método com classe. Ao fazer o acesso proposto ao objeto (e.g., clock.__dict__.keys()), você terá retornada uma lista com os atributos do objeto. Se fizer isso na classe (e.g., Clock.__dict__.keys()), você terá algo como ['__module__', '__doc__', '__init__', '__getattr__', '__setattr__']. A instanciação de um objeto consiste em reservar um espaço de memória para ele e associar este espaço a uma variável, que representa o objeto. Vou te propor um teste, pra ver se você mesmo descobre o que acontece com o objeto nesses acessos. O método embutido id(), retorna o identificador de uma variável, objeto, atributo etc., que também é o endereço deste objeto na memória. Use a classe proposta nesta postagem para instanciar um objeto. Então pegue o id dele. Altere os valores e veja se o id mudou. Faça o mesmo para os atributos. Depois me dê a resposta!
    Abraço!

    ResponderExcluir
  5. @maurim: Ah! Se a validação for complicada, pode-se criar métodos auxiliares para isso e chamá-los dentro do __setattr__()! ;-)

    ResponderExcluir
  6. Fala brother!!!

    Ao meu ver é impossível desvencilhar os módulos das classes, i.e., falamos em módulos sem classe mas não em classe sem módulos, é o meu ponto de vista é claro.

    Hoje devo chegar um pouco mais tarde em ksa, quando falei no __dict__ como um método realmente me expressei errado, mas teste aceito até amanhã cedo terá resposta.

    Abração.

    ResponderExcluir
  7. Sim, sim, sim, sa-la-bim!
    Também acho interessante empacotar classes em métodos, pois promove a reutilização de código. Só que o escopo é diferente e acho que a abordagem também deve ser. Dá pra fazermos um texto sobre classes e outro sobre módulos e ligarmos características dos dois. Daí a buscar características de módulos em classes, confunde a cabeça. Isso porque uma variável de um módulo é um atributo na classe e nesta, há várias premissas, como o encapsulamento, que devem ser levadas em consideração. Também não há como instanciar um módulo e, caso uma classe resida em um módulo, não é usual (e acho que nem é possível) acessar seus atributos, como em módulo.Classe.atributo. Fica a sugestão para que você faça um texto, ou uma linha de textos semelhante a essa, para módulos. Assim, podemos discutir similaridades e aprender mais!
    Abração, bródi! ;-)

    ResponderExcluir
  8. Eu não intendi porque temque colocar isto "__" ante e depois do construtor , get , set ´..

    é algum padrão? SE EU NÃO COLOCAR O COMPILADOR VAI INTENDER TAMBEM?

    eu sei que isso vc colocar quando quer "simular" o modificador de acesso private para ATRIBUTO....

    agora para "função/metodo" tambem usar isto? não intendi!

    outra coisa,,,

    porque definir atributos dentro do construtor?
    não seria ideal declarar na classe?
    consigo acessar esses atributos que está dentro do bloco do construtor? estranho rsrs

    Valew

    ResponderExcluir