207 votes

Comment créer une propriété de classe ?

En python, je peux ajouter une méthode à une classe à l'aide de la commande @classmethod décorateur. Existe-t-il un décorateur similaire pour ajouter une propriété à une classe ? Je peux mieux montrer ce dont 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écessite-t-elle quelque chose de plus ?

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

2 votes

Je ne connais pas python... est-ce que c'est le genre de choses que vous recherchez ? python.org/download/releases/2.2/descrintro/#property

0 votes

Si je lis bien, on peut créer des méthodes pour agir comme setter et getter (comme dans d'autres langages) afin de charger paresseusement la valeur de la propriété lors du premier get... ce qui, je pense, est ce que vous vouliez ?

3 votes

@White Dragon. La fonctionnalité de propriété que vous recherchez ajoute des propriétés aux instances de classe, pas aux classes elles-mêmes. Ma question porte sur Example.I no e.i .

122voto

Mahmoud Abdelkader Points 5622

Voici comment je procéderais :

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 n'a pas fonctionné au moment où nous l'avons appelé. Bar.bar car nous appelons TypeOfBar.bar.__set__ qui n'est pas Bar.bar.__set__ .

L'ajout d'une définition de métaclasse résout ce problème :

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 va bien se passer.

3 votes

Pour ceux qui utilisent python 3, dans la définition de la classe, utilisez class Bar(metaclass= ClassPropertyMetaClass): au lieu de __metaclass__ = ClassPropertyMetaClass à l'intérieur.

2 votes

Il y a juste un petit problème dans __set__ fonction : ligne type_ = type(obj) doit être suivi de if type_ == ClassPropertyMetaClass: type_ = obj (à écrire juste après cette ligne). Sinon, cela ne fonctionne pas correctement au niveau de l'instance. Vous pouvez également utiliser le package classesutilités (voir sur PyPi.org) qui implémente exactement cette logique.

61voto

jchl Points 2538

Si vous définissez classproperty comme suit, alors 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)

L'inconvénient est que vous ne pouvez pas l'utiliser pour les propriétés accessibles en écriture. Bien qu'il s'agisse d'une e.I = 20 soulèvera un AttributeError , Example.I = 20 écrasera l'objet de la propriété lui-même.

1 votes

Des idées sur la manière de procéder au typage mypy dans ce cas ?

47voto

Andrew Points 1429

[réponse rédigée sur la base de python 3.4 ; la syntaxe des métaclasses diffère en 2 mais je pense que la technique fonctionnera toujours].

Vous pouvez le faire avec une métaclasse... en général. Celle de Dappawit fonctionne presque, mais je pense qu'elle a un défaut :

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

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

Cela permet d'obtenir une classproperty 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 accéder à la propriété de la classe à partir d'une instance ?

Je me suis creusé la tête pendant un certain temps avant de trouver ce que je crois être la solution. Les @propriétés Python sont un sous-ensemble des descripteurs, et, d'après les documentation sur les descripteurs (souligné par moi) :

Le comportement par défaut pour l'accès aux attributs est t dans le dictionnaire d'un objet. Par exemple, a.x h commençant par a.__dict__['x'] entonces type(a).__dict__['x'] et en continuant en passant par les classes de base de type(a) excluant les métaclasses .

L'ordre de résolution des méthodes n'inclut donc pas les propriétés de notre classe (ni rien d'autre défini dans la métaclasse). Il n'y a pas d'ordre de résolution des méthodes. es Il 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 en googlant que les développeurs avaient une bonne raison (que je ne comprends pas) de le faire de cette façon.

Cela ne veut pas dire que nous n'avons pas de chance ; nous pouvons accéder aux propriétés de la classe elle-même sans problème... et nous pouvons obtenir la classe à partir de type(self) dans l'instance, que nous pouvons utiliser pour créer des répartiteurs @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 ce qu'il faut si une classe dérivée remplace sa classe sous-jacente de _thingy (ce qui est le cas d'utilisation qui m'a amené à cette chasse à l'origine).

Cela ne me satisfait pas à 100 % - le fait d'avoir à configurer à la fois la métaclasse et la classe d'objet semble violer le principe DRY. Mais ce dernier n'est qu'un dispatcher d'une ligne ; je suis plutôt d'accord avec son existence, et vous pourriez probablement le réduire à une lambda ou quelque chose comme ça si vous le vouliez vraiment.

34voto

Barnabas Szabolcs Points 4109

Si vous utilisez Django, il dispose d'une fonction intégrée de @classproperty décorateur.

from django.utils.decorators import classproperty

Pour Django 4, utilisez :

from django.utils.functional import classproperty

3 votes

Je viens de voir que vous avez ajouté la ligne d'importation. Ah vous avez raison, c'est là mais ce n'est pas documenté ! Je n'ai pas trouvé de résultats ici : docs.djangoproject.com/fr/2.2/search/?q=classproperty Il semble que ce soit le cas maintenant.

1 votes

Oui, en effet, j'ai réussi à déterrer ceci par hasard... Je crois que j'ai fait une recherche dans la base de code de la bibliothèque avec l'idée que cela aurait dû être implémenté quelque part.

0 votes

Cela ressemble à ce qui suit ~ class classproperty : def __init__(self, method=None) : self.fget = method def __get__(self, instance, cls=None) : return self.fget(cls) def getter(self, method) : self.fget = method return self ~ pas sûr que cela soit utile

29voto

dappawit Points 3782

Je pense qu'il est possible de le faire avec la métaclasse. Puisque la métaclasse peut être comme une classe pour la classe (si cela a un sens). Je sais que l'on peut assigner un __call__() à la métaclasse pour surcharger l'appel à la classe, MyClass() . Je me demande si l'utilisation du property sur la métaclasse fonctionne de manière similaire.

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 : il s'agit de Python 2.7. Python 3+ utilise une technique différente pour déclarer une métaclasse. Utiliser : class MyClass(metaclass=MetaClass): , supprimer __metaclass__ et le reste est identique.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X