- PVSM.RU - https://www.pvsm.ru -
По мере разрастания проекта, в котором я сейчас принимаю активное участие, стал все чаще встречаться с подобными опечатками в именах аргументов у функции, как на картинке справа. Особенно дорого в отладке обходились подобные ошибки в конструкторе класса, когда при длинной цепочке наследования передавался неправильный параметр базового класса, или вообще не передавался. Перекраивание интерфейсов на специальные пользовательские структуры вроде namedtuple вместо **kwargs имело несколько проблем:
Решение, к которому я в итоге пришел, не может защитить в 100% всех возможных случаев, однако в тех необходимых 80% (в моем проекте, 100%) прекрасно справляется со своей задачей. Если кратко, оно заключается в анализе исходного (байт)кода функции, построении матрицы расстояний между найденными «настоящими» именами и переданными извне и печати предупреждений по заданным критериям. Исходники [1].
Итак, сперва точно поставим задачу. В следующем примере должно печататься 5 «подозрительных» предупреждений:
def foo(arg1, arg2=1, **kwargs):
kwa1 = kwargs["foo"]
kwa2 = kwargs.get("bar", 200)
kwa3 = kwargs.get("baz") or 3000
return arg1 + arg2 + kwa1 + kwa2 + kwa3
res = foo(0, arg3=100, foo=10, fo=2, bard=3, bas=4, last=5)
Аналогично, в примере с классами и наследованием должны быть те же предупреждения плюс еще одно (вместо boo передали bog):
class Foo(object):
def __init__(self, arg1, arg2=1, **kwargs):
self.kwa0 = arg2
self.kwa1 = kwargs["foo"]
self.kwa2 = kwargs.get("bar", 200)
self.kwa3 = kwargs.get("baz") or 3000
class Bar(Foo):
def __init__(self, arg1, arg2=1, **kwargs):
super(Bar, self).__init__(arg1, arg2, **kwargs)
self.kwa4 = kwargs.get("boo")
bar = Bar(0, arg3=100, foo=10, fo=2, bard=3, bas=4, last=5, bog=6)
Тут должна быть портянка кода, но лучше я дам на нее ссылку [5]. Функция принимает на вход функцию которая принимает на вход функцию которая... и должна возвращать множество найденных именованных аргументов. Я не особо комментариеобилен, так что кратко пройдусь по основным моментам.
Первое, что стоит сделать — узнать, есть ли у функции вообще **kwargs. Если нет — возвращаем пустоту. Дальше уточняем имя «двойной звезды», ведь **kwargs это общепринятое соглашение и не более того. Дальше логика, как это часто бывает в портабельном по версиям коде, раздваивается, но не как обычно на ветки для двойки и для тройки, а на < 3.4 и >=. Дело в том, что вменяемая поддержка дизассемблирования (вместе с тотальным рефакторингом dis) появилась именно в 3.4. До этого, как нb странно, без сторонних модулей можно было лишь печатать питоний байткод в stdout (sic!). Функция dis.get_instructions() [6] возвращает генератор экземпляров всех байткодных инструкций анализируемого объекта. Вообще, насколько я понял, единственным надежным описанием байткода является хидер его опкодов [7], что, конечно, печально, потому что разворачивание в опкоды конкретных инструкций приходилось определять экспериментально.
Мы будем матчить два паттерна: var = kwargs[«key»] и kwargs.get(«key»[, default]).
>>> from dis import dis
>>> def foo(**kwargs):
return kwargs["key"]
>>> dis(foo)
2 0 LOAD_FAST 0 (kwargs)
3 LOAD_CONST 1 ('key')
6 BINARY_SUBSCR
7 RETURN_VALUE
>>> def foo(**kwargs):
return kwargs.get("key", 0)
>>> dis(foo)
2 0 LOAD_FAST 0 (kwargs)
3 LOAD_ATTR 0 (get)
6 LOAD_CONST 1 ('key')
9 LOAD_CONST 2 (0)
12 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
15 RETURN_VALUE
Как видим, в первом случае это комбинация из LOAD_FAST + LOAD_CONST, во втором LOAD_FAST + LOAD_ATTR + LOAD_CONST. Вместо «kwargs» в аргументе инструкций надо искать найденное в начале имя «двойной звезды». Отсылаю за подробным описанием байткода к сведущим людям, ну а мы будем getting things done, то есть двигаться дальше.
А дальше у нас некрасивый workaround для старых версий Питона на регулярных выражениях. С помощью inspect.getsourcelines() получаем список исходных строк функции, и фигачим по каждой прекомпилированной регуляркой. Этот способ еще хуже чем анализ байткода, например, в текущем виде не определятся выражения, состоящие из нескольких строк или несколько выражений, скленных точкой с запятой. Ну, на то он и workaround чтобы сильно не напрягаться… Впрочем, эту часть можно объективно улучшить, хочу pull request :)
Код [8]. На вход получаем результат предыдущего этапа, переданные именованные аргументы, загадочный tolerance и функцию, которой делать предупреждения. Для каждого переданного аргумента нужно найти editing distance до каждого «настоящего», т.е. которого нашли при анализе байткода. На самом деле, незачем считать тупо всю матрицу целиком, если уже нашли идеальное соответствие, дальше можно не продолжать. Ну и, конечно, матрица симметричная, и, следовательно, можно вычислять только ее половину. Думаю, можно еще как-нибудь соптимизировать, но при типичном количестве kwarg-ов, меньшем 30, сойдет и n2. Расстояние будем вычислять Дамерау-Левенштейна [9] как широко известное, популярное и понятное автору :) На хабре о нем писали, например, здесь [10]. Для него написано несколько пакетов под Питон, я выбрал PyxDamerauLevenshtein за портабельность Cython-а, на котором он написан и оптимальное линейное потребление памяти.
Дальше дело техники: если для аргумента не нашлось ни одного даже отдаленно похожего эталона, заявляем о его категорической бесполезности. Если нашлось несколько соответствий с расстоянием меньше tolerance — заявляем о своих смутных подозрениях.
Классический декоратор [11], заранее вычисляем «настоящие» имена именованных аргументов (пардон за тавтологию), и при каждом вызове дергаем check_misprints.
Наш метакласс [12] будет перехватывать момент создания типа класса (__init__, при котором один раз за все время жизни вычислит «настоящие» имена да-да их самых) и момент создания экземпляра класса (__call__, который дергает check_misprints). Единственный момент — у класса есть mro [13] и базовые классы, в конструкторах которых, возможно, тоже используются **kwargs. Так что в __init__-е мы должны пробежать по всем базовым классам и добавить в общее множество имена аргументов каждого.
Просто добавляем описанные выше декоратор к функции или метакласс к классу.
@detect_misprints
def foo(**kwargs):
...
@six.add_metaclass(KeywordArgsMisprintsDetector)
class Foo(object):
def __init__(self, **kwargs):
...
Я рассмотрел один из способов борьбы с опечатками в именах **kwargs, и в моем случае он решил все проблемы и удовлетворил всем требованиям. Сначала мы анализировали байткод функции или просто исходный код на старых версиях Питона, а потом строили матрицу расстояний между именами, которые используются в функции, и переданными пользователем. Расстояние считали по Дамерау-Левенштейну, и в конце писали warning-и по двум случаям ошибок — когда аргумент «совсем левый» и когда он похож на один из «настоящих».
Исходный код из статьи выложен на GitHub [1]. Буду рад исправлениям и улучшениям. Также хочу узнать ваще мнение, стоит ли это творение выкладывать на PyPi.
Автор: markhor
Источник [14]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/81323
Ссылки в тексте:
[1] Исходники: https://github.com/vmarkovtsev/kwarg_misprints_detection
[2] inspect: https://docs.python.org/3/library/inspect.html
[3] dis: https://docs.python.org/3/library/dis.html
[4] pyxDamerauLevenshtein: https://github.com/gfairchild/pyxDamerauLevenshtein
[5] ссылку: https://github.com/vmarkovtsev/kwarg_misprints_detection/blob/master/kwarg_misprints_detection.py#L46
[6] dis.get_instructions(): https://docs.python.org/3/library/dis.html#dis.get_instructions
[7] хидер его опкодов: https://hg.python.org/cpython/file/b3f0d7f50544/Include/opcode.h
[8] Код: https://github.com/vmarkovtsev/kwarg_misprints_detection/blob/master/kwarg_misprints_detection.py#L97
[9] Дамерау-Левенштейна: https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D1%81%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D0%B5_%D0%94%D0%B0%D0%BC%D0%B5%D1%80%D0%B0%D1%83_%E2%80%94_%D0%9B%D0%B5%D0%B2%D0%B5%D0%BD%D1%88%D1%82%D0%B5%D0%B9%D0%BD%D0%B0
[10] здесь: http://habrahabr.ru/post/117063/
[11] Классический декоратор: https://github.com/vmarkovtsev/kwarg_misprints_detection/blob/master/kwarg_misprints_detection.py#L134
[12] Наш метакласс: https://github.com/vmarkovtsev/kwarg_misprints_detection/blob/master/kwarg_misprints_detection.py#L152
[13] mro: http://habrahabr.ru/post/62203/
[14] Источник: http://habrahabr.ru/post/249423/
Нажмите здесь для печати.