如何以可维护和可读的方式访问同级包?

How can I access sibling packages in a maintainable and readable way?

我经常遇到一个包需要使用同级包的情况。我想澄清一下,我不是在问 Python 如何允许您导入同级包,这个问题已经被问过很多次了。相反,我的问题是关于编写可维护代码的最佳实践。

  1. 假设我们有一个 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)
    
  2. 现在,在某些时候我们决定函数已经增长并将它们分成两个文件:

     # 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.

  3. 中自己的行下方列出的工具
  4. 技术方案是导入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 将是一场噩梦,因为现在所有模块别名也应该更改。

希望避免直接导入函数,而是导入包,这样读代码的时候就清楚函数是从哪里来的。我怎样才能以可读和可维护的方式处理这种情况?

我可以直接问你需要什么语法并提供。我不会,但你也可以自己做。

"The problem is that parse_name.py can't just import the tools package which is part of itself."

这确实是一件错误而奇怪的事情。

"At least, this won't allow it to use tools listed below its own line in tools/__init__.py"

同意,但同样,如果结构合理,我们不需要它。

为了简化讨论和减少自由度,我在下面的例子中假设了几个东西。

然后您可以适应不同但相似的场景,因为您可以修改代码以满足您的导入语法要求。

我最后给出了一些改动的提示

场景:

您想构建一个名为 tools 的导入包。

你有很多功能,你想在 client.py 中提供给客户端代码。此文件通过导入使用包 tools。为了简单起见,我使用 from ... import * 形式在工具命名空间下提供所有功能(来自任何地方)。这是危险的,应该在实际场景中进行修改,以防止名称与子包名称发生冲突。

您通过将函数分组到 tools 包(子包)内的导入包中来将它们组织在一起。

子包(根据定义)有自己的文件夹,里面至少有一个 __init__.py。除了 __init__.py 之外,我选择将子包代码放在每个子包文件夹中的单个模块中。你可以有更多的模块 and/or 内包。

.
├── 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__.pys 可用于将导入的名称调整到导入程序代码中,或用于初始化模块。它们也可以是空的,或者实际上作为子包中的唯一文件开始,其中包含所有代码,然后在其他模块中拆分,尽管我不太喜欢这种 "everything in __init__.py" 方法。

它们只是在导入时运行的代码。

您还可以避免导入路径中的重复名称,方法是使用不同的名称,或者将所有内容都放在 __init__.py 中,删除具有重复名称的模块,或者在 __init__.py 中使用别名进口,或在那里有名称归属。您还可以通过将名称分配给 __all__ 列表来限制当进口商使用 * 形式时导出的内容。

为了更安全的可读性,您可能想要进行的更改是强制 client.py 在使用名称时指定子包,即

name1 = tools.splitter.split('foo bar')

更改 __init__.py 以仅导入子模块,如下所示:

from .splitter import splitter
from .formatter import formatter

我并不是建议在实践中实际使用它,只是为了好玩,这是一个使用 pkgutilinspect:

的解决方案
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)