Usando property () em classmethods


174

Eu tenho uma classe com dois métodos de classe (usando a função classmethod ()) para obter e definir o que é essencialmente uma variável estática. Tentei usar a função property () com eles, mas isso resultou em um erro. Consegui reproduzir o erro com o seguinte no intérprete:

class Foo(object):
    _var = 5
    @classmethod
    def getvar(cls):
        return cls._var
    @classmethod
    def setvar(cls, value):
        cls._var = value
    var = property(getvar, setvar)

Eu posso demonstrar os métodos de classe, mas eles não funcionam como propriedades:

>>> f = Foo()
>>> f.getvar()
5
>>> f.setvar(4)
>>> f.getvar()
4
>>> f.var
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'classmethod' object is not callable
>>> f.var=5
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'classmethod' object is not callable

É possível usar a função property () com funções decoradas com método de classe?

Respostas:


90

Uma propriedade é criada em uma classe, mas afeta uma instância. Portanto, se você deseja uma propriedade classmethod, crie a propriedade na metaclasse.

>>> class foo(object):
...     _var = 5
...     class __metaclass__(type):  # Python 2 syntax for metaclasses
...         pass
...     @classmethod
...     def getvar(cls):
...         return cls._var
...     @classmethod
...     def setvar(cls, value):
...         cls._var = value
...     
>>> foo.__metaclass__.var = property(foo.getvar.im_func, foo.setvar.im_func)
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

Mas como você está usando uma metaclasse de qualquer maneira, será melhor ler se você apenas mover os métodos de classe para lá.

>>> class foo(object):
...     _var = 5
...     class __metaclass__(type):  # Python 2 syntax for metaclasses
...         @property
...         def var(cls):
...             return cls._var
...         @var.setter
...         def var(cls, value):
...             cls._var = value
... 
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

ou, usando a metaclass=...sintaxe do Python 3 , a metaclasse definida fora do foocorpo da classe e a metaclasse responsável por definir o valor inicial de _var:

>>> class foo_meta(type):
...     def __init__(cls, *args, **kwargs):
...         cls._var = 5
...     @property
...     def var(cls):
...         return cls._var
...     @var.setter
...     def var(cls, value):
...         cls._var = value
...
>>> class foo(metaclass=foo_meta):
...     pass
...
>>> foo.var
5
>>> foo.var = 3
>>> foo.var
3

1
Isso não parece funcionar para mim no Python 3.2. Se eu alterar foo .__ metaclasse __. Var = property (foo.getvar.im_func, foo.setvar.im_func) para foo .__ metaclass __. Var = property (foo.getvar .__ func__, foo.setvar .__ func__), recebo "AttributeError: type O objeto 'foo' não tem atributo 'var' "ao executar" foo.var ".
Michael Kelley

Correção dupla do SIGH : isso funciona no Python 2.7, mas não no Python 3.2.
Michael Kelley

@MichaelKelley - Isso ocorre porque a sintaxe das metaclasses mudou no Python 3.x
mac

1
Não tenho muita certeza de entender, qual seria a maneira do Python 3.x de escrever isso então?
SylvainD

8
@ Josay: Você precisaria definir primeiro a metaclasse, depois definir a classe usando a nova class Foo(metaclass=...)sintaxe.
Kevin

69

Lendo as notas de versão do Python 2.2 , acho o seguinte.

O método get [de uma propriedade] não será chamado quando a propriedade for acessada como um atributo de classe (Cx) em vez de como um atributo de instância (C (). X). Se você deseja substituir a operação __get__ para propriedades quando usada como um atributo de classe, é possível subclassificar a propriedade - é um tipo de novo estilo - para estender seu método __get__ ou definir um tipo de descritor do zero, criando um novo classe de estilo que define os métodos __get__, __set__ e __delete__.

NOTA: O método abaixo, na verdade, não funciona para setters, apenas getters.

Portanto, acredito que a solução prescrita é criar um ClassProperty como uma subclasse de propriedade.

class ClassProperty(property):
    def __get__(self, cls, owner):
        return self.fget.__get__(None, owner)()

class foo(object):
    _var=5
    def getvar(cls):
        return cls._var
    getvar=classmethod(getvar)
    def setvar(cls,value):
        cls._var=value
    setvar=classmethod(setvar)
    var=ClassProperty(getvar,setvar)

assert foo.getvar() == 5
foo.setvar(4)
assert foo.getvar() == 4
assert foo.var == 4
foo.var = 3
assert foo.var == 3

