Как получить исходное имя позиционного параметра в Python?

Возможно ли в Python автоматически получить исходное имя переменных, переданных текущей функции в качестве позиционных аргументов?

Это может быть удобно для целей отладки. Например, в функции красивого вывода для добавления префикса вывода каждого выгруженного значения его исходным именем вместо того, чтобы вызывающая сторона указывала его вручную.

Вероятно, наиболее очевидным способом получить что-то подобное было бы использование аргументов с ключевыми словами, например:

# caution: Python3 code
def pprint_func(**kwargs):
    for n, v in kwargs.items():
        print("{}: {}".format(n, str(v)))

pprint_func(the_name="a value")
# this will output: "the_name: a value"

Но это довольно неудобно, так как нам приходится вручную называть каждый аргумент :)

Я бы предпочел немного магии, например:

a_var = "Foo"
pprint_func(a_var, some_cat_func("Hello", "World"))

Функция pprint_func найдет исходное имя, а затем напечатает его до дампа фактического значения:

a_var: 'Foo'
some_cat_func: 'Hello World'

Как этого добиться в Python3.x?

Я предполагаю, что нам нужен доступ к исходному коду вызывающего контекста. Обратите внимание, что очень грязные варианты, такие как лексирование/анализ исходного кода Python вручную, определенно не подходят.

Примечание: этот вопрос связан с предыдущим вопросом. Я создал новый вопрос, потому что тема развивалась.

Кроме того, я нашел этот ответ интересным, но он не работает, если переданный параметр является прямым результатом вызова (т.е. :pprint_func(result())), что привело к неполному решению моей проблемы. И окончательное решение, предложенное автором, настолько грязно, насколько это возможно.


person polyvertex    schedule 02.02.2015    source источник


Ответы (1)


Похоже, единственный способ сделать это — просмотреть AST контекст вызывающей стороны, поиск вызова текущей функции. Затем, благодаря структуре AST, мы можем легко найти всю необходимую информацию о наших позиционных аргументах.

Я не помечаю этот ответ как принятый в надежде, что гуру Python пройдет мимо и скажет правду

Я сделал реализацию, которая просто делает трюк и работает очень хорошо для обычных случаев. Код определенно заслуживает некоторых улучшений, особенно часть find_caller_node, поскольку она настолько точна, насколько это возможно, что не на 100%.

Также мне пришлось read выполнить полный модуль вызывающей программы, поскольку inspect.getsource иногда не возвращал полный исходный блок (например: вызывающий объект находился непосредственно в __main__; Python v3.4.2). Это баг или фича?

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

Вы можете найти последнюю версию здесь.

А вот скопированная/вставленная версия для потомков:

#!/usr/bin/env python3
#
# pydump
# A Python3 pretty-printer that also does introspection to detect the original
# name of the passed variables
#
# Jean-Charles Lefebvre <[email protected]>
# Latest version at: http://gist.github.com/polyvertex (pydump)
#
# Usage:
#     dbg_dump(
#         my_var, None, True, 123, "Bar", (4, 5, 6), fcall(), hello="world")
# Result:
#     my_var: 'Foo'
#     None: None
#     Bool: True
#     Num: 123
#     Str: 'Bar'
#     Tuple: (4, 5, 6)
#     fcall(): "Function's Result"
#     hello: 'world'
#

import sys
import pprint
import inspect
import ast

