Usando @property versus getters e setters

727

Aqui está uma pergunta de design puro e específica para Python:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

e

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

Python nos permite fazê-lo de qualquer maneira. Se você projetasse um programa Python, qual abordagem você usaria e por quê?

Zaur Nasibov
fonte

Respostas:

613

Prefere propriedades . É para isso que eles estão lá.

O motivo é que todos os atributos são públicos no Python. Iniciar nomes com um sublinhado ou dois é apenas um aviso de que o atributo fornecido é um detalhe de implementação que pode não permanecer o mesmo em versões futuras do código. Isso não impede que você realmente obtenha ou defina esse atributo. Portanto, o acesso a atributos padrão é a maneira Pythonic normal de, bem, acessar atributos.

A vantagem das propriedades é que elas são sintaticamente idênticas ao acesso ao atributo, para que você possa mudar de uma para outra sem nenhuma alteração no código do cliente. Você pode até ter uma versão de uma classe que usa propriedades (por exemplo, código por contrato ou depuração) e uma que não usa para produção, sem alterar o código que a usa. Ao mesmo tempo, você não precisa escrever getters e setters para tudo, caso precise controlar melhor o acesso posteriormente.

kindall
fonte
90
Os nomes de atributo com um sublinhado duplo são tratados especialmente pelo Python; não é apenas uma mera convenção. Consulte docs.python.org/py3k/tutorial/classes.html#private-variables
6502
63
Eles são tratados de maneira diferente, mas isso não impede que você os acesse. PS: AD 30 C0
tipo
4
e como os caracteres "@" são feios no código python, e o @decorators que dispensa a referência dá a mesma sensação que o código espaguete.
Berry Tsakala
18
Eu não concordo Como o código estruturado é igual ao código de espaguete? Python é uma linguagem bonita. Mas seria ainda melhor com melhor suporte para coisas simples, como encapsulamento adequado e classes estruturadas.
69
Embora eu concorde na maioria dos casos, tenha cuidado ao ocultar métodos lentos por trás de um decorador @property. O usuário da sua API espera que o acesso à propriedade funcione como acesso variável, e se afastar muito dessa expectativa pode tornar sua API desagradável de usar.
Defrex
153

No Python, você não usa getters, setters ou propriedades apenas para se divertir. Você primeiro usa atributos e, posteriormente, somente se necessário, eventualmente migra para uma propriedade sem precisar alterar o código usando suas classes.

De fato, há muito código com a extensão .py que usa getters e setters e herança e classes inúteis em todos os lugares, por exemplo, como uma tupla simples faria, mas é código de pessoas que escrevem em C ++ ou Java usando Python.

Esse não é o código Python.

6502
fonte
46
@ 6502, quando você disse “[...] classes sem sentido em todos os lugares onde, por exemplo, uma tupla simples faria”: a vantagem de uma classe sobre uma tupla é que uma instância de classe fornece nomes explícitos para acessar suas partes, enquanto uma tupla não . Os nomes são melhores em legibilidade e evitar erros, do que a assinatura de tuplas, especialmente quando isso deve ser passado fora do módulo atual.
Hibou57
15
@ Hibou57: Não estou dizendo que a aula é inútil. Mas às vezes uma tupla é mais que suficiente. O problema, porém, é que quem vem, digamos, que Java ou C ++ não tem escolha a não ser criar classes para tudo, porque outras possibilidades são apenas irritantes para serem usadas nesses idiomas. Outro sintoma típico da programação Java / C ++ usando o Python é a criação de classes abstratas e hierarquias de classes complexas sem nenhuma razão em que, no Python, você poderia simplesmente usar classes independentes, graças à digitação do duck.
6502
39
@ Hibou57 para que u também pode usar namedtuple: doughellmann.com/PyMOTW/collections/namedtuple.html
hugo24
5
@JonathonReinhart: ela é na biblioteca padrão desde 2.6 ... veja docs.python.org/2/library/collections.html
6502
1
Quando "eventualmente migrando para uma propriedade, se necessário", é bem provável que você quebre o código usando suas classes. As propriedades geralmente introduzem restrições - qualquer código que não esperava essas restrições será quebrado assim que você as introduzir.
Yaccob
118

