Как создать свойство класса?
В 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
Это лучший из всех ответов:
"Метасвойство" добавляется в класс, так что оно все еще будет свойством экземпляра.
- Не нужно переопределять вещь в любом из классов
- Свойство работает как "свойство класса" как для экземпляра, так и для класса
- У вас есть возможность настроить наследование _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
.