【问题标题】:How to properly structure internal scripts in a Python project?如何在 Python 项目中正确构建内部脚本?
【发布时间】:2020-01-04 18:06:24
【问题描述】:

考虑以下 Python 项目框架:

proj/
├── foo
│   └── __init__.py
├── README.md
└── scripts
    └── run.py

在这种情况下foo 保存主要项目文件,例如

# foo/__init__.py
class Foo():
    def run(self):
        print('Running...')

scripts 包含需要从foo 导入文件的辅助脚本,然后通过以下方式调用:

[~/proj]$ python scripts/run.py

有两种导入Foo 的方法都失败了:

  1. 如果尝试相对导入from ..foo import Foo,则错误为ValueError: attempted relative import beyond top-level package
  2. 如果尝试 absolute 导入 from foo import Foo,则错误为 ModuleNotFoundError: No module named 'foo'

我目前的解决方法是将运行路径附加到sys.path

import sys
sys.path.append('.')

from foo import Foo
Foo().run()

但这感觉像是一种 hack,必须添加到 scripts/ 中的每个新脚本中。

有没有更好的方法来构建此类项目中的脚本?

【问题讨论】:

  • 你有什么理由不把__init__.py 放到proj 或者scripts 直接放到foo 因为它们都是辅助功能?

标签: python python-3.x pythonpath


【解决方案1】:

最佳实践? 在根目录中放置一个入口点

我知道这可能听起来很荒谬,如果你有很多脚本你想能够执行......但它实际上是最干净的选择,它是大型 Python 项目中最常使用的选择,如magage.py in以姜戈为例。它也不需要是一项艰巨的任务。更重要的是,与多个较小的入口点相比,拥有一个入口点总是更安全。

proj/
├── run.py
├── foo
│   └── __init__.py
├── README.md
└── scripts
    └── my_script.py

run.py 位于根目录时,它可以非常轻量级...基本上只是一个包装器,用于从 my_scripts.py 调用您需要的函数。它只是将所有内容联系在一起,因此现在您的所有导入都可以正常工作。

请记住,您的入口点是您的根。根的父级不存在。所以把你的入口点放在根目录下,然后从scripts导入相对于根目录的包,也就是import foo

但是如何调用多个脚本!?

如果您需要能够调用多个脚本,这是一个很好的论据……嗯……arguments!将run.py 保留为您的单一入口点/命令,并利用子命令将功能传递给您关心的脚本。

重新发明轮子?

一般来说,框架已经为您完成了架构,以便您添加自己的子命令,例如 Django,并且为了更小的占用空间,Flask

不过,正如我已经说明的那样,您可以在没有帮助的情况下轻松完成一个小项目。

安全

没有人希望他们的代码在使用几年后更少可重构。没有人希望他们的代码库具有less 安全性。随着我们总体上转向更安全的系统,创建一些看门人脚本来确定什么是安全操作,哪些不是安全操作以及由谁操作是有意义的。将代码移动到基于 LDAP 的系统,并且需要按组锁定内容?没问题。您可以更改单个文件或在代码库中添加 LDAP 安全性,甚至可以创建自己的内部 API。

使用分布式脚本,安全选项的灵活性要低得多,维护起来也困难得多,而且单个漏洞可能会让您大开方便之门。

奖金优势 您正在向脚本库添加抽象。如果您想更改代码库的结构(也许您希望 scripts 拥有更多组织的子文件夹),您/您的用户不需要对任何依赖项进行任何重构,或将路径更改为更长、更详细名字。你的包是独立的,用户唯一需要接触的就是你的proj/run.py入口点。

而且,很明显,您不需要过多地使用 Python 路径!