O uso de propriedades permite iniciar com acessos normais a atributos e, em seguida, fazer backup deles com getters e setters posteriormente, conforme necessário .

Ignacio Vazquez-Abrams
fonte
3
@GregKrsak Parece estranho porque é. O "consentimento dos adultos" era um meme python antes de as propriedades serem adicionadas. Foi a resposta das pessoas que reclamaram da falta de modificadores de acesso. Quando as propriedades foram adicionadas, o encapsulamento repentino se tornou desejável. O mesmo aconteceu com as classes base abstratas. "Python estava sempre em guerra com a quebra de encapsulamento. Liberdade é escravidão. Lambdas deve caber apenas em uma linha."
johncip
71

A resposta curta é: as propriedades vencem as mãos . Sempre.

Às vezes há a necessidade de caçadores e caçadores, mas mesmo assim eu os "esconderia" para o mundo exterior. Há muitas maneiras de fazer isso em Python ( getattr, setattr, __getattribute__, etc ..., mas um muito conciso e limpo é:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Aqui está um breve artigo que apresenta o tópico de getters e setters no Python.

Mac
fonte
1
@BasicWolf - eu pensei que era implicitamente claro que estou do lado da propriedade da cerca! :) Mas eu adiciono um parágrafo à minha resposta para esclarecer isso.
mac
9
DICA: A palavra "sempre" é uma dica de que o autor está tentando convencê-lo com uma afirmação, não um argumento. Assim é a presença da fonte em negrito. (Quero dizer, se você vê o CAPS, então - uau - deve estar certo.) Veja, o recurso "property" é diferente do Java (o inimigo de fato do Python por algum motivo) e, portanto, o pensamento de grupo da comunidade do Python declara que é melhor. Na realidade, as propriedades violam a regra "Explícito é melhor que implícito", mas ninguém deseja admiti-lo. Ele chegou ao idioma e agora é declarado "pitônico" por meio de um argumento tautológico.
Stuart Berg
3
Nenhum sentimento machuca. :-P Estou apenas tentando salientar que as convenções "Pythonic" são inconsistentes neste caso: "Explícito é melhor do que implícito" está em conflito direto com o uso de a property. ( Parece uma tarefa simples, mas chama uma função.) Portanto, "pitônico" é essencialmente um termo sem sentido, exceto pela definição tautológica: "convenções pitônicas são coisas que definimos como pitônicas".
Stuart Berg
1
Agora, a ideia de ter um conjunto de convenções que seguem um tema é ótima . Se esse conjunto de convenções existisse, você poderia usá-lo como um conjunto de axiomas para orientar seu pensamento, não apenas uma longa lista de truques para memorizar, o que é significativamente menos útil. Axiomas podem ser usados ​​para extrapolação e ajudar a abordar problemas que ninguém viu ainda. É uma pena que o propertyrecurso ameace tornar quase inútil a idéia de axiomas pitônicos. Então, tudo o que resta é uma lista de verificação.
Stuart Berg
1
Eu não concordo Prefiro propriedades na maioria das situações, mas quando você deseja enfatizar que definir algo tem efeitos colaterais que não sejam modificar o selfobjeto , setters explícitos podem ser úteis. Por exemplo, user.email = "..."não parece que isso possa gerar uma exceção, pois apenas define um atributo, enquanto user.set_email("...")deixa claro que pode haver efeitos colaterais, como exceções.
bluenote10
65

[ TL; DR? Você pode pular para o final para obter um exemplo de código .]

Na verdade, prefiro usar um idioma diferente, que é um pouco complicado para ser usado como um exemplo, mas é bom se você tiver um caso de uso mais complexo.

Um pouco de fundo primeiro.

As propriedades são úteis, pois nos permitem lidar com a configuração e a obtenção de valores de maneira programática, mas ainda permitem que os atributos sejam acessados ​​como atributos. Podemos transformar 'recebe' em 'cálculos' (essencialmente) e podemos transformar 'conjuntos' em 'eventos'. Então, digamos que temos a seguinte classe, que eu codifiquei com getters e setters do tipo Java.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

    def setX(self, x):
        self.x = x

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

