Как сделать свойство класса? - PullRequest
101 голосов
/ 04 марта 2011

В 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

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

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

Ответы [ 9 ]

73 голосов
/ 04 марта 2011

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

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)

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

33 голосов
/ 04 марта 2011

Если вы определите 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 перезапишет сам объект свойства.

24 голосов
/ 04 марта 2011

Я думаю, вы можете сделать это с метаклассом.Так как метакласс может быть как класс для класса (если это имеет смысл).Я знаю, что вы можете присвоить метаклассу метод __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__, а остальное тоже самое.

17 голосов
/ 07 августа 2016

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

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

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 dispatchers:

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

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

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

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

4 голосов
/ 28 февраля 2016

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

Я обнаружил, что работает следующий метод. Определите метакласс со всеми необходимыми свойствами класса и установщиками. То есть я хотел класс со свойством 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, вы можете определить метакласс внутри тела класса. Таким образом, он не определен в области видимости модуля.

1 голос
/ 16 июля 2019

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

1 голос
/ 04 марта 2011

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

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 QA о реализации общего состояния в Python.

0 голосов
/ 05 июля 2019
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

0 голосов
/ 04 января 2019

Мне довелось найти решение, очень похожее на @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)
...