如何将多个 python 文件组织到一个模块中而不像一个包一样?

Posted

技术标签:

【中文标题】如何将多个 python 文件组织到一个模块中而不像一个包一样?【英文标题】:How to organize multiple python files into a single module without it behaving like a package? 【发布时间】:2012-09-14 10:51:27 【问题描述】:

有没有办法使用__init__.py 将多个文件组织成一个模块

原因:模块比包更容易使用,因为它们没有那么多层的命名空间。

通常它会打包,我明白了。问题出在一个包上,“import thepackage”给了我一个空的命名空间。然后,用户必须要么使用“from thepackage import *”(不赞成),要么确切地知道其中包含什么并手动将其拉出到可用的命名空间中。

我想要的是用户执行“导入包”并拥有看起来像这样的漂亮干净的命名空间,公开与项目相关的函数和类以供使用。

current_module
\
  doit_tools/
  \
   - (class) _hidden_resource_pool
   - (class) JobInfo
   - (class) CachedLookup
   - (class) ThreadedWorker
   - (Fn) util_a
   - (Fn) util_b
   - (Fn) gather_stuff
   - (Fn) analyze_stuff

维护者的工作是避免在不同的文件中定义相同的名称,当项目像我这样小时,这应该很容易。

如果人们可以使用from doit_stuff import JobInfo 并让它检索类,而不是包含该类的模块,那就太好了。

如果我的所有代码都在一个巨大的文件中,这很容易,但我喜欢在事情开始变大时进行组织。我在磁盘上的内容看起来像这样:

place_in_my_python_path/
  doit_tools/
    __init__.py
    JobInfo.py
      - class JobInfo:
    NetworkAccessors.py
      - class _hidden_resource_pool:
      - class CachedLookup:
      - class ThreadedWorker:
    utility_functions.py
      - def util_a()
      - def util_b()
    data_functions.py
      - def gather_stuff()
      - def analyze_stuff()

我只是将它们分开,因此我的文件不会很大且无法导航。它们都是相关的,尽管有人(可能是我)可能想自己使用这些类而不导入所有内容。

我已经阅读了各个主题中的许多建议,以下是我能找到的关于如何执行此操作的每个建议的情况:

如果我不使用__init__.py,我将无法导入任何内容,因为 Python 不会从 sys.path 进入文件夹。

如果我使用空白__init__.py,当我import doit_tools 时,它是一个空的命名空间,其中没有任何内容。我的文件都没有导入,这使得它更难使用。

如果我列出__all__ 中的子模块,我可以使用(皱眉?)from thing import * 语法,但我的所有类都再次位于不必要的命名空间障碍后面。用户必须 (1) 知道他们应该使用 from x import * 而不是 import x,(2) 手动重新洗牌,直到他们能够合理地遵守线宽样式约束。

如果我from thatfile import X 语句添加到__init__.py,我会更接近,但我有命名空间冲突(?)和额外的命名空间用于我不想在那里的东西。在下面的示例中,您会看到:

    JobInfo 类覆盖了名为 JobInfo 的模块对象,因为它们的名称相同。 Python 可以通过某种方式解决这个问题,因为 JobInfo 的类型是 <class 'doit_tools.JobInfo.JobInfo'>。 (doit_tools.JobInfo 是一个类,但 doit_tools.JobInfo.JobInfo 是同一个类...这很纠结,看起来很糟糕,但似乎没有破坏任何东西。) 每个文件名都进入了 doit_tools 命名空间,如果有人正在查看模块的内容,则查看起来会更加混乱。我希望 doit_tools.utility_functions.py 保存一些代码,而不是定义新的命名空间。

.

current_module
\
  doit_tools/
  \
   - (module) JobInfo
      \
       - (class) JobInfo
   - (class) JobInfo
   - (module) NetworkAccessors
      \
       - (class) CachedLookup
       - (class) ThreadedWorker
   - (class) CachedLookup
   - (class) ThreadedWorker
   - (module) utility_functions
      \
       - (Fn) util_a
       - (Fn) util_b
   - (Fn) util_a
   - (Fn) util_b
   - (module) data_functions
      \
       - (Fn) gather_stuff
       - (Fn) analyze_stuff
   - (Fn) gather_stuff
   - (Fn) analyze_stuff

