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

本质上,我想做这样的事情:

class foo:
x = 4
@property
@classmethod
def number(cls):
return x

那么,我希望以下几个方面能够发挥作用:

>>> foo.number
4

不幸的是,上述方法不起作用。而不是给我 4它给我 <property object at 0x101786c58>。有没有办法达到上述目的?

41579 次浏览

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

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的通常情况是这样的:

class Foo(object):
@property
def number(self):
...
foo = Foo()

Foo中定义的属性对于它的实例是只读的。也就是说,foo.number = 6将引发一个 AttributeError

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


请注意,这种只读性并不能免受黑客攻击。 可以通过更改 Foo 的 班级:

class Base(type): pass
Foo.__class__ = Base


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

指纹

6

或者,如果您希望使 Foo.number成为可设置的属性,

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)而不是实例:

>>> 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 的回答; 它似乎可以工作,但是在 巨蟒3上不能使用这种精确的语法(特别是 Python 3.4)。下面是我们必须在 Python 3.4下构建模式的方法,看起来是这样的:

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

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

print(Foo.number)
# 4


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

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

我试图解决这个问题,现在它是这样运作的:

@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类装饰器。

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

以下是实施方案:

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

注意: 代码没有经过很多测试,如果它不能像您期望的那样工作,请随时注意。

米哈伊尔 · 格拉西莫夫的解是相当完整的。不幸的是,这是一个缺点。如果您有一个使用其类属性的类,则没有子类可以使用它 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时继承给定类的元类即可。

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