如何在Python中创建只读类属性?

How to create a read-only class property in Python?

基本上我想做这样的事情:

1
2
3
4
5
6
class foo:
    x = 4
    @property
    @classmethod
    def number(cls):
        return x

那么,我想让以下内容发挥作用:

1
2
>>> foo.number
4

不幸的是,上述方法行不通。不是给我4,而是给我。有没有办法达到上述目标?


这将使Foo.number成为只读属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MetaFoo(type):
    @property
    def number(cls):
        return cls.x

class Foo(object, metaclass=MetaFoo):
    x = 4

print(Foo.number)
# 4

Foo.number = 6
# AttributeError: can't set attribute

说明:使用@property时的常见情况如下:

1
2
3
4
5
class Foo(object):
    @property
    def number(self):
        ...
foo = Foo()

Foo中定义的属性对于其实例是只读的。也就是说,foo.number = 6将增加AttributeError

类似地,如果希望Foo.number提升AttributeError,则需要设置type(Foo)中定义的属性。因此需要一个元类。

请注意,这种只读并不能免除黑客的攻击。通过更改foo的可写属性班级:

1
2
3
4
5
6
class Base(type): pass
Foo.__class__ = Base

# makes Foo.number a normal class attribute
Foo.number = 6  
print(Foo.number)

印刷品

1
6

或者,如果您希望使Foo.number成为可设置财产,

1
2
3
4
5
6
7
8
9
10
11
12
class WritableMetaFoo(type):
    @property
    def number(cls):
        return cls.x
    @number.setter
    def number(cls, value):
        cls.x = value
Foo.__class__ = WritableMetaFoo

# Now the assignment modifies `Foo.x`
Foo.number = 6  
print(Foo.number)

也打印6。


当从类访问时(即当instance在其__get__方法中是None时),property描述符总是返回自身。

如果这不是您想要的,您可以编写一个始终使用类对象(owner而不是实例的新描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class classproperty(object):
...     def __init__(self, getter):
...         self.getter= getter
...     def __get__(self, instance, owner):
...         return self.getter(owner)
...
>>> class Foo(object):
...     x= 4
...     @classproperty
...     def number(cls):
...         return cls.x
...
>>> Foo().number
4
>>> Foo.number
4


我同意Unubtu的回答;但是,它似乎可以工作,但是它不能与Python3上的这种精确语法一起工作(特别是,Python3.4正是我所要解决的问题)。下面是必须如何在Python3.4下形成模式才能使事情正常进行,看起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MetaFoo(type):
   @property
   def number(cls):
      return cls.x

class Foo(metaclass=MetaFoo):
   x = 4

print(Foo.number)
# 4

Foo.number = 6
# AttributeError: can't set attribute

米哈伊尔·杰拉西莫夫的解相当完整。不幸的是,这是一个缺点。如果有一个类使用其ClassProperty,则任何子类都不能使用它,因为TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its basesclass Wrapper一起。

幸运的是,这是可以修复的。创建class Meta时,只从给定类的元类继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def classproperty_support(cls):
 """
  Class decorator to add metaclass to our class.
  Metaclass uses to add descriptors to class attributes, see:
  http://stackoverflow.com/a/26634248/1113207
 """

  # Use type(cls) to use metaclass of given class
  class Meta(type(cls)):
      pass

  for name, obj in vars(cls).items():
      if isinstance(obj, classproperty):
          setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel))

  class Wrapper(cls, metaclass=Meta):
      pass
  return Wrapper

上述解决方案的问题在于,它无法从实例变量访问类变量:

1
2
3
4
5
6
print(Foo.number)
# 4

f = Foo()
print(f.number)
# 'Foo' object has no attribute 'number'

此外,使用元类显式并不像使用常规的property修饰器那样好。

我试图解决这个问题。以下是它的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@classproperty_support
class Bar(object):
    _bar = 1

    @classproperty
    def bar(cls):
        return cls._bar

    @bar.setter
    def bar(cls, value):
        cls._bar = value


# @classproperty should act like regular class variable.
# Asserts can be tested with it.
# class Bar:
#     bar = 1


assert Bar.bar == 1

Bar.bar = 2
assert Bar.bar == 2

foo = Bar()
baz = Bar()
assert foo.bar == 2
assert baz.bar == 2

Bar.bar = 50
assert baz.bar == 50
assert foo.bar == 50

如您所见,我们有与类变量的@property工作方式相同的@classproperty。我们唯一需要的是额外的@classproperty_support类装饰器。

解决方案也适用于只读类属性。

实现方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class classproperty:
   """
    Same as property(), but passes obj.__class__ instead of obj to fget/fset/fdel.
    Original code for property emulation:
    https://docs.python.org/3.5/howto/descriptor.html#properties
   """

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj.__class__)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj.__class__, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj.__class__)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)


def classproperty_support(cls):
   """
    Class decorator to add metaclass to our class.
    Metaclass uses to add descriptors to class attributes, see:
    http://stackoverflow.com/a/26634248/1113207
   """

    class Meta(type):
        pass

    for name, obj in vars(cls).items():
        if isinstance(obj, classproperty):
            setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel))

    class Wrapper(cls, metaclass=Meta):
        pass
    return Wrapper

注意:代码测试不多,如果不能如您所期望的那样工作,请随时注意。