文章目录
像专业人士一样组织Python代码🐍📦
For every minute spent in organizing, an hour is earned.
by Benjamin Franklin(本杰明·富兰克林)
Python与c#或Java等语言不同,在这些语言中,它们强制让类以它们所在的文件命名。
到目前为止,Python是我接触过的最灵活的语言之一,任何过于灵活的东西都会增加错误决策的几率。
- 你想把所有的项目类保存在一个
main.py
文件中吗?是的,它可以。 - 您需要读取操作系统环境变量吗?就在那里读吧。
- 你需要修改一个函数的行为吗?为什么不使用装饰器!?
许多容易实现的决策可能会适得其反,产生极其难以维护的代码。
如果你知道自己在做什么,这并不一定是坏事。
在本章中,我将向你介绍过去在不同的公司和许多不同的人工作时,对我有用的准则。
🌳组织你的Python项目
让我们首先关注目录结构、文件命名和模块组织。
我建议你把所有的模块文件放在src
文件夹里面,所有的测试都和它并排而居。
顶层项目
<project>
├── src
│ ├── <module>/*
│ │ ├── __init__.py
│ │ └── many_files.py
│ │
│ └── tests/*
│ └── many_tests.py
│
├── .gitignore
├── pyproject.toml
└── README.md
其中<module>
是你的主要模块。如果有疑问,请考虑人们会pip install
什么,以及你希望如何import module
。
通常情况下,它与顶级项目的名称相同。但这并不是一个规则。
🎯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的字母排序而使东西(module_a和module_b)分开是很令人厌烦的。
src
文件夹背后的主要原因是让活动的项目代码集中在一个目录中,而设置、CI/CD设置和项目元数据可以驻留在它的外部。
这样做的唯一缺点是不能开箱即用地在python代码中import module_a
。我们需要将项目安装在此资源库下。我们稍后将在本章中讨论如何解决这个问题。
🏷️ 如何命名文件
规则 1:没有文件
首先,在 Python 中没有 "文件 "这种东西,我注意到这是让初学者感到困惑的主要原因。
如果你在一个包含任何__init__.py
的目录里面,那就是一个由模块组成的目录,而不是文件。
把每个模块看作是一个命名空间。
我指的是命名空间,因为你不能确定它们是否有很多函数、类,或者只是常量。它可以有几乎所有的东西,或者只是一堆一些。
规则2:根据需要把东西放在一起
在一个模块内有几个类是可以的,你应该这样做。(当类与模块相关时,显然。)
只有当你的模块变得太大,或处理不同的关注点时,才将其分解。
通常情况下,人们认为这是一种不好的做法,因为其他语言的一些经验,它们执行的是相反的方式(例如Java和C#)。
规则3:默认情况下给出复数的名字
作为一个经验法则,用复数来命名你的模块,并以业务背景来命名。
不过这条规则也有例外!模块可以被命名为core
、main.py
,以及类似的名字,以代表一个单一的东西。使用你的判断力,如果有疑问,坚持使用复数规则。
🔎命名模块时的现实生活例子
我将分享一个我建立的Google Maps Crawler项目,作为一个例子。
这个项目负责使用Selenium从Google Maps抓取数据并输出(如果好奇的话,可以在这里阅读更多内容)。
这是目前的项目树概述,包括了3号规则的例外情况。
gmaps_crawler
├── src
│ └── gmaps_crawler
│ ├── __init__.py
│ ├── config.py 👈 (Singular)
│ ├── drivers.py
│ ├── entities.py
│ ├── exceptions.py
│ ├── facades.py
│ ├── main.py 👈 (Singular)
│ └── 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
中可能有一个或多个exception类,等等。
拥有复数模块的好处在于:
- 它们不会太小(例如,每个类有一个)
- 如果需要,你可以在任何时候将其分解成更小的模块
- 它们给你一种强烈的感觉,让你知道里面可能存在的东西
🔖 命名类、函数和变量
有些人声称给事物命名是很难的。当你定义一些准则时,它就不那么难了。
👊 函数和方法应该是动词
函数和方法代表一个动作或可操作的东西。
有些东西 “不是”。一些东西正在 “发生”。
行动是由动词明确说明的。
从我以前做过的真实项目中举几个好例子。
def get_orders():
...
def acknowledge_event():
...
def get_delivery_information():
...
def publish():
...
一些不好的例子:
def email_send():
...
def api_call():
...
def specific_stuff():
...
他们有点不清楚他们是否返回一个对象让我执行API调用,或者是否真的发送了电子邮件,例如。
我可以想象这样的场景:
email_send.title = "title"
email_send.dispatch()
这条规则的例外情况只是少数,但它们存在。
- 在你的应用程序的主入口处创建一个main()函数来被调用是跳过这条规则的一个好理由。
- 使用@property将类方法视为属性也是有效的。
🐶 变量和常量应该是名词
应该永远是名词,而不是动词(这澄清了函数之间的区别)。
好的例子:
plane = Plane()
customer_id = 5
KEY_COMPARISON = "abc"
坏的例子:
fly = Plane()
get_customer_id = 5
COMPARE_KEY = "abc"
如果你的变量/常量是一个列表或集合,那就用复数吧!
planes: list[Plane] = [Plane()] # 👈 Even if it contains only one item
customer_ids: set[int] = {5, 12, 22}
KEY_MAP: dict[str, str] = {"123": "abc"} # 👈 Dicts are kept singular
🏛️类应该是自解释的,但后缀也可以
更倾向于使用自解释的名字的类。像Service
,Strategy
,Middleware
这样的后缀是可以的,但只有在极其必要的情况下才能使其目的明确。
总是用单数而不是复数来命名它。复数让我们想起了集合(例如,如果我读到订单,我就认为它是一个列表或可迭代的),所以要提醒自己,一旦一个类被实例化,它就会成为一个单一的对象。
代表实体的类
代表商业环境中的事物的类应该按原样命名(名词!)。像Order
、Sale
、Store
、Restaurant
等等。
后缀的使用实例
让我们考虑一下,你想创建一个负责发送电子邮件的类。如果你仅仅把它命名为"Email
",它的目的就不明确了。
有人可能认为它可能代表一个实体,例如:
email = Email() # inferred usage example
email.title = "Title"
email.body = create_body()
email.send_to = "guilatrova.dev"
send_email(email)
你应该把它命名为"EmailSender
"或"EmailService
"。
🐪 命名惯例
默认情况下遵循这些命名惯例。
⚠️ 关于"““私有””"方法的声明
一些人发现,如果你有__method(self)
(任何以两个下划线开头的方法),Python不会让外面的类/方法正常调用它,这导致他们认为这很好。
如果你像我一样来自C#环境,你不能保护一个方法,这听起来可能很奇怪。
但是Guido(Python的创造者)在这背后有一个很好的理由:
“We’re all consenting adults here”
这意味着,如果你知道你不应该调用一个方法,那么你就不应该调用,除非你知道你在做什么。
毕竟,如果你真的决定要调用那个方法,你会做一些肮脏的事情来使之发生(在C#中被称为 “反射”)。
用一个首字母下划线来标记你的私有方法/函数,说明它只用于私有用途,并与之共存。
↪️ 什么时候在Python中创建一个函数或一个类?
这是一个我收到过几次的常见问题。
如果你遵循上述建议,你就会有清晰的模块,清晰的模块是组织函数的有效方式。
from gmaps_crawler import storages
storages.get_storage() # 👈 Similar to a class, except it's not instantied and has a plural name
storages.save_to_storage() # 👈 Potential function inside module
有时你可以在一个模块内确定函数的子集。当这种情况发生时,一个类会更有意义。
关于对不同子集的函数进行分组的例子
考虑同样的storages
模块,有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")
...
这里有一个经验法则:
- 总是从函数开始
- 一旦你觉得你可以对不同的函数子集进行分组,就增长到类。
🚪 创建模块和入口点
每个应用程序都有一个入口点。
这意味着有一个单一的模块(又称文件)来运行你的应用程序。它可以是一个单独的脚本,也可以是一个大模块。
每当你创建一个入口点时,确保添加一个条件以确保它被执行而不是被导入:
def execute_main():
...
if __name__ == "__main__": # 👈 Add this condition
execute_main()
**通过这样做,你确保任何导入都不会意外地触发你的代码。**除非它被明确地执行。
为模块定义main
你可能已经注意到了一些可以通过传递-m
来调用的python包,比如:
python -m pytest
python -m tryceratops
python -m faust
python -m flake8
python -m black
这样的包几乎被当作普通的命令,因为你也可以把它们当作:
pytest
tryceratops
faust
flake8
black
为了实现这一点,你需要在你的main模块中指定一个单独的__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
的方式运行你的项目。