Você pode estar se perguntando por que eu não liguei defaultXe defaultYno __init__método do objeto . O motivo é que, para o nosso caso, quero assumir que os someDefaultComputationmétodos retornam valores que variam ao longo do tempo, digamos um carimbo de data e hora, e sempre que x(ou y) não estiver definido (onde, para os fins deste exemplo, "não definido" significa "definido" para Nenhum ") Quero o valor do cálculo padrão de x's (ou y' s).

Portanto, isso é ruim por várias razões descritas acima. Vou reescrevê-lo usando as propriedades:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

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

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

O que ganhamos? Adquirimos a capacidade de nos referir a esses atributos como atributos, embora, nos bastidores, acabemos executando métodos.

Obviamente, o verdadeiro poder das propriedades é que geralmente queremos que esses métodos façam algo além de apenas obter e definir valores (caso contrário, não faz sentido usar propriedades). Eu fiz isso no meu exemplo getter. Basicamente, estamos executando um corpo de função para selecionar um padrão sempre que o valor não estiver definido. Este é um padrão muito comum.

Mas o que estamos perdendo e o que não podemos fazer?

O principal incômodo, na minha opinião, é que, se você definir um getter (como fazemos aqui), também precisará definir um setter. [1] Esse ruído extra atrapalha o código.

Outro incômodo é que ainda temos para inicializar o xe yvalores __init__. (Bem, é claro que poderíamos adicioná-los usando, setattr()mas isso é mais código extra.)

Terceiro, diferente do exemplo do Java, os getters não podem aceitar outros parâmetros. Agora já posso ouvi-lo dizendo, bem, se está aceitando parâmetros, não é fácil! Em um sentido oficial, isso é verdade. Mas, no sentido prático, não há razão para não podermos parametrizar um atributo nomeado - como x- e definir seu valor para alguns parâmetros específicos.

Seria bom se pudéssemos fazer algo como:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

por exemplo. O mais próximo que podemos chegar é substituir a tarefa para implicar em uma semântica especial:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

e, é claro, garanta que nosso criador saiba como extrair os três primeiros valores como chave de um dicionário e defina seu valor como um número ou algo assim.

Mas mesmo se fizéssemos isso, ainda não poderíamos suportá-lo com propriedades porque não há como obter o valor, porque não podemos passar parâmetros para o getter. Então tivemos que devolver tudo, introduzindo uma assimetria.

O getter / setter no estilo Java nos permite lidar com isso, mas voltamos a precisar de getter / setters.

Na minha opinião, o que realmente queremos é algo que capture os seguintes requisitos:

  • Os usuários definem apenas um método para um determinado atributo e podem indicar se o atributo é somente leitura ou leitura / gravação. As propriedades falham neste teste se o atributo for gravável.

  • Não é necessário que o usuário defina uma variável extra subjacente à função; portanto, não precisamos do código __init__nem setattrno código. A variável existe apenas pelo fato de termos criado esse atributo de novo estilo.

  • Qualquer código padrão para o atributo é executado no próprio corpo do método.

  • Podemos definir o atributo como um atributo e referenciá-lo como um atributo.

  • Nós podemos parametrizar o atributo.

Em termos de código, queremos uma maneira de escrever:

def x(self, *args):
    return defaultX()

e ser capaz de:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

e assim por diante.

Também queremos uma maneira de fazer isso para o caso especial de um atributo parametrizável, mas ainda permitimos que o caso de atribuição padrão funcione. Você verá como lidei com isso abaixo.

Agora ao ponto (yay! O ponto!). A solução que eu vim para isso é a seguinte.

Criamos um novo objeto para substituir a noção de propriedade. O objetivo do objeto é armazenar o valor de uma variável definida, mas também mantém um identificador no código que sabe como calcular um padrão. Seu trabalho é armazenar o conjunto valueou executar o methodse esse valor não estiver definido.

Vamos chamá-lo de UberProperty.

class UberProperty(object):

    def __init__(self, method):
        self.method = method
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

Suponho que methodaqui seja um método de classe, valueseja o valor de UberProperty, e eu adicionei isSetporque Nonepode ser um valor real e isso nos permite uma maneira limpa de declarar que realmente não há "nenhum valor". Outra maneira é um sentinela de algum tipo.

Isso basicamente nos dá um objeto que pode fazer o que queremos, mas como realmente o colocamos em nossa classe? Bem, as propriedades usam decoradores; por que não podemos? Vamos ver como pode parecer (daqui em diante vou usar apenas um 'atributo' x).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

Isso ainda não funciona, é claro. Temos que implementar uberPropertye garantir que ele lide com os conjuntos e conjuntos.

Vamos começar com gets.

Minha primeira tentativa foi simplesmente criar um novo objeto UberProperty e devolvê-lo:

def uberProperty(f):
    return UberProperty(f)

Eu rapidamente descobri, é claro, que isso não funciona: o Python nunca vincula a chamada ao objeto e eu preciso do objeto para chamar a função. Mesmo criar o decorador na classe não funciona, pois, embora agora tenhamos a classe, ainda não temos um objeto para trabalhar.

Então, precisamos fazer mais aqui. Sabemos que um método só precisa ser representado uma vez, então vamos continuar e manter nosso decorador, mas modifique UberPropertypara armazenar apenas a methodreferência:

class UberProperty(object):

    def __init__(self, method):
        self.method = method

Também não é exigível, então, no momento, nada está funcionando.

Como completamos a imagem? Bem, com o que terminamos quando criamos a classe de exemplo usando nosso novo decorador:

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

nos dois casos, recebemos de volta o UberPropertyque, obviamente, não é passível de chamada, portanto, isso não é de muita utilidade.

O que precisamos é de uma maneira de vincular dinamicamente a UberPropertyinstância criada pelo decorador após a classe ter sido criada para um objeto da classe antes que esse objeto tenha sido retornado ao usuário para uso. Hum, sim, é uma __init__ligação, cara.

Vamos escrever o que queremos que nosso resultado seja primeiro. Como vinculamos um UberPropertya uma instância, uma coisa óbvia a ser retornada seria um BoundUberProperty. É aqui que manteremos o estado do xatributo.

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

Agora nós a representação; como obtê-los em um objeto? Existem algumas abordagens, mas a mais fácil de explicar apenas usa o __init__método para fazer esse mapeamento. No momento em que __init__é chamado de nossos decoradores ter executado, então só precisa olhar através do objeto de __dict__e atualizar quaisquer atributos onde o valor do atributo é do tipo UberProperty.

Agora, as uber-properties são legais e provavelmente queremos usá-las muito, por isso faz sentido criar apenas uma classe base que faça isso para todas as subclasses. Eu acho que você sabe como a classe base será chamada.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

Adicionamos isso, alteramos nosso exemplo para herdar de UberObjecte ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

Após modificar xpara ser:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

Podemos executar um teste simples:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

E obtemos a saída que queríamos:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Nossa, estou trabalhando até tarde.)