No entanto, os setters realmente não funcionam:

foo.var = 4
assert foo.var == foo._var # raises AssertionError

foo._var permanecer inalterado, você simplesmente substituiu a propriedade por um novo valor.

Você também pode usar ClassPropertycomo decorador:

class foo(object):
    _var = 5

    @ClassProperty
    @classmethod
    def var(cls):
        return cls._var

    @var.setter
    @classmethod
    def var(cls, value):
        cls._var = value

assert foo.var == 5

20
Eu não acho que a parte setter da ClassProperty realmente funcione como descrito: enquanto todas as afirmações do exemplo passam, no final foo._var == 4 (não 3, como está implícito). Definir a propriedade derruba a própria propriedade. Quando a aula-propriedades foram discutidos em python-dev foi salientado que, embora getters são triviais, setters são difíceis sem uma metaclasse (impossível?)
Gabriel Grant

4
@ Gabriel Totalmente correto. Não acredito que ninguém apontou isso por dois anos.
agf

Também não sei por que você não apenas usa self.fget(owner)e remove a necessidade de usar um @classmethodaqui? (é isso que classmethod faz , traduzir .__get__(instance, owner)(*args, **kwargs)para function(owner, *args, **kwargs)chamadas, por meio de um intermediário; as propriedades não precisam do intermediário).
Martijn Pieters

Sua demonstração não possui nenhuma transformação real no getter ou no setter que demonstraria claramente que sua foo.var = 3atribuição não passa pela propriedade e, em vez disso, simplesmente substituiu o objeto de propriedade por fooum número inteiro. Se você adicionou assert isinstance(foo.__dict__['var'], ClassProperty)chamadas entre suas asserções, verá que a falha após a foo.var = 3execução.
Martijn Pieters

1
Classes Python não descritor de apoio vinculativa para definir sobre a própria classe , apenas em obter (assim instance.attr, instance.attr = valuee del instance.attrtodos se ligam o descritor será encontrado na type(instance), mas enquanto classobj.attrse liga, classobj.attr = valuee del classobj.attrque não e em vez disso substituir ou eliminar o próprio objeto descritor). Você precisa de uma metaclasse para oferecer suporte à configuração e exclusão (tornando a classe objeto a instância e a metaclasse o tipo).
Martijn Pieters

56

Espero que esse @classpropertydecorador simples e de leitura simples ajude alguém a procurar propriedades de classe.

class classproperty(object):

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

    def __get__(self, owner_self, owner_cls):
        return self.fget(owner_cls)

class C(object):

    @classproperty
    def x(cls):
        return 1

assert C.x == 1
assert C().x == 1

2
Isso funciona com subclasses? (pode uma subclasse de substituição classproperties?)
zakdances

1
Umm sim? class D(C): x = 2; assert D.x == 2
precisa saber é o seguinte

