如何创建扩展具有输入验证的 `int` 基 class 的 class?

How to create a class that extends the `int` base class that has input validation?

我想做一个 class 来扩展 int 基础 class,这样对象本身就是一个整数(即你设置它并直接从中读取), 但也有输入验证——例如,只允许给定的范围。

根据我的研究,当您扩展普通基础 classes 时,不会调用 __init__ 方法,所以我不确定该怎么做。我也不清楚您如何访问对象的值(即分配给它的实际整数)或从 class 中修改该值。我看到扩展任何基础 class(字符串、浮点数、元组、列表等)的相同问题。

如果我可以使用 __init__ 它会是这样的:

class MyInt(int):
    def __init__(self, value):
        if value < 0 or value > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(value))
        self = value

如何验证进入我的扩展 int class 的新值?

在研究了 MarkM's suggested link 中的 __new__ 函数之后,该方法将满足我的要求,但我也在质疑它是否是正确的方法。请参阅下面的讨论和解决方案部分。


关于子class基础不可变类型的讨论

但是,我也质疑是否需要这样做,至少对于简单的输入验证检查。 subclass 一个不可变基 class 的好处是你可以获得不可变的所有好处,比如内置哈希、字符串表示等,它们直接从基 [=] 继承43=]。但是,只需将 属性 添加到另一个 class,添加 setter 函数并使其不可变,您就可以获得相同的好处。

subclassing 的优势在于,如果您想要所有具有相同输入验证的不可变对象 and/or 可以应用于许多不同 classes/modules 的附加方法,而无需创建一个单独的 class 将它们转换为可变对象的额外复杂性,然后需要额外级别的 属性 或方法访问(即你从 [=38 创建一个 "myint" 对象=] Class,但需要 "value" 属性 或类似的东西来访问底层 int,例如 myint.value 而不仅仅是 myint).


解决方案/实施

因此,可能有更好的方法来执行此操作,但为了回答我自己的问题,这是我编写的测试脚本,至少可以使它正常工作。

注意int can have multiple arguments,在将字符串解释为特定的基数时,如('1111', 2),会将二进制字符串'1111'转换为十进制的15。基数可以输入为也是一个 kwarg,因此需要将 *args 和 **kwargs 完全传递给 int __new__ 函数。

此外,我最终在进行验证之前先调用了 int,这样它会在尝试验证之前先将浮点数和字符串转换为 int。

请注意,由于 MyInt subclass 是 int,因此您必须 return 一个 int 值 - 您不能 return a "None" 失败(尽管您可以return 一个 0 或 -1)。这导致我提出一个 ValueError 并在主脚本中处理错误。

class MyInt(int):

    def __new__(cls, *args, **kwargs):
        value = super().__new__(cls, *args, **kwargs)
        argstr = ', '.join([str(arg) for arg in args]) # Only for prototype test
        print('MyInt({}); Returned {}'.format(argstr, value)) # Only for prototype test
        if value < 0 or value > 100:
            raise ValueError('  ERROR!! Out of range! Must be 0-100, was {}'.format(value))
        return value


if __name__ == '__main__':
    myint = MyInt('1111', 2)  # Test conversion from binary string, base 2
    print('myint = {} (type: {})\n'.format(myint, type(myint)))

    for index, arg in enumerate([-99, -0.01, 0, 0.01, 0.5, 0.99, 1.5, 100, 100.1, '101']):
        try:
            a = MyInt(arg)
        except ValueError as ex:
            print(ex)
            a = None
        finally:
            print('  a : {} = {}'.format(type(a), a))

在这种情况下,您实际上不必覆盖 __new__。对象创建后,会调用__init__,可以判断是否在范围内。

class MyInt(int):
    def __init__(self, x, **kwargs):
        if self < 0 or self > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(x))

您可以覆盖 __new__ 以在 MyInt(...) returns 新对象之前引发异常。

class MyInt(int):
    def __new__(cls, *args, **kwargs):
        x = super().__new__(cls, *args, **kwargs)  # Let any value error propagate
        if x < 0 or x > 100:
            raise ValueError('MyInt objects must be in range 0-100, value was {}'.format(x))
        return x

您可能想在 调用 super().__new__ 之前尝试验证参数 ,但严格来说,这与其他 类 的表现不佳。

使用 GetAttr 制作普通 Class 对象 return 包装 "Private" 属性

这是我 运行 的另一个答案,值得在这里输入。由于它被埋在一个答案的末尾,这个答案不是对一个有点不同的问题的公认答案,所以我在这里重新发布它。

我一直在为许多案例寻找这个特定的替代方案,而它确实是!!似乎在直接查询任何对象时,在设计 class 时应该有某种方式可以根据您的需要进行回复。这就是这个答案的作用。

因此,例如,您可以使用普通 class,但查询 class 对象 return int。这允许对象将 int 存储在一个单独的变量中,因此它是可变的,但是通过不需要访问特定字段,只需直接查询对象(即 myint = MyInt(1); print(myint)

首先请注意他的建议,以便 真正地 考虑为什么您不能先 class 适当的类型(即使用此处的其他答案之一)。


备用答案

这是从this Whosebug answer. All credit to Francis Avila复制的。这是使用 list 而不是 int 作为 subclass,但同样的想法:


  1. 看看this guide to magic method use in Python
  2. 如果您想要的非常类似于列表,请证明您不 classing list 的原因。如果 subclassing 不合适,您可以委托给包装列表实例:
class MyClass(object):
    def __init__(self):
        self._list = []
    def __getattr__(self, name):
        return getattr(self._list, name)

    # __repr__ and __str__ methods are automatically created
    # for every class, so if we want to delegate these we must
    # do so explicitly
    def __repr__(self):
        return "MyClass(%s)" % repr(self._list)
    def __str__(self):
        return "MyClass(%s)" % str(self._list)

This will now act like a list without being a list (i.e., without subclassing `list`).
```sh
>>> c = MyClass()
>>> c.append(1)
>>> c
MyClass([1])