有人能用Python解释一下吗?

2024-04-25 06:04:14 发布

您现在位置:Python中文网/ 问答频道 /正文

我越来越多地使用Python,并且不断地看到变量__all__在不同的__init__.py文件中设置。有人能解释一下这是怎么回事吗?


Tags: 文件pyinitall
3条回答

链接到,但这里没有明确提到,正是在使用__all__时。它是一个字符串列表,定义在模块上使用from <module> import *时将导出模块中的哪些符号。

例如,foo.py中的以下代码显式导出符号barbaz

__all__ = ['bar', 'baz']

waz = 5
bar = 10
def baz(): return 'baz'

然后,可以像这样导入这些符号:

from foo import *

print(bar)
print(baz)

# The following will trigger an exception, as "waz" is not exported by the module
print(waz)

如果上面的__all__被注释掉,那么这段代码将执行到完成,因为import *的默认行为是从给定的命名空间导入所有不以下划线开头的符号。

引用:https://docs.python.org/tutorial/modules.html#importing-from-a-package

注意:__all__仅影响from <module> import *行为。在__all__中未提及的成员仍然可以从模块外部访问,并且可以使用from <module> import <member>导入。

Explain __all__ in Python?

I keep seeing the variable __all__ set in different __init__.py files.

What does this do?

__all__做什么?

它从模块中声明语义上的“public”名称。如果__all__中有一个名称,那么用户应该使用它,并且他们可以期望它不会改变。

它还将产生程序性影响:

import *

__all__在模块中,例如module.py

__all__ = ['foo', 'Bar']

意味着当您从模块中import *时,只导入__all__中的那些名称:

from module import *               # imports foo and Bar

文档工具

文档和代码自动完成工具可能(实际上,应该)还检查__all__,以确定从模块中显示的可用名称。

__init__.py使目录成为Python包

docs

The __init__.py files are required to make Python treat the directories as containing packages; this is done to prevent directories with a common name, such as string, from unintentionally hiding valid modules that occur later on the module search path.

In the simplest case, __init__.py can just be an empty file, but it can also execute initialization code for the package or set the __all__ variable.

因此__init__.py可以声明__all__

管理API:

包通常由模块组成,这些模块可以相互导入,但它们必须与__init__.py文件绑定在一起。这个文件使目录成为一个实际的Python包。例如,假设包中包含以下文件:

package
├── __init__.py
├── module_1.py
└── module_2.py

让我们用Python创建这些文件,这样您就可以继续-您可以将以下内容粘贴到Python 3 shell中:

from pathlib import Path

package = Path('package')
package.mkdir()

(package / '__init__.py').write_text("""
from .module_1 import *
from .module_2 import *
""")

package_module_1 = package / 'module_1.py'
package_module_1.write_text("""
__all__ = ['foo']
imp_detail1 = imp_detail2 = imp_detail3 = None
def foo(): pass
""")

package_module_2 = package / 'module_2.py'
package_module_2.write_text("""
__all__ = ['Bar']
imp_detail1 = imp_detail2 = imp_detail3 = None
class Bar: pass
""")

现在您已经提供了一个完整的api,其他人可以在导入您的包时使用它,例如:

import package
package.foo()
package.Bar()

当创建模块时,包中不会有您使用的所有其他实现细节,这些模块会使package名称空间变得杂乱无章。

__all____init__.py

经过更多的工作,也许你已经决定模块太大了(像成千上万行?)需要分开。因此,您可以执行以下操作:

package
├── __init__.py
├── module_1
│   ├── foo_implementation.py
│   └── __init__.py
└── module_2
    ├── Bar_implementation.py
    └── __init__.py

首先使用与模块相同的名称创建子包目录:

subpackage_1 = package / 'module_1'
subpackage_1.mkdir()
subpackage_2 = package / 'module_2'
subpackage_2.mkdir()

移动实现:

package_module_1.rename(subpackage_1 / 'foo_implementation.py')
package_module_2.rename(subpackage_2 / 'Bar_implementation.py')

