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