Подтвердить что ты не робот

Можете ли вы исправить * просто * вложенную функцию с закрытием или повторить всю внешнюю функцию?

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

К сожалению, поддерживающие библиотеки несколько медленны с исправлениями, но мы не хотим, чтобы их разветвляли. Мы также не можем проводить наш выпуск до тех пор, пока они не исправят проблему.

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

Внутренняя функция полагается на закрытие переменной; надуманный пример:

def outerfunction(*args):
    def innerfunction(val):
        return someformat.format(val)

    someformat = 'Foo: {}'
    for arg in args:
        yield innerfunction(arg)

где мы хотели бы заменить только реализацию innerfunction(). Фактическая внешняя функция намного длиннее. Разумеется, мы будем повторно использовать закрытую переменную и поддерживать подпись функции.

4b9b3361

Ответ 1

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

  • Вам нужно создать функцию замены как вложенную функцию, чтобы гарантировать, что Python создает одно и то же закрытие. Если исходная функция имеет замыкание над именами foo и bar, вам нужно определить вашу замену как вложенную функцию с теми же именами, которые были закрыты. Что еще более важно, вам нужно использовать эти имена в том же порядке; замыкания ссылаются на индекс.

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

Чтобы понять, как это будет работать, я сначала объясню, как Python обрабатывает вложенные функции. Python использует объекты кода для создания функциональных объектов по мере необходимости. Каждый объект кода имеет связанную последовательность констант, а объекты кода для вложенных функций сохраняются в этой последовательности:

>>> def outerfunction(*args):
...     def innerfunction(val):
...         return someformat.format(val)
...     someformat = 'Foo: {}'
...     for arg in args:
...         yield innerfunction(arg)
... 
>>> outerfunction.__code__
<code object outerfunction at 0x105b27ab0, file "<stdin>", line 1>
>>> outerfunction.__code__.co_consts
(None, <code object innerfunction at 0x100769db0, file "<stdin>", line 2>, 'Foo: {}')

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

Затем нам нужно закрыть закрытие. Во время компиляции Python определяет, что a) someformat не является локальным именем в innerfunction и что b) он закрывается с тем же именем в outerfunction. Python не только затем генерирует байт-код для создания правильного поиска имен, объекты кода для вложенных и внешних функций аннотируются для записи, что someformat должен быть закрыт:

>>> outerfunction.__code__.co_cellvars
('someformat',)
>>> outerfunction.__code__.co_consts[1].co_freevars
('someformat',)

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

Закрытия создаются во время выполнения; байт-код для их создания является частью внешней функции:

>>> import dis
>>> dis.dis(outerfunction)
2           0 LOAD_CLOSURE             0 (someformat)
            3 BUILD_TUPLE              1
            6 LOAD_CONST               1 (<code object innerfunction at 0x1047b2a30, file "<stdin>", line 2>)
            9 MAKE_CLOSURE             0
           12 STORE_FAST               1 (innerfunction)

# ... rest of disassembly omitted ...

Байт-код LOAD_CLOSURE создает замыкание для переменной someformat; Python создает столько закрытий, которые используются функцией в том порядке, в котором они сначала используются во внутренней функции. Это важный факт, который следует помнить позже. Сама функция просматривает эти замыкания по положению:

>>> dis.dis(outerfunction.__code__.co_consts[1])
  3           0 LOAD_DEREF               0 (someformat)
              3 LOAD_ATTR                0 (format)
              6 LOAD_FAST                0 (val)
              9 CALL_FUNCTION            1
             12 RETURN_VALUE        

Операционный код LOAD_DEREF взял крышку в позиции 0 здесь, чтобы получить доступ к закрытию someformat.

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

Теперь для свопинга. Функции - это объекты, подобные любым другим в Python, экземпляры определенного типа. Тип не отображается нормально, но вызов type() все еще возвращает его. То же самое относится к объектам кода, и оба типа даже имеют документацию:

>>> type(outerfunction)
<type 'function'>
>>> print type(outerfunction).__doc__
function(code, globals[, name[, argdefs[, closure]]])

Create a function object from a code object and a dictionary.
The optional name string overrides the name from the code object.
The optional argdefs tuple specifies the default argument values.
The optional closure tuple supplies the bindings for free variables.
>>> type(outerfunction.__code__)
<type 'code'>
>>> print type(outerfunction.__code__).__doc__
code(argcount, nlocals, stacksize, flags, codestring, constants, names,
      varnames, filename, name, firstlineno, lnotab[, freevars[, cellvars]])

Create a code object.  Not for the faint of heart.

Мы будем использовать эти объекты типа для создания нового объекта code с обновленными константами, а затем нового объекта функции с обновленным объектом кода:

