在声明 typing.NamedTuple 对象时提供额外的约束

provide additional constraints when declaring typing.NamedTuple object

使用 typing.NamedTuple object,对它的声明方式实施额外约束的最佳方法是什么?

假设我有一个本科 class 学生有一个专业,但我想强制执行 'undeclared' 对于专业来说是一个不可接受的值。

from typing import NamedTuple

class Undergraduate(NamedTuple):
    name: str
    major: str

    def check_major(self):
        if self.major == "undeclared":
            raise ValueError("must declare a major")

if __name__ == "__main__":
    u1 = Undergraduate("Jane", "computer science") # no errors
    u1.check_major() # no errors
    u2 = Undergraduate("John", "undeclared") # no errors
    u2.check_major() # ValueError

这很好用,但我希望每次声明一个新对象时 check_major() 到 运行,即:

u1 = Undergraduate("John", "undeclared") # immediate ValueError raised

仅使用 NamedTuple 是否可行(我知道如何使用传统的 classes 实现)?

注意:我读了这个related question。这些解决方案提供了一些可行的解决方案,但与 OP 一样,我希望能够实例化对象而无需调用额外的 class 方法。

NamedTuple 保护 __init____new__ 在声明时不被替换。但是,它们可以在 class 创建后被替换。

class Radial2D(NamedTuple):
      angle: float
      length: float

      def _verify_attributes_(self, *args):
          if self.length < 0 or not 0 < self.angle < 360:
             raise ValueError('Arguments out of range')

Radial2D.__init__ = Radial2D._verify_attributes_
print(Radial2D(90, 15.5))  # Radial2D(angle=90, length=15.5)
print(Radial2D(12, -5))    # ValueError: Arguments out of range

可以使用 class 装饰器简化此模式:

from typing import Type, NamedTuple


def verify(tp: Type[NamedTuple]):
    verifier = tp._verify_attributes_
    tp.__init__ = verifier
    return tp


@verify
class Undergraduate(NamedTuple):
    name: str
    major: str

    def _verify_attributes_(self, *args):
        if self.major == "undeclared":
            raise ValueError("must declare a major")


print(Undergraduate("Jane", "computer science"))  # Undergraduate(name='Jane', major='computer science')
print(Undergraduate("John", "undeclared"))        # ValueError: must declare a major