如何创建类属性?

223 投票
9 回答
301153 浏览
提问于 2025-04-16 12:58

在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 个回答

50

这个回答是基于 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 函数之类的。

66

如果你按照下面的方式定义 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 则会直接覆盖掉这个属性对象。

129

这是我会这样做的:

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)

现在一切都会正常了。

撰写回答