为每个声明__all__的子包创建__init__.py

(subpackage_1 / '__init__.py').write_text("""
from .foo_implementation import *
__all__ = ['foo']
""")
(subpackage_2 / '__init__.py').write_text("""
from .Bar_implementation import *
__all__ = ['Bar']
""")

现在仍然在包级别配置了api:

>>> import package
>>> package.foo()
>>> package.Bar()
<package.module_2.Bar_implementation.Bar object at 0x7f0c2349d210>

而且,您可以很容易地向API添加可以在子包级别而不是子包的模块级别管理的内容。如果要向API添加新名称,只需更新__init__.py,例如在模块2中:

from .Bar_implementation import *
from .Baz_implementation import *
__all__ = ['Bar', 'Baz']

如果您还没有准备好在顶级API中发布Baz,那么在顶级API中__init__.py您可以:

from .module_1 import *       # also constrained by __all__'s
from .module_2 import *       # in the __init__.py's
__all__ = ['foo', 'Bar']     # further constraining the names advertised

如果您的用户知道Baz的可用性,他们可以使用它:

import package
package.Baz()

但如果他们不知道,其他工具(比如pydoc)不会通知他们。

稍后,当Baz准备好进入黄金时段时,您可以更改:

from .module_1 import *
from .module_2 import *
__all__ = ['foo', 'Bar', 'Baz']

前缀___all__

默认情况下,Python将导出所有不以_开头的名称。你当然可以依赖这个机制。事实上,Python标准库中的某些包确实依赖于此,但要这样做,它们会将导入别名为,例如在^{}

import os as _os, sys as _sys

使用_约定可能更优雅,因为它消除了再次命名名称的冗余。但是它增加了导入的冗余(如果你有很多导入的话),忘记一致地做这件事是很容易的,而且你最不想做的就是无限期地支持一些你只想成为实现细节的东西,仅仅因为你在命名函数时忘了加上前缀_

我个人在模块开发生命周期的早期为模块编写了一个__all__,这样其他可能使用我的代码的人就知道他们应该使用什么而不是使用什么。

标准库中的大多数包也使用__all__

当避免__all__时是有意义的

在以下情况下,可以使用_前缀约定代替__all__

  • 您仍然处于早期开发模式,没有用户,并且不断地调整您的API。
  • 也许你知道有用户,但是你有覆盖API的单元测试,你仍然在积极地添加到API中并在开发中调整。

装饰器

使用__all__的缺点是,必须编写两次导出的函数和类的名称,并且信息与定义分开保存。我们可以用装饰器来解决这个问题。

我是从大卫·比兹利关于包装的演讲中得到这样一个出口装潢师的主意的。这个实现在CPython的传统导入器中似乎工作得很好。如果您有一个特殊的导入钩子或系统,我不保证,但是如果您采用它,退出是相当简单的—您只需要手动将名称添加回__all__

例如,在实用程序库中,您可以定义decorator:

import sys

def export(fn):
    mod = sys.modules[fn.__module__]
    if hasattr(mod, '__all__'):
        mod.__all__.append(fn.__name__)
    else:
        mod.__all__ = [fn.__name__]
    return fn

然后,在定义__all__的地方,执行以下操作:

$ cat > main.py
from lib import export
__all__ = [] # optional - we create a list if __all__ is not there.

@export
def foo(): pass

@export
def bar():
    'bar'

def main():
    print('main')

if __name__ == '__main__':
    main()

无论是作为主函数运行还是由另一个函数导入,这都可以正常工作。

$ cat > run.py
import main
main.main()

$ python run.py
main

使用import *的API设置也可以工作:

$ cat > run.py
from main import *
foo()
bar()
main() # expected to error here, not exported

$ python run.py
Traceback (most recent call last):
  File "run.py", line 4, in <module>
    main() # expected to error here, not exported
NameError: name 'main' is not defined

它是该模块的公共对象列表,由import *解释。它覆盖了隐藏以下划线开头的所有内容的默认设置。

相关问题 更多 >