Note que eu usei getValue, setValuee clearValueaqui. Isso ocorre porque ainda não vinculei os meios para que eles sejam retornados automaticamente.

Mas acho que este é um bom lugar para parar por enquanto, porque estou cansado. Você também pode ver que a funcionalidade principal que queríamos está em vigor; o resto é vitrine. Importante vestir a janela de usabilidade, mas isso pode esperar até que eu tenha uma alteração para atualizar a postagem.

Terminarei o exemplo na próxima postagem abordando estas coisas:

  • Precisamos garantir que o UberObject __init__seja sempre chamado por subclasses.

    • Portanto, ou forçamos que ele seja chamado em algum lugar ou impedimos que seja implementado.
    • Vamos ver como fazer isso com uma metaclasse.
  • Precisamos garantir que lidemos com o caso comum em que alguém 'aliases' uma função para outra coisa, como:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Precisamos e.xretornar e.x.getValue()por padrão.

    • O que realmente veremos é que essa é uma área em que o modelo falha.
    • Acontece que sempre precisamos usar uma chamada de função para obter o valor.
    • Mas podemos fazer com que pareça uma chamada de função regular e evitar ter que usar e.x.getValue(). (Fazer este é óbvio, se você ainda não o tiver consertado.)
  • Precisamos apoiar a configuração e.x directly, como em e.x = <newvalue>. Também podemos fazer isso na classe pai, mas precisaremos atualizar nosso __init__código para lidar com isso.

  • Por fim, adicionaremos atributos parametrizados. Deve ser bastante óbvio como faremos isso também.

Aqui está o código que existe até agora:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

