一个例子了解如何编写可维护的Python代码
大家好,我是海鸽。
今天,我们将从可维护性角度来介绍 Python 编码规范,思考如何像一个“工匠”一样,把代码雕琢得如同完美的手工艺品一样让人赏心悦目。
引子:一个改编自生产的例子
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 20:51
import json
import urllib.request
def get(endpoint, student):
result = None # 初始化名称
try:
info1 = urllib.request.urlopen(f"{endpoint}/student/{student}/") # 获取学生信息
if info1.status_code == 200: # 判断状态码
result1 = json.loads(info1.read()) # 解析学生信息
info1.close() # 关闭请求
result2 = urllib.request.urlopen(f"{endpoint}/class/{result1['class']}/") # 请求班级信息
if result2.status_code == 200:
info2 = json.loads(result2.read())
result = info2["name"] # 获取班级名称
result2.close()
else: # 如果状态码不等于200,返回unknown
result2.close()
else:
info1.close()
except Exception as e:
return "unknown"
return result
相信大部分人如果看到这样一段代码,都会在心里把写这样代码的人狠狠的鄙视一番。
这个代码的唯一没问题的点可能仅仅是"代码能跑"。
我们可以从以下几个方面来进行优化。
变量名命名技巧
变量名的基本规范
包名、模块名、局部变量名、函数名:
- 全小写 + 下划线式驼峰:this_is_var
全局变量、常量:
- 全大写 + 下划线式驼峰:GLOBAL_VAL
类名:
- 首字母大写式驼峰:ClassName()
使用驼峰式时,缩写全部大写,例如:HTTPServerError 好于 HttpServerError
变量名要有描述性
- 不要用那些过于宽泛的词
# BAD
day, host, cards, temp
- 在可接受的长度范围内,变量名能把它所指向的内容描述的越精确越好
# GOOD
day_of_week, host_to_reboot, expired_cards
良好的命名使人能够轻松推断变量的类型
- python 是一门动态类型语言,除了通过上下文猜测,没法轻易知道它是什么类型。
- PEP 484 为 python 添加了
类型注解
什么样的名字会被当成 bool 类型
让读到变量名的人觉得: 这个变量只会有是
或不是
两种值。
# 是否是超级用户,只会有两种值: 是/不是
is_superuser = True
# 有没有错误,只会有两种值: 有/没有
has_error = False
# 是否允许VIP,只会有两种值: 允许/不允许
allow_vip = True
# 是否使用msgpack,只会有两种值: 使用/不使用
use_msgpack = True
# 是否开启调试模式,被当成bool主要是因为约定俗成,如django配置
DEBUG = True
什么样的名字会被当成数值类型?
- 释义为数字的所有单词,比如:port(端口号)、age(年龄)、radius(半径)等等。
- 使用
_id
结尾的单词,比如: user_id、host_id。 - 使用length/count开头或结尾的单词,比如:length_of_username、max_length、users_count
注意:不要使用普通的复数来表示一个 int 类型变量,比如 apples、trips,最好使用 number_of_apples,trips_count 来代替。
保持同类变量的一致性
- 保持同类变量的名称一致
def fetch_image():
return ""
photo = fetch_image()
image = fetch_image()
疑问:photo 和 image 到底是不是一个东西?
- 同一个变量名指代的变量类型,也需要保持一致性
cities = "shenzhen, shanghai"
def get_cities():
return ["shenzhen", "shanghai"]
# BAD
cities = get_cities()
同一个变量不要一会是字符串,一会儿又是列表类型等。
具体再看如何优化变量名
我们来看下面的两段代码:
def get(endpoint, student):
result = None # 初始化名称
# ...
def get_class_name_by_student_id(endpoint: str, student_id: int) -> str:
class_name = "" # 初始化名称
# ... 其它处理
return class_name
第二段函数相比第一段函数进行了以下优化:
-
更具体的函数名:
get_class_name_by_student_id
更具体、更清晰地描述了函数的作用,可以直观地知道这个函数是根据学生ID获取班级名称。 -
明确的参数类型注解: 使用了类型注解,明确指定了
endpoint
参数为字符串类型,student_id
参数为整数类型,并且指定了函数的返回类型为字符串类型。这样可以提高代码的可读性和可维护性,使得调用者更容易理解函数的输入和输出。 -
更精准的变量命名: 使用了更具描述性的变量名,例如将
student
改为了student_id
,将result
改为了class_name
,更清晰地表达了变量的含义,减少了阅读代码时的歧义和混淆。
案例第一次优化:更清晰的变量名
然后我们基于变量名的命名规范对代码进行优化,使代码稍稍可读了一点点。
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 18:51
import json
import urllib.request
def get(endpoint: str, student_id: int) -> str:
class_name = "" # 初始化名称
try:
student_response = urllib.request.urlopen(f"{endpoint}/student/{student_id}/") # 获取学生信息
if student_response.status_code == 200: # 判断状态码
student_info = json.loads(student_response.read()) # 解析学生信息
student_response.close() # 关闭请求
class_response = urllib.request.urlopen(f"{endpoint}/class/{student_info['class']}/") # 请求班级信息
if class_response.status_code == 200:
class_info = json.loads(class_response.read())
class_name = class_info["name"] # 获取班级名称
class_response.close()
else:
class_response.close()
else:
student_response.close()
except Exception as e:
return "unknown"
return class_name
分支:编写分支代码的技巧
编写优秀的条件分支代码非常重要,因为糟糕复杂的分支处理会让人非常困惑,从而降低代码的质量。
避免多层分支嵌套
代码可读性的最大杀手之一:多层嵌套分支
- if {if {if {} } }:俗称
嵌套 if 地狱
多层嵌套优化技巧一:提前结束
# 多层嵌套的代码
def process_data(data):
if condition1:
if condition2:
if condition3:
return result
return None
在条件允许的情况下,尽早返回函数以减少嵌套层数。这样可以避免深层嵌套的代码块,使得代码更加扁平化。
# 提前返回
def process_data(data):
if not condition1:
return None
if not condition2:
return None
if not condition3:
return None
return result
多层嵌套优化技巧二:使用函数分解
# 多层嵌套的代码
def process_data(data):
for item in data:
if condition1:
if condition2:
# do something
else:
# do something else
else:
# do another thing
将多层嵌套的代码块分解成单独的函数。这样做可以使得代码结构更清晰,每个函数只负责一个特定的任务。
# 使用函数分解
def process_item(item):
if condition2:
# do something
else:
# do something else
def process_data(data):
for item in data:
if condition1:
process_item(item)
else:
# do another thing
多层嵌套优化技巧二:使用列表推导式和生成器表达式
如果可能,尽量使用列表推导式或生成器表达式来替代多层嵌套的循环。这样可以减少嵌套层数,使得代码更加简洁和易读。
# 多层嵌套的循环
result = []
for i in range(10):
for j in range(10):
if i % 2 == 0 and j % 2 == 0:
result.append((i, j))
# 使用列表推导式
result = [(i, j) for i in range(10) for j in range(10) if i % 2 == 0 and j % 2 == 0]
案例第二次优化:优化 if 分支嵌套
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 18:51
import json
import urllib.request
def get(endpoint: str, student_id: int) -> str:
try:
student_response = urllib.request.urlopen(f"{endpoint}/student/{student_id}/") # 获取学生信息
if student_response.status_code != 200: # 判断状态码
student_response.close() # 关闭请求
return "unknown"
student_info = json.loads(student_response.read()) # 解析学生信息
student_response.close()
class_response = urllib.request.urlopen(f"{endpoint}/class/{student_info['class']}/") # 请求班级信息
if class_response.status_code != 200:
class_response.close()
return "unknown"
class_info = json.loads(class_response.read())
class_name = class_info["name"] # 获取班级名称
class_response.close()
return class_name
except Exception as e:
return "unknown"
我们使用提前结束的思想优化改写了案例的条件分支。代码可读性是又提高了,不过仍然可以看到有大量的重复代码。比如关闭请求
:
重复代码是代码质量的天敌。
我们看到这段代码有 4 个关闭请求的重复代码,而 python 中对于这类资源的处理可以借助上下文管理器
优雅的实现。
于是我们接着改造:
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 18:51
import json
import urllib.request
def get(endpoint: str, student_id: int) -> str:
try:
with urllib.request.urlopen(f"{endpoint}/student/{student_id}/") as student_response: # 获取学生信息
if student_response.status_code != 200: # 判断状态码
return "unknown"
student_info = json.loads(student_response.read()) # 解析学生信息
with urllib.request.urlopen(f"{endpoint}/class/{student_info['class']}/") as class_response: # 请求班级信息
if class_response.status_code != 200:
return "unknown"
class_info = json.loads(class_response.read())
class_name = class_info["name"] # 获取班级名称
return class_name
except Exception as e:
return "unknown"
这样,我们不再需要手动关闭请求了。
但是我们还是会发现:我们的 if 代码有重复!
而这部分优化我们可以通过封装复杂或重复的逻辑判断
来实现。
好的封装有以下好处:
- 封装改善可读性:为过程命名。
- 封装增加可维护性:消除大量重复代码。
我们接着优化,抽象出响应状态检测:
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 20:51
import json
import urllib.request
from http.client import HTTPResponse
class HTTPError(Exception):
def __init__(self, message, url):
self.message = message
self.url = url
def __str__(self):
return f"{self.message}: {self.url}"
def raise_for_status(response: HTTPResponse, url: str):
if response.getcode() != 200:
raise HTTPError("HTTP error", url)
def get(endpoint: str, student_id: int) -> str:
try:
with urllib.request.urlopen(f"{endpoint}/student/{student_id}/") as student_response: # 获取学生信息
raise_for_status(student_response, f"{endpoint}/student/{student_id}/")
student_info = json.loads(student_response.read()) # 解析学生信息
with urllib.request.urlopen(f"{endpoint}/class/{student_info['class']}/") as class_response: # 请求班级信息
raise_for_status(class_response, f"{endpoint}/class/{student_info['class']}/")
class_info = json.loads(class_response.read())
class_name = class_info["name"] # 获取班级名称
return class_name
except Exception as e:
return "unknown"
然后我们发现仍然有大量重复代码:
我们发现请求的逻辑其实是一样的,因此我们继续封装如下:
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 20:51
import json
import urllib.request
from http.client import HTTPResponse
class HTTPError(Exception):
def __init__(self, message, url):
self.message = message
self.url = url
def __str__(self):
return f"{self.message}: {self.url}"
def raise_for_status(response: HTTPResponse, url: str):
if response.getcode() != 200:
raise HTTPError("HTTP error", url)
def get_class_name_by_student_id(endpoint: str, student_id: int) -> str:
try:
student_info = request_get(endpoint, f"student/{student_id}/")
class_info = request_get(endpoint, f"class/{student_info['class']}/")
class_name = class_info["name"] # 获取班级名称
return class_name
except Exception as e:
return "unknown"
def request_get(endpoint, path: str) -> dict:
with urllib.request.urlopen(f"{endpoint}/{path}") as student_response:
raise_for_status(student_response, f"{endpoint}/{path}")
student_info = json.loads(student_response.read())
return student_info
异常抽象技巧
无处不在的异常
Python 中有许多常见的异常类型,这些异常类型可以帮助开发人员识别和处理程序中的错误。以下是一些常见的 Python 异常类型及其含义:
-
SyntaxError: 语法错误,通常是由于代码书写不规范导致的错误,比如拼写错误、缺少冒号等。
-
IndentationError: 缩进错误,通常是由于代码缩进不正确导致的错误。
-
NameError: 名称错误,通常是由于引用了一个未定义的变量或函数导致的错误。
-
TypeError: 类型错误,通常是由于操作应用到了不兼容的数据类型上导致的错误,比如对整数和字符串进行加法操作。
-
ValueError: 值错误,通常是由于传递给函数的参数值不合法导致的错误,比如尝试将字符串转换为整数时字符串格式不正确。
-
KeyError: 键错误,通常是由于尝试使用字典中不存在的键来访问字典元素导致的错误。
-
IndexError: 索引错误,通常是由于尝试访问序列中不存在的索引导致的错误,比如尝试访问列表中不存在的元素。
-
AttributeError: 属性错误,通常是由于尝试访问对象不存在的属性或方法导致的错误。
-
IOError: 输入/输出错误,通常是由于文件操作或其他输入/输出操作失败导致的错误。
-
ZeroDivisionError: 零除错误,通常是由于除法操作中除数为零导致的错误。
-
FileNotFoundError: 文件未找到错误,通常是由于尝试打开或操作不存在的文件导致的错误。
-
ImportError: 导入错误,通常是由于尝试导入模块或包失败导致的错误。
这些是 Python 中一些常见的异常类型,当程序出现对应的错误时,Python 解释器会抛出相应的异常对象,开发人员可以通过捕获这些异常并处理它们来改进程序的健壮性和可靠性。
异常处理技巧
只做最精确的异常捕获:
- 永远只捕获哪些可能异常的语句块。
- 永远只捕获精确的异常类型,而不是模糊的 Exception 。
- 可忽略的异常捕获之后,试着记个日志。
案例第三次优化:优化异常处理
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 20:51
import json
import urllib.request
from http.client import HTTPResponse
from urllib import error
from loguru import logger
class HTTPError(Exception):
def __init__(self, message, url):
self.message = message
self.url = url
def __str__(self):
return f"{self.message}: {self.url}"
def raise_for_status(response: HTTPResponse, url: str):
if response.getcode() != 200:
raise HTTPError("HTTP error", url)
def get_class_name_by_student_id(endpoint: str, student_id: int) -> str:
try:
student_info = request_get(endpoint, f"student/{student_id}/")
except (error.URLError, error.ContentTooShortError, error.HTTPError) as e:
logger.warning(f"Request student info failed: {str(e)}")
return "unknown"
try:
class_info = request_get(endpoint, f"class/{student_info['class']}/")
class_name = class_info["name"] # 获取班级名称
return class_name
except (error.URLError, error.ContentTooShortError, error.HTTPError) as e:
logger.warning(f"Request class info failed: {str(e)}")
return "unknown"
def request_get(endpoint, path: str) -> dict:
with urllib.request.urlopen(f"{endpoint}/{path}") as student_response:
raise_for_status(student_response, f"{endpoint}/{path}")
student_info = json.loads(student_response.read())
return student_info
这样一改,我们发现又有了大量重复代码:
对于这类异常,我的建议是包装一层,也即是包装到我们自定义的异常中。
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 20:51
import json
import socket
import urllib.request
from http.client import HTTPResponse
from urllib import error
from loguru import logger
class HTTPError(Exception):
def __init__(self, message, url):
self.message = message
self.url = url
def __str__(self):
return f"{self.message}: {self.url}"
def raise_for_status(response: HTTPResponse, url: str):
if response.getcode() != 200:
raise HTTPError("HTTP error", url)
def get_class_name_by_student_id(endpoint: str, student_id: int) -> str:
try:
student_info = request_get(endpoint, f"student/{student_id}/")
except HTTPError as e:
logger.warning(f"Request student info failed: {str(e)}")
return "unknown"
try:
class_info = request_get(endpoint, f"class/{student_info['class']}/")
class_name = class_info["name"] # 获取班级名称
return class_name
except HTTPError as e:
logger.warning(f"Request class info failed: {str(e)}")
return "unknown"
def request_get(endpoint, path: str) -> dict:
try:
with urllib.request.urlopen(f"{endpoint}/{path}") as student_response:
raise_for_status(student_response, f"{endpoint}/{path}")
student_info = json.loads(student_response.read())
return student_info
except (error.URLError, error.ContentTooShortError, socket.timeout, error.HTTPError, HTTPError) as e:
raise HTTPError("Request error", f"{endpoint}/{path}") from e
防御性编程
并不是只能用异常捕获的方式来处理异常,比如使用.get()
获取字典指定键的值,避免键不存在的情况:
user_info = {"age": 18}
user_name = user_info.get("name")
防御性编程可以避免很多安全性问题,但是会有对应的性能代价。
比如案例可以设置防御性代码:
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 20:51
import json
import socket
import urllib.request
from http.client import HTTPResponse
from urllib import error
from loguru import logger
class HTTPError(Exception):
def __init__(self, message, url):
self.message = message
self.url = url
def __str__(self):
return f"{self.message}: {self.url}"
def raise_for_status(response: HTTPResponse, url: str):
if response.getcode() != 200:
raise HTTPError("HTTP error", url)
def get_class_name_by_student_id(endpoint: str, student_id: int) -> str:
try:
student_info = request_get(endpoint, f"student/{student_id}/")
except HTTPError as e:
logger.warning(f"Request student info failed: {str(e)}")
return "unknown"
if "class" not in student_info:
return "unknown"
try:
class_info = request_get(endpoint, f"class/{student_info['class']}/")
return class_info.get("name", "unknown")
except HTTPError as e:
logger.warning(f"Request class info failed: {str(e)}")
return "unknown"
def request_get(endpoint, path: str) -> dict:
try:
with urllib.request.urlopen(f"{endpoint}/{path}") as student_response:
raise_for_status(student_response, f"{endpoint}/{path}")
student_info = json.loads(student_response.read())
return student_info
except (error.URLError, error.ContentTooShortError, socket.timeout, error.HTTPError, HTTPError) as e:
raise HTTPError("Request error", f"{endpoint}/{path}") from e
对于是否防御性编程,其实是仁者见仁,智者见智。
使用工具保证编码风格
除了借鉴优秀的编程规范和代码实践外,合理运用开源工具也能帮助我们写出高质量 Python 代码。
以下是一些优秀的 Python 代码自动格式化工具:
-
Black: Black 是一个非常流行的 Python 代码格式化工具,它使用一种称为 "uncompromising code formatter" 的方法来格式化 Python 代码。Black 旨在生成统一且可读性高的 Python 代码,不留下任何注释。
-
YAPF (Yet Another Python Formatter): YAPF 是由 Google 开发的 Python 代码格式化工具,它也旨在生成一致和可读性高的 Python 代码。YAPF 支持自定义配置,以便根据项目的特定需求进行调整。
-
autopep8: autopep8 是一个用于自动格式化 Python 代码的工具,它遵循 PEP 8 风格指南,并尝试将代码调整为符合规范的格式。autopep8 还提供了许多选项,以便根据个人偏好进行自定义配置。
-
pylint: pylint 是一个 Python 代码静态分析工具,它可以检查代码中的错误、风格问题和潜在的 bug,并提供详细的反馈和建议。尽管 pylint 本身并不是一个代码格式化工具,但它可以与其他工具(如 autopep8)结合使用来自动修复检测到的问题。
-
isort: isort 是一个用于自动排序 Python 导入语句的工具,它可以确保导入语句的顺序符合 PEP 8 规范,并提供了一些额外的选项来调整排序的行为。
对比优化前后的案例代码
我们再来看下案例优化前和优化后的代码:
最初版
#! -*-conding=: UTF-8 -*-
# 2023/11/23 20:21
import json
import urllib.request
def get(endpoint, student):
result = None # 初始化名称
try:
info1 = urllib.request.urlopen(f"{endpoint}/student/{student}/") # 获取学生信息
if info1.status_code == 200: # 判断状态码
result1 = json.loads(info1.read()) # 解析学生信息
info1.close() # 关闭请求
result2 = urllib.request.urlopen(f"{endpoint}/class/{result1['class']}/") # 请求班级信息
if result2.status_code == 200:
info2 = json.loads(result2.read())
result = info2["name"] # 获取班级名称
result2.close()
else:
info1.close()
except Exception as e:
return "unknown"
return result
终版
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 20:51
import json
import socket
import urllib.request
from http.client import HTTPResponse
from urllib import error
from loguru import logger
class HTTPError(Exception):
def __init__(self, message, url):
self.message = message
self.url = url
def __str__(self):
return f"{self.message}: {self.url}"
def raise_for_status(response: HTTPResponse, url: str):
if response.getcode() != 200:
raise HTTPError("HTTP error", url)
def get_class_name_by_student_id(endpoint: str, student_id: int) -> str:
try:
student_info = request_get(endpoint, f"student/{student_id}/")
except HTTPError as e:
logger.warning(f"Request student info failed: {str(e)}")
return "unknown"
if "class" not in student_info:
return "unknown"
try:
class_info = request_get(endpoint, f"class/{student_info['class']}/")
return class_info.get("name", "unknown")
except HTTPError as e:
logger.warning(f"Request class info failed: {str(e)}")
return "unknown"
def request_get(endpoint, path: str) -> dict:
try:
with urllib.request.urlopen(f"{endpoint}/{path}") as student_response:
raise_for_status(student_response, f"{endpoint}/{path}")
student_info = json.loads(student_response.read())
return student_info
except (error.URLError, error.ContentTooShortError, socket.timeout, error.HTTPError, HTTPError) as e:
raise HTTPError("Request error", f"{endpoint}/{path}") from e
进阶小彩蛋
其实很多时候,Python的一些第三方库会帮我们优化很多重复的工作,使我们避免重复造轮子,比如使用 requests
库来优化案例代码:
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公众号:海哥Python
# __time__:2024/2/22 20:51
import requests
from loguru import logger
def get_class_name_by_student_id(endpoint: str, student_id: int) -> str:
try:
student_info = request_get(endpoint, f"student/{student_id}/")
except requests.HTTPError as e:
logger.warning(f"Request student info failed: {str(e)}")
return "unknown"
if "class" not in student_info:
return "unknown"
try:
class_info = request_get(endpoint, f"class/{student_info['class']}/")
return class_info.get("name", "unknown")
except requests.HTTPError as e:
logger.warning(f"Request class info failed: {str(e)}")
return "unknown"
def request_get(endpoint, path: str) -> dict:
response = requests.get(f"{endpoint}/{path}")
response.raise_for_status()
return response.json()
在优化后的代码中,我们使用了 requests
库替换了 urllib.request
库,使代码更加简洁和易读。requests
库提供了简洁且功能强大的接口来发送 HTTP 请求,并且处理异常更加方便。
总结
在 Python 编码优化中,应该遵循以下几个方面:
-
编码规范: 遵循 PEP 8 编码规范,保持代码的一致性和可读性。使用工具如 Black 、YAPF 等进行自动格式化,确保代码风格统一。
-
if 分支嵌套优化: 减少 if 分支的嵌套,提高代码的可读性和可维护性。可以通过提前返回错误条件、使用字典或函数映射等方法来简化复杂的条件判断逻辑。
-
重复代码封装: 避免重复代码,将重复的逻辑封装成函数或类,提高代码的复用性。同时,避免过度封装,保持函数和类的单一职责。
-
异常捕获: 合理处理异常,避免捕获过于宽泛的异常类型,应该尽量具体化异常处理。同时,在必要的地方添加日志记录,方便故障排查和代码调试。
综上所述,通过遵循编码规范、优化 if 分支嵌套、封装重复代码和合理处理异常等方法,可以有效提高 Python 代码的质量和性能。
最后
如果文章对你有帮助,在看
、关注
走一波呗~
转载自:https://juejin.cn/post/7366899841455292426