另外,仅导入数据抽象类的人在执行“from doit_tools import JobInfo”时会得到与他们预期不同的结果:

current_namespace
\
 JobInfo (module)
  \
   -JobInfo (class)

instead of:

current_namespace
\
 - JobInfo (class)

那么,这只是组织 Python 代码的错误方式吗?如果不是,那么将相关代码拆分但仍以类似模块的方式收集它的正确方法是什么?

也许最好的情况是执行“从 doit_tools 导入 JobInfo”对于使用该软件包的人来说有点混乱?

也许是一个名为“api”的 python 文件,以便使用该代码的人执行以下操作?:

import doit_tools.api
from doit_tools.api import JobInfo

=============================================

响应 cmets 的示例:

在 python 路径中的文件夹 'foo' 中获取以下包内容。

foo/__init__.py

__all__ = ['doit','dataholder','getSomeStuff','hold_more_data','SpecialCase']
from another_class import doit
from another_class import dataholder
from descriptive_name import getSomeStuff
from descriptive_name import hold_more_data
from specialcase import SpecialCase

foo/specialcase.py

class SpecialCase:
    pass

foo/more.py

def getSomeStuff():
    pass

class hold_more_data(object):
    pass

foo/stuff.py

def doit():
    print "I'm a function."

class dataholder(object):
    pass

这样做:

>>> import foo
>>> for thing in dir(foo): print thing
... 
SpecialCase
__builtins__
__doc__
__file__
__name__
__package__
__path__
another_class
dataholder
descriptive_name
doit
getSomeStuff
hold_more_data
specialcase

another_classdescriptive_name 是否有杂乱无章的东西,并且还有额外的副本,例如doit() 在它们的命名空间下。

如果我在名为 Data.py 的文件中有一个名为 Data 的类,当我执行“从数据导入数据”时,我会遇到命名空间冲突,因为 Data 是当前命名空间中位于模块 Data 内的一个类,不知何故是也在当前命名空间中。 (但 Python 似乎能够处理这个问题。)

【问题讨论】:

【参考方案1】:

您可以这样做,但这并不是一个好主意,而且您正在与 Python 模块/包的工作方式作斗争。通过在__init__.py 中导入适当的名称,您可以使它们在包命名空间中可访问。通过删除模块名称,您可以使它们无法访问。 (关于为什么需要删除它们,请参阅this question)。所以你可以通过这样的方式接近你想要的东西(__init__.py):

from another_class import doit
from another_class import dataholder
from descriptive_name import getSomeStuff
from descriptive_name import hold_more_data
del another_class, descriptive_name
__all__ = ['doit', 'dataholder', 'getSomeStuff', 'hold_more_data']

但是,这将中断对import package.another_class 的后续尝试。一般来说,如果不将 package.module 作为对该模块的可导入引用进行访问,则无法从 package.module 导入任何内容(尽管使用 __all__ 您可以阻止 from package import module)。

更一般地说,通过按类/函数拆分代码,您正在使用 Python 包/模块系统。 Python 模块通常应该包含您想要作为一个单元导入的内容。为方便起见,直接在***包命名空间中导入子模块组件并不少见,但反过来 --- 试图隐藏子模块并允许通过***包命名空间访问其内容---会导致问题。此外,尝试“清理”模块的包命名空间也无济于事。这些模块应该在包命名空间中;那是他们所属的地方。

【讨论】:

