Utilisation de property () sur les méthodes de classe

174

J'ai une classe avec deux méthodes de classe (en utilisant la fonction classmethod ()) pour obtenir et définir ce qui est essentiellement une variable statique. J'ai essayé d'utiliser la fonction property () avec ceux-ci, mais cela entraîne une erreur. J'ai pu reproduire l'erreur avec ce qui suit dans l'interpréteur:

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

Je peux démontrer les méthodes de classe, mais elles ne fonctionnent pas comme des propriétés:

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

Est-il possible d'utiliser la fonction property () avec des fonctions décorées par classmethod?

Mark Roddy
la source

Réponses:

90

Une propriété est créée sur une classe mais affecte une instance. Donc, si vous voulez une propriété de méthode de classe, créez la propriété sur la métaclasse.

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

Mais comme vous utilisez de toute façon une métaclasse, elle se lira mieux si vous y déplacez simplement les méthodes de classe.

>>> 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, en utilisant la metaclass=...syntaxe de Python 3 , et la métaclasse définie en dehors du foocorps de la classe, et la métaclasse chargée de définir la valeur initiale 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
A. Coady
la source
1
Cela ne semble pas fonctionner pour moi dans Python 3.2. Si je change foo .__ metaclass __. Var = property (foo.getvar.im_func, foo.setvar.im_func) en foo .__ metaclass __. Var = property (foo.getvar .__ func__, foo.setvar .__ func__) j'obtiens "AttributeError: type l'objet 'foo' n'a pas d'attribut 'var' "lors de l'exécution de" foo.var ".
Michael Kelley
SIGH double correction: cela fonctionne en Python 2.7, mais pas en Python 3.2.
Michael Kelley
@MichaelKelley - C'est parce que la syntaxe des métaclasses a changé dans Python 3.x
mac
1
Je ne suis pas tout à fait sûr de comprendre, quelle serait alors la manière Python 3.x d'écrire ceci?
SylvainD
8
@Josay: Vous devez d'abord définir la métaclasse, puis définir la classe en utilisant la nouvelle class Foo(metaclass=...)syntaxe.
Kevin
69

En lisant les notes de publication de Python 2.2 , je trouve ce qui suit.

