我常常遇到一个包需要使用兄弟包的情况。我想澄清一点,我并没有询问Python如何允许您导入兄弟包,这已被多次询问。相反,我的问题是编写可维护代码的最佳实践。
我们假设我们有tools
个包,而tools.parse_name()
函数取决于tools.split_name()
。最初,两者都可能存在于同一文件中,一切都很简单:
# tools/__init__.py
from .name import parse_name, split_name
# tools/name.py
def parse_name(name):
splits = split_name(name) # Can access from same file.
return do_something_with_splits(splits)
def split_name(name):
return do_something_with_name(name)
现在,在某些时候我们决定功能增长并将它们分成两个文件:
# tools/__init__.py
from .parse_name import parse_name
from .split_name import split_name
# tools/parse_name.py
import tools
def parse_name(name):
splits = tools.split_name(name) # Won't work because of import order!
return do_something_with_splits(splits)
# tools/split_name.py
def split_name(name):
return do_something_with_name(name)
问题是parse_name.py
无法导入工具包,而工具包本身就是其中的一部分。至少,这不允许它使用tools/__init__.py
中自己的行下面列出的工具。
技术解决方案是导入tools.split_name
而不是tools
:
# tools/__init__.py
from .parse_name import parse_name
from .split_name import split_name
# tools/parse_name.py
import tools.split_name as tools_split_name
def parse_name(name):
splits = tools_split_name.split_name(name) # Works but ugly!
return do_something_with_splits(splits)
# tools/split_name.py
def split_name(name):
return do_something_with_name(name)
此解决方案在技术上有效但如果使用的不仅仅是一个兄弟包,很快就会变得混乱。此外,将包tools
重命名为utilities
将是一场噩梦,因为现在所有模块别名都应该更改。
它希望避免直接导入函数,而是导入包,以便在读取代码时清楚显示函数的来源。如何以可读和可维护的方式处理这种情况?
答案 0 :(得分:1)
我可以从字面上问你需要什么语法并提供它。我不会,但你也可以自己做。
"问题在于parse_name.py
无法导入属于其自身的工具包。"
这确实是一件错误和奇怪的事情。
"至少,这不允许它在tools/__init__.py
"
同意,但如果事情结构合理,我们也不需要这样做。
为了简化讨论并减少自由度,我在下面的例子中假设了几件事。
然后,您可以适应不同但相似的场景,因为您可以修改代码以满足导入语法要求。
我最后给出了一些改变的提示。
情景:
您想要构建名为tools
的导入包。
您有很多功能,希望client.py
中的客户端代码可用。此文件通过导入包使用包tools
。为了保持简洁,我使用from ... import *
表单在工具命名空间下方提供了所有功能(来自任何地方)。这很危险,应该在实际场景中进行修改,以防止与子包名称之间发生名称冲突。
您可以通过在tools
包(子包)内的导入包中对这些功能进行分组来组织这些功能。
子包(根据定义)有自己的文件夹,里面至少有一个__init__.py
。除了__init__.py
之外,我选择将子包代码放在每个子包文件夹中的单个模块中。您可以拥有更多模块和/或内部包。
.
├── client.py
└── tools
├── __init__.py
├── splitter
│ ├── __init__.py
│ └── splitter.py
└── formatter
├── __init__.py
└── formatter.py
我保持__init__.py
为空,除了外部的tools
,它负责在#/tools/__init.py___
# note that relative imports avoid using the outer package name
# which is good if later you change your mind for its name
from .splitter.splitter import *
from .formatter.formatter import *
# tools/client.py
# this is user code
import tools
text = "foo bar"
splits = tools.split(text) # the two funcs came
# from different subpackages
text = tools.titlefy(text)
print(splits)
print(text)
# tools/formatter/formatter.py
from ..splitter import splitter # tools formatter sibling
# subpackage splitter,
# module splitter
def titlefy(name):
splits = splitter.split(name)
return ' '.join([s.title() for s in splits])
# tools/splitter/splitter.py
def split(name):
return name.split()
命名空间中为客户端导入代码提供所有想要的名称。
这当然可以改变。
from
您可以根据自己的喜好定制导入语法,回答您对其外观的评论。
相对导入需要 tools.
表单。否则使用绝对导入,方法是在路径前加上__init__.py
__init__.py
可用于将导入的名称调整为导入器代码,或初始化模块。它们也可以是空的,或者实际上是作为子包中唯一的文件开始的,包含其中的所有代码,然后在其他模块中分开,尽管我不喜欢这个" {{1中的所有内容}}"尽可能接近。
它们只是在导入时运行的代码。
您还可以通过使用不同的名称,或通过将所有内容放入__init__.py
,使用重复的名称删除模块,或使用__init__.py
导入中的别名来避免导入路径中重复的名称,或者在那里有名称归属。您还可以限制导入器使用*表单时导出的内容,方法是将名称分配给__all__
列表。
您可能希望获得更安全的可读性的更改是在使用名称时强制client.py
指定子包,
name1 = tools.splitter.split('foo bar')
更改__init__.py
以仅导入子模块,如下所示:
from .splitter import splitter
from .formatter import formatter
答案 1 :(得分:0)
我并不认为这实际上是在实践中使用,但为了好玩,以下是使用pkgutil
和inspect
的解决方案:
import inspect
import os
import pkgutil
def import_siblings(filepath):
"""Import and combine names from all sibling packages of a file."""
path = os.path.dirname(os.path.abspath(filepath))
merged = type('MergedModule', (object,), {})
for importer, module, _ in pkgutil.iter_modules([path]):
if module + '.py' == os.path.basename(filepath):
continue
sibling = importer.find_module(module).load_module(module)
for name, member in inspect.getmembers(sibling):
if name.startswith('__'):
continue
if hasattr(merged, name):
message = "Two sibling packages define the same name '{}'."
raise KeyError(message.format(name))
setattr(merged, name, member)
return merged
问题的例子变成:
# tools/__init__.py
from .parse_name import parse_name
from .split_name import split_name
# tools/parse_name.py
tools = import_siblings(__file__)
def parse_name(name):
splits = tools.split_name(name) # Same usage as if this was an external module.
return do_something_with_splits(splits)
# tools/split_name.py
def split_name(name):
return do_something_with_name(name)