def replace_inner_function(outer, new_inner):
    """Replace a nested function code object used by outer with new_inner

    The replacement new_inner must use the same name and must at most use the
    same closures as the original.

    """
    if hasattr(new_inner, '__code__'):
        # support both functions and code objects
        new_inner = new_inner.__code__

    # find original code object so we can validate the closures match
    ocode = outer.__code__
    function, code = type(outer), type(ocode)
    iname = new_inner.co_name
    orig_inner = next(
        const for const in ocode.co_consts
        if isinstance(const, code) and const.co_name == iname)
    # you can ignore later closures, but since they are matched by position
    # the new sequence must match the start of the old.
    assert (orig_inner.co_freevars[:len(new_inner.co_freevars)] ==
            new_inner.co_freevars), 'New closures must match originals'
    # replace the code object for the inner function
    new_consts = tuple(
        new_inner if const is orig_inner else const
        for const in outer.__code__.co_consts)

    # create a new function object with the new constants
    return function(
        code(ocode.co_argcount, ocode.co_nlocals, ocode.co_stacksize,
             ocode.co_flags, ocode.co_code, new_consts, ocode.co_names,
             ocode.co_varnames, ocode.co_filename, ocode.co_name,
             ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars,
             ocode.co_cellvars),
        outer.__globals__, outer.__name__, outer.__defaults__,
        outer.__closure__)

Вышеупомянутая функция подтверждает, что новая внутренняя функция (которая может быть передана как объект кода или как функция) действительно будет использовать те же замыкания, что и оригинал. Затем он создает новые объекты кода и функции для соответствия старому объекту outer, но с вложенной функцией (расположенной по имени) заменяется патчем обезьяны.

Чтобы продемонстрировать, что все это работает, замените innerfunction на тот, который увеличивает каждое отформатированное значение на 2:

>>> def create_inner():
...     someformat = None  # the actual value doesn't matter
...     def innerfunction(val):
...         return someformat.format(val + 2)
...     return innerfunction
... 
>>> new_inner = create_inner()

Новая внутренняя функция создается как вложенная функция; это важно, поскольку это гарантирует, что Python будет использовать правильный байт-код для поиска закрытия someformat. Я использовал оператор return для извлечения объекта функции, но вы также можете посмотреть create_inner._co_consts, чтобы захватить объект кода.

Теперь мы можем исправить исходную внешнюю функцию, заменяя только внутреннюю функцию:

>>> new_outer = replace_inner_function(outerfunction, new_inner)
>>> list(outerfunction(6, 7, 8))
['Foo: 6', 'Foo: 7', 'Foo: 8']
>>> list(new_outer(6, 7, 8))
['Foo: 8', 'Foo: 9', 'Foo: 10']

Исходная функция выбрала исходные значения, но новые возвращаемые значения увеличились на 2.

Вы даже можете создавать новые заменяющие внутренние функции, которые используют меньшее количество замыканий:

>>> def demo_outer():
...     closure1 = 'foo'
...     closure2 = 'bar'
...     def demo_inner():
...         print closure1, closure2
...     demo_inner()
... 
>>> def create_demo_inner():
...     closure1 = None
...     def demo_inner():
...         print closure1
... 
>>> replace_inner_function(demo_outer, create_demo_inner.__code__.co_consts[1])()
foo

Итак, чтобы завершить изображение:

  • Создайте свою внутреннюю функцию monkey-patch как вложенную функцию с тем же закрытием
  • Используйте replace_inner_function() для создания новой внешней функции
  • Обезьяна исправляет исходную внешнюю функцию для использования новой внешней функции, созданной на шаге 2.

Ответ 2

Ответ Martijn хорош, но есть один недостаток, который было бы неплохо удалить:

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

Это не является особенно трудным ограничением для нормального случая, но не нравится быть зависимым от поведения undefined, такого как упорядочение имен, и когда что-то не так, есть потенциально опасные ошибки и, возможно, даже жесткие сбои.

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

Основной подход - загрузить источник с помощью inspect.getsource, изменить его и затем оценить. Это делается на уровне АСТ, чтобы все было в порядке.

Вот код:

import ast
import inspect
import sys

class AstReplaceInner(ast.NodeTransformer):
    def __init__(self, replacement):
        self.replacement = replacement

    def visit_FunctionDef(self, node):
        if node.name == self.replacement.name:
            # Prevent the replacement AST from messing
            # with the outer AST line numbers
            return ast.copy_location(self.replacement, node)

        self.generic_visit(node)
        return node

def ast_replace_inner(outer, inner, name=None):
    if name is None:
        name = inner.__name__

    outer_ast = ast.parse(inspect.getsource(outer))
    inner_ast = ast.parse(inspect.getsource(inner))

    # Fix the source lines for the outer AST
    outer_ast = ast.increment_lineno(outer_ast, inspect.getsourcelines(outer)[1] - 1)

    # outer_ast should be a module so it can be evaluated;
    # inner_ast should be a function so we strip the module node
    inner_ast = inner_ast.body[0]

    # Replace the function
    inner_ast.name = name
    modified_ast = AstReplaceInner(inner_ast).visit(outer_ast)

    # Evaluate the modified AST in the original module scope
    compiled = compile(modified_ast, inspect.getsourcefile(outer), "exec")
    outer_globals = outer.__globals__ if sys.version_info >= (3,) else outer.func_globals
    exec_scope = {}

    exec(compiled, outer_globals, exec_scope)
    return exec_scope.popitem()[1]

