Как создать свойство класса?

В python я могу добавить метод в класс с декоратором @classmethod. Есть ли аналогичный декоратор для добавления свойства в класс? Я могу лучше показать, о чем я говорю.

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

Является ли синтаксис, который я использовал выше, или ему потребуется нечто большее?

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

Ответ 1

Вот как я это сделаю:

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

Сеттер не работал в то время, когда мы вызываем Bar.bar, потому что мы вызываем TypeOfBar.bar.__set__, который не является Bar.bar.__set__.

Добавление определения метакласса решает следующее:

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)

Теперь все будет хорошо.

Ответ 2

Если вы определяете classproperty следующим образом, то ваш пример работает точно так, как вы просили.

class classproperty(object):
    def __init__(self, f):
        self.f = f
    def __get__(self, obj, owner):
        return self.f(owner)

Предостережение заключается в том, что вы не можете использовать это для доступных для записи свойств. Пока e.I = 20 поднимет AttributeError, Example.I = 20 перезапишет сам объект свойства.

Ответ 3

Я думаю, что вы можете сделать это с помощью метакласса. Поскольку метакласс может быть как класс для класса (если это имеет смысл). Я знаю, что вы можете назначить метод __call__() для метакласса, чтобы переопределить вызов класса, MyClass(). Интересно, работает ли аналогичный декор property на метаклассе. (Я не пробовал это раньше, но теперь мне любопытно...)

[Обновление:]

Ничего себе, он работает:

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

Примечание. Это в Python 2.7. Python 3+ использует другую технику для объявления метакласса. Используйте: class MyClass(metaclass=MetaClass):, удалите __metaclass__, а остальное то же самое.

Ответ 4

[ответ написан на основе python 3.4; синтаксис метакласса отличается в 2, но я думаю, что техника все равно будет работать]

Вы можете сделать это с помощью метакласса... в основном. Даппавит почти работает, но я думаю, что у него есть недостаток:

class MetaFoo(type):
    @property
    def thingy(cls):
        return cls._thingy

class Foo(object, metaclass=MetaFoo):
    _thingy = 23

Это дает вам классное свойство в Foo, но есть проблема...

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

Что, черт возьми, здесь происходит? Почему я не могу получить свойство класса из экземпляра?

Я довольно долго бил головой об этом, прежде чем найти то, что я считаю ответом. Python @properties - это подмножество дескрипторов, и из описательная документация (выделение мое):

Поведение по умолчанию для доступа к атрибутам - это получение, установка или удаление атрибут из словаря объектов. Например, a.x имеет цепочку поиска начиная с a.__dict__['x'], затем type(a).__dict__['x'] и продолжая через базовые классы type(a) исключая метаклассы.

Таким образом, порядок разрешения метода не включает наши свойства класса (или что-либо еще, определенное в метаклассе). Можно сделать подкласс встроенного декоратора свойств, который ведет себя по-другому, но (цитата). У меня есть впечатление, что у разработчиков есть веская причина (что я не понимаю) для этого.

Это не значит, что нам не повезло; мы можем получить доступ к свойствам самого класса просто... и мы можем получить класс от type(self) внутри экземпляра, который мы можем использовать для создания диспетчеров @property:

class Foo(object, metaclass=MetaFoo):
    _thingy = 23

    @property
    def thingy(self):
        return type(self).thingy

Теперь Foo().thingy работает как для класса, так и для экземпляров! Он также будет продолжать делать правильные вещи, если производный класс заменяет его базовый _thingy (который является прецедентом, который первоначально получил меня на этой охоте).

Это не на 100% удовлетворяет мне - делать настройку как в метаклассе, так и в объектном классе, похоже, что это нарушает принцип DRY. Но последний - это просто диспетчер с одной строкой; Я в основном согласен с этим, и вы, вероятно, могли бы сжать его до лямбды или что-то еще, если бы вы действительно хотели.

Ответ 5

Насколько я могу судить, нет способа написать сеттер для свойства класса без создания нового метакласса.

Я обнаружил, что работает следующий метод. Определите метакласс со всеми свойствами и настройками класса, которые вы хотите. IE, мне нужен класс с свойством title с установщиком. Вот что я написал:

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

Теперь сделайте фактический класс, который вы хотите, как обычно, за исключением того, что он использует метакласс, который вы создали выше.

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

Немного странно определять этот метакласс, как мы делали выше, если мы будем использовать его только в одном классе. В этом случае, если вы используете стиль Python 2, вы можете определить метакласс внутри тела класса. Таким образом, он не определен в области модуля.

Ответ 6

Если вам нужна только ленивая загрузка, тогда у вас может быть только метод инициализации класса.

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

Но метаклассовый подход кажется более чистым и более предсказуемым.

Возможно, вы ищете шаблон дизайна Singleton. Там хороший SO Q о реализации общего состояния в Python.

Ответ 7

Я случайно нашел решение, очень похожее на @Andrew, только 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

Это лучший из всех ответов:

"Метасвойство" добавляется в класс, так что оно все еще будет свойством экземпляра.

  1. Не нужно переопределять вещь в любом из классов
  2. Свойство работает как "свойство класса" как для экземпляра, так и для класса
  3. У вас есть возможность настроить наследование _thingy

В моем случае я фактически настроил _thingy чтобы он был разным для каждого ребенка, без определения его в каждом классе (и без значения по умолчанию) с помощью:

   def __new__(mc1, name, bases, nmspc):
       nmspc.update({'thingy': MetaFoo.services, '_thingy': None})
       return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc)

Ответ 8

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

Ответ 9

Если вы используете Django, у него есть встроенный декоратор @classproperty.