Eu desejo que isso funcionou quando eu usá-lo .formatcomo "{x}".format(**dict(self.__class__.__dict__, **self.__dict__)):(
2rs2ts

@ Nathan Não apenas ... quando você o define, substitui todo o xacesso por 10. Eu gosto dessa abordagem porque é elegante e simples, mas soa como um antipadrão
Michele d'Amico

Facilmente corrigido: adicione um __set__que gera um ValueErrorpara impedir a substituição.
Kiran Jonnalagadda

30

É possível usar a função property () com funções decoradas com método de classe?

Não.

No entanto, um método de classe é simplesmente um método vinculado (uma função parcial) em uma classe acessível a partir de instâncias dessa classe.

Como a instância é uma função da classe e você pode derivá-la, você pode obter o comportamento desejado de uma propriedade de classe com property:

class Example(object):
    _class_property = None
    @property
    def class_property(self):
        return self._class_property
    @class_property.setter
    def class_property(self, value):
        type(self)._class_property = value
    @class_property.deleter
    def class_property(self):
        del type(self)._class_property

Este código pode ser usado para testar - ele deve passar sem gerar erros:

ex1 = Example()
ex2 = Example()
ex1.class_property = None
ex2.class_property = 'Example'
assert ex1.class_property is ex2.class_property
del ex2.class_property
assert not hasattr(ex1, 'class_property')

E observe que não precisávamos de metaclasses - e você não acessa diretamente uma metaclasse através das instâncias de suas classes.

escrevendo um @classpropertydecorador

Você pode realmente criar um classpropertydecorador em apenas algumas linhas de código subclassificando property(ele é implementado em C, mas você pode ver o equivalente em Python aqui ):

class classproperty(property):
    def __get__(self, obj, objtype=None):
        return super(classproperty, self).__get__(objtype)
    def __set__(self, obj, value):
        super(classproperty, self).__set__(type(obj), value)
    def __delete__(self, obj):
        super(classproperty, self).__delete__(type(obj))

Trate o decorador como se fosse um método de classe combinado com a propriedade:

class Foo(object):
    _bar = 5
    @classproperty
    def bar(cls):
        """this is the bar attribute - each subclass of Foo gets its own.
        Lookups should follow the method resolution order.
        """
        return cls._bar
    @bar.setter
    def bar(cls, value):
        cls._bar = value
    @bar.deleter
    def bar(cls):
        del cls._bar

E esse código deve funcionar sem erros:

def main():
    f = Foo()
    print(f.bar)
    f.bar = 4
    print(f.bar)
    del f.bar
    try:
        f.bar
    except AttributeError:
        pass
    else:
        raise RuntimeError('f.bar must have worked - inconceivable!')
    help(f)  # includes the Foo.bar help.
    f.bar = 5

    class Bar(Foo):
        "a subclass of Foo, nothing more"
    help(Bar) # includes the Foo.bar help!
    b = Bar()
    b.bar = 'baz'
    print(b.bar) # prints baz
    del b.bar
    print(b.bar) # prints 5 - looked up from Foo!

    
if __name__ == '__main__':
    main()

Mas não tenho certeza de quão bem aconselhado isso seria. Um artigo antigo de lista de discussão sugere que não deve funcionar.

Colocando a propriedade para trabalhar na classe:

A desvantagem do exposto acima é que a "propriedade de classe" não é acessível a partir da classe, porque simplesmente substitui o descritor de dados da classe __dict__.

No entanto, podemos substituir isso por uma propriedade definida na metaclasse __dict__. Por exemplo:

class MetaWithFooClassProperty(type):
    @property
    def foo(cls):
        """The foo property is a function of the class -
        in this case, the trivial case of the identity function.
        """
        return cls

E então uma instância de classe da metaclasse pode ter uma propriedade que acessa a propriedade da classe usando o princípio já demonstrado nas seções anteriores:

class FooClassProperty(metaclass=MetaWithFooClassProperty):
    @property
    def foo(self):
        """access the class's property"""
        return type(self).foo

E agora vemos tanto a instância

>>> FooClassProperty().foo
<class '__main__.FooClassProperty'>

e a classe

>>> FooClassProperty.foo
<class '__main__.FooClassProperty'>

tenha acesso à propriedade da classe


3
Clara, concisa, completa: essa deve ser a resposta aceita.
pfabri

25

Python 3!

Pergunta antiga, muitos pontos de vista, extremamente necessitados de uma maneira única de Python 3.

Felizmente, é fácil com o metaclasskwarg:

class FooProperties(type):

    @property
    def var(cls):
        return cls._var

class Foo(object, metaclass=FooProperties):
    _var = 'FOO!'

Então, >>> Foo.var

'FOO!'


1
ou seja não há nenhuma maneira simples para fora da caixa
mehmet

@mehmet Isso não é simples? Fooé uma instância de sua metaclasse e @propertypode ser usada para seus métodos, assim como para as instâncias de Foo.
OJFord

2
você tinha que definir outra classe para uma classe, que é o dobro da complexidade, assumindo que a metaclasse não é reutilizável.
achou

Um método de classe funciona a partir da classe e da instância. Esta propriedade funciona apenas a partir da classe. Eu não acho que isso é o que está sendo pedido.
Aaron Hall

1
@AaronHall Se isso é importante, é facilmente adicionado Foo.__new__. Embora nesse ponto possa valer a pena usar getattribute ou questionar se fingir que um recurso de linguagem existe é realmente a abordagem que você deseja adotar.
OJFord

16

Não há uma maneira razoável de fazer esse sistema de "propriedade de classe" funcionar em Python.

Aqui está uma maneira irracional de fazê-lo funcionar. Você certamente pode torná-lo mais transparente com quantidades crescentes de magia de metaclasse.

class ClassProperty(object):
    def __init__(self, getter, setter):
        self.getter = getter
        self.setter = setter
    def __get__(self, cls, owner):
        return getattr(cls, self.getter)()
    def __set__(self, cls, value):
        getattr(cls, self.setter)(value)

class MetaFoo(type):
    var = ClassProperty('getvar', 'setvar')

class Foo(object):
    __metaclass__ = MetaFoo
    _var = 5
    @classmethod
    def getvar(cls):
        print "Getting var =", cls._var
        return cls._var
    @classmethod
    def setvar(cls, value):
        print "Setting var =", value
        cls._var = value

x = Foo.var
print "Foo.var = ", x
Foo.var = 42
x = Foo.var
print "Foo.var = ", x

O nó da questão é que as propriedades são o que Python chama de "descritores". Não há uma maneira curta e fácil de explicar como esse tipo de metaprogramação funciona; portanto, devo apontar o descritor como .

Você só precisa entender esse tipo de coisa se estiver implementando uma estrutura bastante avançada. Como uma persistência transparente de objeto ou sistema RPC ou um tipo de linguagem específica de domínio.

No entanto, em um comentário a uma resposta anterior, você diz que

É necessário modificar um atributo que seja visto por todas as instâncias de uma classe e no escopo do qual esses métodos de classe são chamados não tenha referências a todas as instâncias da classe.

Parece-me que o que você realmente deseja é um padrão de design do Observer .


Gosto da ideia do exemplo de código, mas parece que seria um pouco complicado na prática.
Mark Roddy

O que estou tentando realizar é a configuração e a obtenção bem simples de um único atributo que é usado como sinalizador para modificar o comportamento de todas as instâncias, então acho que o Observer seria exagerado pelo que estou tentando fazer. Se houvesse vários atributos em questão, eu estaria mais inclinado.
Mark Roddy

Parece que apenas tornar públicas as funções e chamá-las diretamente foi a solução simplista. Fiquei curioso se estava fazendo algo errado ou se estava tentando fazer era impossível. A propósito, desculpe pelos vários comentários. O limite de 300 caracteres é péssimo.
Mark Roddy

O interessante do exemplo de código é que você pode implementar todos os bits desajeitados uma vez e depois herdá-los. Basta mover o _var para a classe derivada. classe D1 (Foo): _var = 21 classe D2 (Foo) _var = "Olá" D1.var 21 D2.var Olá
Thomas L Holaday 27/08/08

6

A configuração apenas na meta classe não ajuda se você deseja acessar a propriedade da classe por meio de um objeto instanciado; nesse caso, você também precisa instalar uma propriedade normal no objeto (que é despachada para a propriedade da classe). Eu acho que o seguinte é um pouco mais claro:

#!/usr/bin/python

class classproperty(property):
    def __get__(self, obj, type_):
        return self.fget.__get__(None, type_)()

    def __set__(self, obj, value):
        cls = type(obj)
        return self.fset.__get__(None, cls)(value)

class A (object):

    _foo = 1

    @classproperty
    @classmethod
    def foo(cls):
        return cls._foo

    @foo.setter
    @classmethod
    def foo(cls, value):
        cls.foo = value

a = A()

print a.foo

b = A()

print b.foo

b.foo = 5

print a.foo

A.foo = 10

print b.foo

print A.foo

3

Metade de uma solução, __set__ na classe, ainda não funciona. A solução é uma classe de propriedade customizada implementando uma propriedade e um staticmethod

class ClassProperty(object):
    def __init__(self, fget, fset):
        self.fget = fget
        self.fset = fset

    def __get__(self, instance, owner):
        return self.fget()

    def __set__(self, instance, value):
        self.fset(value)

class Foo(object):
    _bar = 1
    def get_bar():
        print 'getting'
        return Foo._bar

    def set_bar(value):
        print 'setting'
        Foo._bar = value

    bar = ClassProperty(get_bar, set_bar)

f = Foo()
#__get__ works
f.bar
Foo.bar

f.bar = 2
Foo.bar = 3 #__set__ does not

3

Porque preciso modificar um atributo que seja visto por todas as instâncias de uma classe e no escopo do qual esses métodos de classe são chamados não tenha referências a todas as instâncias da classe.

Você tem acesso a pelo menos uma instância da classe? Posso pensar em uma maneira de fazer isso:

class MyClass (object):
    __var = None

    def _set_var (self, value):
        type (self).__var = value

    def _get_var (self):
        return self.__var

    var = property (_get_var, _set_var)

a = MyClass ()
b = MyClass ()
a.var = "foo"
print b.var

2

Faça uma tentativa, ele faz o trabalho sem precisar alterar / adicionar muito código existente.

>>> class foo(object):
...     _var = 5
...     def getvar(cls):
...         return cls._var
...     getvar = classmethod(getvar)
...     def setvar(cls, value):
...         cls._var = value
...     setvar = classmethod(setvar)
...     var = property(lambda self: self.getvar(), lambda self, val: self.setvar(val))
...
>>> f = foo()
>>> f.var
5
>>> f.var = 3
>>> f.var
3

A propertyfunção precisa de dois callableargumentos. dê a eles wrappers lambda (que passam a instância como seu primeiro argumento) e está tudo bem.


Como Florian Bösch aponta, a sintaxe necessária (por bibliotecas de terceiros ou código legado) é foo.var.
Thomas L Holaday

2

Aqui está uma solução que deve funcionar tanto para acesso via classe quanto para acesso através de uma instância que usa uma metaclasse.

In [1]: class ClassPropertyMeta(type):
   ...:     @property
   ...:     def prop(cls):
   ...:         return cls._prop
   ...:     def __new__(cls, name, parents, dct):
   ...:         # This makes overriding __getattr__ and __setattr__ in the class impossible, but should be fixable
   ...:         dct['__getattr__'] = classmethod(lambda cls, attr: getattr(cls, attr))
   ...:         dct['__setattr__'] = classmethod(lambda cls, attr, val: setattr(cls, attr, val))
   ...:         return super(ClassPropertyMeta, cls).__new__(cls, name, parents, dct)
   ...:

In [2]: class ClassProperty(object):
   ...:     __metaclass__ = ClassPropertyMeta
   ...:     _prop = 42
   ...:     def __getattr__(self, attr):
   ...:         raise Exception('Never gets called')
   ...:

In [3]: ClassProperty.prop
Out[3]: 42

In [4]: ClassProperty.prop = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-e2e8b423818a> in <module>()
----> 1 ClassProperty.prop = 1

AttributeError: can't set attribute

In [5]: cp = ClassProperty()

In [6]: cp.prop
Out[6]: 42

In [7]: cp.prop = 1
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-e8284a3ee950> in <module>()
----> 1 cp.prop = 1

<ipython-input-1-16b7c320d521> in <lambda>(cls, attr, val)
      6         # This makes overriding __getattr__ and __setattr__ in the class impossible, but should be fixable
      7         dct['__getattr__'] = classmethod(lambda cls, attr: getattr(cls, attr))
----> 8         dct['__setattr__'] = classmethod(lambda cls, attr, val: setattr(cls, attr, val))
      9         return super(ClassPropertyMeta, cls).__new__(cls, name, parents, dct)

AttributeError: can't set attribute

Isso também funciona com um setter definido na metaclasse.


1

Depois de pesquisar lugares diferentes, encontrei um método para definir uma propriedade de classe válida com Python 2 e 3.

from future.utils import with_metaclass

class BuilderMetaClass(type):
    @property
    def load_namespaces(self):
        return (self.__sourcepath__)

class BuilderMixin(with_metaclass(BuilderMetaClass, object)):
    __sourcepath__ = 'sp'        

print(BuilderMixin.load_namespaces)

Espero que isso possa ajudar alguém :)


