如何创建类属性?
在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 个回答
这个回答是基于 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 调度器:
class Foo(object, metaclass=MetaFoo):
_thingy = 23
@property
def thingy(self):
return type(self).thingy
现在 Foo().thingy
对于类和实例都能按预期工作!如果派生类替换了它的基础 _thingy
,它也会继续正常工作(这正是我最初开始寻找这个解决方案的原因)。
这对我来说并不是 100% 令人满意——在元类和对象类中都要进行设置,感觉有点违反了 DRY 原则。但后者只是一个一行的调度器;我对它的存在大致上是可以接受的,如果你真的想的话,可能还可以把它简化成一个 lambda 函数之类的。
如果你按照下面的方式定义 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
则会直接覆盖掉这个属性对象。
这是我会这样做的:
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
的时候,setter 没有起作用,因为我们实际上是在调用 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)
现在一切都会正常了。