Qual é a maneira pitônica de usar getters e setters?

340

Estou fazendo assim:

def set_property(property,value):  
def get_property(property):  

ou

object.property = value  
value = object.property

Eu sou novo no Python, por isso ainda estou explorando a sintaxe e gostaria de alguns conselhos para fazer isso.

Jorge Guberte
fonte

Respostas:

684

Tente isto: Propriedade Python

O código de exemplo é:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I'm the 'x' property."""
        print("getter of x called")
        return self._x

    @x.setter
    def x(self, value):
        print("setter of x called")
        self._x = value

    @x.deleter
    def x(self):
        print("deleter of x called")
        del self._x


c = C()
c.x = 'foo'  # setter called
foo = c.x    # getter called
del c.x      # deleter called
Grissiom
fonte
2
O setter para x é chamado no inicializador ao instanciar _x?
Casey #
7
@Casey: Não. As referências a ._x(que não são uma propriedade, apenas um atributo comum) ignoram o propertyempacotamento. Somente referências para .xpassar pelo property.
ShadowRanger
273

Qual é a maneira pitônica de usar getters e setters?

O caminho "pitônico" não é usar "getters" e "setters", mas usar atributos simples, como demonstra a pergunta, e delpara excluir (mas os nomes são alterados para proteger os inocentes ... embutidos):

value = 'something'

obj.attribute = value  
value = obj.attribute
del obj.attribute

Se, posteriormente, você desejar modificar a configuração e obter, poderá fazê-lo sem precisar alterar o código do usuário, usando o comando property decorador:

class Obj:
    """property demo"""
    #
    @property            # first decorate the getter method
    def attribute(self): # This getter method name is *the* name
        return self._attribute
    #
    @attribute.setter    # the property decorates with `.setter` now
    def attribute(self, value):   # name, e.g. "attribute", is the same
        self._attribute = value   # the "value" name isn't special
    #
    @attribute.deleter     # decorate with `.deleter`
    def attribute(self):   # again, the method name is the same
        del self._attribute

