[ 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 defaultX
e defaultY
no __init__
método do objeto . O motivo é que, para o nosso caso, quero assumir que os someDefaultComputation
mé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 x
e y
valores __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 setattr
no 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 value
ou executar o method
se 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 method
aqui seja um método de classe, value
seja o valor de UberProperty
, e eu adicionei isSet
porque None
pode 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 uberProperty
e 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 UberProperty
para armazenar apenas a method
referê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 UberProperty
que, obviamente, não é passível de chamada, portanto, isso não é de muita utilidade.
O que precisamos é de uma maneira de vincular dinamicamente a UberProperty
instâ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 UberProperty
a uma instância, uma coisa óbvia a ser retornada seria um BoundUberProperty. É aqui que manteremos o estado do x
atributo.
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 UberObject
e ...
e = Example()
print e.x -> <__main__.BoundUberProperty object at 0x104604c90>
Após modificar x
para 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
, setValue
e clearValue
aqui. 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.x
retornar 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.