目录

《从 Gopher 到 Pythonista,Python 快速上手指南》- 项目结构

概述

关于本系列文章的详细介绍,请看这篇博客:《从 Gopher 到 Pythonista,Python 快速上手指南 - 序》

欢迎来到 Python 旅程第一站!我们先来解读 Python 项目的结构,这是理解和编写 Python 代码的基础。

项目结构概览

在 Python 中,项目结构可能会根据项目的性质和大小而有所不同。一个典型的 Python 项目的目录结构可能会如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
my_project/

├── my_project/
   ├── __init__.py
   ├── module1.py
   ├── module2.py
   ├── package1/
      ├── __init__.py
      ├── module3.py
      ├── module4.py
      └── ...
   └── ...

├── tests/
   ├── __init__.py
   ├── test_module1.py
   ├── test_module2.py
   └── ...

├── docs/
   └── ...

├── setup.py
├── requirements.txt
└── README.md

在这个结构中:

  • my_project/ 是项目的根目录。
  • my_project/my_project/ 目录包含项目的源代码,__init__.py 文件是一个特殊的文件,它标志着该目录是一个 Python 包。
  • module1.pymodule2.py 是 Python 模块,它们包含 Python 代码。
  • package1/ 是一个 Python 包,它可以包含其他模块和子包。包内的 __init__.py 文件用于组织包的内容。
  • tests/ 目录包含项目的测试代码。
  • docs/ 目录包含项目的文档。
  • setup.py 是一个常见的 Python 项目配置文件,它用于安装,卸载,打包等任务。
  • requirements.txt 列出了项目的依赖。

Python 项目结构并没有严格的规范,不过这种结构基本就是 Python 社区中通用的最佳实践,你可以在很多开源 Python 项目中看到这种结构。

Gopher 看 Python 的项目结构

如你所见,Python 项目的目录结构和 Golang 还是有一定的差异。

  1. 首先可能你注意到的是 Python 项目的源代码没有直接放在项目的根目录下,而是会被放在一个和项目同名的子目录中,如 my_project/my_project/,这与 Golang 的习惯不同。Golang 项目的源代码通常会直接放在项目的根目录下。这种差异其实来自于 Python 项目里会使用 setup.py 文件描述如何安装项目,这时候 setup.py 处在项目根目录,如果所有的源码在根目录的同名子目录里,就很方便被当作包导入;相应地,在 Golang 里其实没有这种需求,Golang 项目直接在根目录下编译源码就可以得到可执行文件,自然也就没必要多加一层同名子目录了。关于 setup.py 在后面的文章里会更有详细的介绍。

  2. 其次你可能会想问 Python 项目的 main.py 在哪里?这就取决于项目的具体需求了,也就是说 main.py 文件其实可有可无。在一些情况下,Python 项目会有一个 main.py 文件作为项目的入口,这个文件通常会放在项目的根目录下,或者在和项目同名的子目录中。当然 Golang 项目也不一定非要存在 main.go。如果一个 Python 脚本需要被直接执行,那么这个脚本文件就可以视为项目的入口点。在这种情况下,这个脚本文件通常会包含一个 if __name__ == "__main__": 代码块来定义当文件被直接执行时应该执行的代码,这个体验和 Golang 挺不一样的,后面你会看到例子。

总的来说,虽然 Python 的项目结构和 Golang 的项目结构有所不同,但是一旦你理解了 Python 的包和模块的概念,你就会发现这个结构其实也是很符合逻辑的。(提示:下一篇文章里,我们会详细介绍 Python 的包和模块)

多个可执行脚本的管理

前面提到了 main.py 可有可无,其实 Python 里还会时常出现“存在多个可执行脚本入口”,而在 Golang 里虽然技术上允许,但是可能很少有人这样玩。

在一些较大的 Python 项目中,可能会有多个可以直接运行的 Python 脚本。这些脚本可能完成各种各样的任务,如数据清洗,模型训练,或者启动一个 web 服务器等。这时候,一个常见的做法是将所有的可执行脚本放置在一个单独的目录中,以便于管理和查找。

这个目录通常被命名为 bin/scripts/。例如,一个可能的项目结构可能如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
my_project/

├── my_project/
   ├── __init__.py
   ├── module1.py
   ├── module2.py
   └── ...

├── scripts/
   ├── data_cleaning.py
   ├── train_model.py
   ├── start_server.py
   └── ...

