组织Python代码技巧
一.构建Python项目
首先关注目录结构、文件命名和模块组织。
我
如下图:
<project>
├── src
│ ├── <module>/*
│ │ ├── __init__.py
│ │ └── many_files.py
│ │
│ └── tests/*
│ └── many_tests.py
│
├── .gitignore
├── pyproject.toml
└── README.md
<module> 是您的主要模块。
对此有疑问,可以想象一下使用pip安装模块的时候包名如 pip install pymsql
其中mysql就是包名,再想象一下我们导入模块 import pymysql
其中pymysql也是包名。
通常情况下,它与顶级项目的名称相同,但是你也可以不遵守。
1.设置src目录的原因
有很多项目有不同的做法,有些项目没有src
目录,所有项目模块都分散开,显得项目很杂乱不易于管理。
如下案例:
non_recommended_project
├── <module_a>/*
│ ├── __init__.py
│ └── many_files.py
│
├── .gitignore
│
├── tests/*
│ └── many_tests.py
│
├── pyproject.toml
│
├── <module_b>/*
│ ├── __init__.py
│ └── many_files.py
│
└── README.md
由IDE会对模块名称进行排序,就会导致我们查看时很麻烦。
将模块设置在src内部的主要原因是将所有项目代码集中到一个目录中,CI/CD设置、项目元数据可以放在项目模块之外。
这样做唯一的缺点是你不能直接在Python代码中直接import module_a
,我们需要将项目设置为安装在这个资源库下。
我们将在下面解决这个问题。
2.如何命名文件
2.1.无'文件'
首先,在Python中没有"文件"这一概念,这就是让初学者感到困惑的主要原因。
如果任意目录包含`__init__.py`,那他就是一个由模块组成的目录而不是文件。如果任意目录包含__init__.py
,那他就是一个由模块组成的目录而不是文件。
将每个模块视为一个命名空间
我指的是命名空间,因为你不能确定他们是否有很多函数、类,或者只是常量。它可以包含所有这些,也可以只是一些。
2.2.根据需要将事务放在一起
应该在一个模块中存放多个类(必须这些类与模块相关)。
只有当一个模块太大了或者需要处理不同问题的时候,我们才需要考虑是不是要将其分割。
受其他开发语言(eg:java,c#)的开发经验影响,有很多人认为这是一种不好的做法,但是在Python我推荐这样做
2.3.模块命名尽量命名为复数
根据经验,尽量结合业务背景用复数来给模块进行命名。
不过这条规则也有例外!模块可以命名为 core、main.py 等,以表示一个单一的事物。
根据自身项目进行判断,如果不知道如何命名可以直接用复数命名。
3.命名模块的真实案例
以Google Maps Crawler 项目作为示例。
该项目负责使用Selenium从Google地图中抓取数据并输出
以下是该项目的目录(符合以上三条规则):
gmaps_crawler
├── src
│ └── gmaps_crawler
│ ├── __init__.py
│ ├── config.py 👈 (单数)
│ ├── drivers.py
│ ├── entities.py
│ ├── exceptions.py
│ ├── facades.py
│ ├── main.py 👈 (单数)
│ └── storages.py
│
├── .gitignore
├── pyproject.toml
└── README.md
导入模块也是非常简单:
from gmaps_crawler.storages import get_storage
from gmaps_crawler.entities import Place
from gmaps_crawler.exceptions import CantEmitPlace
我明白我可能在exceptions
中有一个或多个异常类。
拥有多个模块的好处在于:
- 可以丰富模块功能
- 如果需要,您可以随时将其分解成更小的模块
- 他可以让您想了解模块内部存在的功能
二、命名类,函数,变量
有些人觉得命名很难。当您定义一些准则时,它会变得不那么困难。
1.函数和方法应该是动词
函数和方法的命令规则 ---函数和方法应该是动词
函数和方法代表一个动作或可操作的东西,可以是未发生的动作也可以是正在发生的动作。
所以用动词描述更加准确。
如下:
建议的命名:
def get_orders():
...
def acknowledge_event():
...
def get_delivery_information():
...
def publish():
...
不建议的命名:
def email_send(): # send_email()
...
def api_call(): # call_api()
...
def specific_stuff():
...
上面的例子不清楚函数是否返回一个对象允许我对API调用,或者它是否实际发送了电子邮件。
我们可以想象下列的场景:
# 误导性函数名称的例子
email_send.title = "title"
email_send.dispatch()
通常都会使用这条规则作为函数和方法的命名,但是也有例外如:
- 创建一个要在应用程序的主入口点调用的 main() 函数
- 使用@property 将类方法视为属性
2.变量和常量应该为名词
变量和常量的命令规则 ---变量和常量应该为名词
必须是名词,绝对不能使用动词(这是和函数/方法之间的区别)。
如下:推荐的命名
plane = Plane()
customer_id = 5
KEY_COMPARISON = "abc"
不推荐命名:
fly = Plane()
get_customer_id = 5
COMPARE_KEY = "abc"
如果您的变量/常量是列表或集合,请将其设为复数!
python的列表和集合怎么命名
---
如果您的变量/常量是列表或集合,请将其设为复数!
planes: list[Plane] = [Plane()] # 👈 即使他只有一项
customer_ids: set[int] = {5, 12, 22}
KEY_MAP: dict[str, str] = {"123": "abc"} # 👈 字典保持单数
3.类名应该见名知意,后缀除外
建议类命名的时候尽量做到见名知意,在一些必要的时候加上 Service
,Strategy
,Middkeware
这样的后缀也是可以的。
-
类的命名尽量使用单数
-
建议类命名的时候尽量做到见名知意,在一些必要的时候加上
Service
,Strategy
,Middkeware
这样的后缀也是可以的。
复数总让人想起集合。(例如,我读到以orders命名的类,我会认为它是列表或者可迭代函数),
所以要提醒自己,一旦一个类被实例化,它就成为一个单一的对象。
类代表的是实体
代表商业环境中事物的类应该以原样命名(名词).如:Order
,Sale
,Store
,Restaurant
等等
后准用法示例
假设您想创建一个负责发送电子邮件的类。如果你将类命名为Email
,这个类的功能就不是很清晰。
有的可能认为其实一个实体。
例如:
email = Email() # 推断的用法示例
email.title = "Title"
email.body = create_body()
email.send_to = "guilatrova.dev"
send_email(email)
你应该将其命名为:EmailSender
或 EmailService
。
三、大小写约定
默认情况下遵守下列命名约定
类型 | 公开 | 内部 |
---|---|---|
Packages (包目录) | lower_with_under(小驼峰下划线分割) | - |
Modules (模块文件) | lower_with_under.py(小驼峰下划线分割) | - |
Classes(类) | CapWords(大驼峰) | - |
Functions and methods( 函数方法) | lower_with_under()(小驼峰下划线分割) | _lower_with_under()(小驼峰下划线分割) |
Constants(常量) | ALL_CAPS_UNDER(大驼峰下划线分割) | _ALL_CAPS_UNDER(大驼峰下划线分割) |
四、关于"私有"化方法的声明
有些认为__method(self)(任何以两个下划线开头的方法)Python将不允许外部类/方法正常调用它,这样很好。
如果你了解过其他语言例如c#,那么这样做你会感到十分奇怪。
但Guido(Python 的创造者)这样解释道:
"We're all consenting adults here" # 我们都是成年人
这意味着如果你知道你不应该调用一个方法,那么你不应该调用,除非你知道你在做什么。
毕竟,如果您真的决定调用该方法,您将要做一些复杂的事情来实现它(在 C# 中称为“反射”)。
用一个初始下划线标记您的私有方法/函数,以声明它仅供私人使用并接受它。
五、何时在Python中创建函数或类?
如果您遵循上述建议,您将拥有清晰的模块,而清晰的模块是组织功能的有效方式:
from gmaps_crawler import storages
storages.get_storage() # 👈 类似于一个类,除了它没有实例化并且有一个复数名称
storages.save_to_storage() # 👈 模块内的潜在功能
有时,你可以在一个模块内确定函数的子集。当这种情况发生时,一个类会更有意义。
对不同函数子集进行分组的示例
考虑具有 4 个功能的相同存储模块:
def format_for_debug(some_data):
...
def save_debug(some_data):
"""Prints in the screen"""
formatted_data = format_for_debug(some_data)
print(formatted_data)
def create_s3(bucket):
"""Create s3 bucket if it doesn't exists"""
...
def save_s3(some_data):
s3 = create_s3("bucket_name")
...
S3 是一种云存储,用于存储亚马逊 (AWS) 提供的任何类型的数据。这就像软件的 Google Drive。
我们可以这样认为:
- 开发者可以将数据保存在DEBUG模式(即只在屏幕上打印)或S3模式(即在云上存储数据)。
- save_debug使用format_for_debug函数。
- save_s3使用create_s3函数
我可以看到两组函数,没有理由把它们放在不同的模块中,因为它们看起来很小,因此我喜欢把它们定义为类。
class DebugStorage:
def format_for_debug(self, some_data):
...
def save_debug(self, some_data):
"""Prints in the screen"""
formatted_data = self.format_for_debug(some_data)
print(formatted_data)
class S3Storage:
def create_s3(self, bucket):
"""Create s3 bucket if it doesn't exists"""
...
def save_s3(self, some_data):
s3 = self.create_s3("bucket_name")
...
规则:
1. 始终从函数开始 2. 一旦你觉得你可以将不同的功能子集分组,就封装成类六、创建模块和入口
每一个程序都应该有一个入口。
这意味着有一个单一的模块(又称文件)来运行你的应用程序。
它可以是一个单一的脚本,也可以是一个大的模块。
每当您创建入口时,请务必添加一个条件以确保他在导入前就能够执行:
def execute_main():
...
if __name__ == "__main__": # 👈 添加此条件
execute_main()
通过这样做,您可以确保任何导入都不会意外触发您的代码。除非明确执行。
为模块定义main
一些 python 包可以通过传递 -m 来调用,比如:
执行python包python -m pytest
python -m tryceratops
python -m faust
python -m flake8
python -m black
这样的软件包几乎被当作普通命令对待,因为你也可以把它们当作普通命令来运行。
pytest
tryceratops
faust
flake8
black
为此,您需要在主模块中指定一个 __main__.py 文件:
<project>
├── src
│ ├── example_module 👈 Main module
│ │ ├── __init__.py
│ │ ├── __main__.py 👈 Add it here
│ │ └── many_files.py
│ │
│ └── tests/*
│ └── many_tests.py
│
├── .gitignore
├── pyproject.toml
└── README.md
不要忘记您仍然需要在 __main__.py 文件中包含检查 __name__ == "main" 。
安装模块时,可以将项目作为 python -m example_module 运行。