как проще всего решить следующую проблему при расширении/изменении функциональности сторонней библиотеки?
Библиотека предлагает класс LibraryClass
с функцией func_to_be_changed
. Эта функция имеет локальную переменную internal_variable
, которая является экземпляром другого класса SimpleCalculation
этой библиотеки. Я создал новый класс BetterCalculation
в своем собственном модуле и теперь хочу LibraryClass.func_to_be_changed
использовать экземпляр этого нового класса.
# third party library
from third_party_library.utils import SimpleCalculation
class LibraryClass:
def func_to_be_changed(self, x):
# many complicated things go on
internal_variable = SimpleCalculation(x)
# many more complicated things go on
Самым простым решением было бы просто скопировать код из сторонней библиотеки, создать подкласс LibraryClass
и перезаписать функцию func_to_be_changed
:
# my module
from third_party_library import LibraryClass
class BetterLibraryClass(LibraryClass):
def func_to_be_changed(self, x):
"""This is an exact copy of LibraryClass.func_to_be_changed."""
# many complicated things go on
internal_variable = BetterCalculation(x) # Attention: this line has been changed!!!
# many more complicated things go on
Однако это требует копирования многих строк кода. Когда новая версия стороннего класса улучшает код, который был скопирован без изменений, эти модификации необходимо включить вручную на другом этапе копирования.
Я пытался использовать unittest.mock.patch
, так как знаю, что следующие два фрагмента работают:
# some script
from unittest.mock import patch
import third_party_library
from my_module import BetterCalculation
with patch('third_party_library.utils.SimpleCalculation', BetterCalculation):
local_ = third_party_library.utils.SimpleCalculation(x) # indeed uses `BetterCalculation`
def foo(x):
return third_party_library.utils.SimpleCalculation(x)
with patch('third_party_library.utils.SimpleCalculation', BetterCalculation):
local_ = foo(x) # indeed uses `BetterCalculation`
Однако следующее не работает:
# some script
from unittest.mock import patch
from third_party_library.utils import SimpleCalculation
from my_module import BetterCalculation
def foo(x):
return SimpleCalculation(x)
with patch('third_party_library.utils.SimpleCalculation', BetterCalculation):
local_ = foo(x) # does not use `BetterCalculation`
# this works again
with patch('__main__.SimpleCalculation', BetterCalculation):
local_ = foo(x) # indeed uses `BetterCalculation`
Следовательно, следующее также не будет работать:
# my module
from unittest.mock import patch
from third_party_library import LibraryClass
from my_module import BetterCalculation
class BetterLibraryClass(LibraryClass):
def func_to_be_changed(self, x):
with patch(
'third_party_library.utils.SimpleCalculation',
BetterCalculation
):
super().func_to_be_changed(x)
Есть ли элегантный питонический способ сделать это? Я предполагаю, что это сводится к вопросу: каков эквивалент __main__
в последнем фрагменте кода, который нужно заменить third_party_library.utils
?
Первый строковый аргумент в функции patch
может иметь два разных значения в зависимости от ситуации. В первой ситуации описанный объект не был импортирован и недоступен для программы, что, следовательно, привело бы к NameError
без мока. Однако в вопросе объект необходимо перезаписать. Следовательно, он доступен для программы, и неиспользование patch
не приведет к ошибке.
Возможно, я использовал здесь совершенно неправильный язык, поскольку для всех описанных понятий наверняка есть точные термины Python.
Как показано в вопросе, локально импортированный SimpleCalculation
может быть перезаписан __main__.SimpleCalculation
. Поэтому важно помнить, что патчу нужно указывать путь к локальному объекту, а не то, как этот же объект будет импортирован в текущий скрипт.
Предположим, следующий модуль:
# thirdpartymodule/__init__.py
from .utils import foo
def local_foo():
print("Hello local!")
class Bar:
def __init__(self):
foo()
local_foo()
и
# thirdpartymodule/utils.py
def foo():
print("third party module")
Мы хотим переопределить функции foo
и local_foo
. Но мы не хотим переопределять какие-либо функции, мы хотим переопределять функции foo
и local_foo
в контексте файла thirdpartymodule/__init__.py
. Неважно, что функция foo
входит в контекст файла через оператор import
, а local_foo
определяется локально. Итак, мы хотим переопределить функции в контексте thirdpartymodule.foo
и thirdpartymodule.local_foo
. Контекст thirdpartymodule.utils.foo
здесь не важен и нам не поможет. Следующий фрагмент иллюстрирует это:
from unittest.mock import patch
from thirdpartymodule import Bar
bar = Bar()
# third party module
# Hello local!
def myfoo():
print("patched function")
with patch("thirdpartymodule.foo", myfoo):
bar = Bar()
# patched function
# Hello local!
# will not work!
with patch("thirdpartymodule.utils.foo", myfoo):
bar = Bar()
# third party module
# Hello local!
with patch("thirdpartymodule.local_foo", myfoo):
bar = Bar()
# third party module
# patched function
В гипотетическом модуле вопроса нам сначала нужно предположить, что класс LibraryClass
определен в файле third_party_library/library_class.py
. Затем мы хотим переопределить third_party_library.library_class.SimpleCalculation
, и правильный патч будет таким:
# my module
from unittest.mock import patch
from third_party_library import LibraryClass
from my_module import BetterCalculation
class BetterLibraryClass(LibraryClass):
def func_to_be_changed(self, x):
with patch(
'third_party_library.library_class.SimpleCalculation',
BetterCalculation
):
super().func_to_be_changed(x)