这有我上面提到的问题,公共名称可用,但文件名也可用,作为命名空间。此外,命名空间与我提出的 JobInfo 冲突,这似乎很糟糕,我的替代方法是重命名 JobInfo.py 以便该文件包含一个与文件名不同名称的类。完成后,我将不同名称的文件显示为类中的命名空间。它变得一团糟,但这是我能做的最好的吗? @Brian:我不明白你的意思。您不导入的任何名称都将不可用。如果你想排除包名,你可以使用@J.F. 提到的__all__ 技术。塞巴斯蒂安。顺便说一句,您的示例不必要地大且令人困惑。您能否创建一个简单的示例并说明您希望如何引用不同的部分? 已响应示例,刚刚在交互式终端中测试。在示例中,我不希望由 python 文件的名称创建的命名空间位于模块中,因为它很混乱。模块应该公开两个类和两个函数,而不是两个类、两个函数和两个子模块,每个子模块都包含一个类和一个函数。将所有代码移动到一个文件中的解决方案似乎...生硬。 另外请注意,如果我删除 __init__.py 的所有内容,子模块命名空间不会显示在导入 foo 时。必须手动检索它们。 'with __all__ you can block from package import module' 似乎具有误导性 __all__ 不会阻止 from package import module。它只是控制如果您执行 from package import * 时可用的内容【参考方案2】:

__init__.py 中定义__all__ = ['names', 'that', 'are', 'public'] 例如:

__all__ = ['foo']

from ._subpackage import foo

实际示例:numpy/__init__.py


您对 Python 包的工作方式有一些误解:

如果我不使用__init__.py,我将无法导入任何内容,因为 Python 不会从 sys.path 进入文件夹。

您需要在 Python 3.3 之前的 Python 版本中使用 __init__.py 文件来将目录标记为包含 Python 包。

如果我使用空白 __init__.py,当我导入 doit_tools 时,它是一个空的命名空间,其中没有任何内容。我的文件都没有导入,这使得它更难使用。

它不会阻止导入:

from doit_tools import your_module

它按预期工作。

如果我在__all__ 中列出子模块,我可以使用(皱眉?)from thing import * 语法,但我所有的类都在不必要的命名空间障碍后面。用户必须 (1) 知道他们应该使用 from x import * 而不是 import x,(2) 手动重新洗牌,直到他们可以合理地遵守线宽样式约束。

(1) 您的用户(在大多数情况下)应该在交互式 Python shell 之外使用from your_package import *

(2) 你可以使用() 来打断很长的导入行:

from package import (function1, Class1, Class2, ..snip many other names..,
                     ClassN)

如果我将 from thatfile import X 语句添加到 __init__.py,我会更接近,但我有命名空间冲突 (?) 和额外的命名空间用于我不想在那里的东西。

您可以自行解决命名空间冲突(具有相同名称的不同对象)。该名称可以引用任何对象:整数、字符串、包、模块、类、函数等。Python 无法知道您可能更喜欢哪个对象,即使在这种特殊情况下忽略某些名称绑定也会不一致关于在所有其他情况下使用名称绑定。

要将名称标记为非公开,您可以在它们前面加上 _,例如 package/_nonpublic_module.py

【讨论】:

这有我上面提到的问题,我得到公共名称可用,但文件名也可用,作为命名空间,公共名称也埋在其中。这是意料之中的事吗?这是我能做的最好的事情吗? 另外,我是否应该担心与模块 JobInfo 的命名空间冲突被包命名空间中的类 JobInfo 替换?这是那些看起来很脏但实际上并不脏的事情之一,我应该让 Python 来处理它吗? @Brian: __all__ 完全用于区分公共名称和偶然可用的名称。您明确__all__添加您认为是公开的的名称(注意:您的示例foo/__init__.py没有定义__all__)。 @Brian:__init__.py 中定义的任何内容优先。在这种特殊情况下,请遵循 pep-8 并为模块名称使用小写字母(s/JobInfo/jobinfo/ 为模块名称)。 @Brian:您不需要将公共类放在单独的模块中(它不是 Java),因此 abstractionclass 可能是 Python 中模块的一个非常纯粹的名称。要将其标记/记录为非公开,您可以在模块名称前加上 _: _nonpublic_module.pydir() 在我尝试过的所有 Python 版本中返回不在 __all__ 中的名称:Python 2.7、Python 3.3、Jython 2.5、Pypy 1.9。我已更新我的答案以解决您问题中的要点。【参考方案3】:

隐藏包的子结构有完全正当的理由(不仅在调试时)。其中包括方便效率。当试图用一个包做一个快速原型时,不得不打断思路只是为了查找完全无用的信息,具体函数或类的确切子模块可能是什么,这非常烦人。

