Ответ 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.