如何创建类属性?[重复]
- 2025-01-03 08:40:00
- admin 原创
- 93
问题描述:
在 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
当我们调用时,setter 没有工作 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:
[答案基于 python 3.4;元类语法在 2 中有所不同,但我认为该技术仍然有效]
您可以使用元类来实现这一点...大多数情况下。Dappawit 几乎可以实现这一点,但我认为它有一个缺陷:
class MetaFoo(type):
@property
def thingy(cls):
return cls._thingy
class Foo(object, metaclass=MetaFoo):
_thingy = 23
这会让你获得 Foo 上的 classproperty,但是有一个问题......
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 原则。但后者只是一个单行调度程序;我基本可以接受它的存在,如果你真的想要的话,你可以将它压缩为 lambda 或其他东西。
解决方案 4:
如果您使用 Django,它有一个内置的@classproperty
装饰器。
from django.utils.decorators import classproperty
对于 Django 4,使用:
from django.utils.functional import classproperty
解决方案 5:
我认为您可以使用元类来实现这一点。因为元类可以像类的类一样(如果这有意义的话)。我知道您可以__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__
,其余操作相同。
解决方案 6:
据我所知,如果不创建新的元类,就无法为类属性编写设置器。
我发现以下方法有效。定义一个元类,其中包含您想要的所有类属性和设置器。例如,我想要一个带有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 样式,您实际上可以在类主体内定义元类。这样它就不会在模块范围内定义。
解决方案 7:
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
bar = Bar()
assert Bar.bar==1
Bar.bar=2
assert bar.bar==2
nbar = Bar()
assert nbar.bar==2
解决方案 8:
我碰巧想出了一个与@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)
解决方案 9:
如果您只需要延迟加载,那么您只需一个类初始化方法。
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设计模式。这里有一个关于在 Python 中实现共享状态的不错的 SO QA 。