如何以可维护和可读的方式访问兄弟包?

时间:2018-02-28 15:24:48

标签: python python-3.x import package maintainability

我常常遇到一个包需要使用兄弟包的情况。我想澄清一点,我并没有询问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. 技术解决方案是导入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)
    
  4. 此解决方案在技术上有效但如果使用的不仅仅是一个兄弟包,很快就会变得混乱。此外,将包tools重命名为utilities将是一场噩梦,因为现在所有模块别名都应该更改。

    它希望避免直接导入函数,而是导入包,以便在读取代码时清楚显示函数的来源。如何以可读和可维护的方式处理这种情况?

2 个答案:

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

我并不认为这实际上是在实践中使用,但为了好玩,以下是使用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)