Исправление обезьян Python: создание экземпляра в методе библиотеки/объекта

как проще всего решить следующую проблему при расширении/изменении функциональности сторонней библиотеки?

Библиотека предлагает класс 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?

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
0
271
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Некоторый контекст

Первый строковый аргумент в функции 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)

Другие вопросы по теме