Есть ли способ указать диапазон допустимых значений для аргумента функции с указанием типа в python?

#python #python-3.x

#python #python-3.x

Вопрос:

Я большой поклонник подсказки типа в python, однако мне любопытно, есть ли способ указать допустимый диапазон значений для данного параметра с помощью подсказки типа.

Я имел в виду что-то вроде

 from typing import *

def function(
        number: Union[float, int],
        fraction: Float[0.0, 1.0] = 0.5 # give a hint that this should be between 0 and 1,
):
    return fraction * number

 

Я могу себе представить, что можно применить это с помощью утверждения или, возможно, указать, какой допустимый диапазон значений находится в строке документа, но кажется, что что-то вроде Float[0.0, 1.0] выглядело бы более элегантно.

Комментарии:

1. Очевидным решением было бы создать подкласс Float с более конкретным типом, который отклоняет значения, выходящие за пределы допустимого диапазона.

2. @tripleee Это звучит как интересное предложение. Не могли бы вы опубликовать примерный пример того, как это может выглядеть?

Ответ №1:

Представлен Python 3.9 typing.Annotated :

 In [75]: from typing import *

In [76]: from dataclasses import dataclass

In [77]: @dataclass
    ...: class ValueRange:
    ...:     min: float
    ...:     max: float
    ...:

In [78]: def function(
    ...:         number: Union[float, int],
    ...:         fraction: Annotated[float, ValueRange(0.0, 1.0)] = 0.5
    ...: ):
    ...:     return fraction * number
    ...:
 

Как и любая другая подсказка типа, она не выполняет никаких проверок во время выполнения:

 In [79]: function(1, 2)
Out[79]: 2
 

Однако вы можете реализовать свои собственные проверки во время выполнения. Приведенный ниже код является лишь примером, он не охватывает все случаи и, вероятно, является излишним для вашей простой функции:

 In [88]: import inspect

In [89]: @dataclass
    ...: class ValueRange:
    ...:     min: float
    ...:     max: float
    ...:
    ...:     def validate_value(self, x):
    ...:         if not (self.min <= x <= self.max):
    ...:             raise ValueError(f'{x} must be in range [{self.min}, {self.max}]')
    ...:

In [90]: def check_annotated(func):
    ...:     hints = get_type_hints(func, include_extras=True)
    ...:     spec = inspect.getfullargspec(func)
    ...:
    ...:     def wrapper(*args, **kwargs):
    ...:         for idx, arg_name in enumerate(spec[0]):
    ...:             hint = hints.get(arg_name)
    ...:             validators = getattr(hint, '__metadata__', None)
    ...:             if not validators:
    ...:                 continue
    ...:             for validator in validators:
    ...:                 validator.validate_value(args[idx])
    ...:
    ...:         return func(*args, **kwargs)
    ...:     return wrapper
    ...:
    ...:

In [91]: @check_annotated
    ...: def function_2(
    ...:         number: Union[float, int],
    ...:         fraction: Annotated[float, ValueRange(0.0, 1.0)] = 0.5
    ...: ):
    ...:     return fraction * number
    ...:
    ...:

In [92]: function_2(1, 2)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-92-c9345023c025> in <module>
----> 1 function_2(1, 2)

<ipython-input-90-01115cb628ba> in wrapper(*args, **kwargs)
     10                 continue
     11             for validator in validators:
---> 12                 validator.validate_value(args[idx])
     13
     14         return func(*args, **kwargs)

<ipython-input-87-7f4ac07379f9> in validate_value(self, x)
      6     def validate_value(self, x):
      7         if not (self.min <= x <= self.max):
----> 8             raise ValueError(f'{x} must be in range [{self.min}, {self.max}]')
      9

ValueError: 2 must be in range [0.0, 1.0]

In [93]: function_2(1, 1)
Out[93]: 1