在工作中,我们编写代码时尽可能地使其易于阅读。这意味着以下几点:

  • 变量名有意义且更长(而不是 a, b 和 c)
  • 函数名有意义且更长
  • 许多注释和文档解释代码
  • 到处都是类型提示
  • 字符串似乎更长、更啰嗦
  • 等等

以下是我在过去几年的工作中学到的一些生产级别的 Python 代码风格。

1) 使用括号的元组解包

这是一些正常的元组解包:

a, b = (1, 2)

在生产级别的代码中,我们通常不使用像 ab 这样的变量名 —— 相反,我们的变量名会变得更长且描述性更强。

因此,我们可能会使用括号来帮助元组解包,如下所示:

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,我们也会得到一个错误
  • 基本上,这个代码块没有保护 dogdog.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” 进行比较

通过这种方式,我们额外保护了 dogdog.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 方法使用其他方法 cleantransform

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多平台发布


Miniwa
29 声望1 粉丝