Python 枚举如何强制只读访问?

use*_*544 5 python enums

据我所知,在Python中,常量是由大写变量名隐含的,但不是强制的。我还了解,通过从 Enum 子类化,类可以实现枚举类型,从而防止单个枚举值被覆盖。

在我看来,这两个习惯用法似乎有些不一致,我想知道 Enum 解决方案如何强制对变量值进行只读访问。

sha*_*ker 4

有两个 Python 功能可以实现这一点:

  1. 魔术方法
  2. 元类

我将在整个答案中使用类型注释,希望使代码更清晰,但请记住它们不是必需的,并且在运行时没有效果。

魔术方法

Python 语言提供了几个可以自定义对象行为的“钩子”。

例如,如果一个对象有一个__str__方法,则内置函数str()会调用该方法来获取结果。此类方法被 Python 社区称为“魔术方法”或“dunders”(“双下划线”的缩写),并在https://docs.python.org/3/reference/datamodel.html中的各个位置进行了记录。

令人惊讶的是,Python 提供了一个__setattr__钩子方法,每当在对象上设置属性时就会调用该方法。例子:

from typing import Any

class Talkative:
    def __setattr__(self, name: str, value: Any) -> None:
        print(f"Setting attribute {name!r} on {self!r}: {value!r}")
        super().__setattr__(name, value)

obj = Talkative()
obj.a = "xyz"
obj.b = -1
Run Code Online (Sandbox Code Playgroud)

输出:

Setting attribute 'a' on <__main__.Talkative object at 0x104ea3250>: 'xyz'
Setting attribute 'b' on <__main__.Talkative object at 0x104ea3250>: -1
Run Code Online (Sandbox Code Playgroud)

但是,此__setattr__自定义适用于类的实例Talkative,但不适用于Talkative类对象本身。例如,以下内容不会产生输出,因为__setattr__未调用该方法:

Talkative.idea = "lightbulb"
Run Code Online (Sandbox Code Playgroud)

Python 类本身只是类的常规 Python 对象type

assert isinstance(Talkative, type)
Run Code Online (Sandbox Code Playgroud)

作为对象,类本身可以具有方法、属性等。重要的是,这包括像__setattr__. 所以Talkative类本身只是一个对象,它可以有自己的__setattr__方法。

我们通常的定义方法的机制在这里不起作用。我们需要一种机制来在类对象本身而不是类的实例上定义自定义方法。为此,我们使用元类。

元类

“元类”是类中的类。(其他地方已经写了很多关于它们的文章,所以我不会在这里详细介绍。)

通常,类是 的实例type,但我们可以定义自己的自定义子类type该子类实现我们想要的任何自定义行为。在本例中,我们需要__setattr__元类中的一个方法来自定义类对象本身的设置属性。

class TalkativeType(type):
    def __setattr__(cls, name: str, value: Any) -> None:
        print(f"Setting attribute {name!r} on {cls!r}: {value!r}")
        super().__setattr__(name, value)

class Talkative(metaclass=TalkativeType):
    pass

Talkative.q = "bert"
Run Code Online (Sandbox Code Playgroud)

输出:

Setting attribute 'q' on <class '__main__.Talkative'>: 'bert'
Run Code Online (Sandbox Code Playgroud)

请注意,我们使用参数名称cls而不是self, 来指示我们正在操作类,而不是类的实例。这个参数的名称与Python无关,所以我们可以使用任何我们喜欢的名称。

给高级读者的旁注

为什么我们不能手动将自定义函数绑定到该类Talkative

例如,您可以尝试以下操作:

from types import MethodType

class Talkative2:
    pass

def talkative_setattr(self, name, value):
    print(f"Setting attribute {name!r} on {self!r}: {value!r}")
    object.__setattr__(self, name, value)

Talkative2.__setattr__ = MethodType(talkative_setattr, Talkative2)
Thing.q = "bert"
Run Code Online (Sandbox Code Playgroud)

原因是没有使用正常的属性机制来查找 dunders。它们直接在正在操作的对象的类上查找,而不是在对象实例本身上查找。因此,类型Talkative2本身必须提供特殊的__setattr__功能,而 Talkative2.__setattr__属性则被忽略。在本例中, 的类Talkative2只是type,它没有方法__setattr__,尽管确实Talkative2__setattr__方法,因此会产生默认行为。

非常感谢 Python Discord 服务器的这些成员帮助我理解本节的详细信息:L3viathangodlygeeklakmatiol

把它放在一起

如果我们查看 Python 源代码,我们可以准确地看到这两种技术的使用:一个元类EnumType,它Enum是一个实例,它实现了一个__setattr__方法:

https://github.com/python/cpython/blob/2305ca51448552542b2414186252123a8dc87db7/Lib/enum.py#L837-L848

class EnumType(type):
    """
    Metaclass for Enum
    """

    ...

    def __setattr__(cls, name, value):
        """
        Block attempts to reassign Enum members.

        A simple assignment to the class namespace only changes one of the
        several possible ways to get an Enum member from the Enum class,
        resulting in an inconsistent Enumeration.
        """
        member_map = cls.__dict__.get('_member_map_', {})
        if name in member_map:
            raise AttributeError('cannot reassign member %r' % (name, ))
        super().__setattr__(name, value)

...

class Enum(metaclass=EnumType):
Run Code Online (Sandbox Code Playgroud)

这要归功于jonrsharpe链接到源代码的良好判断力。