在工作中,我们编写代码时尽可能地使其易于阅读。这意味着以下几点:
- 变量名有意义且更长(而不是 a, b 和 c)
- 函数名有意义且更长
- 许多注释和文档解释代码
- 到处都是类型提示
- 字符串似乎更长、更啰嗦
- 等等
以下是我在过去几年的工作中学到的一些生产级别的 Python 代码风格。
1) 使用括号的元组解包
这是一些正常的元组解包:
a, b = (1, 2)
在生产级别的代码中,我们通常不使用像 a
或 b
这样的变量名 —— 相反,我们的变量名会变得更长且描述性更强。
因此,我们可能会使用括号来帮助元组解包,如下所示:
x_coordinate, y_coordinate = (1, 2)
注意,通过这种方式,我们的元组解包可以容纳更长(且更具描述性)的变量名。
一个更现实的例子:
first_name, last_name = ("John", "Doe")
2) 多行列表推导式
这是正常的列表推导式的样子:
squared_numbers = [i**2 for i in range(10)]
在生产代码中,我们通常不使用像 i
这样的变量名 —— 我们的变量通常是更长且描述性的,以至于我们不能将整个列表推导式放在一行代码中。
我会这样重写上述代码:
squared_numbers = [number**2 for number in range(10)]
一个更真实的例子:
full_names = [f"{first} {last}" for first, last in [("John", "Doe"), ("Jane", "Doe")]]
3) 使用括号组合字符串
生产级别的字符串通常由于过于啰嗦而无法在一行内完成。因此我们使用括号来组合它们。
message = ("Hello, " "my name is " "John Doe")
注意 —— 在括号内,字符串字面量(使用引号)会自动拼接在一起,我们不需要使用 +
操作符来实现这一点。
4) 多行方法链式调用,借助括号
正常的方法链式调用:
result = object.do_something().do_another()
在生产级别的代码中,方法名通常更长,我们通常会有更多的方法链式调用在一起。
再次,我们使用括号将所有这些内容放入多行中,而不是缩短任何方法名或变量名。
result = (object.do_something()
.do_another()
.do_a_third())
注意,如果我们在括号内进行方法链式调用,我们不需要使用反斜杠 \
来明确换行。
5) 索引嵌套字典
正常索引嵌套字典的方式:
value = dictionary["key"]["subkey"]
这里有一些问题:
- 生产级别的代码中的字典有更多的嵌套层级
- 字典的键名更长
- 我们通常无法将整个嵌套索引代码挤在一行内。
因此,我们将其拆分为多行,如下所示:
value = (dictionary
["key"]
["subkey"])
如果这样还不够,我们可以将索引代码拆分为更多的行:
value = (dictionary["key"]
["subkey"]["subsubkey"])
或者如果我们仍然觉得这样难以阅读,我们可以这样做:
key1 = "key"
key2 = "subkey"
key3 = "subsubkey"
value = dictionary[key1][key2][key3]
6) 编写可读且信息丰富的函数
我们以前作为学生时这样写函数:
def calculate(a, b):
return a + b
^ 包含此类代码的 PRs 很可能会被拒绝
- 函数名没有描述性
- 参数变量名不好
- 没有类型提示,所以我们不知道每个参数应该是什么数据类型
- 没有类型提示,所以我们也不知道函数应该返回什么
- 没有 docstring,所以我们不得不推断我们的函数是做什么的
以下是我们在生产级别的 Python 代码中编写函数的方式
def add_two_numbers(number1: int, number2: int) -> int:
"""
Add two numbers together.
Parameters:
number1 (int): The first number to add.
number2 (int): The second number to add.
Returns:
int: The sum of the two numbers.
"""
return number1 + number2
- 函数名应该有描述性
- 参数名应该有描述性,而不是例如 a, b, c
- 每个参数都应该有类型提示
- 函数的返回类型也应该包含
- 应该包含一个 docstring,详细说明函数的作用、它接受的参数以及它的输出,作为一个字符串,用三引号括起来。
7) 尽可能减少缩进级别
这是一个 for 循环。如果我们的条件满足,我们做一些事情。
for item in items:
if condition:
do_something()
^ 一些同事和高级工程师实际上可能会对这段代码吹毛求疵 —— 它可以通过减少 do_something()
的缩进级别来写得更好。
让我们重写这个代码,同时减少 do_something()
的缩进级别:
for item in items:
if not condition:
continue
do_something()
注意,do_something()
的缩进级别已经减少了 1 级,只是通过使用 if not condition
而不是 if condition
。
在生产级别的代码中,可能会有更多的缩进级别,如果缩进太多,我们的代码就会变得烦人且难以阅读。因此,这个技巧使我们能够使我们的代码稍微更整洁和易于人类阅读。
8) 使用括号的布尔条件
这是一个带有 3 个条件的 if 语句,使用 and
关键字连接。
if condition1 and condition2 and condition3:
do_something()
在生产级别的代码中,条件变得更长,可能有更多的条件。因此,我们解决这个问题的一种方式是将这个巨大的条件重构为一个函数。
或者如果我们认为没有必要仅仅为了这个条件就编写一个新函数,我们可以使用括号编写我们的条件语句。
if (condition1 and
condition2 and
condition3):
do_something()
这样,我们就不用被迫为这个单一的条件语句编写一个新函数或变量,同时我们能够保持它的整洁和可读性。
有时我实际上可能更喜欢这样写,尽管这只是基于个人偏好:
if all([
condition1,
condition2,
condition3
]):
do_something()
9) 防御 None 值
正常访问对象某个嵌套属性的代码。
name = dog.owner.name
这段代码可能存在的问题,可能会导致我们的 PR 被拒绝:
- 如果
dog
是 None,我们会得到一个错误 - 如果
dog.owner
是 None,我们也会得到一个错误 - 基本上,这个代码块没有保护
dog
或dog.owner
可能是 None 的可能性。
在生产级别的代码中,我们需要积极防御这种情况。以下是我会如何重写这段代码。
if dog and dog.owner:
name = dog.owner.name
Python 中的 and
& or
操作符是短路操作符,这意味着它们一旦有了明确的答案就停止评估整个表达式。
- 如果
dog
是 None,我们的表达式在if dog
处终止 - 如果
dog
不是 None,但dog.owner
是 None,我们的表达式在if dog and dog.owner
处终止 - 如果我们没有任何 None 值,
dog.owner.name
被成功访问,并用于与字符串 “bob” 进行比较
通过这种方式,我们额外保护了 dog
或 dog.owner
可能是 None 值的可能性。
10) 防御遍历 None 值
这是我们可能遍历某个可迭代对象(例如列表、字典、元组等)的方式。
for item in mylist:
process(item)
这个问题在于它没有保护 mylist
可能是 None —— 如果 mylist
碰巧是 None,我们会因为不能遍历 None 而得到一个错误。
以下是我如何改进这段代码:
for item in (mylist or []):
process(item)
表达式 “mylist or None”
:
- 如果
mylist
是真值(例如非空的可迭代对象),则返回mylist
- 如果
mylist
是假值(例如 None 或空的可迭代对象),则返回[]
(空列表)
因此,如果 mylist
是 None,表达式 “mylist or []”
返回 []
代替,我们就不会遇到不想要的异常。
11) 内部函数以 _ 开头
这是一个示例类。这里,run
方法使用其他方法 clean
和 transform
。
class Processor:
def run(self, data):
clean_data = self.clean(data)
transformed_data = self.transform(clean_data)
return transformed_data
def clean(self, data):
# Clean data
pass
def transform(self, data):
# Transform data
pass
在生产级别的代码中,我们力求尽可能明确,因此尝试区分内部方法和外部方法。
- 外部方法 —— 被其他类和对象使用的方法
- 内部方法 —— 被类本身使用的方法
按照惯例,内部方法以下划线 _
开头是一个好的实践。
如果我们重写上述代码,我们得到:
class Processor:
def run(self, data):
clean_data = self._clean(data)
transformed_data = self._transform(clean_data)
return transformed_data
def _clean(self, data):
# Clean data
pass
def _transform(self, data):
# Transform data
pass
注意 —— 在方法名前添加下划线并不会使其他类和对象隐藏它。实际上,功能上没有区别。
12) 装饰器用于常见功能
这是一个有 3 个函数的类,每个函数做不同的事情。然而,注意不同函数之间有相似的步骤 —— try-except 块,以及日志记录功能。
class Processor:
def process_one(self):
try:
# Process something
pass
except Exception as e:
print(f"Error: {e}")
def process_two(self):
try:
# Process something else
pass
except Exception as e:
print(f"Error: {e}")
def process_three(self):
try:
# Another process
pass
except Exception as e:
print(f"Error: {e}")
减少重复代码的一个好习惯是编写一个包含共同功能的装饰器函数。
def with_logging_and_exception(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Error in {func.__name__}: {e}")
return wrapper
class Processor:
@with_logging_and_exception
def process_one(self):
# Process something
pass
@with_logging_and_exception
def process_two(self):
# Process something else
pass
@with_logging_and_exception
def process_three(self):
# Another process
pass
这样,如果我们想更新共同代码(try-except 和日志记录代码),我们就不再需要在 3 个地方更新 —— 我们只需要更新包含共同功能的装饰器代码。
本文由mdnice多平台发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。