Como fazer uma propriedade de classe? [duplicado]

em python Posso adicionar um método a uma classe com o decorador @classmethod. Existe um decorador semelhante para adicionar uma propriedade a uma classe? É melhor Mostrar do que estou a falar.

class Example(object):
   the_I = 10
   def __init__( self ):
      self.an_i = 20

   @property
   def i( self ):
      return self.an_i

   def inc_i( self ):
      self.an_i += 1

   # is this even possible?
   @classproperty
   def I( cls ):
      return cls.the_I

   @classmethod
   def inc_I( cls ):
      cls.the_I += 1

e = Example()
assert e.i == 20
e.inc_i()
assert e.i == 21

assert Example.I == 10
Example.inc_I()
assert Example.I == 11

a sintaxe que usei acima é possível ou exigiria algo mais?

a razão pela qual quero propriedades da classe é para poder carregar atributos preguiçosos da classe, o que parece razoável.

Author: martineau, 2011-03-04

9 answers

Eis como eu faria isto:
class ClassPropertyDescriptor(object):

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

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        return self.fget.__get__(obj, klass)()

    def __set__(self, obj, value):
        if not self.fset:
            raise AttributeError("can't set attribute")
        type_ = type(obj)
        return self.fset.__get__(obj, type_)(value)

    def setter(self, func):
        if not isinstance(func, (classmethod, staticmethod)):
            func = classmethod(func)
        self.fset = func
        return self

def classproperty(func):
    if not isinstance(func, (classmethod, staticmethod)):
        func = classmethod(func)

    return ClassPropertyDescriptor(func)


class Bar(object):

    _bar = 1

    @classproperty
    def bar(cls):
        return cls._bar

    @bar.setter
    def bar(cls, value):
        cls._bar = value


# test instance instantiation
foo = Bar()
assert foo.bar == 1

baz = Bar()
assert baz.bar == 1

# test static variable
baz.bar = 5
assert foo.bar == 5

# test setting variable on the class
Bar.bar = 50
assert baz.bar == 50
assert foo.bar == 50
O setter não funcionou na altura em que ligámos. TypeOfBar.bar.__set__, que não é Bar.bar.__set__.

Adicionar uma definição metaclass resolve isto:

class ClassPropertyMetaClass(type):
    def __setattr__(self, key, value):
        if key in self.__dict__:
            obj = self.__dict__.get(key)
        if obj and type(obj) is ClassPropertyDescriptor:
            return obj.__set__(self, value)

        return super(ClassPropertyMetaClass, self).__setattr__(key, value)

# and update class define:
#     class Bar(object):
#        __metaclass__ = ClassPropertyMetaClass
#        _bar = 1

# and update ClassPropertyDescriptor.__set__
#    def __set__(self, obj, value):
#       if not self.fset:
#           raise AttributeError("can't set attribute")
#       if inspect.isclass(obj):
#           type_ = obj
#           obj = None
#       else:
#           type_ = type(obj)
#       return self.fset.__get__(obj, type_)(value)
Agora vai correr tudo bem.
 103
Author: Mahmoud Abdelkader, 2018-02-01 21:37:03

Se definir classproperty como se segue, então o seu exemplo funciona exactamente como pediu.

class classproperty(object):
    def __init__(self, f):
        self.f = f
    def __get__(self, obj, owner):
        return self.f(owner)

A ressalva é que você não pode usar isso para propriedades de escrita. Enquanto e.I = 20AttributeError, Example.I = 20 irá sobrepor o objecto da propriedade em si.

 51
Author: jchl, 2011-03-04 10:13:47

[resposta escrita com base no python 3.4; A sintaxe do metaclass difere em 2, mas acho que a técnica ainda vai funcionar]

Podes fazer isto com um metaclass...principalmente. Dappawit está quase a funcionar, mas acho que tem uma falha:
class MetaFoo(type):
    @property
    def thingy(cls):
        return cls._thingy

class Foo(object, metaclass=MetaFoo):
    _thingy = 23