当一切都在包的顶层可用时,成语:

python -c 'import pkg; help(pkg)'

显示整个帮助,而不仅仅是一些微不足道的模块名称。

您可以随时关闭生产代码的子模块导入,或在开发后清理包模块。

以下是迄今为止我想出的最佳方法。它在尽量不抑制有效错误的同时最大限度地提高便利性。另请参阅full source with doctest documentation。


定义要导入的包名和子模块以避免容易出错的重复:

_package_ = 'flat_export'
_modules_ = ['sub1', 'sub2', 'sub3']

在可用时使用相对导入(这是必要的,请参阅 is_importing_package):

_loaded = False
if is_importing_package(_package_, locals()):
    for _module in _modules_:
        exec ('from .' + _module + ' import *')
    _loaded = True
    del(_module)

尝试导入包,包括__all__。 当执行模块文件作为脚本时会发生这种情况 搜索路径中的包(例如python flat_export/__init__.py

if not _loaded:
    try:
        exec('from ' + _package_ + ' import *')
        exec('from ' + _package_ + ' import __all__')
        _loaded = True
    except (ImportError):
        pass

作为最后的手段,尝试直接导入子模块。 这发生在执行模块文件作为脚本内 搜索路径中没有包的包目录 (例如cd flat_export; python __init__.py)。

if not _loaded:
    for _module in _modules_:
        exec('from ' + _module + ' import *')
    del(_module)

构造__all__(省略模块),除非它已被导入 之前:

if not __all__:
    _module_type = type(__import__('sys'))
    for _sym, _val in sorted(locals().items()):
        if not _sym.startswith('_') and not isinstance(_val, _module_type) :
            __all__.append(_sym)
    del(_sym)
    del(_val)
    del(_module_type)

这里是函数is_importing_package

def is_importing_package(_package_, locals_, dummy_name=None):
    """:returns: True, if relative package imports are working.

    :param _package_: the package name (unfortunately, __package__
      does not work, since it is None, when loading ``:(``).
    :param locals_: module local variables for auto-removing function
      after use.
    :param dummy_name: dummy module name (default: 'dummy').

    Tries to do a relative import from an empty module `.dummy`. This
    avoids any secondary errors, other than::

        ValueError: Attempted relative import in non-package
    """

    success = False
    if _package_:
        import sys
        dummy_name = dummy_name or 'dummy'
        dummy_module = _package_ + '.' + dummy_name
        if not dummy_module in sys.modules:
            import imp
            sys.modules[dummy_module] = imp.new_module(dummy_module)
        try:
            exec('from .' + dummy_name + ' import *')
            success = True
        except:
            pass
    if not 'sphinx.ext.autodoc' in __import__('sys').modules:
        del(locals_['is_importing_package'])
    return success

【讨论】:

“del(unwanted_module)”的妙用,这是关键一步【参考方案4】:

python 不是 java。模块文件名不需要与类名相同。事实上,python 建议使用全部小写的模块文件名。

此外,“从数学导入 sqrt”只会将 sqrt 添加到命名空间,而不是数学。

【讨论】:

尝试在包含文件的文件夹中使用__init__.py。请注意,我说的是使用与文件同名的类导致似乎是一件坏事。另请注意,我将文件与类一起命名的原因是出于组织目的,以便我知道该类的定义存在于该文件中,而不是出于句法原因,并且我提到了将名称更改为不同名称的不愉快副作用. 注意:其他人说他们在这种情况下的行为与我得到的不同。这可能是 python 版本之间的差异。

以上是关于如何将多个 python 文件组织到一个模块中而不像一个包一样?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 API 调用返回的数据直接加载到 BigQuery 中而不存储在 GCS 中?

C# 将“X”次保存到一个 .txt 文件中而不覆盖最后一个字符串

时间循环 - 将矢量数据存储在文件中而不覆盖

将 .so 合并到另一个 .so 中而不依赖?

如何在不同文件夹中组织的多个模块的项目中在 python 中进行导入?

如何将值添加到数组中而不将其在for循环外重置为0