【讨论】:

    【解决方案2】:

    解决方案

    有多种方法可以实现这一点。两者都需要通过添加 setup.py(基于@matejcik 的答案)来创建 python 包。

    选项 1(推荐): entry_point + console_scripts 在您的项目中注册一个函数作为脚本执行的入口点(即:proj:foo:cli:run)。

    选项 2:scripts:setup() 方法中使用此关键字参数来引用脚本的路径(即:`bin/script.py)。

    注意

    我建议使用像 Click 这样的 CLI 库/框架,这样您的代码库只关心维护应用程序特定的业务逻辑,而不是 CLI 健壮的框架特性逻辑。另外,click推荐使用entry_point+console_scripts的脚本集成方式,因为跨平台兼容。

    设置工具 - 自动创建脚本:https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation

    设置工具 - 关键字参数:https://setuptools.readthedocs.io/en/latest/setuptools.html#new-and-changed-setup-keywords

    点击GitHub:https://github.com/pallets/click/

    点击Setuptools集成:https://click.palletsprojects.com/en/master/setuptools/

    【讨论】:

      【解决方案3】:

      另一种解决方案是在 Python 目录中添加一个.pth 文件

      并写出以下内容,

      # your.pth 
      
      #↓ input the directory of proj
      C:\...\proj  
      

      完成

      # scripts.py
      from foo import Foo
      Foo().run()
      

      它会很好用。

      .. 注意:: 如果您的 IDE 是 PyCharm,那么您也可以使用 Source roots 来帮助您。

      【讨论】:

        【解决方案4】:

        如果您喜欢简单,并且对您的要求没有其他限制,请将一个 __init__.py 添加到 scripts 文件夹和任何其他同级文件夹中,使其成为包,然后始终使用绝对导入形式,正如您所说,您不希望 proj 作为它们的父包,因此那里没有 __init__.py,然后从 proj 文件夹中调用您的脚本(而不是):

        python -m scripts.run

        或者你给除run.py以外的其他脚本起的任何名字

        这类似于@matejcik 答案的选项 2,但更简单。

        【讨论】:

          【解决方案5】:

          您需要将 __init__.py 文件添加到 scriptsproj 文件夹,以便将这些文件视为 Python 包,并让您能够从中导入。

          这也是常用的一种方法,是将您的 fooscripts 文件夹放入 proj/src 文件夹中,然后该文件夹有一个 __init__.py 文件,因此是一个 Python 包。

          【讨论】:

          • 这并不能真正回答我的问题,将__init__.py 添加到顶级目录是不好的品味,也不能真正解决错误。在这个例子中,init 文件中的类只是为了简单起见。
          • @YuvalAdam 是的,__init__.py 中关于该课程的评论是毫无根据的,在那里有它并不可怕。但我坚持将__init__.py 添加到顶级目录是肯定的方法。
          【解决方案6】:

          Python 在sys.path 中列出的目录中查找包/模块。有几种方法可以确保您感兴趣的目录(在本例中为 proj)是这些目录之一:

          1. 将脚本移至proj 目录。 Python 将包含输入脚本的目录添加到sys.path
          2. 将目录proj 放入PYTHONPATH 环境变量的内容中。
          3. 使模块成为可安装包的一部分并安装它,无论是否在虚拟环境中。
          4. 在运行时,将目录proj动态添加到sys.path

          选项 1 是最合乎逻辑的,不需要更改源代码。如果您担心这可能会破坏某些内容,您可以将 scripts 设置为指向 proj 的符号链接? p>

          如果你不愿意这样做,那么……

          您可能认为这是一种 hack,但我建议您修改您的脚本以在运行时更新 sys.path。而是附加一个绝对路径,以便无论当前目录是什么都可以执行脚本。在您的情况下,目录proj 是目录scripts 的父目录,脚本所在的位置,所以:

          import sys
          import os.path
          
          parent_directory = os.path.split(os.path.dirname(__file__))[0]
          if parent_directory not in sys.path:
              #sys.path.insert(0, parent_directory) # the first entry is directory of the running script, so maybe insert after that at index 1
              sys.append(parent_directory)
          

          【讨论】:

            【解决方案7】:

            有两种方法可以解决这个问题。

            (1) 把你的项目变成一个可安装的包

            添加一个proj/setup.py文件,内容如下:

            import setuptools
            
            setuptools.setup(
                name="my-project",
                version="1.0.0",
                author="You",
                author_email="you@example.com",
                description="This is my project",
                packages=["foo"],
            )
            

            创建一个virtualenv:

            python3 -m venv virtualenv  # this creates a directory "virtualenv" in your project
            source ./virtualenv/bin/activate  # this switches you into the new environment
            python setup.py develop  # this places your "foo" package in the environment
            

            在 virtualenv 中,foo 表现为已安装的包,可通过 import foo 导入。

            因此您可以在脚本中使用绝对导入。

            要让它们从任何地方运行,而无需激活 virtualenv,您可以将路径指定为 shebang。

            scripts/run.py(第一行很重要):

            #!/path/to/proj/virtualenv/bin/python
            
            import foo
            
            print(foo.callfunc())
            

            (2) 使脚本成为foo 包的一部分

            创建一个子包,而不是单独的子目录scripts。在proj/foo/commands/run.py:

            from .. import callfunc()
            
            def main():
                print(callfunc())
            
            if __name__ == "__main__":
                main()
            

            然后从顶级proj/ 目录执行脚本:

            python -m foo.commands.run
            

            如果您将它与 (1) 结合并安装您的软件包,您就可以在任何地方运行 python -m foo.commands.run

            【讨论】:

            • 虽然此解决方案确实使模块可用——为了使其可作为典型脚本调用——需要使用两个参数选项之一将脚本添加到 setup.py,如我的回答。
            猜你喜欢
            • 2011-05-26
            • 2018-09-14
            • 2014-07-10
            • 1970-01-01
            • 2022-06-14
            • 1970-01-01
            • 1970-01-01
            • 2021-03-02
            • 1970-01-01
            相关资源
            最近更新 更多