Isto dá-te um classsproperty no Foo, mas há um problema...
print("Foo.thingy is {}".format(Foo.thingy))
# Foo.thingy is 23
# Yay, the classmethod-property is working as intended!
foo = Foo()
if hasattr(foo, "thingy"):
    print("Foo().thingy is {}".format(foo.thingy))
else:
    print("Foo instance has no attribute 'thingy'")
# Foo instance has no attribute 'thingy'
# Wha....?
Que raio se passa aqui? Porque não consigo chegar à propriedade de uma instância? Estava a bater com a cabeça nisto há algum tempo. encontrar aquilo em que acredito é a resposta. Python @properties são um subconjunto de descritores, e, a partir da documentação descritor (emphasis mine):

O comportamento por omissão para o acesso ao atributo é obter, definir ou apagar o atributo do dicionário de um objeto. Por exemplo, a.x tem uma cadeia de procura começando com a.__dict__['x'], depois type(a).__dict__['x'], e continuando através das classes de base de type(a) excluindo metaclasses.

A resolução do método a ordem não inclui as propriedades da nossa classe (ou qualquer outra coisa definida no metaclass). Ele é possível fazer uma subclasse da propriedade interna decorador que se comporta de forma diferente, mas (carece de fontes) que eu comecei a impressão de googling que os desenvolvedores tinham um bom motivo (que eu não entendo) para fazê-lo dessa forma. Isso não significa que estamos sem sorte, podemos aceder às propriedades da própria classe muito bem...e podemos conseguir a aula de dentro. a instância, que podemos usar para fazer @ property dispatchers:
class Foo(object, metaclass=MetaFoo):
    _thingy = 23

    @property
    def thingy(self):
        return type(self).thingy

Agora Foo().thingy funciona como previsto para a classe e as instâncias! Ele também continuará a fazer a coisa certa se uma classe derivada substitui seu subjacente _thingy (que é o caso de uso que me levou a esta caçada originalmente).

Isto não é 100% satisfatório para mim.ter de fazer a configuração tanto no metaclass como na classe de objectos parece que viola o princípio seco. Mas este último é apenas um despachante de uma linha; Na maioria das vezes, não me importo que exista, e tu podes compactá-lo para um lambda ou algo assim, se realmente quiseres.
 29
