Можно ли получить доступ к внутренним функциям и классам через объекты кода?

Скажем, есть функция func

def func():
    class a:
        def method(self):
            return 'method'
    def a(): return 'function'
    lambda x: 'lambda'

что мне нужно изучить.

В рамках экзамена я хочу «извлечь» исходный код или объекты всех вложенных классов и функций (если таковые имеются). Однако я понимаю, что они пока не существуют, и нет прямого/чистого способа доступа к ним без запуска func или определения их вне (до) func. К сожалению, максимум, что я могу сделать, это импортировать модуль, содержащий func, чтобы получить объект функции func.

Я обнаружил, что у функций есть атрибут __code__, содержащий объект code, у которого есть атрибут co_consts, поэтому я написал это:

In [11]: [x for x in func.__code__.co_consts if iscode(x) and x.co_name == 'a']
Out[11]: 
[<code object a at 0x7fe246aa9810, file "<ipython-input-6-31c52097eb5f>", line 2>,
 <code object a at 0x7fe246aa9030, file "<ipython-input-6-31c52097eb5f>", line 4>]

Эти объекты code выглядят ужасно похожими, и я не думаю, что они содержат данные, необходимые для того, чтобы помочь мне различать типы объектов, которые они представляют (например, type и function).

Q1: я прав?

Вопрос 2. Существует ли какой-либо способ доступа к классам/функциям (обычным и лямбда-выражениям), определенным в теле функции?


person vaultah    schedule 16.09.2015    source источник


Ответы (2)


A1: Вещи, которые могут вам помочь:

Константы объекта кода

Из документации:

Если объект кода представляет функцию, первый элемент в co_consts является строкой документации функции или None, если он не определен.

Кроме того, если объект кода представляет класс, первый элемент co_consts всегда является полным именем этого класса. Вы можете попробовать использовать эту информацию.

Следующее решение будет правильно работать в большинстве случаев, но вам придется пропустить объекты кода, которые Python создает для списков/наборов/слов и выражений генератора:

from inspect import iscode

for x in func.__code__.co_consts:
    if iscode(x):
        # Skip <setcomp>, <dictcomp>, <listcomp> or <genexp>
        if x.co_name.startswith('<') and x.co_name != '<lambda>':
            continue
        firstconst = x.co_consts[0]
        # Compute the qualified name for the current code object
        # Note that we don't know its "type" yet
        qualname = '{func_name}.<locals>.{code_name}'.format(
                        func_name=func.__name__, code_name=x.co_name)
        if firstconst is None or firstconst != qualname:
            print(x, 'represents a function {!r}'.format(x.co_name))
        else:
            print(x, 'represents a class {!r}'.format(x.co_name))

отпечатки

<code object a at 0x7fd149d1a9c0, file "<ipython-input>", line 2> represents a class 'a'
<code object a at 0x7fd149d1ab70, file "<ipython-input>", line 5> represents a function 'a'
<code object <lambda> at 0x7fd149d1aae0, file "<ipython-input>", line 6> represents a function '<lambda>'

Флаги кода

Есть способ получить необходимую информацию от co_flags. Ссылаясь на документацию, которую я связал выше:

Следующие флаговые биты определены для co_flags: бит 0x04 устанавливается, если функция использует синтаксис *arguments для приема произвольного количества позиционных аргументов; бит 0x08 устанавливается, если функция использует синтаксис **keywords для приема произвольных аргументов ключевого слова; бит 0x20 устанавливается, если функция является генератором.

Другие биты в co_flags зарезервированы для внутреннего использования.

Управление флагами осуществляется в compute_code_flags (Python/compile.c< /эм>):

static int
compute_code_flags(struct compiler *c)
{
    PySTEntryObject *ste = c->u->u_ste;
    ...
    if (ste->ste_type == FunctionBlock) {
        flags |= CO_NEWLOCALS | CO_OPTIMIZED;
        if (ste->ste_nested)
            flags |= CO_NESTED;
        if (ste->ste_generator)
            flags |= CO_GENERATOR;
        if (ste->ste_varargs)
            flags |= CO_VARARGS;
        if (ste->ste_varkeywords)
            flags |= CO_VARKEYWORDS;
    }

    /* (Only) inherit compilerflags in PyCF_MASK */
    flags |= (c->c_flags->cf_flags & PyCF_MASK);

    n = PyDict_Size(c->u->u_freevars);
    ...
    if (n == 0) {
        n = PyDict_Size(c->u->u_cellvars);
        ...
        if (n == 0) {
            flags |= CO_NOFREE;
        }
    }
    ...
}

Есть 2 флага кода (CO_NEWLOCALS и CO_OPTIMIZED), которые не будут установлены для классов. Вы можете использовать их для проверки типа (это не означает, что вы должны это делать — плохо документированные детали реализации могут измениться в будущем):

from inspect import iscode

