web-dev-qa-db-fra.com

Comment faire une propriété de classe?

Dans python je peux ajouter une méthode à une classe avec le @classmethod décorateur. Existe-t-il un décorateur similaire pour ajouter une propriété à une classe? Je peux mieux montrer de quoi je parle.

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

La syntaxe que j'ai utilisée ci-dessus est-elle possible ou nécessiterait-elle quelque chose de plus?

La raison pour laquelle je veux des propriétés de classe est que je peux charger des attributs de classe paresseux, ce qui semble assez raisonnable.

109
deft_code

Voici comment je ferais ceci:

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

Le passeur ne travaillait pas au moment où nous appelons Bar.bar, parce que nous appelons TypeOfBar.bar.__set__, qui n'est pas Bar.bar.__set__.

Ajouter une définition de métaclasse résout le problème suivant:

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)

Maintenant tout ira bien.

76

Si vous définissez classproperty comme suit, votre exemple fonctionne exactement comme vous l'avez demandé.

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

La mise en garde est que vous ne pouvez pas utiliser cela pour les propriétés en écriture. Tandis que e.I = 20 va déclencher un AttributeError, Example.I = 20 écrasera l'objet de propriété lui-même.

35
jchl

Je pense que vous pourrez peut-être faire cela avec la métaclasse. Puisque la métaclasse peut être comme une classe pour la classe (si cela a du sens). Je sais que vous pouvez affecter une méthode __call__() à la métaclasse pour ignorer l'appel de la classe, MyClass(). Je me demande si l’utilisation du décorateur property sur la métaclasse fonctionne de la même manière. (Je n'ai jamais essayé cela auparavant, mais maintenant je suis curieux ...)

[mise à jour:]

Wow, ça marche:

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

Remarque: C’est dans Python 2.7. Python 3+ utilise une technique différente pour déclarer une métaclasse. Utilisez: class MyClass(metaclass=MetaClass):, remove __metaclass__, Et le reste est le même.

24
dappawit

[réponse écrite basée sur python 3.4; la syntaxe de la métaclasse diffère en 2 mais je pense que la technique fonctionnera toujours]

Vous pouvez le faire avec une métaclasse ... principalement. Dappawit fonctionne presque, mais je pense qu'il a un défaut:

class MetaFoo(type):
    @property
    def thingy(cls):
        return cls._thingy

class Foo(object, metaclass=MetaFoo):
    _thingy = 23

Cela vous donne une propriété de classe sur Foo, mais il y a un problème ...

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

Qu'est ce qui se passe ici? Pourquoi ne puis-je pas atteindre la propriété de classe à partir d'une instance?

Je me suis cogné la tête pendant un bon moment avant de trouver ce que je crois être la réponse. Python @ Les propriétés sont un sous-ensemble de descripteurs et, à partir de la documentation du descripteur (c'est moi qui souligne):

Le comportement par défaut pour l’accès aux attributs consiste à obtenir, définir ou supprimer l’attribut du dictionnaire de l’objet. Par exemple, a.x A une chaîne de recherche commençant par a.__dict__['x'], Puis type(a).__dict__['x'], puis dans les classes de base de type(a)à l'exclusion des métaclasses.

Donc, l'ordre de résolution de la méthode n'inclut pas nos propriétés de classe (ni tout autre élément défini dans la métaclasse). Il est est possible de créer une sous-classe du décorateur de propriété intégré qui se comporte différemment, mais (citation nécessaire) J'ai eu l'impression, sur Google, que les développeurs avaient une bonne raison ( que je ne comprends pas) pour le faire de cette façon.

Cela ne signifie pas que nous n'avons pas de chance. nous pouvons très bien accéder aux propriétés de la classe elle-même ... et nous pouvons obtenir la classe à partir de type(self) dans l'instance, que nous pouvons utiliser pour créer des dispatcheurs @property:

class Foo(object, metaclass=MetaFoo):
    _thingy = 23

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

Maintenant Foo().thingy fonctionne comme prévu pour la classe et les instances! Il continuera également à faire le bon choix si une classe dérivée remplace son sous-jacent _thingy (Qui est le cas d'utilisation qui m'a amené à la chasse à l'origine).

Cela ne me satisfait pas à 100% - le fait de devoir configurer à la fois la métaclasse et la classe d'objets donne l'impression d'enfreindre le principe DRY. Mais ce dernier n’est qu’un répartiteur d’une ligne; Je suis plutôt d'accord avec son existence, et vous pourriez probablement la réduire à un lambda ou à quelque chose du genre si vous le souhaitiez vraiment.

19
Andrew

Autant que je sache, il n'y a aucun moyen d'écrire un séparateur pour une propriété de classe sans créer une nouvelle métaclasse.

J'ai trouvé que la méthode suivante fonctionne. Définissez une métaclasse avec toutes les propriétés de classe et les paramètres que vous souhaitez. IE, je voulais une classe avec une propriété title avec un setter. Voici ce que j'ai écrit:

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

Maintenant, faites la classe que vous voulez comme d'habitude, sauf qu'elle utilise la métaclasse que vous avez créée ci-dessus.

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

C'est un peu bizarre de définir cette métaclasse comme nous l'avons fait ci-dessus si nous ne l'utilisons jamais que sur une seule classe. Dans ce cas, si vous utilisez le style Python 2, vous pouvez définir la métaclasse à l'intérieur du corps de la classe. De cette manière, il n'est pas défini dans la portée du module.

4
ArtOfWarfare

Si vous utilisez Django, il a un construit en @classproperty décorateur.

from Django.utils.decorators import classproperty
3
Barnabas Szabolcs

Si vous n'avez besoin que d'un chargement paresseux, vous pouvez simplement utiliser une méthode d'initialisation 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

Mais l'approche par métaclasse semble plus propre et avec un comportement plus prévisible.

Vous recherchez peut-être le modèle Singleton . Il y a a Nice SO QA à propos de l'implémentation de l'état partagé en Python.

1
Apalala
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

0
thinker3

Il m'est arrivé de trouver une solution très similaire à @Andrew, seulement DRY

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

Cela a le meilleur de toutes les réponses:

La "méta-propriété" est ajoutée à la classe, de sorte que ce sera toujours une propriété de l'instance

  1. Pas besoin de redéfinir les choses dans les cours
  2. La propriété fonctionne comme une "propriété de classe" pour à la fois l'instance et la classe
  3. Vous avez la possibilité de personnaliser la manière dont _thingy est hérité

Dans mon cas, j’ai personnalisé _thingy être différent pour chaque enfant, sans le définir dans chaque classe (et sans valeur par défaut) par:

   def __new__(mc1, name, bases, nmspc):
       nmspc.update({'thingy': MetaFoo.services, '_thingy': None})
       return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc)
0
Andy