Быстрое пошаговое руководство. AstReplaceInner - это ast.NodeTransformer, который позволяет вам изменять АСТ путем сопоставления определенных узлов с некоторыми другими узлами. В этом случае для замены ast.FunctionDef node требуется replacement node, когда имена совпадают.

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

Анализируются АСТ:

    outer_ast = ast.parse(inspect.getsource(outer))
    inner_ast = ast.parse(inspect.getsource(inner))

Преобразование выполнено:

    modified_ast = AstReplaceInner(inner_ast).visit(outer_ast)

Код оценивается и функция извлекается:

    exec(compiled, outer_globals, exec_scope)
    return exec_scope.popitem()[1]

Вот пример использования. Предположим, что этот старый код находится в buggy.py:

def outerfunction():
    numerator = 10.0

    def innerfunction(denominator):
        return denominator / numerator

    return innerfunction

Вы хотите заменить innerfunction на

def innerfunction(denominator):
    return numerator / denominator

Вы пишете:

import buggy

def innerfunction(denominator):
    return numerator / denominator

buggy.outerfunction = ast_replace_inner(buggy.outerfunction, innerfunction)

В качестве альтернативы вы можете написать:

def divide(denominator):
    return numerator / denominator

buggy.outerfunction = ast_replace_inner(buggy.outerfunction, divide, "innerfunction")

Основным недостатком этого метода является то, что требуется inspect.getsource работать как с целью, так и с заменой. Это не удастся, если цель будет "встроена" (написана на C) или скомпилирована в байт-код перед распространением. Обратите внимание, что если он встроен, техника Martijn тоже не будет работать.

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

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

def outerfunction():
    numerator = 10.0

    innerfunction = lambda denominator: denominator / numerator

    return innerfunction

таким же образом; потребуется другое преобразование АСТ.

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

Ответ 3

Мне это нужно, но в классе и python2/3. Таким образом, я расширил решение @MartijnPieters

import types, inspect, six

def replace_inner_function(outer, new_inner, class_class=None):
    """Replace a nested function code object used by outer with new_inner

    The replacement new_inner must use the same name and must at most use the
    same closures as the original.

    """
    if hasattr(new_inner, '__code__'):
        # support both functions and code objects
        new_inner = new_inner.__code__

    # find original code object so we can validate the closures match
    ocode = outer.__code__

    iname = new_inner.co_name
    orig_inner = next(
        const for const in ocode.co_consts
        if isinstance(const, types.CodeType) and const.co_name == iname)
    # you can ignore later closures, but since they are matched by position
    # the new sequence must match the start of the old.
    assert (orig_inner.co_freevars[:len(new_inner.co_freevars)] ==
            new_inner.co_freevars), 'New closures must match originals'
    # replace the code object for the inner function
    new_consts = tuple(
        new_inner if const is orig_inner else const
        for const in outer.__code__.co_consts)

    if six.PY3:
        new_code = types.CodeType(ocode.co_argcount, ocode.co_kwonlyargcount, ocode.co_nlocals, ocode.co_stacksize,
             ocode.co_flags, ocode.co_code, new_consts, ocode.co_names,
             ocode.co_varnames, ocode.co_filename, ocode.co_name,
             ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars,
             ocode.co_cellvars)
    else:
    # create a new function object with the new constants
        new_code = types.CodeType(ocode.co_argcount, ocode.co_nlocals, ocode.co_stacksize,
             ocode.co_flags, ocode.co_code, new_consts, ocode.co_names,
             ocode.co_varnames, ocode.co_filename, ocode.co_name,
             ocode.co_firstlineno, ocode.co_lnotab, ocode.co_freevars,
             ocode.co_cellvars)

    new_function= types.FunctionType(new_code, outer.__globals__, 
                                     outer.__name__, outer.__defaults__,
                                     outer.__closure__)

    if hasattr(outer, '__self__'):
        if outer.__self__ is None:
            if six.PY3:
                return types.MethodType(new_function, outer.__self__, class_class)
            else:
                return types.MethodType(new_function, outer.__self__, outer.im_class)
        else:
            return types.MethodType(new_function, outer.__self__, outer.__self__.__class__)

    return new_function

Теперь это должно работать для функций, связанных методов класса и методов несвязанных классов. (Аргумент class_class необходим только для python3 для несвязанных методов). Спасибо @MartijnPieters за большую часть работы! Я никогда бы не подумал об этом;)