[1] Eu posso ficar para trás se esse ainda é o caso.

Adam Donahue
fonte
53
Sim, isso é 'tldr'. Você pode resumir o que está tentando fazer aqui?
poolieby
9
@ Adam, return self.x or self.defaultX()este é um código perigoso. O que acontece quando self.x == 0?
Kelly Thomas
Para sua informação, você pode fazer isso para poder parametrizar o getter, mais ou menos. Isso envolveria tornar a variável uma classe personalizada, da qual você substituiu o __getitem__método. Seria estranho, pois você teria um python completamente fora do padrão.
vai
2
@KellyThomas Apenas tentando manter o exemplo simples. Para fazer isso corretamente, você precisa criar e excluir a entrada x dict completamente, porque mesmo um valor None pode ter sido definido especificamente. Mas sim, você está absolutamente certo de que isso é algo que você precisaria considerar em um caso de uso de produção.
Adam Donahue
Os getters do tipo Java permitem que você faça exatamente o mesmo cálculo, não é?
qed
26

Eu acho que ambos têm o seu lugar. Um problema com o uso @propertyé que é difícil estender o comportamento de getters ou setters em subclasses usando mecanismos de classe padrão. O problema é que as funções getter / setter reais estão ocultas na propriedade.

Você pode realmente se apossar das funções, por exemplo, com

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

você pode acessar as funções getter e setter como C.p.fgete C.p.fset, mas não pode usar facilmente os recursos normais de herança de método (por exemplo, super) para estendê-los. Após algumas investigações sobre os meandros do super, você pode realmente usar o super desta maneira:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

Usar super () é, no entanto, bastante desajeitado, pois a propriedade precisa ser redefinida e você deve usar o mecanismo super (cls, cls) um pouco contra-intuitivo para obter uma cópia não acoplada de p.

NeilenMarais
fonte
20

Usar propriedades é para mim mais intuitivo e se encaixa melhor na maioria dos códigos.

Comparando

o.x = 5
ox = o.x

vs.

o.setX(5)
ox = o.getX()

é óbvio para mim que é mais fácil de ler. Também as propriedades permitem variáveis ​​privadas muito mais fáceis.

Hobblin
fonte
12

Eu preferiria usar nenhum dos dois na maioria dos casos. O problema com as propriedades é que elas tornam a classe menos transparente. Especialmente, esse é um problema se você criar uma exceção de um levantador. Por exemplo, se você tiver uma propriedade Account.email:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

o usuário da classe não espera que atribuir um valor à propriedade possa causar uma exceção:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

Como resultado, a exceção pode não ser tratada e se propagar muito alto na cadeia de chamadas para ser tratada adequadamente ou resultar em um retorno de retorno muito inútil sendo apresentado ao usuário do programa (o que é muito comum no mundo de python e java )

Também evitaria o uso de getters e setters:

  • porque defini-las para todas as propriedades antecipadamente consome muito tempo,
  • torna a quantidade de código desnecessariamente mais longa, o que dificulta a compreensão e a manutenção do código,
  • se você os definisse para propriedades apenas conforme necessário, a interface da classe mudaria, prejudicando todos os usuários da classe

Em vez de propriedades e getters / setters, prefiro fazer a lógica complexa em locais bem definidos, como em um método de validação:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

ou um método semelhante Account.save.

Observe que não estou tentando dizer que não há casos em que as propriedades são úteis, apenas que você pode estar melhor se puder tornar suas aulas simples e transparentes o suficiente para que você não precise delas.


fonte
3
@ user2239734 Acho que você entendeu errado o conceito de propriedades. Embora você possa validar o valor ao definir a propriedade, não há necessidade de fazê-lo. Você pode ter propriedades e um validate()método em uma classe. Uma propriedade é simplesmente usada quando você tem uma lógica complexa por trás de uma obj.x = yatribuição simples , e depende da lógica.
Zaur Nasibov 03/04
12

Eu sinto que as propriedades são sobre permitir que você obtenha a sobrecarga de escrever getters e setters somente quando você realmente precisa deles.

A cultura de programação em Java aconselha vivamente que nunca dê acesso a propriedades e, em vez disso, passe por getters e setters, e somente aqueles que são realmente necessários. É um pouco detalhado escrever sempre esses trechos óbvios de código e observe que 70% das vezes eles nunca são substituídos por uma lógica não trivial.

