2016-09-12 13 views
19

Nói rằng tôi có hai tập tin:Tôi gần nhất có thể gọi hàm Python bằng cách sử dụng một phiên bản Python khác?

# spam.py 
import library_Python3_only as l3 

def spam(x,y) 
    return l3.bar(x).baz(y) 

# beans.py 
import library_Python2_only as l2 

... 

Bây giờ giả sử tôi muốn gọi spam từ bên trong beans. Nó không thể trực tiếp vì cả hai tệp đều phụ thuộc vào các phiên bản Python không tương thích. Tất nhiên tôi có thể Popen một quá trình python khác nhau, nhưng làm thế nào tôi có thể vượt qua trong các đối số và lấy kết quả mà không có quá nhiều đau phân tích luồng?

+0

Chức năng của bạn có trong backport không? –

Trả lời

12

Dưới đây là triển khai ví dụ hoàn chỉnh bằng cách sử dụng subprocesspickle mà tôi đã thực sự thử nghiệm. Lưu ý rằng bạn cần sử dụng giao thức phiên bản 2 một cách rõ ràng để tẩy bên Python 3 (ít nhất là cho kết hợp Python 3.5.2 và Python 2.7.3).

# py3bridge.py 

import sys 
import pickle 
import importlib 
import io 
import traceback 
import subprocess 

class Py3Wrapper(object): 
    def __init__(self, mod_name, func_name): 
     self.mod_name = mod_name 
     self.func_name = func_name 

    def __call__(self, *args, **kwargs): 
     p = subprocess.Popen(['python3', '-m', 'py3bridge', 
           self.mod_name, self.func_name], 
           stdin=subprocess.PIPE, 
           stdout=subprocess.PIPE) 
     stdout, _ = p.communicate(pickle.dumps((args, kwargs))) 
     data = pickle.loads(stdout) 
     if data['success']: 
      return data['result'] 
     else: 
      raise Exception(data['stacktrace']) 

def main(): 
    try: 
     target_module = sys.argv[1] 
     target_function = sys.argv[2] 
     args, kwargs = pickle.load(sys.stdin.buffer) 
     mod = importlib.import_module(target_module) 
     func = getattr(mod, target_function) 
     result = func(*args, **kwargs) 
     data = dict(success=True, result=result) 
    except Exception: 
     st = io.StringIO() 
     traceback.print_exc(file=st) 
     data = dict(success=False, stacktrace=st.getvalue()) 

    pickle.dump(data, sys.stdout.buffer, 2) 

if __name__ == '__main__': 
    main() 

Các Python 3 mô-đun (sử dụng các mô-đun pathlib cho showcase)

# spam.py 

import pathlib 

def listdir(p): 
    return [str(c) for c in pathlib.Path(p).iterdir()] 

Các Python 2 mô-đun sử dụng spam.listdir

# beans.py 

import py3bridge 

delegate = py3bridge.Py3Wrapper('spam', 'listdir') 
py3result = delegate('.') 
print py3result 
+0

Tuyệt vời. Tôi gặp phải một số vấn đề với các biến môi trường (môi trường Python2 là một cài đặt cục bộ được nhúng vào một chương trình khác, ghi đè 'PATH', vv), nhưng điều này có thể được sửa bằng cách thay thế lệnh gọi trực tiếp' python3' Popen bằng '['env' , '-i', 'bash', '-l', '-c', 'python3 -m py3bridge' + self.mod_name + '' + self.func_name] '. – leftaroundabout

+0

@leftaroundabout Ngoài ra còn có tham số 'env' của' subprocess.Popen', nơi bạn có thể chuyển các biến môi trường cho tiến trình con. –

+0

Ah, nhưng điều đó cũng có thể được sử dụng để đơn giản bỏ qua các tham số đã cho/hoàn nguyên về mặc định đăng nhập? – leftaroundabout

10

Giả sử người gọi là Python3.5 +, bạn có quyền truy cập vào mô-đun subprocess đẹp hơn. Có lẽ bạn có thể sử dụng subprocess.run và giao tiếp thông qua các đối tượng Python được gửi qua stdin và stdout, tương ứng. Sẽ có một số thiết lập để làm, nhưng không có phân tích về phía bạn, hoặc mucking với chuỗi, vv

Dưới đây là một ví dụ về mã python2 qua subprocess.Popen