Author: Andrew, 2016-08-07 03:42:44
Acho que podes conseguir fazer isto com o metaclass. Uma vez que o metaclass pode ser como uma classe para a classe (se isso faz sentido). Sei que podes atribuir um método ao metaclass para anular a chamada para a classe, {[[2]}. Pergunto-me se usar o decorador property no metaclass funciona da mesma forma. (Eu não tentei isso antes, mas agora estou curioso...)

[actualização:]

Realmente funciona.
class MetaClass(type):    
    def getfoo(self):
        return self._foo
    foo = property(getfoo)

    @property
    def bar(self):
        return self._bar

class MyClass(object):
    __metaclass__ = MetaClass
    _foo = 'abc'
    _bar = 'def'

print MyClass.foo
print MyClass.bar

Nota: Isto é no Python 2.7. O Python 3+ usa um diferente técnica para declarar um metaclass. Usar: class MyClass(metaclass=MetaClass):, Remover __metaclass__, e o resto é o mesmo.

 27
Author: dappawit, 2011-03-04 04:58:57
Se usares o Django, ele tem um decorador.
from django.utils.decorators import classproperty
 14
Author: Barney Szabolcs, 2019-08-10 02:25:36
Até onde posso dizer, não há maneira de escrever um setter para uma propriedade de classe sem criar um novo metaclass. Descobri que o seguinte método funciona. Defina um metaclass com todas as propriedades da classe e setters que desejar. IE, eu queria uma aula com uma propriedade com um setter. Eis o que escrevi:
class TitleMeta(type):
    @property
    def title(self):
        return getattr(self, '_title', 'Default Title')

    @title.setter
    def title(self, title):
        self._title = title
        # Do whatever else you want when the title is set...

Agora faça a classe real que deseja como normal, excepto que use o metaclass que criou acima.

# Python 2 style:
class ClassWithTitle(object):
    __metaclass__ = TitleMeta
    # The rest of your class definition...

# Python 3 style:
class ClassWithTitle(object, metaclass = TitleMeta):
    # Your class definition...
É um pouco estranho definir isto. metaclass como fizemos acima, se o usarmos apenas numa única classe. Nesse caso, se você estiver usando o estilo Python 2, você pode realmente definir o metaclass dentro do corpo da classe. Dessa forma, não é definido no escopo do módulo.
 5
Author: ArtOfWarfare, 2016-02-28 20:08:26

Se você só precisa de carga preguiçosa, então você poderia ter apenas um método de inicialização de classe.

EXAMPLE_SET = False
class Example(object):
   @classmethod 
   def initclass(cls):
       global EXAMPLE_SET 
       if EXAMPLE_SET: return
       cls.the_I = 'ok'
       EXAMPLE_SET = True

   def __init__( self ):
      Example.initclass()
      self.an_i = 20

try:
    print Example.the_I
except AttributeError:
    print 'ok class not "loaded"'
foo = Example()
print foo.the_I
print Example.the_I
Mas a abordagem metaclass parece mais limpa e com um comportamento mais previsível. Talvez o que procura seja o padrão de design de Singleton. Há uma boa pergunta sobre a implementação do estado compartilhado em Python.
 1
Author: Apalala, 2017-05-23 12:34:37
Por acaso, encontrei uma solução muito semelhante a @Andrew, só que seca.
class MetaFoo(type):

    def __new__(mc1, name, bases, nmspc):
        nmspc.update({'thingy': MetaFoo.thingy})
        return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc)

    @property
    def thingy(cls):
        if not inspect.isclass(cls):
            cls = type(cls)
        return cls._thingy

    @thingy.setter
    def thingy(cls, value):
        if not inspect.isclass(cls):
            cls = type(cls)
        cls._thingy = value

class Foo(metaclass=MetaFoo):
    _thingy = 23

class Bar(Foo)
    _thingy = 12

Esta tem a melhor de todas as respostas:

O" metaproperty " é adicionado à classe, de modo que ainda será uma propriedade da instância {[[5]}

    Não é preciso redefinir essa coisa em nenhuma das classes.
  1. a propriedade funciona como uma "propriedade de classe" em ambos os casos e classe
  2. tens a flexibilidade para personalizar como a coisa é herdada

Na minha case, Eu realmente personalizei _thingy para ser diferente para cada criança, sem defini-lo em cada classe (e sem um valor padrão) por:

   def __new__(mc1, name, bases, nmspc):
       nmspc.update({'thingy': MetaFoo.services, '_thingy': None})
       return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc)
 1
Author: Andy, 2019-01-04 20:29:13
def _create_type(meta, name, attrs):
    type_name = f'{name}Type'
    type_attrs = {}
    for k, v in attrs.items():
        if type(v) is _ClassPropertyDescriptor:
            type_attrs[k] = v
    return type(type_name, (meta,), type_attrs)


class ClassPropertyType(type):
    def __new__(meta, name, bases, attrs):
        Type = _create_type(meta, name, attrs)
        cls = super().__new__(meta, name, bases, attrs)
        cls.__class__ = Type
        return cls


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

    def __get__(self, obj, owner):
        if self in obj.__dict__.values():
            return self.fget(obj)
        return self.fget(owner)

    def __set__(self, obj, value):
        if not self.fset:
            raise AttributeError("can't set attribute")
        return self.fset(obj, value)

    def setter(self, func):
        self.fset = func
        return self


def classproperty(func):
    return _ClassPropertyDescriptor(func)



class Bar(metaclass=ClassPropertyType):
    __bar = 1

    @classproperty
    def bar(cls):
        return cls.__bar

    @bar.setter
    def bar(cls, value):
        cls.__bar = value

bar = Bar()
assert Bar.bar==1
Bar.bar=2
assert bar.bar==2
nbar = Bar()
assert nbar.bar==2

 1
Author: thinker3, 2019-12-29 18:48:18