Как создать свойство класса только для чтения в Python? - PullRequest
62 голосов
/ 08 июля 2010

По сути, я хочу сделать что-то вроде этого:

class foo:
    x = 4
    @property
    @classmethod
    def number(cls):
        return x

Тогда мне бы хотелось, чтобы сработало следующее:

>>> foo.number
4

К сожалению, вышесказанное не работает. Вместо того, чтобы дать мне 4, он дает мне <property object at 0x101786c58>. Есть ли способ достичь вышеуказанного?

Ответы [ 5 ]

62 голосов
/ 08 июля 2010

Это сделает Foo.number доступным только для чтения свойство:

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

Объяснение : обычный сценарий при использовании @property выглядит следующим образомthis:

class Foo(object):
    @property
    def number(self):
        ...
foo = Foo()

Свойство, определенное в Foo, доступно только для чтения для своих экземпляров.То есть, foo.number = 6 вызовет AttributeError.

Аналогично, если вы хотите, чтобы Foo.number поднял AttributeError, вам необходимо установить свойство, определенное в type(Foo).Отсюда необходимость метакласса.


Обратите внимание, что эта доступность только для чтения не защищена от хакеров.Свойство можно сделать доступным для записи, изменив класс Foo:

class Base(type): pass
Foo.__class__ = Base

# makes Foo.number a normal class attribute
Foo.number = 6   
print(Foo.number)

print

6

или, если вы хотите сделать Foo.number настраиваемым свойством,

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)

также печатает 6.

42 голосов
/ 08 июля 2010

Дескриптор property всегда возвращает себя, когда к нему обращаются из класса (то есть, когда instance равен None в его методе __get__).

Если это не то, что вам нужно, вы можете написать новый дескриптор, который всегда использует объект класса (owner) вместо экземпляра:

>>> 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
15 голосов
/ 29 октября 2014

Я согласен с ответом unubtu ;кажется, что он работает, однако он не работает с этим точным синтаксисом на Python 3 (в частности, Python 3.4 - это то, с чем я боролся)Вот как нужно формировать шаблон в Python 3.4, чтобы все заработало, кажется:

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
8 голосов
/ 24 мая 2018

Решение Михаила Герасимова вполне завершено. К сожалению, это был один недостаток. Если у вас есть класс, использующий его classproperty, ни один дочерний класс не может использовать его из-за TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases с class Wrapper.

К счастью, это можно исправить. Просто наследуйте от метакласса данного класса при создании 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
6 голосов
/ 26 февраля 2016

Проблема с решениями выше состоит в том, что это не будет работать для доступа к переменным класса из переменной экземпляра:

print(Foo.number)
# 4

f = Foo()
print(f.number)
# 'Foo' object has no attribute 'number'

Более того, использование явного метакласса не так приятно, как использование обычного property декоратора.

Я пытался решить эту проблему. Вот как это работает сейчас:

@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

Как видите, у нас есть @classproperty, который работает так же, как и @property для переменных класса. Нам понадобится только дополнительный декоратор класса @classproperty_support.

Решение также работает для свойств класса только для чтения.

Вот реализация:

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

Примечание: код не проверен много, не стесняйтесь заметить, если он не работает, как вы ожидаете.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...