for x in complex_func.__code__.co_consts:
    if iscode(x):
        # Skip <setcomp>, <dictcomp>, <listcomp> or <genexp>
        if x.co_name.startswith('<') and x.co_name != '<lambda>':
            continue
        flags = x.co_flags
        # CO_OPTIMIZED = 0x0001, CO_NEWLOCALS = 0x0002
        if flags & 0x0001 and flags & 0x0002:
            print(x, 'represents a function {!r}'.format(x.co_name))
        else:
            print(x, 'represents a class {!r}'.format(x.co_name))

Выход точно такой же.

Байт-код внешней функции

Также возможно получить тип объекта, проверив байт-код внешней функции.

Инструкции по поиску байт-кода для поиска блоков с LOAD_BUILD_CLASS означают создание класса (LOAD_BUILD_CLASS - Помещает встроенную функцию.__build_class__() в стек. Позже она вызывается CALL_FUNCTION для создания класса.)

from dis import Bytecode
from inspect import iscode
from itertools import groupby

def _group(i):
    if i.starts_line is not None: _group.starts = i
    return _group.starts

bytecode = Bytecode(func)

for _, iset in groupby(bytecode, _group):
    iset = list(iset)
    try:
        code = next(arg.argval for arg in iset if iscode(arg.argval))
        # Skip <setcomp>, <dictcomp>, <listcomp> or <genexp>
        if code.co_name.startswith('<') and code.co_name != '<lambda>':
            raise TypeError
    except (StopIteration, TypeError):
        continue
    else:
        if any(x.opname == 'LOAD_BUILD_CLASS' for x in iset):
            print(code, 'represents a function {!r}'.format(code.co_name))
        else:
            print(code, 'represents a class {!r}'.format(code.co_name)) 

Результат тот же (снова).

А2: Конечно.

Исходный код

Чтобы получить исходный код для объектов кода, вы должны использовать inspect.getsource< /a> или эквивалент:

from inspect import iscode, ismethod, getsource
from textwrap import dedent


def nested_sources(ob):
    if ismethod(ob):
        ob = ob.__func__
    try:
        code = ob.__code__
    except AttributeError:
        raise TypeError('Can\'t inspect {!r}'.format(ob)) from None
    for c in code.co_consts:
        if not iscode(c):
            continue
        name = c.co_name
        # Skip <setcomp>, <dictcomp>, <listcomp> or <genexp>
        if not name.startswith('<') or name == '<lambda>':
            yield dedent(getsource(c))

Например nested_sources(complex_func) (см. ниже)

def complex_func():
    lambda x: 42

    def decorator(cls):
        return lambda: cls()

    @decorator
    class b():
        def method():
            pass

    class c(int, metaclass=abc.ABCMeta):
        def method():
            pass

    {x for x in ()}
    {x: x for x in ()}
    [x for x in ()]
    (x for x in ())

должен предоставить исходный код для первых lambda, decorator, b (включая @decorator) и c:

In [41]: nested_sources(complex_func)
Out[41]: <generator object nested_sources at 0x7fd380781d58>

In [42]: for source in _:
   ....:     print(source, end='=' * 30 + '\n')
   ....:     
lambda x: 42
==============================
def decorator(cls):
    return lambda: cls()
==============================
@decorator
class b():
    def method():
        pass
==============================
class c(int, metaclass=abc.ABCMeta):
    def method():
        pass
==============================

Объекты функций и типов

Если вам все еще нужен объект функции/класса, вы можете eval/< a href="https://docs.python.org/3/library/functions.html#exec" rel="nofollow">exec исходный код.

Пример

  • для lambda функций:

    In [39]: source = sources[0]
    
    In [40]: eval(source, func.__globals__)
    Out[40]: <function __main__.<lambda>>
    
  • для обычных функций

    In [21]: source, local = sources[1], {}
    
    In [22]: exec(source, func.__globals__, local)
    
    In [23]: local.popitem()[1]
    Out[23]: <function __main__.decorator>
    
  • для занятий

    In [24]: source, local = sources[3], {}
    
    In [25]: exec(source, func.__globals__, local)
    
    In [26]: local.popitem()[1] 
    Out[26]: __main__.c
    
person vaultah    schedule 16.09.2015

Disassemble the x object. x can denote either a module, a class, a method, a function, a generator, an asynchronous generator, a coroutine, a code object, a string of source code or a byte sequence of raw bytecode. For a module, it disassembles all functions. For a class, it disassembles all methods (including class and static methods). For a code object or sequence of raw bytecode, it prints one line per bytecode instruction. It also recursively disassembles nested code objects (the code of comprehensions, generator expressions and nested functions, and the code used for building nested classes). Strings are first compiled to code objects with the compile() built-in function before being disassembled. If no object is provided, this function disassembles the last traceback.

The disassembly is written as text to the supplied file argument if provided and to sys.stdout otherwise.

The maximal depth of recursion is limited by depth unless it is None. depth=0 means no recursion.

Changed in version 3.4: Added file parameter.

Changed in version 3.7: Implemented recursive disassembling and added depth parameter.

Changed in version 3.7: This can now handle coroutine and asynchronous generator objects.

https://docs.python.org/3/library/dis.html#dis.dis

person SuperNova    schedule 25.11.2018