Essentiellement, je veux faire quelque chose comme ça:
class foo:
x = 4
@property
@classmethod
def number(cls):
return x
Ensuite, je voudrais que les éléments suivants fonctionnent:
>>> foo.number
4
Malheureusement, ce qui précède ne fonctionne pas. Au lieu de me donner 4
ça me donne <property object at 0x101786c58>
. Existe-t-il un moyen d'atteindre ces objectifs?
Le descripteur property
renvoie toujours lui-même lorsqu'il est accessible à partir d'une classe (c'est-à-dire lorsque instance
est None
dans son __get__
méthode).
Si ce n'est pas ce que vous voulez, vous pouvez écrire un nouveau descripteur qui utilise toujours l'objet classe (owner
) au lieu de l'instance:
>>> class classproperty(object):
... def __init__(self, getter):
... self.getter= getter
... def __get__(self, instance, owner):
... return self.getter(owner)
...
>>> class Foo(object):
... x= 4
... @classproperty
... def number(cls):
... return cls.x
...
>>> Foo().number
4
>>> Foo.number
4
Cela fera de Foo.number
Une propriété en lecture seule :
class MetaFoo(type):
@property
def number(cls):
return cls.x
class Foo(object, metaclass=MetaFoo):
x = 4
print(Foo.number)
# 4
Foo.number = 6
# AttributeError: can't set attribute
Explication: Le scénario habituel lors de l'utilisation de @property ressemble à ceci:
class Foo(object):
@property
def number(self):
...
foo = Foo()
Une propriété définie dans Foo
est en lecture seule par rapport à ses instances. Autrement dit, foo.number = 6
Lèverait un AttributeError
.
De même, si vous voulez que Foo.number
Élève un AttributeError
, vous devrez configurer une propriété définie dans type(Foo)
. D'où la nécessité d'une métaclasse.
Notez que cette lecture seule n'est pas à l'abri des pirates. La propriété peut être rendue accessible en écriture en changeant la classe de Foo:
class Base(type): pass
Foo.__class__ = Base
# makes Foo.number a normal class attribute
Foo.number = 6
print(Foo.number)
impressions
6
ou, si vous souhaitez faire de Foo.number
une propriété définissable,
class WritableMetaFoo(type):
@property
def number(cls):
return cls.x
@number.setter
def number(cls, value):
cls.x = value
Foo.__class__ = WritableMetaFoo
# Now the assignment modifies `Foo.x`
Foo.number = 6
print(Foo.number)
imprime également 6.
Je suis d'accord avec réponse d'unubt ; il semble fonctionner, cependant, il ne fonctionne pas avec cette syntaxe précise sur Python 3 (spécifiquement, Python 3.4 est ce avec quoi j'ai lutté. Voici comment on doit former le motif sous Python 3.4 pour que les choses fonctionnent, il semble:
class MetaFoo(type):
@property
def number(cls):
return cls.x
class Foo(metaclass=MetaFoo):
x = 4
print(Foo.number)
# 4
Foo.number = 6
# AttributeError: can't set attribute
La solution de Mikhail Gerasimov est assez complète. Malheureusement, c'était un inconvénient. Si vous avez une classe utilisant sa propriété de classe, aucune classe enfant ne peut l'utiliser en raison d'une TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
avec class Wrapper
.
Heureusement, cela peut être corrigé. Héritez simplement de la métaclasse de la classe donnée lors de la création de class Meta
.
def classproperty_support(cls):
"""
Class decorator to add metaclass to our class.
Metaclass uses to add descriptors to class attributes, see:
http://stackoverflow.com/a/26634248/1113207
"""
# Use type(cls) to use metaclass of given class
class Meta(type(cls)):
pass
for name, obj in vars(cls).items():
if isinstance(obj, classproperty):
setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel))
class Wrapper(cls, metaclass=Meta):
pass
return Wrapper
Le problème avec les solutions ci-dessus est qu'il ne fonctionnerait pas pour accéder aux variables de classe à partir d'une variable d'instance:
print(Foo.number)
# 4
f = Foo()
print(f.number)
# 'Foo' object has no attribute 'number'
De plus, l'utilisation de la métaclasse explicite n'est pas aussi agréable que l'utilisation du décorateur property
normal.
J'ai essayé de résoudre ces problèmes. Voici comment cela fonctionne maintenant:
@classproperty_support
class Bar(object):
_bar = 1
@classproperty
def bar(cls):
return cls._bar
@bar.setter
def bar(cls, value):
cls._bar = value
# @classproperty should act like regular class variable.
# Asserts can be tested with it.
# class Bar:
# bar = 1
assert Bar.bar == 1
Bar.bar = 2
assert Bar.bar == 2
foo = Bar()
baz = Bar()
assert foo.bar == 2
assert baz.bar == 2
Bar.bar = 50
assert baz.bar == 50
assert foo.bar == 50
Comme vous le voyez, nous avons @classproperty
qui fonctionne de la même manière que @property
pour les variables de classe. La seule chose dont nous aurons besoin est un supplément de @classproperty_support
décorateur de classe.
La solution fonctionne également pour les propriétés de classe en lecture seule.
Voici la mise en œuvre:
class classproperty:
"""
Same as property(), but passes obj.__class__ instead of obj to fget/fset/fdel.
Original code for property emulation:
https://docs.python.org/3.5/howto/descriptor.html#properties
"""
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
raise AttributeError("unreadable attribute")
return self.fget(obj.__class__)
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("can't set attribute")
self.fset(obj.__class__, value)
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("can't delete attribute")
self.fdel(obj.__class__)
def getter(self, fget):
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
return type(self)(self.fget, self.fset, fdel, self.__doc__)
def classproperty_support(cls):
"""
Class decorator to add metaclass to our class.
Metaclass uses to add descriptors to class attributes, see:
http://stackoverflow.com/a/26634248/1113207
"""
class Meta(type):
pass
for name, obj in vars(cls).items():
if isinstance(obj, classproperty):
setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel))
class Wrapper(cls, metaclass=Meta):
pass
return Wrapper
Remarque: le code n'est pas beaucoup testé, n'hésitez pas à noter s'il ne fonctionne pas comme prévu.