【问题标题】:What's the closest I can get to calling a Python function using a different Python version?使用不同的 Python 版本调用 Python 函数最接近的方法是什么?
【发布时间】:2017-01-19 23:31:40
【问题描述】:

假设我有两个文件:

# 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

...

现在假设我希望从beans 中调用spam。这不是直接可能的,因为这两个文件都依赖于不兼容的 Python 版本。当然,我可以Popen 一个不同的 python 进程,但是我怎样才能传入参数并检索结果而不会有太多的流解析痛苦呢?

【问题讨论】:

  • 你的函数在反向移植吗?

标签: python compatibility popen


【解决方案1】:

这是我实际测试过的使用subprocesspickle 的完整示例实现。请注意,您需要在 Python 3 端显式使用协议版本 2 进行酸洗(至少对于 Python 3.5.2 和 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()

Python 3 模块(使用 pathlib 模块进行展示)

# spam.py

import pathlib

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

Python 2 模块使用spam.listdir

# beans.py

import py3bridge

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

【讨论】:

  • 优秀。我遇到了一些环境变量问题(Python2 环境是嵌入到另一个程序中的本地安装,它会覆盖PATH 等),但这可以通过用[ 'env', '-i', 'bash', '-l', '-c', 'python3 -m py3bridge '+self.mod_name+' '+self.func_name ] 替换直接python3 Popen 调用来解决。
  • @leftaroundabout 还有subprocess.Popenenv参数,你可以在其中传递子进程的环境变量。
  • 啊,但这也可以用来简单地忽略给定的参数/恢复到登录默认值吗?
  • 我不知道默认登录,但您可以执行env = dict(os.environ)del env['HAZARDOUS_VAR'] 之类的操作。
【解决方案2】:

假设调用者是 Python3.5+,您可以访问更好的 subprocess 模块。也许您可以使用subprocess.run,并通过分别通过标准输入和标准输出发送的腌制 Python 对象进行通信。需要进行一些设置,但您无需解析,也无需处理字符串等。

这是通过 subprocess.Popen 的 Python2 代码示例

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

【讨论】:

  • 实际上我在我的例子中写错了版本:我真的希望从 python2 调用 python3。无论如何,我希望这并没有真正产生影响……肯定酸洗也可以朝那个方向工作吗?如果不是,那么仅在调用者是 Python3 时才有效的答案也会有所帮助(也许我实际上可以扭转依赖关系)。
  • 使用腌制对象的选项仍然存在,设置标准输入和读取子进程的标准输出将更加困难,因为您必须使用 subprocess.call 或 subprocess.Popen,它们有一个笨拙的界面。您仍然避免手动解析,因为问题的核心仍然是对 pickle.dumps/loads 的调用,它只是多一点代码。
  • @leftaroundabout subprocess 模块是在 Python 2.4 中添加的,因此您似乎没有理由不能在您的情况下使用它。
  • @ray 是的,子进程模块在那里,但它没有更好的“运行”方法。我添加了一个带有“Popen”的示例,它也没有那么糟糕。只需要一些异常处理。
【解决方案3】:

你可以这样创建一个简单的脚本:

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

你可以这样称呼它:

pythonx.x wrapper.py myfunction param1 param2

这显然是一个安全隐患,但要小心。

还要注意,如果你的参数不是字符串或整数,你会遇到一些问题,所以不妨考虑将参数作为 json 字符串传输,并在包装​​器中使用 json.loads() 进行转换。

【讨论】:

  • 看起来不错。但是,当参数包含大量数据时,我认为这不合适吗?对于我现在想到的应用程序,它可能没问题,但通常我会觉得通过命令行传递如此大的 json 编码字符串感到不舒服。
  • 我自己用这个会觉得不舒服!哈哈。也许它可以提供帮助,但最好的解决方案可能是使用 2to3 来转换你的 python2 库。
  • 我肯定宁愿将所有东西都迁移到 Python3,但这并不是一个真正的选择,因为特定的 Python2 引擎是嵌入在第三方 C++ 项目中的修改版本。
【解决方案4】:

可以使用multiprocessing.managers 模块来实现您想要的。不过,它确实需要少量的黑客攻击。

给定一个具有您想要公开的功能的模块,那么您需要创建一个可以为这些功能创建代理的Manager

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

我重新定义了 spam 以包含两个名为 addsub 的函数。

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

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

使用SpamManager公开的py3函数的客户端进程。

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

设置后,此表单提供了一种访问函数和值的简单方法。它还允许以类似的方式使用这些函数和值,如果它们不是代理,则可以使用它们。最后,它允许您在服务器进程上设置密码,以便只有授权的进程才能访问管理器。管理器长时间运行,也意味着不必为您进行的每个函数调用启动一个新进程。

一个限制是我使用xmlrpclib 模块而不是pickle 在服务器和客户端之间来回发送数据。这是因为 python2 和 python3 对pickle 使用不同的协议。您可以通过将自己的客户端添加到 multiprocessing.managers.listener_client 来解决此问题,该客户端使用商定的协议来酸洗对象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-19
    • 2013-10-05
    • 1970-01-01
    • 2017-05-05
    相关资源
    最近更新 更多