No Python, as pessoas realmente cuidam desse tipo de sobrecarga, para que você possa adotar a seguinte prática:

  • Não use getters e setters no início, quando se não forem necessários
  • Use @propertypara implementá-los sem alterar a sintaxe do restante do seu código.
fulmicoton
fonte
1
"e observe que 70% das vezes eles nunca são substituídos por alguma lógica não trivial". - esse é um número bastante específico, é proveniente de algum lugar ou você está pretendendo que seja uma espécie de "a grande maioria", ondulada (não estou sendo ridícula, se houver um estudo que quantifique esse número, eu estaria genuinamente interessado em lê-lo)
Adam Parkin
1
Oh não, desculpe. Parece que eu tenho alguns estudos para fazer backup desse número, mas eu só quis dizer isso como "na maioria das vezes".
Fulmicoton
7
Não é que as pessoas se importem com a sobrecarga, é que, no Python, você pode alterar o acesso direto aos métodos de acessador sem alterar o código do cliente, para que você não tenha nada a perder expondo diretamente as propriedades primeiro.
Neil G
10

Estou surpreso que ninguém tenha mencionado que propriedades são métodos vinculados de uma classe de descritor, Adam Donohue e NeilenMarais têm exatamente essa ideia em seus posts - que getters e setters são funções e podem ser usados ​​para:

  • validar
  • alterar dados
  • tipo de pato (coagir o tipo para outro tipo)

Isso apresenta uma maneira inteligente de ocultar os detalhes da implementação e a quantidade de códigos, como expressão regular, digitar lançamentos, tentar ... exceto blocos, asserções ou valores computados.

Em geral, fazer CRUD em um objeto pode ser bastante comum, mas considere o exemplo de dados que serão persistidos em um banco de dados relacional. Os ORMs podem ocultar detalhes de implementação de vernáculos SQL específicos nos métodos vinculados a fget, fset, fdel definidos em uma classe de propriedade que gerenciará as terríveis escadas se .. elif .. else que são tão feias no código OO - expondo as simples e as elegante self.variable = somethinge obviar os detalhes para o desenvolvedor usando o ORM.

Se alguém considera as propriedades apenas como um vestígio sombrio de uma linguagem Bondage and Discipline (isto é, Java), elas estão perdendo o objetivo dos descritores.

sincero
fonte
6

Em projetos complexos, prefiro usar propriedades somente leitura (ou getters) com função explícita de setter:

class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

Em projetos de longa duração, a depuração e refatoração leva mais tempo do que a escrita do próprio código. Existem várias desvantagens no uso @property.setterque tornam a depuração ainda mais difícil:

1) python permite criar novos atributos para um objeto existente. Isso dificulta o rastreamento de uma impressão incorreta a seguir:

my_object.my_atttr = 4.

Se o seu objeto for um algoritmo complicado, você passará algum tempo tentando descobrir por que ele não converge (observe um 't' extra na linha acima)

2) o setter às vezes pode evoluir para um método complicado e lento (por exemplo, acessar um banco de dados). Seria muito difícil para outro desenvolvedor descobrir por que a função a seguir é muito lenta. Ele pode gastar muito tempo no do_something()método de criação de perfil , enquanto my_object.my_attr = 4.na verdade é a causa do abrandamento:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()
otognan
fonte
5

Ambos, @propertygetters e setters tradicionais, têm suas vantagens. Depende do seu caso de uso.

Vantagens de @property

  • Você não precisa alterar a interface enquanto altera a implementação do acesso a dados. Quando seu projeto é pequeno, você provavelmente deseja usar o acesso direto a atributos para acessar um membro da classe. Por exemplo, digamos que você tenha um objeto foodo tipo Foo, que tenha um membro num. Então você pode simplesmente obter esse membro num = foo.num. À medida que o seu projeto cresce, você pode sentir que precisa haver algumas verificações ou depurações no acesso ao atributo simples. Então você pode fazer isso com um @property dentro da classe. A interface de acesso a dados permanece a mesma para que não seja necessário modificar o código do cliente.

    Citado em PEP-8 :

    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.

  • Usando @property para acesso a dados em Python é considerado Pythonic :

    • Ele pode fortalecer sua auto-identificação como programador Python (não Java).

    • Pode ajudar sua entrevista de emprego se o entrevistador considerar que os getters e setters no estilo Java são anti-padrões .