La méthode get [d'une propriété] ne sera pas appelée lorsque la propriété est accédée en tant qu'attribut de classe (Cx) plutôt qu'en tant qu'attribut d'instance (C (). X). Si vous souhaitez remplacer l'opération __get__ pour les propriétés lorsqu'elle est utilisée comme attribut de classe, vous pouvez sous-classer la propriété - il s'agit d'un type de nouveau style lui-même - pour étendre sa méthode __get__, ou vous pouvez définir un type de descripteur à partir de zéro en créant un nouveau -style classe qui définit les méthodes __get__, __set__ et __delete__.

REMARQUE: la méthode ci-dessous ne fonctionne pas réellement pour les setters, uniquement les getters.

Par conséquent, je crois que la solution prescrite est de créer un ClassProperty en tant que sous-classe de propriété.

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

Cependant, les setters ne fonctionnent pas réellement:

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

foo._var est inchangé, vous avez simplement remplacé la propriété par une nouvelle valeur.

Vous pouvez également utiliser ClassPropertycomme décorateur:

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
Jason R. Coombs
la source
20
Je ne pense pas que la partie setter de la ClassProperty fonctionne réellement comme décrit: alors que les assertions de l'exemple passent toutes, à la fin foo._var == 4 (pas 3, comme implicite). La définition de la propriété écrase la propriété elle-même. Lorsque les propriétés de classe ont été discutées sur python-dev, il a été souligné que, bien que les getters soient triviaux, les setters sont difficiles (impossible?) Sans métaclasse
Gabriel Grant
4
@Gabriel Totalement correct. Je ne peux pas croire que personne n'ait signalé cela pendant deux ans.
agf
Je ne sais pas non plus pourquoi vous ne vous contentez pas d'utiliser self.fget(owner)et de supprimer le besoin d'utiliser un @classmethoddu tout ici? (c'est ce classmethod qui se traduit .__get__(instance, owner)(*args, **kwargs)par des function(owner, *args, **kwargs)appels, via un intermédiaire; les propriétés n'ont pas besoin de l'intermédiaire).
Martijn Pieters
Votre démonstration ne contient aucune transformation réelle dans le getter ou dans le setter qui démontrerait parfaitement que votre foo.var = 3affectation ne passe pas réellement par la propriété , et a simplement remplacé l'objet de propriété foopar un entier. Si vous avez ajouté des assert isinstance(foo.__dict__['var'], ClassProperty)appels entre vos assertions, vous verrez que l'échec après l' foo.var = 3exécution.
Martijn Pieters
1
Classes Python ne pas descripteur support obligatoire pour la mise sur la classe elle - même , que sur l' obtention (donc instance.attr, instance.attr = valueet del instance.attrse lient tous le descripteur trouvé sur type(instance), mais alors que classobj.attrse fixe, classobj.attr = valueet del classobj.attrne pas et au lieu ou supprimer l'objet descripteur lui - même). Vous avez besoin d'une métaclasse pour prendre en charge la définition et la suppression (faisant de l'objet de classe l'instance et de la métaclasse le type).
Martijn Pieters
56

J'espère que ce @classpropertydécorateur en lecture seule extrêmement simple aiderait quelqu'un à rechercher des propriétés 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
Denis Ryzhkov
la source
2
Cela fonctionne-t-il avec les sous-classes? (Une sous-classe peut-elle remplacer les propriétés de la classe?)
zakdances
1
Euh oui? class D(C): x = 2; assert D.x == 2
dtheodor
Je souhaite que cela fonctionne quand je l'utilise dans .formatcomme "{x}".format(**dict(self.__class__.__dict__, **self.__dict__)):(
2rs2ts
@Nathan Non seulement ... lorsque vous le définissez, vous remplacez tous les xaccès par 10. J'aime cette approche parce qu'elle est nette et simple mais sonne comme un antipatron
Michele d'Amico
Facilement corrigé: ajoutez un __set__qui lève un ValueErrorpour éviter le remplacement.
Kiran Jonnalagadda
30

Est-il possible d'utiliser la fonction property () avec des fonctions décorées par classmethod?

Non.

Cependant, une méthode de classe est simplement une méthode liée (une fonction partielle) sur une classe accessible à partir des instances de cette classe.

Puisque l'instance est une fonction de la classe et que vous pouvez dériver la classe de l'instance, vous pouvez obtenir le comportement souhaité d'une propriété de classe avec 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

Ce code peut être utilisé pour tester - il devrait réussir sans générer d'erreurs:

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')

Et notez que nous n'avons pas du tout besoin de métaclasses - et que vous n'accédez pas directement à une métaclasse via les instances de ses classes de toute façon.

écrire un @classpropertydécorateur

Vous pouvez en fait créer un classpropertydécorateur en seulement quelques lignes de code en sous-classant property(il est implémenté en C, mais vous pouvez voir l'équivalent Python ici ):

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

Traitez ensuite le décorateur comme s'il s'agissait d'une méthode de classe combinée à une propriété:

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

Et ce code devrait fonctionner sans erreur:

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()

Mais je ne sais pas à quel point cela serait bien avisé. Un ancien article de liste de diffusion suggère que cela ne devrait pas fonctionner.

Faire fonctionner la propriété sur la classe:

L'inconvénient de ce qui précède est que la "propriété de classe" n'est pas accessible depuis la classe, car elle écraserait simplement le descripteur de données de la classe __dict__.

Cependant, nous pouvons remplacer cela avec une propriété définie dans la métaclasse __dict__. Par exemple:

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

Et puis une instance de classe de la métaclasse pourrait avoir une propriété qui accède à la propriété de la classe en utilisant le principe déjà démontré dans les sections précédentes:

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

Et maintenant nous voyons à la fois l'instance

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

et la classe

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

avoir accès à la propriété de classe.

Salle Aaron
la source
3
Claire, concise, approfondie: telle devrait être la réponse acceptée.
pfabri
25

Python 3!

Ancienne question, beaucoup de points de vue, qui ont cruellement besoin d'une seule vraie méthode Python 3.

Heureusement, c'est facile avec le metaclasskwarg:

class FooProperties(type):

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

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

Ensuite, >>> Foo.var

«FOO!

OJFord
la source
1
c'est-à-dire qu'il n'y a pas de moyen simple de sortir de la boîte
mehmet
@mehmet N'est-ce pas simple? Fooest une instance de sa métaclasse, et @propertypeut être utilisée pour ses méthodes tout comme elle le peut pour celles des instances de Foo.
OJFord
2
vous deviez définir une autre classe pour une classe, soit le double de la complexité en supposant que la métaclasse ne soit pas réutilisable.
mehmet
Une méthode de classe fonctionne à la fois à partir de la classe et de l'instance. Cette propriété ne fonctionne qu'à partir de la classe. Je ne pense pas que ce soit ce qui est demandé.
Aaron Hall
1
@AaronHall Si c'est important, il est facile à ajouter Foo.__new__. Bien qu'à ce stade, cela puisse valoir la peine d'utiliser getattribute à la place, ou de se demander si prétendre qu'une fonctionnalité de langage existe est vraiment l'approche que vous voulez adopter.
OJFord
16

Il n'y a aucun moyen raisonnable de faire fonctionner ce système de «propriété de classe» en Python.

Voici une façon déraisonnable de le faire fonctionner. Vous pouvez certainement le rendre plus transparent avec des quantités croissantes de magie de métaclasse.

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

Le nœud du problème est que les propriétés sont ce que Python appelle des «descripteurs». Il n'y a pas de moyen court et facile d'expliquer comment ce type de métaprogrammation fonctionne, je dois donc vous indiquer le descripteur howto .

Vous n'avez besoin de comprendre ce genre de choses que si vous implémentez un cadre assez avancé. Comme une persistance d'objet transparente ou un système RPC, ou une sorte de langage spécifique à un domaine.

Cependant, dans un commentaire à une réponse précédente, vous dites que vous

besoin de modifier un attribut qui, d'une manière telle qu'il est vu par toutes les instances d'une classe, et dans la portée à partir de laquelle ces méthodes de classe sont appelées, n'a pas de références à toutes les instances de la classe.

Il me semble que vous voulez vraiment un modèle de conception Observer .

ddaa
la source
J'aime l'idée de l'exemple de code, mais il semble que ce serait un peu maladroit en pratique.
Mark Roddy
Ce que j'essaie d'accomplir, c'est de définir et d'obtenir un attribut unique qui est utilisé comme indicateur pour modifier le comportement de toutes les instances, donc je pense qu'Observer serait exagéré pour ce que j'essaie de faire. S'il y avait plusieurs attributs en question, je serais plus enclin.
Mark Roddy
Il semble que le simple fait de rendre les fonctions publiques et de les appeler directement était la solution simpliste. J'étais curieux de savoir si je faisais quelque chose de mal ou si j'essayais de faire était impossible. Désolé pour les multiples commentaires au fait. La limite de 300 caractères est nulle.
Mark Roddy
La chose intéressante à propos de l'exemple de code est que vous pouvez implémenter tous les bits maladroits une fois, puis en hériter. Déplacez simplement le _var dans la classe dérivée. class D1 (Foo): _var = 21 class D2 (Foo) _var = "Bonjour" D1.var 21 D2.var Bonjour
Thomas L Holaday
6

Le définir uniquement sur la méta-classe n'aide pas si vous souhaitez accéder à la propriété de classe via un objet instancié, dans ce cas, vous devez également installer une propriété normale sur l'objet (qui se distribue à la propriété de classe). Je pense que ce qui suit est un peu plus clair:

#!/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
Nils Philippsen
la source
3

Une demi-solution, __set__ sur la classe ne fonctionne toujours pas. La solution est une classe de propriété personnalisée implémentant à la fois une propriété et une méthode statique

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
Florian Bösch
la source
3

Parce que j'ai besoin de modifier un attribut qui d'une manière qui est vu par toutes les instances d'une classe, et dans la portée à partir de laquelle ces méthodes de classe sont appelées n'a pas de références à toutes les instances de la classe.

Avez-vous accès à au moins une instance de la classe? Je peux penser à un moyen de le faire alors:

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
John Millikin
la source
2

Essayez ceci, cela fait le travail sans avoir à changer / ajouter beaucoup de code existant.

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

La propertyfonction a besoin de deux callablearguments. donnez-leur des wrappers lambda (dont il passe l'instance comme premier argument) et tout va bien.

Soufian
la source
Comme le souligne Florian Bösch, la syntaxe requise (par les bibliothèques tierces ou le code hérité) est foo.var.
Thomas L Holaday
2

Voici une solution qui devrait fonctionner à la fois pour l'accès via la classe et l'accès via une instance qui utilise une métaclasse.

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

Cela fonctionne également avec un setter défini dans la métaclasse.

papercrane
la source
1

Après avoir recherché différents endroits, j'ai trouvé une méthode pour définir une propriété de classe valide avec Python 2 et 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)

J'espère que cela peut aider quelqu'un :)

non dévoilé
la source
1
Si cette méthode est quelque chose que vous avez trouvé quelque part, il serait bon de donner un lien (voir Comment référencer du matériel écrit par d'autres )
Andrew Myers
-27

Voici ma suggestion. N'utilisez pas de méthodes de classe.

Sérieusement.

Quelle est la raison d'utiliser des méthodes de classe dans ce cas? Pourquoi ne pas avoir un objet ordinaire d'une classe ordinaire?


Si vous souhaitez simplement modifier la valeur, une propriété n'est pas vraiment très utile, n'est-ce pas? Définissez simplement la valeur de l'attribut et en avez terminé.

Une propriété ne doit être utilisée que s'il y a quelque chose à cacher - quelque chose qui pourrait changer dans une implémentation future.

Peut-être que votre exemple est bien dépouillé et qu'il y a un calcul infernal que vous avez laissé de côté. Mais il ne semble pas que la propriété ajoute une valeur significative.

Les techniques de "confidentialité" influencées par Java (en Python, les noms d'attributs commençant par _) ne sont pas vraiment très utiles. Privé de qui? Le point de privé est un peu nébuleux lorsque vous avez la source (comme vous le faites en Python.)

Les getters et setters de style EJB influencés par Java (souvent réalisés en tant que propriétés en Python) sont là pour faciliter l'introspection primitive de Java ainsi que pour passer le rassemblement avec le compilateur de langage statique. Tous ces getters et setters ne sont pas aussi utiles en Python.

S.Lott
la source
14
Parce que j'ai besoin de modifier un attribut qui d'une manière qui est vu par toutes les instances d'une classe, et dans la portée à partir de laquelle ces méthodes de classe sont appelées n'a pas de références à toutes les instances de la classe.
Mark Roddy