如何以可维护和可读的方式访问同级包?
How can I access sibling packages in a maintainable and readable way?
我经常遇到一个包需要使用同级包的情况。我想澄清一下,我不是在问 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
将是一场噩梦,因为现在所有模块别名也应该更改。
希望避免直接导入函数,而是导入包,这样读代码的时候就清楚函数是从哪里来的。我怎样才能以可读和可维护的方式处理这种情况?
我可以直接问你需要什么语法并提供。我不会,但你也可以自己做。
"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__.py
s 可用于将导入的名称调整到导入程序代码中,或用于初始化模块。它们也可以是空的,或者实际上作为子包中的唯一文件开始,其中包含所有代码,然后在其他模块中拆分,尽管我不太喜欢这种 "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
我并不是建议在实践中实际使用它,只是为了好玩,这是一个使用 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)
我经常遇到一个包需要使用同级包的情况。我想澄清一下,我不是在问 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
将是一场噩梦,因为现在所有模块别名也应该更改。
希望避免直接导入函数,而是导入包,这样读代码的时候就清楚函数是从哪里来的。我怎样才能以可读和可维护的方式处理这种情况?
我可以直接问你需要什么语法并提供。我不会,但你也可以自己做。
"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__.py
s 可用于将导入的名称调整到导入程序代码中,或用于初始化模块。它们也可以是空的,或者实际上作为子包中的唯一文件开始,其中包含所有代码,然后在其他模块中拆分,尽管我不太喜欢这种 "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
我并不是建议在实践中实际使用它,只是为了好玩,这是一个使用 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)