Python类型提示友好类型,限制可能的值

7tofc5zh  于 12个月前  发布在  Python
关注(0)|答案(2)|浏览(124)

我想要一个python类型提示友好的方式来创建一个具有约束范围的值的类型。
例如,基于str类型的 URL 类型只接受看起来像“http”URL的字符串。

# this code is made up and will not compile
class URL(typing.NewType('_URL', str)):
    def __init__(self, value: str, *args, **kwargs):
        if not (value.startswith('http://') or value.startswith('https://')):
            raise ValueError('string is not an acceptable URL')

字符串

gfttwv5a

gfttwv5a1#

覆盖内置不可变类型效果良好

覆盖str; http URL字符串

下面是一个覆盖str的例子。这不需要typing模块,但仍然可以使用类型提示。
这个str派生类Assert初始化的字符串看起来像一个http URL字符串。

class URL(str):
    def __new__(cls, *value):
        if value:
            v0 = value[0]
            if not type(v0) is str:
                raise TypeError('Unexpected type for URL: "%s"' % type(v0))
            if not (v0.startswith('http://') or v0.startswith('https://')):
                raise ValueError('Passed string value "%s" is not an'
                                 ' "http*://" URL' % (v0,))
        # else allow None to be passed. This allows an "empty" URL instance, e.g. `URL()`
        # `URL()` evaluates False

        return str.__new__(cls, *value)

字符串
这将导致一个类只允许某些字符串,否则,它的行为就像一个不可变的str示例。

# these are okay
URL()
URL('http://example.com')
URL('https://example.com')
URL('https://')

# these raise ValueError
URL('example')  # ValueError: Passed string value "example" is not an "http*://" URL
URL('')  # ValueError: Passed string value "" is not an "http*://" URL

# these evaluate as you would expect
for url in (URL(),  # 'False'
            URL('https://'),  # 'True'
            URL('https://example.com'),  # 'True'
           ):
    print('True') if url else print('False')


(更新:后来我发现了purl Python库)
另一个例子,

覆盖int;限制整数范围Number

这个int派生类只允许值19(包括19)。
这也有一个特殊的功能。如果一个示例被初始化为空(Number()),那么这个值就等于0(这个行为是从int类派生的)。在这种情况下,__str__应该是'.'(程序要求)。

class Number(int):
    """integer type with constraints; part of a Sudoku game"""

    MIN = 1  # minimum
    MAX = 9  # maximum

    def __new__(cls, *value):
        if value:
            v0 = int(value[0])
            if not (cls.MIN <= v0 <= cls.MAX):
                raise ValueError('Bad value "%s" is not acceptable in'
                                 ' Sudoku' % (v0,))
        # else:
        #    allow None to be passed. This allows an "empty" Number instance that
        #    evaluates False, e.g. `Number()`

        return int.__new__(cls, *value)

    def __str__(self):
        """print the Number accounting for an "empty" value"""
        if self == 0:
            return '.'
        return int.__str__(self)


这确保错误的输入被尽早处理,否则,它的行为就像一个int

# these are okay
Number(1)
Number(9)
Number('9')

# this will evaluate True, just like an int
Number(9) == int(9)
Number('9') == int(9)
Number('9') == float(9)

# this is okay, it will evaluate False
Number()
print('True') if Number() else print('False')  # 'False'

# these raise ValueError
Number(0)  # ValueError: Bad value "0" is not acceptable in Sudoku
Number(11)  # ValueError: Bad value "11" is not acceptable in Sudoku
Number('11')  # ValueError: Bad value "11" is not acceptable in Sudoku


而特别的“功能”

print(Number(1)) # '1' (expected)
print(Number())  # '.' (special feature)

overriding int ; class RomanNumeral

同样的技术可以用来创建一个“罗马数字”类,它接受从0到10的值,写成Roman Numerals。通过重载__new__,传递的value可以与传递给底层int.__new__int.__init__的文字值相关。

from typing_extensions import Self

class RomanNumeral(int):
    """integer type that takes Roman Numerals strings from one to ten"""

    _value_s: str
    """value stored as Roman Numeral uppercase"""

    def __new__(cls, *value) -> Self:
        if value is None or not len(value):
            raise ValueError("RomanNumeral requires a string value, given None")
        val = value[0]
        if not isinstance(val, str):
            raise TypeError("value must be type str, given %s" % type(val))
        val_u = val.upper()
        if val_u == "I":
            rn = super().__new__(cls, 1)
        elif val_u == "II":
            rn = super().__new__(cls, 2)
        elif val_u == "III":
            rn = super().__new__(cls, 3)
        elif val_u == "IV":
            rn = super().__new__(cls, 4)
        elif val_u == "V":
            rn = super().__new__(cls, 5)
        elif val_u == "VI":
            rn = super().__new__(cls, 6)
        elif val_u == "VII":
            rn = super().__new__(cls, 7)
        elif val_u == "VIII":
            rn = super().__new__(cls, 8)
        elif val_u == "IX":
            rn = super().__new__(cls, 9)
        elif val_u == "X":
            rn = super().__new__(cls, 10)
        else:
            raise ValueError("Unknown Roman Numeral value %r" % val_u)
        rn._value_s = val_u
        return rn

    def __str__(self):
        return self._value_s


技术inheriting immutable types is derived from this SO answer.

des4xlb0

des4xlb02#

内建类型的子类化可能会导致一些奇怪的情况(考虑严格检查type(...) is str的代码)
这里有一个纯类型的方法,它是类型安全的,并且完全保留了字符串的类型:

from typing import NewType

_Url = NewType('_Url', str)

def URL(s: str) -> _Url:
    if not s.startswith('https://'):
        raise AssertionError(s)
    return _Url(s)

print(type(URL('https://example.com')) is str)  # prints `True`

字符串
这里的方法将运行时检查“隐藏”在一个函数后面,从API的Angular 来看,这个函数看起来像一个构造函数,但实际上只是一个tiny type(我找不到对“微小类型”的规范引用,这似乎是我能找到的最好的资源)。

相关问题