如何在 Python 中创建只读类属性? [复制]

Posted

技术标签:

【中文标题】如何在 Python 中创建只读类属性? [复制]【英文标题】:How to create a read-only class property in Python? [duplicate] 【发布时间】:2011-03-13 07:26:06 【问题描述】:

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

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

然后我希望以下工作:

>>> foo.number
4

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

【问题讨论】:

【参考方案1】:

这将使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

【讨论】:

@unutbu :是否可以执行相反的操作:即在不重建 Foo 类的情况下使 Foo.number 可写。 @user2284570:您可以通过将Foo 的元类从MetaFoo 更改为其他内容来使Foo.number 可写。您可以将number 设置为可设置属性,或者干脆不提及它,在这种情况下设置Foo.number 将使Foo.number 成为普通类属性。我在上面添加了一些代码来说明我的意思。 @unutbu : 我能把它的元类改回来吗?这对代码对象有用吗?【参考方案2】:

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

如果这不是您想要的,您可以编写一个始终使用类对象 (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

【讨论】:

这不是只读属性。 Python 描述符故意不使用 __set____delete__ 方法拦截对 setattrdelattr 的类级调用。所以Foo.number = 6 仍然可以工作。【参考方案3】:

我同意unubtu's answer;它似乎有效,但是,它不适用于 Python 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

【讨论】:

【参考方案4】:

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

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

如您所见,@classproperty 的工作方式与 @property 用于类变量的方式相同。我们唯一需要的是额外的 @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://***.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

注意:代码没有经过太多测试,如果没有按预期工作,请随时注意。

【讨论】:

【参考方案5】:

米哈伊尔·格拉西莫夫的解决方案相当完整。不幸的是,这是一个缺点。如果您有一个类使用他的类属性,则没有子类可以使用它,因为 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://***.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

【讨论】:

以上是关于如何在 Python 中创建只读类属性? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

如何在 python 抽象类中创建抽象属性

如何在没有后备表的 Rails 中创建只读模型

在 C++ 中创建只读(公共)类成员

是否可以在 SOAP Web 服务中创建只读元素?

Flutter:如何在我的 Style 类中创建构造函数? [复制]

如何在 python 中创建列表? [复制]