Я ищу эффективный способ проверки переменных функции python. Например, я хотел бы проверить тип и значение аргументов. Есть ли модуль для этого? Или я должен использовать что-то вроде декораторов или какой-либо конкретной идиомы?
Ответ 2
В этом расширенном ответе мы реализуем специфический для Python 3.x декоратор проверки типа, основанный на подсказках типа PEP 484 -style, содержащих менее 275 строк чистого Python (большинство из которых - пояснительные строки документации и комментарии ) - в значительной степени оптимизированы для реального промышленного применения в сочетании с набором тестов py.test
-driven, в котором используются все возможные крайние варианты.
Насладитесь неожиданным набором символов bear typing:
>>> @beartype
... def spirit_bear(kermode: str, gitgaata: (str, int)) -> tuple:
... return (kermode, gitgaata, "Moksgm'ol", 'Ursus americanus kermodei')
>>> spirit_bear(0xdeadbeef, 'People of the Cane')
AssertionError: parameter kermode=0xdeadbeef not of <class "str">
Как показывает этот пример, медвежья типизация явно поддерживает проверку типов параметров и возвращаемых значений, аннотированных как простые типы или кортежи таких типов. Golly!
О.К., это на самом деле не впечатляет. @beartype
напоминает любой другой специфический для Python 3.x декоратор проверки типа, основанный на подсказках типа PEP 484 -style в менее чем 275 строках чистого Python. Так что за руб, баб?
Pure Bruteforce Hardcore Efficiency
Типизация Bear значительно эффективнее как в пространстве, так и во времени, чем все существующие реализации проверки типов в Python, насколько мне известно из моих ограниченных областей знаний. (Подробнее об этом позже.)
Однако в Python эффективность обычно не имеет значения. Если бы это было так, вы бы не использовали Python. Действительно ли проверка типов отклоняется от устоявшейся нормы избегания преждевременной оптимизации в Python? Да. Да, это так.
Рассмотрим профилирование, которое добавляет неизбежные накладные расходы к каждой интересующей профилированной метрике (например, вызовам функций, строкам). Чтобы гарантировать точные результаты, эти издержки уменьшаются за счет использования оптимизированных расширений C (например, расширения _lsprof
C, используемых модулем cProfile
), а не неоптимизированного чистого Python (например, модуля profile
). Эффективность действительно имеет значение при профилировании.
Проверка типов ничем не отличается. Проверка типа добавляет накладные расходы к каждому типу вызова функции, проверяемому вашим приложением - в идеале, все они. Чтобы не дать добросовестным (но, к сожалению, недалеким) коллегам убрать проверку типов, которую вы молча добавили после прошлой пятницы с добавлением кофеина allnighter в старое веб-приложение Django, предназначенное для пожилых людей, проверка типов должна быть быстрой. Так быстро что никто не замечает этого там, когда вы добавляете это, никому не говоря I do this all the time! Stop reading this if you are a coworker.
Если даже нелепой скорости недостаточно для вашего прожорливого приложения, однако, медвежья типизация может быть глобально отключена путем включения оптимизации Python (например, путем передачи опции -O
интерпретатору Python):
$ python3 -O
# This succeeds only when type checking is optimized away. See above!
>>> spirit_bear(0xdeadbeef, 'People of the Cane')
(0xdeadbeef, 'People of the Cane', "Moksgm'ol", 'Ursus americanus kermodei')
Просто потому что. Добро пожаловать на печатную машинку.
Что за...? Почему "медведь"? Ты Шея Борода, верно?
Медвежья типизация - это проверка типа с нуля, то есть проверка типа, максимально приближенная к ручному подходу проверки типов в Python. Медведь типизации предназначен не налагать никаких потерь производительности, ограничений совместимости или сторонних зависимостей (в любом случае, сверх того, что навязывается ручным подходом). Печатание медведей может быть без проблем интегрировано в существующие кодовые базы и тестовые наборы.
Все, наверное, знакомы с ручным подходом. Вы вручную assert
передаете каждый параметр и/или возвращаете значение, возвращаемое каждой функцией в вашей кодовой базе. Какой шаблон может быть проще или банальнее? Мы все видели это сто раз в googleplex раз, и каждый раз, когда мы это делали, нас рвало по рту. Повторение быстро стареет. СУХОЙ, лет
Приготовь рвотные мешки. Для краткости предположим, что упрощенная функция easy_spirit_bear()
принимает только один параметр str
. Вот как выглядит ручной подход:
def easy_spirit_bear(kermode: str) -> str:
assert isinstance(kermode, str), 'easy_spirit_bear() parameter kermode={} not of <class "str">'.format(kermode)
return_value = (kermode, "Moksgm'ol", 'Ursus americanus kermodei')
assert isinstance(return_value, str), 'easy_spirit_bear() return value {} not of <class "str">'.format(return_value)
return return_value
Python 101, верно? Многие из нас прошли этот класс.
Медвежья типизация извлекает проверку типов, выполненную вручную с помощью вышеуказанного подхода, в динамически определенную функцию-обертку, автоматически выполняющую те же проверки - с дополнительным преимуществом повышения гранулярных TypeError
, а не неоднозначных AssertionError
исключений. Вот как выглядит автоматизированный подход:
def easy_spirit_bear_wrapper(*args, __beartype_func=easy_spirit_bear, **kwargs):
if not (
isinstance(args[0], __beartype_func.__annotations__['kermode'])
if 0 < len(args) else
isinstance(kwargs['kermode'], __beartype_func.__annotations__['kermode'])
if 'kermode' in kwargs else True):
raise TypeError(
'easy_spirit_bear() parameter kermode={} not of {!r}'.format(
args[0] if 0 < len(args) else kwargs['kermode'],
__beartype_func.__annotations__['kermode']))
return_value = __beartype_func(*args, **kwargs)
if not isinstance(return_value, __beartype_func.__annotations__['return']):
raise TypeError(
'easy_spirit_bear() return value {} not of {!r}'.format(
return_value, __beartype_func.__annotations__['return']))
return return_value
Это многословно. Но это также в основном * так же быстро, как и ручной подход. * Squinting suggested.
Обратите внимание на полное отсутствие проверки или итерации функции в функции-обертке, которая содержит такое же количество тестов, что и исходная функция, хотя и с дополнительными (возможно незначительными) затратами на проверку того, передаются ли проверяемые параметры и как текущий вызов функции. Вы не можете выиграть каждую битву.
Могут ли такие функции-обертки действительно генерироваться для проверки типов произвольных функций менее чем в 275 строках чистого Python? Снейк Плисскин говорит: "Правдивая история. Есть дым?"
И да. У меня может быть шея.
Нет, Срсли Почему "медведь"?
Медведь бьет утку. Утка может летать, но медведь может бросить лосося в утку. In Canada, nature can surprise you.
Следующий вопрос.
Что же так горячо в медведях?
Существующие решения не выполняют проверку типа "голый металл" - по крайней мере, ни одно из них я не рассмотрел. Все они итеративно повторно проверяют сигнатуру проверенной на тип функции в при каждом вызове функции. Несмотря на то, что затраты на повторный контроль незначительны для одного вызова, они обычно ничтожны, когда агрегируются по всем вызовам. Действительно, действительно ничтожно мало.
Однако это касается не просто эффективности. Существующие решения также часто не учитывают общие крайние случаи. Это включает в себя большинство, если не все игрушечные декораторы, предоставленные как ответы stackoverflow здесь и в других местах. Классические ошибки включают в себя:
- Не удается набрать контрольные ключевые аргументы и/или возвращаемые значения (например, sweeneyrod
@checkargs
decorator).
- Неспособность поддерживать кортежи (т.е. объединения) типов, принятых встроенным
isinstance()
.
- Не удалось распространить имя, строку документации и другие идентифицирующие метаданные из исходной функции в функцию-оболочку.
- Неспособность предоставить хотя бы видимость модульных тестов. (Вид критического.)
- Повышение общих исключений
AssertionError
, а не конкретных исключений TypeError
при неудачных проверках типов. Для детализации и разумности проверка типов никогда не должна вызывать общих исключений.
Медведь печатает успешно там, где не медведи терпят неудачу. Все один, все медведь!
Медведь печатает без обнаженного
Типографский набор переносит пространственные и временные затраты на проверку сигнатур функций от времени вызова функции до времени определения функции, то есть от функции-оболочки, возвращаемой декоратором @beartype
, в сам декоратор. Поскольку декоратор вызывается только один раз для каждого определения функции, эта оптимизация дает радость для всех.
Печатание с помощью медведя - это попытка съесть свой торт для проверки типа и съесть его. Для этого @beartype
:
- Проверяет подпись и аннотации исходной функции.
- Динамически создает тело типа функции-оболочки, проверяя исходную функцию. Даааа Python-код, генерирующий Python-код.
- Динамически объявляет эту функцию-обертку через встроенную функцию
exec()
.
- Возвращает эту функцию-обертку.
А не ___ ли нам? Давай погрузимся в глубокий конец.
# If the active Python interpreter is *NOT* optimized (e.g., option "-O" was
# *NOT* passed to this interpreter), enable type checking.
if __debug__:
import inspect
from functools import wraps
from inspect import Parameter, Signature
def beartype(func: callable) -> callable:
'''
Decorate the passed **callable** (e.g., function, method) to validate
both all annotated parameters passed to this callable _and_ the
annotated value returned by this callable if any.
This decorator performs rudimentary type checking based on Python 3.x
function annotations, as officially documented by PEP 484 ("Type
Hints"). While PEP 484 supports arbitrarily complex type composition,
this decorator requires _all_ parameter and return value annotations to
be either:
* Classes (e.g., 'int', 'OrderedDict').
* Tuples of classes (e.g., '(int, OrderedDict)').
If optimizations are enabled by the active Python interpreter (e.g., due
to option '-O' passed to this interpreter), this decorator is a noop.
Raises
----------
NameError
If any parameter has the reserved name '__beartype_func'.
TypeError
If either:
* Any parameter or return value annotation is neither:
* A type.
* A tuple of types.
* The kind of any parameter is unrecognized. This should _never_
happen, assuming no significant changes to Python semantics.
'''
# Raw string of Python statements comprising the body of this wrapper,
# including (in order):
#
# * A "@wraps" decorator propagating the name, docstring, and other
# identifying metadata of the original function to this wrapper.
# * A private "__beartype_func" parameter initialized to this function.
# In theory, the "func" parameter passed to this decorator should be
# accessible as a closure-style local in this wrapper. For unknown
# reasons (presumably, a subtle bug in the exec() builtin), this is
# not the case. Instead, a closure-style local must be simulated by
# passing the "func" parameter to this function at function
# definition time as the default value of an arbitrary parameter. To
# ensure this default is *NOT* overwritten by a function accepting a
# parameter of the same name, this edge case is tested for below.
# * Assert statements type checking parameters passed to this callable.
# * A call to this callable.
# * An assert statement type checking the value returned by this
# callable.
#
# While there exist numerous alternatives (e.g., appending to a list or
# bytearray before joining the elements of that iterable into a string),
# these alternatives are either slower (as in the case of a list, due to
# the high up-front cost of list construction) or substantially more
# cumbersome (as in the case of a bytearray). Since string concatenation
# is heavily optimized by the official CPython interpreter, the simplest
# approach is (curiously) the most ideal.
func_body = '''
@wraps(__beartype_func)
def func_beartyped(*args, __beartype_func=__beartype_func, **kwargs):
'''
# "inspect.Signature" instance encapsulating this callable signature.
func_sig = inspect.signature(func)
# Human-readable name of this function for use in exceptions.
func_name = func.__name__ + '()'
# For the name of each parameter passed to this callable and the
# "inspect.Parameter" instance encapsulating this parameter (in the
# passed order)...
for func_arg_index, func_arg in enumerate(func_sig.parameters.values()):
# If this callable redefines a parameter initialized to a default
# value by this wrapper, raise an exception. Permitting this
# unlikely edge case would permit unsuspecting users to
# "accidentally" override these defaults.
if func_arg.name == '__beartype_func':
raise NameError(
'Parameter {} reserved for use by @beartype.'.format(
func_arg.name))
# If this parameter is both annotated and non-ignorable for purposes
# of type checking, type check this parameter.
if (func_arg.annotation is not Parameter.empty and
func_arg.kind not in _PARAMETER_KIND_IGNORED):
# Validate this annotation.
_check_type_annotation(
annotation=func_arg.annotation,
label='{} parameter {} type'.format(
func_name, func_arg.name))
# String evaluating to this parameter annotated type.
func_arg_type_expr = (
'__beartype_func.__annotations__[{!r}]'.format(
func_arg.name))
# String evaluating to this parameter current value when
# passed as a keyword.
func_arg_value_key_expr = 'kwargs[{!r}]'.format(func_arg.name)
# If this parameter is keyword-only, type check this parameter
# only by lookup in the variadic "**kwargs" dictionary.
if func_arg.kind is Parameter.KEYWORD_ONLY:
func_body += '''
if {arg_name!r} in kwargs and not isinstance(
{arg_value_key_expr}, {arg_type_expr}):
raise TypeError(
'{func_name} keyword-only parameter '
'{arg_name}={{}} not a {{!r}}'.format(
{arg_value_key_expr}, {arg_type_expr}))
'''.format(
func_name=func_name,
arg_name=func_arg.name,
arg_type_expr=func_arg_type_expr,
arg_value_key_expr=func_arg_value_key_expr,
)
# Else, this parameter may be passed either positionally or as
# a keyword. Type check this parameter both by lookup in the
# variadic "**kwargs" dictionary *AND* by index into the
# variadic "*args" tuple.
else:
# String evaluating to this parameter current value when
# passed positionally.
func_arg_value_pos_expr = 'args[{!r}]'.format(
func_arg_index)
func_body += '''
if not (
isinstance({arg_value_pos_expr}, {arg_type_expr})
if {arg_index} < len(args) else
isinstance({arg_value_key_expr}, {arg_type_expr})
if {arg_name!r} in kwargs else True):
raise TypeError(
'{func_name} parameter {arg_name}={{}} not of {{!r}}'.format(
{arg_value_pos_expr} if {arg_index} < len(args) else {arg_value_key_expr},
{arg_type_expr}))
'''.format(
func_name=func_name,
arg_name=func_arg.name,
arg_index=func_arg_index,
arg_type_expr=func_arg_type_expr,
arg_value_key_expr=func_arg_value_key_expr,
arg_value_pos_expr=func_arg_value_pos_expr,
)
# If this callable return value is both annotated and non-ignorable
# for purposes of type checking, type check this value.
if func_sig.return_annotation not in _RETURN_ANNOTATION_IGNORED:
# Validate this annotation.
_check_type_annotation(
annotation=func_sig.return_annotation,
label='{} return type'.format(func_name))
# Strings evaluating to this parameter annotated type and
# currently passed value, as above.
func_return_type_expr = (
"__beartype_func.__annotations__['return']")
# Call this callable, type check the returned value, and return this
# value from this wrapper.
func_body += '''
return_value = __beartype_func(*args, **kwargs)
if not isinstance(return_value, {return_type}):
raise TypeError(
'{func_name} return value {{}} not of {{!r}}'.format(
return_value, {return_type}))
return return_value
'''.format(func_name=func_name, return_type=func_return_type_expr)
# Else, call this callable and return this value from this wrapper.
else:
func_body += '''
return __beartype_func(*args, **kwargs)
'''
# Dictionary mapping from local attribute name to value. For efficiency,
# only those local attributes explicitly required in the body of this
# wrapper are copied from the current namespace. (See below.)
local_attrs = {'__beartype_func': func}
# Dynamically define this wrapper as a closure of this decorator. For
# obscure and presumably uninteresting reasons, Python fails to locally
# declare this closure when the locals() dictionary is passed; to
# capture this closure, a local dictionary must be passed instead.
exec(func_body, globals(), local_attrs)
# Return this wrapper.
return local_attrs['func_beartyped']
_PARAMETER_KIND_IGNORED = {
Parameter.POSITIONAL_ONLY, Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD,
}
'''
Set of all 'inspect.Parameter.kind' constants to be ignored during
annotation- based type checking in the '@beartype' decorator.
This includes:
* Constants specific to variadic parameters (e.g., '*args', '**kwargs').
Variadic parameters cannot be annotated and hence cannot be type checked.
* Constants specific to positional-only parameters, which apply to non-pure-
Python callables (e.g., defined by C extensions). The '@beartype'
decorator applies _only_ to pure-Python callables, which provide no
syntactic means of specifying positional-only parameters.
'''
_RETURN_ANNOTATION_IGNORED = {Signature.empty, None}
'''
Set of all annotations for return values to be ignored during annotation-
based type checking in the '@beartype' decorator.
This includes:
* 'Signature.empty', signifying a callable whose return value is _not_
annotated.
* 'None', signifying a callable returning no value. By convention, callables
returning no value are typically annotated to return 'None'. Technically,
callables whose return values are annotated as 'None' _could_ be
explicitly checked to return 'None' rather than a none-'None' value. Since
return values are safely ignorable by callers, however, there appears to
be little real-world utility in enforcing this constraint.
'''
def _check_type_annotation(annotation: object, label: str) -> None:
'''
Validate the passed annotation to be a valid type supported by the
'@beartype' decorator.
Parameters
----------
annotation : object
Annotation to be validated.
label : str
Human-readable label describing this annotation, interpolated into
exceptions raised by this function.
Raises
----------
TypeError
If this annotation is neither a new-style class nor a tuple of
new-style classes.
'''
# If this annotation is a tuple, raise an exception if any member of
# this tuple is not a new-style class. Note that the "__name__"
# attribute tested below is not defined by old-style classes and hence
# serves as a helpful means of identifying new-style classes.
if isinstance(annotation, tuple):
for member in annotation:
if not (
isinstance(member, type) and hasattr(member, '__name__')):
raise TypeError(
'{} tuple member {} not a new-style class'.format(
label, member))
# Else if this annotation is not a new-style class, raise an exception.
elif not (
isinstance(annotation, type) and hasattr(annotation, '__name__')):
raise TypeError(
'{} {} neither a new-style class nor '
'tuple of such classes'.format(label, annotation))
# Else, the active Python interpreter is optimized. In this case, disable type
# checking by reducing this decorator to the identity decorator.
else:
def beartype(func: callable) -> callable:
return func
И Лейчек сказал: пусть @beartype
быстро проведет проверку типов: и так было.
Предостережения, проклятия и пустые обещания
Ничто не совершенно. Даже медведь печатает.
Предупреждение I: значения по умолчанию сняты
Медвежья типография не печатает, проверяет непазданные параметры, присвоенные значения по умолчанию Теоретически это возможно. Но не в 275 строках или меньше, и, конечно, не в качестве ответа на переполнение стека.
Безопасное (... возможно, совершенно небезопасное) предположение состоит в том, что разработчики функций утверждают, что они знали, что делали, когда определяли значения по умолчанию. Поскольку значения по умолчанию, как правило, являются константами (... им лучше быть!), Перепроверка типов констант, которые никогда не меняются при каждом вызове функции, которому назначено одно или несколько значений по умолчанию, противоречила бы основному принципу медвежьей типизации: "Не повторять себя снова и снова и снова и снова. "
Покажи мне неправильно, и я засыплю тебя голосами.
Предостережение II: Нет PEP 484
PEP 484 ("Типовые подсказки") формализовало использование аннотаций функций, впервые введенных в PEP 3107 ("Аннотации функций"). Python 3.5 поверхностно поддерживает эту формализацию с помощью нового модуля typing
верхнего уровня, стандартного API для составления произвольно сложных типов из более простых типов (например, Callable[[Arg1Type, Arg2Type], ReturnType]
, типа, описывающего функцию, принимающую два аргумента: введите Arg1Type
и Arg2Type
и верните значение типа ReturnType
).
Медведь печатать не поддерживает ни один из них. Теоретически это возможно. Но не в 275 или менее строках и, конечно, не в качестве ответа на переполнение стека.
Однако типизация медведей поддерживает объединения типов так же, как встроенная функция isinstance()
поддерживает объединения типов: как кортежи. Это внешне соответствует типу typing.Union
- с очевидным предостережением, что typing.Union
поддерживает произвольно сложные типы, в то время как кортежи, принятые в @beartype
, поддерживают только простые классы. В мою защиту 275 строк.
Тесты или не случилось
Вот суть gist этого. Get it, gist? I'll stop now.
Как и в случае самого декоратора @beartype
, эти тесты py.test
могут быть без проблем интегрированы в существующие тестовые наборы. Драгоценно, не так ли?
Теперь об обязательной напыщенной шее бороды никто не просил.
История насилия API
Python 3.5 не обеспечивает реальной поддержки использования типов PEP 484. ват?
Это правда: нет проверки типов, нет вывода типов, нет типов nuthin '. Вместо этого ожидается, что разработчики будут регулярно запускать все свои кодовые базы через сторонние обертки интерпретатора CPython, реализующие факсимиле такой поддержки (например, mypy). Конечно, эти обертки навязывают:
- A штраф за совместимость. Как признается в официальном FAQ по Dpy official mypy FAQ в ответ на часто задаваемый вопрос "Могу ли я использовать mypy для проверки типа моего существующего кода Python?": "Зависит от. Совместимость довольно хорошая, но некоторые функции Python еще не реализованы или не полностью поддерживаются ". последующий ответ на часто задаваемые вопросы разъясняет эту несовместимость, утверждая, что:
- "... ваш код должен делать атрибуты явными и использовать явное представление протокола". Grammar police see your "a explicit" and raise you an implicit frown.
- "Mypy будет поддерживать модульную, эффективную проверку типов, и это, по-видимому, исключает проверку типов в некоторых языковых функциях, таких как произвольное добавление методов во время выполнения. Однако вполне вероятно, что многие из этих функций будут поддерживаться в ограниченной форме (например, изменение во время выполнения поддерживается только для классов или методов, зарегистрированных как динамические или "исправляемые". "
- Полный список синтаксических несовместимостей см. в "Решение общих проблем". Это не красиво. Вы просто хотели проверить тип, и теперь вы реорганизовали всю свою кодовую базу и сломали все сборки за два дня с момента релиза кандидата, и приятный миниатюрный специалист по персоналу в повседневной деловой одежде проскользнул сквозь щель в вашем кабинете. Большое спасибо, Mypy.
- снижение производительности, несмотря на интерпретацию статически типизированного кода. Сорок лет бурной информатики говорят нам, что (при прочих равных условиях) интерпретация статически типизированного кода должна быть быстрее, а не медленнее, чем интерпретация динамически типизированного кода. В Python вверх - это новый вниз.
- Дополнительные нетривиальные зависимости, увеличивающиеся:
- Хрупкая ошибка развертывания проекта, особенно кроссплатформенная.
- Бремя обслуживания разработки проекта.
- Возможная поверхность атаки.
Я спрашиваю Гвидо: "Почему? Зачем изобретать абстрактный API, если вы не хотите создавать конкретный API, который действительно что-то делает с этой абстракцией?" Зачем оставлять судьбу миллиона Pythonistas артритической руке свободного рынка с открытым исходным кодом? Зачем создавать еще одну техно-проблему, которую можно было бы тривиально решить с помощью 275-строчного декоратора в официальном stdlib Python?
У меня нет Питона, и я должен кричать.