한 원숭이가 파이썬에서 함수를 어떻게 패치합니까?


80

다른 모듈의 기능을 다른 기능으로 교체하는 데 문제가있어 미치게 만듭니다.

다음과 같은 bar.py 모듈이 있다고 가정 해 보겠습니다.

from a_package.baz import do_something_expensive

def a_function():
    print do_something_expensive()

그리고 다음과 같은 또 다른 모듈이 있습니다.

from bar import a_function
a_function()

from a_package.baz import do_something_expensive
do_something_expensive = lambda: 'Something really cheap.'
a_function()

import a_package.baz
a_package.baz.do_something_expensive = lambda: 'Something really cheap.'
a_function()

결과를 기대합니다.

Something expensive!
Something really cheap.
Something really cheap.

그러나 대신 이것을 얻습니다.

Something expensive!
Something expensive!
Something expensive!

내가 도대체 ​​뭘 잘못하고있는 겁니까?


두 번째는 작동하지 않습니다. 로컬 범위에서 do_something_expensive의 의미를 재정의하기 때문입니다. 3 일이 ... 작동하지 않는 이유는, 그러나 모르는
— pajton

1
Nicholas가 설명했듯이 참조를 복사하고 참조 중 하나만 대체합니다. from module import non_module_member이러한 이유로 모듈 수준의 몽키 패치는 호환되지 않으며 일반적으로 피하는 것이 가장 좋습니다.
— bobince

기본 패키지 이름 지정 체계는 밑줄이없는 소문자입니다 apackage.
— Mike Graham

1
@bobince, 이와 같은 모듈 수준의 변경 가능한 상태는 인식 된 지 오래 전부터 글로벌의 나쁜 결과와 함께 피하는 것이 가장 좋습니다. 그러나 from foo import bar괜찮으며 실제로 적절할 때 권장됩니다.
— Mike Graham

답변:


87

파이썬 네임 스페이스가 작동하는 방식을 생각하면 도움이 될 수 있습니다. 본질적으로 사전입니다. 따라서 이렇게하면 :

from a_package.baz import do_something_expensive
do_something_expensive = lambda: 'Something really cheap.'

다음과 같이 생각하십시오.

do_something_expensive = a_package.baz['do_something_expensive']
do_something_expensive = lambda: 'Something really cheap.'

이 후 작동하지 않는 이유 :-) 네임 스페이스로 이름을 가져 오면, 가져온 네임 스페이스의 이름 값 희망 당신은 실현할 수 에서는 무관하다. 로컬 모듈의 네임 스페이스 또는 위의 a_package.baz 네임 스페이스에서만 do_something_expensive 값을 수정하고 있습니다. 그러나 bar는 모듈 네임 스페이스에서 참조하는 대신 do_something_expensive를 직접 가져 오므로 네임 스페이스에 작성해야합니다.

import bar
bar.do_something_expensive = lambda: 'Something really cheap.'

여기 와 같은 타사의 패키지는 어떻습니까?
— Tengerye

21

이를위한 정말 우아한 데코레이터가 있습니다 : Guido van Rossum : Python-Dev list : Monkeypatching Idioms .

거기이기도 dectools의 나도 이런 맥락에서 사용될 수있을 수있는 PyCon 2010 년, 본 패키지는하지만, (당신이하지 않은 경우 ... 방법 선언적 수준에서 monkeypatching)이 실제로는 다른 길을 갈 수 있습니다


4
이러한 데코레이터는이 경우에 적용되지 않는 것으로 보입니다.
— Mike Graham

1
@MikeGraham : Guido의 이메일에는 그의 예제 코드가 새로운 방법을 추가 할뿐만 아니라 모든 방법을 대체 할 수 있다고 언급하지 않습니다. 그래서 나는 그들이이 사건에 적용될 것이라고 생각합니다.
— tuomassalo

@MikeGraham Guido 예제는 메서드 문을 조롱하는 데 완벽하게 작동합니다. 방금 직접 시도했습니다! setattr은 '='를 말하는 멋진 방법입니다. 따라서 'a = 3'은 'a'라는 새 변수를 생성하고이를 3으로 설정하거나 기존 변수의 값을 3으로 대체합니다
— Chris Huang-Leaver

6

호출을 위해서만 패치하고 그렇지 않으면 원본 코드를 그대로두고 싶다면 https://docs.python.org/3/library/unittest.mock.html#patch(Python 3.3부터)를 사용할 수 있습니다 .

with patch('a_package.baz.do_something_expensive', new=lambda: 'Something really cheap.'):
    print do_something_expensive()
    # prints 'Something really cheap.'

print do_something_expensive()
# prints 'Something expensive!'

3

첫 번째 스 니펫에서는 그 순간 bar.do_something_expensive을 a_package.baz.do_something_expensive참조 하는 함수 객체를 참조합니다. 실제로 "monkeypatch"를 사용하려면 함수 자체를 변경해야합니다 (이름이 참조하는 것만 변경). 이것은 가능하지만 실제로 그렇게하고 싶지는 않습니다.

의 동작을 변경하려는 시도에서 다음 a_function두 가지를 수행했습니다.

  1. 첫 번째 시도에서 do_something_expensive를 모듈의 전역 이름으로 만듭니다. 그러나 a_function이름을 확인하기 위해 모듈에서 찾지 않는을 호출 하고 있으므로 여전히 동일한 함수를 참조합니다.

  2. 두 번째 예에서는 a_package.baz.do_something_expensive참조하는 내용을 변경 하지만 bar.do_something_expensive마술처럼 연결되지는 않습니다. 이 이름은 초기화 될 때 조회했던 함수 객체를 참조합니다.

가장 간단하지만 이상적인 접근 방식은 다음과 같이 변경 bar.py하는 것입니다.

import a_package.baz

def a_function():
    print a_package.baz.do_something_expensive()

올바른 해결책은 아마도 다음 두 가지 중 하나 일 것입니다.

  • a_function함수를 인수로 취하고 그것을 호출하도록 재정의 하십시오.
  • 클래스의 인스턴스에서 사용할 함수를 저장합니다. 이것이 파이썬에서 변경 가능한 상태를 수행하는 방법입니다.

전역을 사용하는 것은 (다른 모듈에서 모듈 수준의 항목을 변경하는 것 입니다) 유지 관리 할 수없고, 혼란스럽고, 테스트 할 수없고, 확장 할 수없는 코드로 이어지는 나쁜 일 이며 흐름을 추적하기 어렵습니다.


0

do_something_expensive에서 a_function()기능 함수 객체 모듈의 좌표 공간 내의 단지 변수이다. 모듈을 재정의하면 다른 네임 스페이스에서 수행하는 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.