1
Se este método é algo que você encontrou em algum lugar, que seria bom para dar um link (veja Como material de referência escrito por outros )
Andrew Myers

-27

Aqui está a minha sugestão. Não use métodos de classe.

Seriamente.

Qual o motivo do uso de métodos de classe neste caso? Por que não ter um objeto comum de uma classe comum?


Se você simplesmente deseja alterar o valor, uma propriedade não é realmente muito útil, é? Basta definir o valor do atributo e concluir com ele.

Uma propriedade só deve ser usada se houver algo a ocultar - algo que pode mudar em uma implementação futura.

Talvez o seu exemplo seja muito simples e exista algum cálculo infernal que você tenha deixado de fazer. Mas não parece que a propriedade agregue um valor significativo.

As técnicas de "privacidade" influenciadas por Java (no Python, nomes de atributos que começam com _) não são realmente muito úteis. Privado de quem? O ponto privado é um pouco nebuloso quando você tem a fonte (como você faz no Python.)

Os getters e setters do estilo EJB influenciados por Java (geralmente feitos como propriedades em Python) estão lá para facilitar a introspecção primitiva do Java, bem como para passar o agrupamento com o compilador de linguagem estática. Todos esses getters e setters não são tão úteis em Python.


14
Porque eu preciso modificar um atributo que seja visto por todas as instâncias de uma classe e no escopo do qual esses métodos de classe são chamados não tenha referências a todas as instâncias da classe.
Mark Roddy
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.