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.
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.
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.
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.
[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 para.__dict__['x']
, Puistype(a).__dict__['x']
, puis dans les classes de base detype(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.
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.
Si vous utilisez Django, il a un construit en @classproperty
décorateur.
from Django.utils.decorators import classproperty
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.
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
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
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)