Vantagens dos getters e setters tradicionais

  • Os getters e setters tradicionais permitem um acesso a dados mais complicado do que o simples acesso a atributos. Por exemplo, quando você está configurando um membro da classe, às vezes você precisa de um sinalizador indicando onde deseja forçar essa operação, mesmo que algo não pareça perfeito. Embora não seja óbvio como aumentar o acesso direto a um membro foo.num = num, você pode aumentar facilmente seu setter tradicional com um forceparâmetro adicional :

    def Foo:
        def set_num(self, num, force=False):
            ...
  • Os getters e setters tradicionais tornam explícito que o acesso de um membro da classe é feito através de um método. Isso significa:

    • O que você obtém como resultado pode não ser o mesmo que é exatamente armazenado nessa classe.

    • Mesmo que o acesso pareça um acesso simples a atributos, o desempenho pode variar bastante disso.

    A menos que os usuários de sua classe esperem uma @property esconder atrás de cada declaração de acesso a atributos, tornar essas coisas explícitas pode ajudar a minimizar as surpresas dos usuários de sua classe.

  • Como mencionado por @NeilenMarais e neste post , estender getters e setters tradicionais em subclasses é mais fácil do que estender propriedades.

  • Os getters e setters tradicionais são amplamente utilizados há muito tempo em diferentes idiomas. Se você tem pessoas de diferentes origens em sua equipe, elas parecem mais familiares do que @property. Além disso, conforme seu projeto cresce, se você precisar migrar do Python para outro idioma que não tenha@property , o uso de getters e setters tradicionais tornaria a migração mais suave.

Ressalvas

  • Nem @propertyos getters e setters tradicionais tornam o membro da classe privado, mesmo que você use sublinhado duplo antes do nome:

    class Foo:
        def __init__(self):
            self.__num = 0
    
        @property
        def num(self):
            return self.__num
    
        @num.setter
        def num(self, num):
            self.__num = num
    
        def get_num(self):
            return self.__num
    
        def set_num(self, num):
            self.__num = num
    
    foo = Foo()
    print(foo.num)          # output: 0
    print(foo.get_num())    # output: 0
    print(foo._Foo__num)    # output: 0
Cyker
fonte
2

Aqui estão alguns trechos de "Python eficaz: 90 maneiras específicas de escrever melhor Python" (livro surpreendente. Eu recomendo).

Coisas para lembrar

✦ Defina novas interfaces de classe usando atributos públicos simples e evite definir métodos setter e getter.

@ Use @property para definir um comportamento especial quando atributos forem acessados ​​em seus objetos, se necessário.

✦ Siga a regra da menor surpresa e evite efeitos colaterais estranhos nos seus métodos @property.

✦ Verifique se os métodos @property são rápidos; para trabalhos lentos ou complexos - especialmente envolvendo E / S ou causando efeitos colaterais - use métodos normais.

Um uso avançado, mas comum, da propriedade @ é a transição do que antes era um atributo numérico simples para um cálculo on-the-fly. Isso é extremamente útil porque permite migrar todo o uso existente de uma classe para ter novos comportamentos sem exigir a reescrita de qualquer um dos sites de chamada (o que é especialmente importante se houver código de chamada que você não controla). O @property também fornece uma importante parada para melhorar as interfaces ao longo do tempo.

Eu gosto especialmente de @property porque permite que você faça um progresso incremental em direção a um melhor modelo de dados ao longo do tempo.
O @property é uma ferramenta para ajudá-lo a resolver problemas que você encontrará no código do mundo real. Não use demais. Quando você se vê estendendo os métodos @property repetidamente, provavelmente é hora de refatorar sua classe em vez de pavimentar mais o design inadequado do seu código.

@ Use @property para dar aos atributos da instância existente uma nova funcionalidade.

✦ Faça progressos incrementais em direção a melhores modelos de dados usando @property.

✦ Considere refatorar uma classe e todos os sites de chamada quando se encontrar usando @property demais.

Vlad Bezden
fonte