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


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_expensivea_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.