p = subprocess.Popen(python3_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 
stdout, stderr = p.communicate(pickle.dumps(python3_args)) 
result = pickle.load(stdout) 
+0

Thực ra tôi đã viết các phiên bản sai cách xung quanh trong ví dụ của mình: Tôi thực sự hy vọng sẽ gọi python3 từ python2. Dù sao tôi hy vọng rằng điều này đã không thực sự làm cho sự khác biệt ... chắc chắn tẩy cũng làm việc theo hướng đó? Nếu không, một câu trả lời chỉ hoạt động nếu người gọi là Python3 cũng sẽ hữu ích (có lẽ tôi thực sự có thể quay lại sự phụ thuộc). – leftaroundabout

+1

Tùy chọn sử dụng đối tượng ngâm vẫn đứng, nó sẽ chỉ khó thiết lập stdin và đọc stdout của quá trình con, vì bạn phải sử dụng subprocess.call hoặc subprocess.Popen, có giao diện clunkier. Bạn vẫn tránh phải làm phân tích thủ công, vì thịt của vấn đề vẫn sẽ là một cuộc gọi đến pickle.dumps/tải, nó chỉ là một chút mã hơn. –

+2

@leftaroundabout Mô-đun ['subprocess'] (https://docs.python.org/2/library/subprocess.html) đã được thêm vào Python 2.4, vì vậy dường như không có lý do gì khiến bạn không thể sử dụng nó trong trường hợp của bạn. – ray

1

Bạn có thể tạo ra một kịch bản đơn giản như vậy :

import sys 
import my_wrapped_module 
import json 

params = sys.argv 
script = params.pop(0) 
function = params.pop(0) 
print(json.dumps(getattr(my_wrapped_module, function)(*params))) 

Bạn sẽ có thể gọi nó như vậy:

pythonx.x wrapper.py myfunction param1 param2 

này rõ ràng là một mối nguy hiểm bảo mật, hãy cẩn thận. Cũng lưu ý rằng nếu params của bạn là bất cứ điều gì khác hơn là chuỗi hoặc số nguyên, bạn sẽ có một số vấn đề, do đó, có thể suy nghĩ về truyền params như một chuỗi json, và chuyển đổi nó bằng cách sử dụng json.loads() trong wrapper này.

+0

Có vẻ không tệ. Tuy nhiên tôi không cho rằng điều này là phù hợp khi các tham số chứa rất nhiều dữ liệu? Đối với các ứng dụng tôi có trong tâm trí ngay bây giờ, nó có thể là ok, nhưng nói chung tôi sẽ cảm thấy không thoải mái đi qua các chuỗi mã hóa json lớn như vậy thông qua dòng lệnh. – leftaroundabout

+0

Tôi cảm thấy không thoải mái khi sử dụng bản thân này! haha. Có lẽ nó có thể giúp đỡ, nhưng giải pháp tốt nhất có lẽ sẽ là sử dụng '2to3' để chuyển đổi thư viện python2 của bạn. –

+1

Tôi chắc chắn sẽ di chuyển mọi thứ sang Python3, nhưng đó không thực sự là một lựa chọn vì động cơ Python2 cụ thể đó là phiên bản được sửa đổi được nhúng trong dự án C++ của bên thứ ba. – leftaroundabout

1

Có thể sử dụng các mô-đun multiprocessing.managers để đạt được những gì bạn muốn. Nó đòi hỏi một số lượng nhỏ của hack mặc dù.

Với mô-đun có chức năng bạn muốn hiển thị, bạn cần tạo một Manager có thể tạo proxy cho các chức năng đó.

quá trình quản lý phục vụ proxy để các chức năng py3:

from multiprocessing.managers import BaseManager 
import spam 

class SpamManager(BaseManager): 
    pass 
# Register a way of getting the spam module. 
# You can use the exposed arg to control what is exposed. 
# By default only "public" functions (without a leading underscore) are exposed, 
# but can only ever expose functions or methods. 
SpamManager.register("get_spam", callable=(lambda: spam), exposed=["add", "sub"]) 

# specifying the address as localhost means the manager is only visible to 
# processes on this machine 
manager = SpamManager(address=('localhost', 50000), authkey=b'abc', 
    serializer='xmlrpclib') 
server = manager.get_server() 
server.serve_forever() 

Tôi đã định nghĩa lại spam để chứa hai chức năng gọi addsub.

# spam.py 
def add(x, y): 
    return x + y 

def sub(x, y): 
    return x - y 

quy trình khách hàng sử dụng chức năng py3 được hiển thị bởi SpamManager.

from __future__ import print_function 
from multiprocessing.managers import BaseManager 

class SpamManager(BaseManager): 
    pass 
SpamManager.register("get_spam") 

m = SpamManager(address=('localhost', 50000), authkey=b'abc', 
    serializer='xmlrpclib') 
m.connect() 

spam = m.get_spam() 
print("1 + 2 = ", spam.add(1, 2)) # prints 1 + 2 = 3 
print("1 - 2 = ", spam.sub(1, 2)) # prints 1 - 2 = -1 
spam.__name__ # Attribute Error -- spam is a module, but its __name__ attribute 
# is not exposed 

Sau khi thiết lập, biểu mẫu này cung cấp cách dễ dàng để truy cập các chức năng và giá trị. Nó cũng cho phép các hàm và giá trị này sử dụng chúng theo cách tương tự mà bạn có thể sử dụng chúng nếu chúng không phải là proxy. Cuối cùng, nó cho phép bạn đặt mật khẩu trên quy trình máy chủ để chỉ các quy trình được ủy quyền mới có thể truy cập trình quản lý. Rằng người quản lý đang chạy dài, cũng có nghĩa là một quy trình mới không phải được bắt đầu cho mỗi cuộc gọi hàm mà bạn thực hiện.

Một hạn chế là tôi đã sử dụng mô-đun xmlrpclib thay vì pickle để gửi dữ liệu qua lại giữa máy chủ và ứng dụng khách. Điều này là do python2 và python3 sử dụng các giao thức khác nhau cho pickle. Bạn có thể sửa lỗi này bằng cách thêm ứng dụng khách của riêng bạn vào multiprocessing.managers.listener_client sử dụng giao thức đã thỏa thuận cho các đối tượng tẩy.

Các vấn đề liên quan