def dbg_dump(
        *args,
        dumpopt_stream=sys.stderr,
        dumpopt_forcename=True,
        dumpopt_pformat={'indent': 2},
        dumpopt_srcinfo=1,
        **kwargs):
    """
    Pretty-format every passed positional and named parameters, in that order,
    prefixed by their **original** name (i.e.: the one used by the caller), or
    by their type name for literals.

    Depends on the *pprint*, *inspect* and *ast* modules, which are part of the
    Python3 standard library.

    Jean-Charles Lefebvre <[email protected]>
    Latest version at: http://gist.github.com/polyvertex (pydump)

    Note that the names of the keyword arguments you want to dump must not start
    with "dumpopt_" since this prefix is used internally to differentiate
    options over values to dump.

    Also, the introspection code won't behave as expected if do recursive calls
    to this function.

    Options can be passed as keyword arguments to tweak behavior and output
    format:
        dumpopt_stream
            May you wish to print() the result directly, you can pass a stream
            object (e.g.: sys.stdout) through this option, that will be given
            to print()'s "file" keyword argument.
            You can also specify None in case you just want the output string
            to be returned without further ado.
        dumpopt_forcename
            A boolean value to indicate wether you want every dumped value to
            be prepended by its name (i.e.: its name or its type).
            If False, only non-literal values will be named.
        dumpopt_forcename
            The dictionary of keyword arguments to give to pprint.pformat()
        dumpopt_srcinfo
            Specify a false value (None, False, zero) to skip caller's info.
            Specify 1 to output caller's line number only.
            Specify 2 to output caller's file name and line number.
            Specify 3 or greater to output caller's file path and line number.

    Example:
        dbg_dump(
            my_var, None, True, 123, "Bar", (4, 5, 6), fcall(), hello="world")
    Result:
        my_var: 'Foo'
        None: None
        Bool: True
        Num: 123
        Str: 'Bar'
        Tuple: (4, 5, 6)
        fcall(): "Function's Result"
        hello: 'world'
    """
    try:
        def _find_caller_node(root_node, func_name, last_lineno):
            # find caller's node by walking down the ast, searching for an
            # ast.Call object named func_name of which the last source line is
            # last_lineno
            found_node = None
            lineno = 0
            def _luke_astwalker(parent):
                nonlocal found_node
                nonlocal lineno
                for child in ast.iter_child_nodes(parent):
                    # break if we passed the last line
                    if hasattr(child, "lineno") and child.lineno:
                        lineno = child.lineno
                    if lineno > last_lineno:
                        break
                    # is it our candidate?
                    if (isinstance(child, ast.Name)
                            and isinstance(parent, ast.Call)
                            and child.id == func_name):
                        found_node = parent
                        break
                    _luke_astwalker(child)
            _luke_astwalker(root_node)
            return found_node

        frame = inspect.currentframe()
        backf = frame.f_back
        this_func_name = frame.f_code.co_name
        #this_func = backf.f_locals.get(
        #    this_func_name, backf.f_globals.get(this_func_name))

        # get the source code of caller's module
        # note that we have to reload the entire module file since the
        # inspect.getsource() function doesn't work in some cases (i.e.:
        # returned source content was incomplete... Why?!).
        # --> is inspect.getsource broken???
        #     source = inspect.getsource(backf.f_code)
        #source = inspect.getsource(backf.f_code)
        with open(backf.f_code.co_filename, "r") as f:
            source = f.read()

        # get the ast node of caller's module
        # we don't need to use ast.increment_lineno() since we've loaded the
        # whole module
        ast_root = ast.parse(source, backf.f_code.co_filename)
        #ast.increment_lineno(ast_root, backf.f_code.co_firstlineno - 1)

        # find caller's ast node
        caller_node = _find_caller_node(ast_root, this_func_name, backf.f_lineno)
        if not caller_node:
            raise Exception("Caller's AST node not found")

        # keep some useful info for later
        src_info = {
            'file': backf.f_code.co_filename,
            'name': (
                backf.f_code.co_filename.replace("\\", "/").rpartition("/")[2]),
            'lineno': caller_node.lineno}

        # if caller's node has been found, we now have the AST of our parameters
        args_names = []
        for arg_node in caller_node.args:
            if isinstance(arg_node, ast.Name):
                args_names.append(arg_node.id)
            elif isinstance(arg_node, ast.Attribute):
                if hasattr(arg_node, "value") and hasattr(arg_node.value, "id"):
                    args_names.append(arg_node.value.id + "." + arg_node.attr)
                else:
                    args_names.append(arg_node.attr)
            elif isinstance(arg_node, ast.Subscript):
                args_names.append(arg_node.value.id + "[]")
            elif (isinstance(arg_node, ast.Call)
                    and hasattr(arg_node, "func")
                    and hasattr(arg_node.func, "id")):
                args_names.append(arg_node.func.id + "()")
            elif dumpopt_forcename:
                if (isinstance(arg_node, ast.NameConstant)
                        and arg_node.value is None):
                    args_names.append("None")
                elif (isinstance(arg_node, ast.NameConstant)
                        and arg_node.value in (False, True)):
                    args_names.append("Bool")
                else:
                    args_names.append(arg_node.__class__.__name__)
            else:
                args_names.append(None)
    except:
        src_info = None
        args_names = [None] * len(args)

    args_count = len(args) + len(kwargs)

    output = ""
    if dumpopt_srcinfo and src_info:
        if dumpopt_srcinfo <= 1:
            fmt = "D({2}):"
        elif dumpopt_srcinfo == 2:
            fmt = "{1}({2}):"
        else:
            fmt = "{0}({2}):"
        output += fmt.format(
            src_info['file'], src_info['name'], src_info['lineno'])
        output += "\n" if args_count > 1 else " "
    else:
        src_info = None

    for name, obj in zip(
            args_names + list(kwargs.keys()),
            list(args) + list(kwargs.values())):
        if name and name.startswith("dumpopt_"):
            continue
        if src_info and args_count > 1:
            output += "  "
        if name:
            output += name + ": "
        output += pprint.pformat(obj, **dumpopt_pformat) + "\n"

    if dumpopt_stream:
        print(output, end="", file=dumpopt_stream)
        return None # explicit is better than implicit
    else:
        return output.rstrip()


if __name__ == "__main__":
    def fcall():
        return "Function's Result"
    my_var = "Foo"
    dbg_dump(
        my_var, None, True, 123, "Bar", (4, 5, 6), fcall(),
        dbg_dump(1, dumpopt_stream=None), hello="world")
person polyvertex    schedule 02.02.2015