├── tests/
   ├── __init__.py
   ├── test_module1.py
   ├── test_module2.py
   └── ...

├── docs/
   └── ...

├── setup.py
├── requirements.txt
└── README.md

在这个例子中,scripts/ 目录包含了三个可执行脚本:data_cleaning.pytrain_model.py,和 start_server.py。每个脚本都可以通过 Python 直接执行,如 python scripts/data_cleaning.py

使用这种方式来组织你的可执行脚本有几个优点:

  1. 易于查找:将所有的可执行脚本放在一个地方,可以让它们更容易被找到。
  2. 清晰的职责划分:这样做也可以帮助清晰地划分项目的不同部分。例如,my_project/ 目录包含项目的主要源代码,tests/ 目录包含测试代码,scripts/ 目录包含可执行脚本。
  3. 灵活的工作流:如果你的项目需要执行一系列的任务,你可以将这些任务分解到不同的脚本中。然后,你可以根据需要来运行这些脚本,或者使用 shell 脚本来自动化这个过程。

在管理多个可执行脚本时,还需要注意保持脚本的独立性。每个脚本都应该可以独立运行,并且完成一个明确的任务。这样,当你需要修改一个脚本或添加一个新的脚本时,你就不需要修改其他的脚本。

源文件组织

接下来我们继续研究一下 Python 源文件,也就是 .py 文件的组织结构。

在 Python 中,一个 .py 文件,或者说一个模块,通常包含以下几个部分:

  1. 模块文档字符串
  2. import 语句
  3. 全局变量和常量的定义
  4. 类和函数的定义
  5. 可执行代码(通常放在 if __name__ == "__main__" 代码块中)

让我们通过一个具体的例子来了解这个结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# module1.py

"""
This is a module docstring. It provides a high-level overview of what this module does.
"""

# Import statements
import os
import sys

# Global variables and constants
CONSTANT1 = 10

# Class definitions
class MyClass:
    """
    This is a class docstring. It provides a high-level overview of what this class does.
    """
    def __init__(self):
        pass

# Function definitions
def my_function():
    """
    This is a function docstring. It provides a high-level overview of what this function does.
    """
    pass

# Executable code
if __name__ == "__main__":
    my_function()

这个例子中展示了一个典型 Python 模块的基本组织结构:

  • 首先,我们有一个模块级别的文档字符串(docstring),这是一种特殊的注释,用来为模块提供一个高级别的概述。
  • 然后,我们有一些 import 语句,用来导入模块需要的其他模块。
  • 接着,我们定义了一些全局变量和常量。
  • 然后我们定义了一个类和一个函数,每个都有自己的文档字符串。
  • 最后,我们有一个 if __name__ == "__main__": 代码块,这是模块的可执行部分。当模块被直接执行时,这个代码块中的代码会被运行。

在 Python 中,我们通常会遵循这个结构来组织我们的源代码文件,这样可以使我们的代码更容易阅读和理解。当然,这不是强制的,根据你的项目的特点和需求,你可以选择自己的组织方式。

或许你还对这段代码感觉陌生,虽然你似乎都能看懂,但是多多少少会纠结一些里面的细节问题,比如 Golang 里并没有 class,Python 里的 class 具体怎么玩?别急,后面都会聊到。

可执行代码

前面提到了“可执行代码”的概念,我们再来仔细掰扯下。

在 Python 中,“可执行代码” 是指那些在模块被导入或直接运行时被执行的代码。这种代码通常位于模块的顶层(不在函数或类的定义内),或者在 if __name__ == "__main__": 代码块中。

  1. 模块顶层的可执行代码:这种代码会在模块被导入时立即执行。例如,你可能在模块的顶层设置一些全局变量,或者执行一些需要在模块导入时就进行的初始化操作。
1
2
3
4
5
6
7
# module1.py

# Executable code at the top level of the module
print("This code is executed when the module is imported")

def my_function():
    print("This code is executed when my_function is called")

在上面的例子中,当你导入 module1 时,print("This code is executed when the module is imported") 这行代码会立即被执行。而 my_function 函数内的代码则只有在函数被调用时才会执行。

  1. if __name__ == "__main__": 代码块中的可执行代码:这种代码只有在模块被直接运行时才会被执行,而在模块被导入时则不会执行。这种特性常常被用于编写只在模块被作为脚本直接运行时才需要执行的代码,例如单元测试或演示代码。
