Python-тип-дружественный тип, который ограничивает возможные значения - PullRequest
0 голосов
/ 10 февраля 2019

Мне нужен удобный для Python способ создания типа, который имеет ограниченный диапазон значений.

Например, URL Тип основан на типе str, который будет толькопринимать строки, которые выглядят как URL "http".

# 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')

Ответы [ 2 ]

0 голосов
/ 11 февраля 2019

Подклассы встроенных типов могут привести к некоторым странным случаям (рассмотрим код, который проверяет ровно 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, но в действительности это просто крошечный тип (я не смог найти каноническийссылка на «крошечные типы», кажется, это лучший ресурс, который я смог найти).

0 голосов
/ 10 февраля 2019

переопределение встроенных неизменяемых типов работает хорошо

переопределение str;http URL strings

Вот пример переопределения str.Для этого не требуется модуль typing, но он по-прежнему работает с подсказками типов.

Этот производный класс str утверждает, что инициализированная строка выглядит как строка URL-адреса http.

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)

Thisв результате получается класс, который допускает только некоторые строки.В противном случае он ведет себя как неизменный экземпляр 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')

(обновление: позже я обнаружил библиотеку Python purl )

Другой пример:

overriding int;ограниченный целочисленный диапазон Number

Этот производный класс int допускает только значения от 1 до 9 включительно.

Также имеется специальная функция.Если экземпляр инициализируется ничем (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)


Техника для наследования неизменяемых типов происходит из этого ответа SO .

...