web-dev-qa-db-fra.com

Comment créer une propriété de classe en lecture seule en Python?

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?

63
Björn Pollex

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

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.

64
unutbu

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
16
Viktor Haag

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
8

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.

7
Mikhail Gerasimov