1
2
3
4
5
6
7
8
9
# module1.py

def my_function():
    print("This code is executed when my_function is called")

# Executable code in the __main__ block
if __name__ == "__main__":
    print("This code is executed only when the module is run directly")
    my_function()

在上面的例子中,当你直接运行 module1.py(例如,通过命令 python module1.py)时,print("This code is executed only when the module is run directly") 这行代码和 my_function() 这行代码会被执行。但是,如果你导入 module1(例如,通过 import module1),这些代码则不会被执行。

文档字符串

Golang 里没有文档字符串的概念,所以我想再细说下前面提到的文档字符串。

在 Python 中,文档字符串(docstrings)是一种内置的注释方式,它们可以用来解释代码的功能和用法。通常,文档字符串是在函数、类或模块的开头写的一段多行的字符串,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def add(a, b):
    """
    Returns the sum of two numbers.
    
    Args:
        a: The first number.
        b: The second number.
        
    Returns:
        The sum of a and b.
    """
    return a + b

在这个例子中,add 函数的文档字符串解释了函数的功能、输入参数和返回值。这些信息对于理解和使用这个函数非常有用。

文档字符串不仅可以提高代码的可读性,还可以用来自动生成 API 文档。通过使用工具如 Sphinx,我们可以从源代码中提取文档字符串,然后生成格式化的 HTML 或 PDF 文档。这种方式可以确保代码和文档之间的一致性,因为它们都在同一个地方维护。

在编写文档字符串时,我们通常遵循一些格式化的规则,以便于工具可以解析这些文档字符串。最常见的两种规则是 Google 风格和 numpydoc 风格。选择哪种风格主要取决于你的个人喜好和项目的要求。

总的来说,文档字符串是 Python 中的一个重要特性,它可以帮助我们编写可读性高、易于维护的代码。同时,它也可以方便我们生成和维护 API 文档,从而提高项目的可用性和专业性。

Gopher 看 Python 的文档字符串

Golang 没有 Python 中的文档字符串(docstrings)这样的概念。不过 Golang 的注释和 Python 的文档字符串有相似的目的,即为代码提供文档。Golang 在函数、类型、常量和变量声明之前的注释被视为对其的文档。

这里是一个 Go 函数的示例:

1
2
3
4
// Add returns the sum of two integers.
func Add(a int, b int) int {
    return a + b
}

在这个例子中,Add 函数前的注释解释了函数的功能。

对于 Golang 来说,其标准库提供了一个叫做 godoc 的工具,这个工具可以从源代码中提取这些注释,并自动生成文档。这使得 Golang 的源代码可以和文档保持一致,并且易于维护。godoc 可以生成 HTML 或者其他格式的文档,可以本地查看,也可以发布到特定网站上。

值得注意的是,godoc 对注释的格式有一些要求,例如,每个导出的(即首字母大写的)函数、类型、方法或字段的注释都应以该名称开头,并且以句号结束。这样的规范有助于生成清晰、一致的文档。

总之,Golang 和 Python 都具有文档生成能力,不过 Golang 没有 Python 中的文档字符串这种语法,Golang 是通过注释和 godoc 工具达到类似的目的。

总结

本文到此,我们回顾下 Gophers 在编写 Python 代码时需要注意的一些细节。

首先,Gophers 一开始可能会忽视 Python 中的文档字符串(docstrings)。在 Golang 中,我们习惯于在函数上添加注释以提供文档,但在 Python 中,文档字符串是一种更正式的文档形式,它在源代码中被视为首选的文档形式。因此我们应该记得在模块、类、函数和方法中添加适当的文档字符串。

其次,Python 的项目结构和 Golang 不太一样,我们应当遵循 Python 社区中公认的最佳实践,将代码组织为模块和包,并在需要时提供多个可执行入口脚本。

最后,别忘了重视 Python 代码的格式化和风格指南。Golang 有强制的格式化规则和 gofmt 工具,Python 有 PEP 8 风格指南和 black 这样的格式化工具。本文没有细说相关内容,我建议你下来自己了解下 PEP 8,尝试用下 black 工具。

总的来说,虽然 Golang 和 Python 在许多方面有所不同,不过我希望 Gopher 能够坦然接受这种差异,相信它们没有优劣之分。

希望这篇文章可以帮助到你!

(本系列文章将在微信公众号“胡说云原生”连载)