支持两个版本的Python包,而无需客户端更改代码。

5

我试图支持多个版本的Python包,而不影响客户端代码。

考虑以下存储库:

.
|-- client_code.py
`-- lib
    |-- __init__.py
    `-- foo.py

client_code.py:

from lib.foo import f
...
f()

我希望您能够保持 client_code.py 文件不变。我首先尝试了以下操作:
lib
|-- __init__.py
|-- v1
|   |-- __init__.py
|   `-- foo.py
`-- v2
    |-- __init__.py
    `-- foo.py

lib/__init__.py:

import os

if os.environ.get("USE_V2", "0") == "0": # Or some other runtime check
    from .v1 import foo
else:
    from .v2 import foo

但客户端代码出现以下错误:

Traceback (most recent call last):
  File "client_code.py", line 1, in <module>
    from lib.foo import f
ImportError: No module named foo

我知道以下选项可以解决问题,但需要让客户更改代码:

if os.environ.get("USE_V2", "0") == "0":
    from lib.v1.foo import f
else:
    from lib.v2.foo import f

f()

if os.environ.get("USE_V2", "0") == "0":
    import lib.v1.foo as foo
else:
    import lib.v2.foo as foo

foo.f()

这种情况是否可能出现?

一个更一般的问题在这里:如何支持两个版本的Python包而不需要客户端更改代码


2
在v1和v2的初始化文件中将foo添加到__all__ - Mad Physicist
@MadPhysicist 感谢您的阅读和提供帮助。我尝试在两个__init__.py文件中添加 from . import foo; __all__ = ["foo"],但不幸的是,这似乎没有影响任何内容。错误似乎表明 lib 没有名为 foo 的模块。(请注意,即使在 lib/__init__.py 中执行 from .v1 import foo,我仍然会得到相同的 ImportError - JKD
我不是很擅长开发,但为什么不发布多个版本的库,而不是将v1和v2打包在一起呢? - wjandrea
1
@wjandrea 很好的观点。通常我会这样做。但是,对于我的用例,v1和v2必须涉及不同的运行时环境:CPU架构(v1=x86,v2=arm),CUDA版本(v1=CUDA10,v2=CUDA11),Python版本(v1=py27,v2=py3)等。子模块包括多个pybind共享对象,这些对象在不同的环境中构建并链接到不同的库。代码可以在异构计算集群上运行,其中机器根据需要运行的任务类型(训练/评估模型、加载数据集等)或云GPU可用性具有不同的功能。 - JKD
1个回答

2

我不确定这是否是最优雅的方法,但这似乎可行。

├── client.py
└── lib
    ├── __init__.py
    ├── foo.py
    ├── v1
    │   └── foo.py
    └── v2
        └── foo.py

foo.py

import os
if os.environ.get("USE_V2", "0") == "0":
    from lib.v1.foo import *
else:
    from lib.v2.foo import *

v1/foo.py

def f():
    print("I'm v1.f")

v2/foo.py

def f():
    print("I'm v2.f")

client.py

from lib.foo import f

f()

运行输出:

$ env | grep USE_V2
USE_V2=1
$ python client.py
I'm v2.f
$ unset USE_V2
$ python client.py
I'm v1.f

实际上,foo.py 中的 import * 看起来很糟糕,但这只是一种懒惰的方法。如果给出了略有不同的 v1 和 v2 内容,你可以让 foo.py 适应 导入,以在两种情况下呈现统一的 API。或者,你可以为 V1 函数准备一个 functools.partial 版本,该版本在 V2 中不存在。

__init__.py 是空的,甚至在 Python 3 下都不需要存在。


谢谢!这绝对回答了我提出的问题。不幸的是,我可能过于简化了。您有没有想法如何使其适用于lib下的任意包布局?换句话说,如果不编写bar.pybaz.py(它们与foo.py实际上是相同的),也能支持from lib.bar import gfrom lib.baz import h将会很好。 - JKD
1
嗯,我不确定。包的 init.py 行为可能会被使用?那已经是几个层次太动态和模块化了,超出了我的能力范围。但我可以说一件事:在某个点之后,动态运行时系统变得非常难以理解。在构建这种类型的架构之前,你必须深思熟虑。 - JL Peyret
但是在另一个层面上,也许可以让lib.py来解决版本问题,而不是让lib/foo.py来解决? - JL Peyret
很有趣,你提到了这个。在阅读了你的解决方案后我立刻尝试了,但是我遇到了同样的问题。你可以在客户端代码中看到这个问题,比如from lib.foo.bar import f。除非你创建了 lib/foo/bar.py 来处理到 lib/v{1,2}/foo/bar.py 的间接引用,否则这将无法正常工作。也许上面回应 @wjandrea 的 so 动机更清楚地说明了我所面临的挑战。 - JKD
@JKD 我只能回答你提出的问题,而不能回答背后更复杂的要求。我认为避免过多了解库代码结构的一种方法 可能 是避免深度导入:不要使用 from lib.foo import f,而是使用 import lib,然后调用 lib.foo.f()。这样可以更自由地安排动态代码加载,无论是通过 lib.__init__ 还是 lib.foo - JL Peyret
好的,我会接受你的答案并发布一个更完整的问题。感谢你的帮助! - JKD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接