(Cada uso do decorador copia e atualiza o objeto de propriedade anterior, observe que você deve usar o mesmo nome para cada conjunto, obter e excluir função / método.

Após definir o acima, a configuração original, obtendo e excluindo o código é a mesma:

obj = Obj()
obj.attribute = value  
the_value = obj.attribute
del obj.attribute

Você deve evitar isso:

def set_property(property,value):  
def get_property(property):  

Em primeiro lugar, o acima não funciona, porque você não fornece um argumento para a instância em que a propriedade seria configurada para (geralmente self), que seria:

class Obj:

    def set_property(self, property, value): # don't do this
        ...
    def get_property(self, property):        # don't do this either
        ...

Em segundo lugar, isso duplica o objetivo de dois métodos especiais, __setattr__e __getattr__.

Em terceiro lugar, também temos as funções setattre getattrbuiltin.

setattr(object, 'property_name', value)
getattr(object, 'property_name', default_value)  # default is optional

O @propertydecorador é para criar getters e setters.

Por exemplo, podemos modificar o comportamento da configuração para colocar restrições no valor que está sendo definido:

class Protective(object):

    @property
    def protected_value(self):
        return self._protected_value

    @protected_value.setter
    def protected_value(self, value):
        if acceptable(value): # e.g. type or range check
            self._protected_value = value

Em geral, queremos evitar o uso de property e apenas usar atributos diretos.

É o que se espera dos usuários de Python. Seguindo a regra da menor surpresa, você deve tentar dar a seus usuários o que eles esperam, a menos que você tenha uma razão muito convincente para o contrário.

Demonstração

Por exemplo, digamos que precisávamos que o atributo protegido do nosso objeto fosse um número inteiro entre 0 e 100, inclusive, e evite sua exclusão, com mensagens apropriadas para informar o usuário sobre o uso adequado:

class Protective(object):
    """protected property demo"""
    #
    def __init__(self, start_protected_value=0):
        self.protected_value = start_protected_value
    # 
    @property
    def protected_value(self):
        return self._protected_value
    #
    @protected_value.setter
    def protected_value(self, value):
        if value != int(value):
            raise TypeError("protected_value must be an integer")
        if 0 <= value <= 100:
            self._protected_value = int(value)
        else:
            raise ValueError("protected_value must be " +
                             "between 0 and 100 inclusive")
    #
    @protected_value.deleter
    def protected_value(self):
        raise AttributeError("do not delete, protected_value can be set to 0")

(Observe que __init__se refere a, self.protected_valuemas os métodos de propriedade se referem self._protected_value. Isso é feito para __init__usar a propriedade por meio da API pública, garantindo que ela esteja "protegida".)

E uso:

>>> p1 = Protective(3)
>>> p1.protected_value
3
>>> p1 = Protective(5.0)
>>> p1.protected_value
5
>>> p2 = Protective(-5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> p1.protected_value = 7.3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 17, in protected_value
TypeError: protected_value must be an integer
>>> p1.protected_value = 101
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 15, in protected_value
ValueError: protectected_value must be between 0 and 100 inclusive
>>> del p1.protected_value
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in protected_value
AttributeError: do not delete, protected_value can be set to 0

Os nomes importam?

Sim eles fazem . .settere .deleterfaça cópias da propriedade original. Isso permite que as subclasses modifiquem adequadamente o comportamento sem alterar o comportamento no pai.

class Obj:
    """property demo"""
    #
    @property
    def get_only(self):
        return self._attribute
    #
    @get_only.setter
    def get_or_set(self, value):
        self._attribute = value
    #
    @get_or_set.deleter
    def get_set_or_delete(self):
        del self._attribute

Agora, para que isso funcione, você deve usar os respectivos nomes:

obj = Obj()
# obj.get_only = 'value' # would error
obj.get_or_set = 'value'  
obj.get_set_or_delete = 'new value'
the_value = obj.get_only
del obj.get_set_or_delete
# del obj.get_or_set # would error

Não tenho certeza de onde isso seria útil, mas o caso de uso é se você deseja uma propriedade get, set e / ou delete-only. Provavelmente é melhor manter semanticamente a mesma propriedade com o mesmo nome.

Conclusão

Comece com atributos simples.

Se, posteriormente, você precisar de funcionalidade em torno da configuração, obtenção e exclusão, adicione-a com o decorador de propriedades.

Evite funções nomeadas set_...e get_...- é para isso que servem as propriedades.

Aaron Hall
fonte
Além de duplicar a funcionalidade disponível, por que evitar seus próprios setters e getters? Entendo que pode não ser o caminho pitonico, mas existem problemas realmente sérios que alguém pode encontrar de outra maneira?
user1350191
4
Na sua demonstração, o __init__método se refere a, self.protected_valuemas o getter e setter se referem self._protected_value. Poderia explicar como isso funciona? Testei seu código e ele funciona como está - portanto, não é um erro de digitação.
Code283
2
@codeforester Eu esperava responder na minha resposta mais cedo, mas até que eu possa, esse comentário deve ser suficiente. Espero que você possa ver que ele usa a propriedade através da API pública, garantindo que ela seja "protegida". Não faria sentido "protegê-lo" com uma propriedade e, em vez disso, use a API não pública na __init__mesma?
Aaron Hall
2
Sim, @AaronHall conseguiu agora. Eu não sabia que self.protected_value = start_protected_valueestava realmente chamando a função setter; Eu pensei que era uma tarefa.
Codeforester 01/08/19
11
imho esta deve ser a resposta aceita, se eu entendi corretamente python leva exatamente o ponto oposto em comparação com, por exemplo, java. Em vez de fazer tudo privados por padrão e escrever algum código extra quando for necessário publicamente em python, você pode fazer tudo público e adicionar privacidade mais tarde
idclev 463035818
27
In [1]: class test(object):
    def __init__(self):
        self.pants = 'pants'
    @property
    def p(self):
        return self.pants
    @p.setter
    def p(self, value):
        self.pants = value * 2
   ....: 
In [2]: t = test()
In [3]: t.p
Out[3]: 'pants'
In [4]: t.p = 10
In [5]: t.p
Out[5]: 20
Autoplectic
fonte
17

O uso @propertye @attribute.setterajuda você a não apenas usar a maneira "pitônica", mas também para verificar a validade dos atributos ao criar o objeto e ao alterá-lo.

class Person(object):
    def __init__(self, p_name=None):
        self.name = p_name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        if type(new_name) == str: #type checking for name property
            self._name = new_name
        else:
            raise Exception("Invalid value for name")

Com isso, você realmente 'esconde' o _nameatributo dos desenvolvedores do cliente e também realiza verificações no tipo de propriedade do nome. Observe que, seguindo esta abordagem, mesmo durante a iniciação, o setter é chamado. Assim:

p = Person(12)

Levará a:

Exception: Invalid value for name

Mas:

>>>p = person('Mike')
>>>print(p.name)
Mike
>>>p.name = 'George'
>>>print(p.name)
George
>>>p.name = 2.3 # Causes an exception
Farzad Vertigo
fonte
16

Confira o @propertydecorador .

Kevin Little
fonte
33
Esta é praticamente uma resposta apenas de link.
Aaron Hall
6
@AaronHall: Não é, no entanto. "Retire a marcação e você ainda terá pelo menos um pouco de informação útil."
Jean-François Corbett
7
Como isso é uma resposta completa? Um link não é uma resposta.
Andy_A̷n̷d̷y̷ 22/08
Eu acho que é uma boa resposta, porque a documentação indica claramente como usá-la (e permanecerá atualizada se a implementação do python mudar, e direciona o OP para o método sugerido pelo respondente. @ Jean-FrançoisCorbett afirmou claramente 'como' é uma resposta completa.
HunnyBear 30/10/19
De qualquer forma, essa resposta não adiciona nada útil a outras respostas e, idealmente, deve ser excluída.
Georgy
5

Você pode usar acessadores / mutadores (ie @attr.settere @property) ou não, mas o mais importante é ser consistente!

Se você estiver usando @propertypara simplesmente acessar um atributo, por exemplo,

class myClass:
    def __init__(a):
        self._a = a

    @property
    def a(self):
        return self._a

use-o para acessar todos os * atributos! Seria uma má prática acessar alguns atributos usando @propertye deixar outras propriedades públicas (ou seja, nome sem sublinhado) sem um acessador, por exemplo , não

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b

    @property
    def a(self):
        return self.a

Observe que self.bnão há um acessador explícito aqui, mesmo que seja público.

Da mesma forma com os levantadores (ou mutadores ), fique à vontade para usar, @attribute.settermas seja consistente! Quando você faz, por exemplo

class myClass:
    def __init__(a, b):
        self.a = a
        self.b = b 

    @a.setter
    def a(self, value):
        return self.a = value

É difícil para mim adivinhar sua intenção. Por um lado, você está dizendo que ambos ae bsão públicos (sem sublinhado principal em seus nomes); portanto, teoricamente eu deveria ter permissão para acessar / alterar (obter / definir) ambos. Mas então você especifica apenas um mutador explícito para a, o que me diz que talvez eu não deva ser capaz de definir b. Como você forneceu um mutador explícito, não tenho certeza se a falta de accessor explícito ( @property) significa que eu não deveria poder acessar nenhuma dessas variáveis ​​ou você estava simplesmente sendo frugal em usá-lo @property.

* A exceção é quando você deseja explicitamente tornar algumas variáveis ​​acessíveis ou mutáveis, mas não as duas, ou deseja executar alguma lógica adicional ao acessar ou alterar um atributo. É quando estou usando pessoalmente @propertye @attribute.setter(caso contrário, não há acessores / mutadores explícitos para atributos públicos).

Por fim, as sugestões do PEP8 e do Guia de Estilo do Google:

PEP8, Designing for Herança, diz:

Para atributos simples de dados públicos, é melhor expor apenas o nome do atributo, sem métodos complicados de acessador / mutador . Lembre-se de que o Python fornece um caminho fácil para aprimoramentos futuros, caso você descubra que um atributo de dados simples precisa aumentar o comportamento funcional. Nesse caso, use propriedades para ocultar a implementação funcional atrás da sintaxe de acesso a atributos de dados simples.

Por outro lado, de acordo com as Regras / Propriedades da linguagem Python do Guia de Estilo do Google, a recomendação é:

Use propriedades no novo código para acessar ou definir dados nos quais você normalmente usaria métodos simples e leves de acessador ou configurador. As propriedades devem ser criadas com o @propertydecorador.

Os profissionais dessa abordagem:

A legibilidade é aumentada ao eliminar chamadas explícitas de método get e set para acesso simples a atributos. Permite que os cálculos sejam preguiçosos. Considerada a maneira Pythonic de manter a interface de uma classe. Em termos de desempenho, permitir propriedades ignora a necessidade de métodos acessadores triviais quando um acesso direto a variável é razoável. Isso também permite que métodos acessadores sejam adicionados no futuro sem interromper a interface.

e contras:

Deve herdar do objectPython 2. Pode ocultar efeitos colaterais como sobrecarga do operador. Pode ser confuso para subclasses.

Tomasz Bartkowiak
fonte
11
Eu discordo fortemente. Se eu tenho 15 atributos no meu objeto e quero que um seja computado @property, fazer o resto também usar @propertyparece uma péssima decisão.
Quelklef 13/01
Concorde, mas somente se houver algo específico sobre esse atributo específico que você precisa @property(por exemplo, executar alguma lógica especial antes de retornar um atributo). Caso contrário, por que você decoraria um atributo com @properye não outros?
Tomasz Bartkowiak 13/01
@Quelklef veja a nota lateral na postagem (marcada com asterisco).
Tomasz Bartkowiak
Bem ... Se você não está fazendo uma das coisas mencionadas na nota @propertylateral, não deveria estar usando para começar, certo? Se o seu getter é return this._xe seu setter é this._x = new_x, então usar @propertyé meio bobo.
Quelklef 13/01
11
Hmm, talvez. Pessoalmente, eu diria que não está bem - é totalmente supérfluo. Mas eu posso ver de onde você está vindo. Acho que acabei de ler sua postagem dizendo que "a coisa mais importante ao usar @propertyé ser consistente".
Quelklef 14/01
-1

Você pode usar os métodos mágicos __getattribute__e __setattr__.

class MyClass:
    def __init__(self, attrvalue):
        self.myattr = attrvalue
    def __getattribute__(self, attr):
        if attr == "myattr":
            #Getter for myattr
    def __setattr__(self, attr):
        if attr == "myattr":
            #Setter for myattr

Esteja ciente de que __getattr__e __getattribute__não é o mesmo. __getattr__é invocado apenas quando o atributo não é encontrado.

Pika, o Mago das Baleias
fonte