Làm thế nào để một con khỉ vá một chức năng trong python?


80

Tôi đang gặp sự cố khi thay thế một chức năng từ một mô-đun khác bằng một chức năng khác và điều đó khiến tôi phát điên.

Giả sử tôi có một mô-đun bar.py trông giống như sau:

from a_package.baz import do_something_expensive

def a_function():
    print do_something_expensive()

Và tôi có một mô-đun khác trông giố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 đợi nhận được kết quả:

Something expensive!
Something really cheap.
Something really cheap.

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

Something expensive!
Something expensive!
Something expensive!

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


Cái thứ hai không thể hoạt động, bởi vì bạn chỉ xác định lại ý nghĩa của do_something_expensive trong phạm vi cục bộ của bạn. Tôi không biết tuy nhiên, lý do tại sao một trong 3 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. from module import non_module_membervà bản vá khỉ cấp mô-đun không tương thích vì lý do này và tốt nhất nên tránh cả hai.
bobince

Lược đồ đặ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.
Mike Graham

1
@bobince, tốt nhất nên tránh trạng thái có thể thay đổi ở cấp độ mô-đun như thế này, với những hậu quả xấu của hình cầu đã được công nhận từ lâu. Tuy nhiên, from foo import barchỉ là tốt và trên thực tế được khuyến khích khi thích hợp.
Mike Graham

Câu trả lời:


87

Có thể hữu ích khi nghĩ về cách không gian tên 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.'

Hãy nghĩ về 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 hoạt động sau đó :-) Khi bạn nhập tên vào không gian tên, giá trị của tên trong không gian tên bạn đã nhập từ đó là 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. Nhưng vì thanh nhập trực tiếp do_something_expensive, thay vì tham chiếu nó từ không gian tên mô-đun, bạn cần ghi vào không gian tên của nó:

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

Điều gì về các gói từ bên thứ ba, như ở đây ?
Tengerye

21

Có một nhà trang trí thực sự thanh lịch cho điều này: Guido van Rossum: Danh sách Python-Dev: Thành ngữ Monkeypatching .

Ngoài ra còn có gói dectools , mà tôi đã thấy một PyCon 2010, có thể cũng có thể được sử dụng trong ngữ cảnh này, nhưng điều đó thực sự có thể đi theo hướng khác (khớp khỉ ở cấp độ khai báo phương thức ... khi bạn không có)


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

1
@MikeGraham: Email của Guido không đề cập đến việc 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 một 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

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

6

Nếu bạn chỉ muốn vá nó cho cuộc gọi của mình và để lại mã gốc, 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!'

3

Trong đoạn mã đầu tiên, bạn thực hiện bar.do_something_expensivetham chiếu đến đối tượng hàm được a_package.baz.do_something_expensiveđề cập tại thời điểm đó. Để thực sự "Monkeypatch", bạn sẽ cần phải thay đổi chính chức năng (bạn chỉ thay đổi những tên tham chiếu đế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 thay đổi hành vi của a_functionbạn, bạn đã làm được hai điều:

  1. Trong lần thử đầu tiên, bạn đặt do_something_expensive làm tên chung trong mô-đun của mình. Tuy nhiên, bạn đang gọi a_function, mô-đun này không tìm trong mô-đun của bạn để phân giải tên, vì vậy nó vẫn tham chiếu đến cùng một chức năng.

  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_expensivekhông bị ràng buộc một cách kỳ diệu với nó. Tên đó vẫn đề cập đến đối tượng hàm mà nó đã tìm kiếm khi nó được khởi tạo.

Cách tiếp cận đơn giản nhất nhưng không lý tưởng là thay đổi bar.pyđể nói

import a_package.baz

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

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

  • Xác định lại a_functionđể lấy một hàm làm đối số và gọi hàm đó, thay vì cố gắng lẻn vào và thay đổi hàm mà nó được mã hóa cứng để tham chiếu đến, hoặc
  • Lưu trữ hàm được sử dụng trong một thể hiện của một lớp; đây là cách chúng tôi thực hiện trạng thái có thể thay đổi trong Python.

Sử dụng toàn cầu (đây là cách thay đổi nội dung cấp mô-đun từ các mô-đun khác) là một điều tồi tệ dẫn đến mã không thể xác định được, khó hiểu, không thể kiểm tra, không thể thay đổi mà dòng chảy khó theo dõi.


0

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.