2010-03-03 23 views
61

Tôi đang gặp sự cố khi thay thế một chức năng từ một mô-đun khác với chức năng khác và điều đó khiến tôi phát điên.Làm thế nào để một con khỉ vá một hàm trong python?

Hãy nói rằng tôi có một mô-đun bar.py trông như thế này:

from a_package.baz import do_something_expensive 

def a_function(): 
    print do_something_expensive() 

Và tôi có một mô-đun đó trông như thế này:

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() 

tôi mong chờ để có được kết quả:

Something expensive! 
Something really cheap. 
Something really cheap. 

Nhưng thay vào đó tôi có được điều này:

Something expensive! 
Something expensive! 
Something expensive! 

Tôi đang làm gì sai?

+0

Điều thứ hai không thể làm việc, bởi vì bạn chỉ cần xác định lại ý nghĩa của do_something_expensive trong phạm vi địa phương của bạn. Tuy nhiên, tôi không biết tại sao cái thứ ba không hoạt động ... – pajton

+1

Như Nicholas giải thích, bạn đang sao chép một tham chiếu và chỉ thay thế một trong các tham chiếu. 'từ mô-đun nhập khẩu non_module_member' và vá lỗi cấp mô-đun không tương thích vì lý do này và cả hai đều tránh được tốt nhất. – bobince

+0

Đề án đặt tên gói ưu tiên là chữ thường không có dấu gạch dưới, tức là 'apackage'. –

Trả lời

69

Có thể giúp suy nghĩ về cách các không gian tên của Python hoạt động: về cơ bản chúng là từ điển. Vì vậy, khi bạn làm điều này:

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

suy nghĩ của nó như thế này:

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

Hy vọng rằng bạn có thể nhận ra lý do tại sao điều này không làm việc sau đó :-) Khi bạn nhập một tên vào một namespace, các giá trị của tên trong không gian tên bạn đã nhập từ không liên quan. Bạn chỉ sửa đổi giá trị của do_something_expensive trong không gian tên của mô-đun cục bộ, hoặc trong không gian tên của a_package.baz, ở trên. Tuy nhiên, do nhập khẩu thanh do_something_expensive trực tiếp, chứ không phải là tham khảo nó từ không gian tên module, bạn cần phải viết để namespace của nó:

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

Có một trang trí thực sự thanh lịch cho việc này: Guido van Rossum: Python-Dev list: Monkeypatching Idioms.

Ngoài ra còn có gói dectools, mà tôi thấy PyCon 2010, có thể được sử dụng trong ngữ cảnh này, nhưng thực sự có thể đi theo cách khác (monkeypatching ở cấp khai báo phương thức ... nơi bạn ' re not)

+3

Những người trang trí đó dường như không áp dụng cho trường hợp này. –

+1

@MikeGraham: Email của Guido không đề cập đến mã ví dụ của anh ấy cũng cho phép thay thế bất kỳ phương thức nào, không chỉ thêm phương thức mới. Vì vậy, tôi nghĩ rằng họ áp dụng cho trường hợp này. – tuomassalo

+0

@MikeGraham Ví dụ Guido hoạt động hoàn hảo cho việc mô phỏng một câu lệnh, tôi chỉ thử bản thân mình! setattr chỉ là một cách ưa thích để nói '='; Vì vậy, 'a = 3' hoặc tạo một biến mới gọi là 'a' và đặt nó thành ba hoặc thay thế giá trị của biến hiện tại bằng 3 –

4

Trong đoạn đầu tiên, bạn thực hiện bar.do_something_expensive tham chiếu đến đối tượng hàm a_package.baz.do_something_expensive đề cập đến tại thời điểm đó. Để thực sự "monkeypatch" mà bạn sẽ cần phải thay đổi các chức năng chính nó (bạn chỉ thay đổi những gì tên đề cập đến); điều này là có thể, nhưng bạn không thực sự muốn làm điều đó.

Trong nỗ lực của bạn để thay đổi hành vi của a_function, bạn đã làm hai điều:

  1. Trong nỗ lực đầu tiên, bạn thực hiện do_something_expensive một tên toàn cầu trong module của bạn. Tuy nhiên, bạn đang gọi a_function, mà không tìm trong mô-đun của bạn để giải quyết tên, do đó, nó vẫn đề cập đến chức năng tương tự.

  2. Trong ví dụ thứ hai, bạn thay đổi những gì a_package.baz.do_something_expensive đề cập đến, nhưng bar.do_something_expensive không được gắn với nó một cách kỳ diệu. Tên đó vẫn đề cập đến đối tượng hàm mà nó nhìn lên khi nó được khởi tạo.

Cách đơn giản nhất nhưng xa từ lý tưởng cách tiếp cận sẽ được thay đổi bar.py nói

import a_package.baz 

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

Các giải pháp phù hợp có lẽ là một trong hai điều:

  • Định nghĩa lại a_function để có một chức năng như một đối số và gọi đó, thay vì cố gắng lẻn vào và thay đổi chức năng nó là har d được mã hóa để tham chiếu hoặc
  • Lưu trữ hàm được sử dụng trong một phiên bản của một lớp; đây là cách chúng ta làm trạng thái biến đổi trong Python.

Sử dụng globals (đây là những gì thay đổi thứ mô-đun cấp từ các module khác là) là một điều xấu dẫn đến unmaintainable, gây nhầm lẫn, untestestable, mã không thể leo dòng chảy trong số đó là khó khăn để theo dõi.

0

do_something_expensive trong hàm a_function() chỉ là một biến trong vùng tên của mô-đun trỏ đến đối tượng hàm. Khi bạn xác định lại mô-đun bạn đang làm trong một không gian tên khác.

2

Nếu bạn chỉ muốn vá nó cho cuộc gọi của bạn và nếu không rời khỏi mã ban đầu bạn có thể sử dụng https://docs.python.org/3/library/unittest.mock.html#patch (kể từ 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!' 
Các vấn đề liên quan