现代 Python 学习笔记:Statements & Syntax(Advanced)
前言
笔者最后一小段自由时间了,打算仔细的学习现代的Python编程。这个也算是希望给自己的自动化脚本编程搞一点摸鱼的小技术了
理解现代Python的脚本与模块化
在 Python 中,每个 .py 文件本质上都是一个模块(Module)。将代码组织到不同的模块中,而不是将所有逻辑堆砌在一个巨大的脚本里,是专业软件开发的基石。这个思想非常常见,熟悉C/C++的朋友会把不同模块的功能分门别类组织起来,打包成为一个一个非常可具备移植性的模块,这样下一次工程使用的时候就可以进行复用。
简单的说,模块化代码可以提高可维护性、可读性和重用性。为了详细说明,笔者将一个计算圆面积的程序分为了geometry.py,使用代码完成业务的部分放到了main.py上。
可维护性 (Maintainability)的提高
当我们需要优化圆面积的计算方法(比如使用更高精度的 math.pi),我们只需要修改geometry.py下的相关函数 。main.py 或其他任何使用此模块的文件都无需改动 。 当出现计算错误时,您可以立刻确定问题出在 geometry.py 模块中,而不是在数千行的 main.py 中大海捞针。
可读性 (Readability)的提高
geometry.py 这个文件名本身就具有描述性。当另一位开发者(或未来的您)看到 from geometry import ... 时,能立刻明白代码的意图是处理几何相关的操作。而且,我们不需要关心 area_of_circle 是如何实现的(是用 math.pi 还是 3.14)。他/她只需要知道"我传入半径,它返回面积"。这使得 main.py 的逻辑更清晰、更高级。
重用性 (Reusability)的提高
geometry.py 模块可以被复制到任何其他需要计算面积的项目中。所以之后我们需要几何图形的面积计算的时候,随意引入,这个概念在现代软件架构设计中被认为是一个库。
Tips: DRY 原则 (Don't Repeat Yourself): 如果您有三个不同的脚本都需要计算圆的面积,您不需要在三个脚本中都写一遍
math.pi * r ** 2。您只需要导入geometry模块并调用函数即可。
命名空间隔离 (Namespace Isolation)
模块创建了独立的命名空间 。这意味着您可以在 geometry.py 中定义一个名为 version 的变量,同时在 main.py 中也定义一个 version 变量,它们不会相互冲突。这对于大型项目至关重要,可以有效避免函数和变量重名导致的问题。
示例:geometry.py
python
"""Geometry module
Provides functions to calculate areas of basic shapes.
"""
import math
def area_of_circle(r):
"""Compute the area of a circle.
:param r: radius of the circle
:return: area (float)
"""
return math.pi * r ** 2
def area_of_square(a):
"""Compute the area of a square.
:param a: side length
:return: area (float)
"""
return a * a
这样我们就能很自然的写下代码
python
from geometry import area_of_circle, area_of_square
print("Circle area (r=5):", area_of_circle(5))
print("Square area (a=4):", area_of_square(4))
导入(Import)的更多方式
from ... import ...是最常见的方式之一。了解不同的导入方式有助于您更好地控制命名空间。假设我们有 geometry.py。
方式一:导入特定函数
Python
from geometry import area_of_circle, area_of_square
# 直接使用函数名
print(area_of_circle(5))
print(area_of_square(4))
- 优点: 代码简洁。
- 缺点: 如果导入的函数很多,或者函数名很通用(例如
read),可能会与您main.py中的函数名冲突。
方式二:导入整个模块
Python
import geometry
# 必须通过 "模块名." 来访问
print(geometry.area_of_circle(5))
print(geometry.area_of_square(4))
- 优点: 命名空间完全隔离。
geometry.area_of_circle永远不会和您本地的area_of_circle冲突。可读性强,一眼就知道函数来源。 - 缺点: 代码稍微冗长。
方式三:使用别名 (Alias)
import geometry as geo
from geometry import area_of_circle as circle
print(geo.area_of_square(4)) # 模块别名
print(circle(5)) # 函数别名
- 优点: 在模块名很长(如 matplotlib.pyplot常被别名为 plt)或存在命名冲突时非常有用。
⚠️ 避免使用的导入方式:from geometry import *这种方式!因为会导入模块中所有非下划线开头的变量和函数。这会严重"污染"当前文件的命名空间,导致命名冲突,并使代码极难阅读(您根本不知道某个函数是从哪里来的)。
处理长行代码
PEP 8 建议每行不超过 79 字符。使用括号隐式换行是 Python 推荐方式,举个例子,如果一行话非常非常长,考虑分开一下。
python
result = compute_total_price(
base_price,
tax_rate,
discount_rate,
shipping_fee,
special_coupon
)
多if elif优化
在日常 Python 开发中,我们经常需要根据条件返回不同结果,例如根据分数返回等级(A--F)。传统的 if...elif 链写法虽然直观,但当条件变多时可读性下降。那么,我们要如何进行一定的优化呢?
当然,第一个方法自然是整齐我们的代码,看起来更好读一些。。。
def grade(score: int) -> str:
"""Return letter grade for a single score using if...elif.
:param score: Exam score (0-100)
:type score: int
:return: Letter grade (A-F)
:rtype: str
"""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
elif score >= 60:
return "D"
else:
return "F"
字典映射 / 阈值列表优化
我们可以用 字典或阈值列表映射分数到等级,遍历阈值判断即可,代码更简洁易扩展。
python
from typing import List, Union
# 规则集中在 THRESHOLDS 中,修改非常方便。
THRESHOLDS: list[tuple[int, str]] = [
(90, "A"),
(80, "B"),
(70, "C"),
(60, "D"),
(0, "F")
]
def grade_threshold(score: int) -> str:
"""Return letter grade using threshold mapping.
:param score: Exam score (0-100)
:type score: int
:return: Letter grade (A-F)
:rtype: str
:raises ValueError: If score is out of 0-100 range
"""
if not 0 <= score <= 100:
raise ValueError("Score must be between 0 and 100")
for threshold, grade in THRESHOLDS:
if score >= threshold:
return grade
raise RuntimeError("Unexpected error in grade_threshold")
支持单个分数或分数列表
利用列表推导式,可以方便地批量处理分数:
python
def grade_general(scores: Union[int, List[int]]) -> List[str]:
"""Return letter grades for single score or list of scores.
:param scores: Single score or list of scores
:type scores: int or List[int]
:return: List of letter grades
:rtype: List[str]
"""
if isinstance(scores, list):
return [grade_threshold(s) for s in scores]
return [grade_threshold(scores)]
示例使用:
print(grade_threshold(85)) # 输出: 'B'
print(grade_general([95, 82, 67, 58])) # 输出: ['A', 'B', 'D', 'F']
print(grade_general(73)) # 输出: ['C']
海象运算符 :=
Python 3.8 引入海象运算符,可以在表达式中直接赋值。
python
sentence = "Writing Python code is surprisingly enjoyable"
words = sentence.split()
max_word = ""
max_len = 0
for w in words:
if (l := len(w)) > max_len:
max_len = l
max_word = w
print(f"Longest word: {max_word} ({max_len})")
Tips:
- 避免重复计算,提高代码可读性。
- 常用于循环或条件表达式。
- 参考资料:PEP 572 --- Assignment Expressions
Python 的异常处理部分讨论
Python利用经典的try catch throw机制来处理异常,当然,关键字是try...except 结构,他不仅仅是用来"防止程序崩溃"的工具,它更是一种强大的控制流机制。正确地使用它,可以极大地提高代码的健壮性、可读性和可调试性。
基础:捕获特定的异常 (Safe Exception Catching)
异常处理的第一原则是**永远只捕获你"预期"会发生,并且"知道"如何处理的异常。**其他的异常不属于你管辖的范畴,甚至,是只捕捉你打算处理的异常!
python
try:
# 尝试执行一个可能失败的操作
result = 1 / 0
except ZeroDivisionError as e:
# 明确处理 "除零错误"
print(f"Error occurred: {e}")
result = 0 # 提供一个回退值
Tip 1: "使用具体异常类型,而不是裸 except:"
- 为什么? 裸露的
except:(或except Exception:) 会捕获所有 类型的异常。这包括:MemoryError(内存耗尽)KeyboardInterrupt(用户按下了 Ctrl+C)SystemExit(程序被要求退出,例如sys.exit())
- 危险性: 当您捕获了
KeyboardInterrupt,您的程序将变得无法通过 Ctrl+C 停止。当您捕获MemoryError时,您可能在隐藏一个严重的内存泄漏。裸露的except:会让调试变得异常困难,因为它隐藏了所有未预料到的错误。
Tip 2: "可用 as e 捕获异常对象"
-
e是什么?e是ZeroDivisionError类的一个实例。 -
它有什么用?
-
打印信息:
print(e)会调用该对象的__str__方法,通常会给出一个人类可读的错误信息(例如:"division by zero")。 -
获取参数:
e.args是一个包含传递给异常构造函数参数的元组。 -
日志记录: 在生产环境中,您会使用
logging模块记录完整的异常信息:pythonimport logging try: 1 / 0 except ZeroDivisionError as e: logging.error(f"A division error occurred: {e}", exc_info=True)exc_info=True会将完整的堆栈跟踪(Traceback)记录到日志中。
-
异常匹配的继承与顺序
异常在 Python 中是类 (Class) 。它们存在继承关系,except 语句块在匹配异常时,会从上到下 依次检查。它会执行第一个 匹配到的 except 块。"匹配"的定义是:isinstance(raised_exception, ExceptType)。
python
class CustomError(Exception): pass
class SubCustomError(CustomError): pass # SubCustomError 是 CustomError 的子类
正确的捕获顺序(子类在前,父类在后)
python
try:
# 我们抛出一个非常具体的 "子类" 异常
raise SubCustomError("This is a sub-error")
except SubCustomError as e:
# 1. Python 检查: isinstance(SubCustomError(), SubCustomError) -> True
# 2. 匹配成功!执行此块。
print(f"Caught SubCustomError specifically: {e}")
except CustomError as e:
# 3. 此块被跳过
print(f"Caught general CustomError: {e}")
- 输出:
Caught SubCustomError specifically: This is a sub-error
错误的捕获顺序(父类在前,子类在后)
让我们故意把顺序写错,看看会发生什么:
Python
try:
raise SubCustomError("This is a sub-error")
except CustomError as e:
# 1. Python 检查: isinstance(SubCustomError(), CustomError) -> True
# (因为 SubCustomError 是 CustomError 的子类)
# 2. 匹配成功!执行此块。
print(f"Caught general CustomError: {e}")
except SubCustomError as e:
# 3. 此块永远不会被执行!它成为了 "死代码" (Dead Code)。
print(f"Caught SubCustomError specifically: {e}")
- 输出:
Caught general CustomError: This is a sub-error
这是因为异常匹配从上到下。由于子类也是父类的一个实例,所以必须在 except 语句中将**更具体(子类)的异常放在更通用(父类)**的异常之前。否则,父类的 except 块会"过早"地捕获子类异常,导致子类的特定处理逻辑永远无法执行。
高级:异常链与上下文 (Exception Chaining)
在复杂的系统中,一个函数调用另一个函数。当底层函数(例如数据库访问)失败时,上层函数(例如业务逻辑)可能需要捕获该异常,并抛出一个更具上下文含义的新异常。 如果您只是简单地 raise NewError,原始的错误("根因")就会丢失,这会给调试带来灾难。 这就要求我们使用 raise ... from ... 来构建异常链 (Exception Chaining)。
python
def process_data():
try:
# 底层操作:可能发生各种具体错误
result = 10 / 0
except ZeroDivisionError as e:
# 我们捕获了具体错误,但想抛出一个更高级别的、
# 描述"业务逻辑"的错误。
# Tip: 使用 "from e" 来链接异常
raise RuntimeError("Calculation failed during data processing") from e
try:
process_data()
except RuntimeError as final_error:
print(f"--- Top Level Error Caught ---")
print(f"Error: {final_error}")
# 异常对象 "final_error" 包含了完整的上下文
输出的堆栈跟踪 (Traceback) 会是这样的:
Traceback (most recent call last):
File "...", line 3, in process_data
result = 10 / 0
ZeroDivisionError: division by zero
During handling of the above exception (ZeroDivisionError), another exception occurred:
Traceback (most recent call last):
File "...", line 12, in <module>
process_data()
File "...", line 9, in process_data
raise RuntimeError("Calculation failed during data processing") from e
RuntimeError: Calculation failed during data processing
- Python 清楚地告诉您,原始错误是
ZeroDivisionError: division by zero。 - 然后它说:"在处理上述异常时,发生了另一个异常"。
- 最后它显示了您抛出的新异常
RuntimeError: Calculation failed...。
所以,raise ... from e 会将 e (原始异常) 存储在新异常的 __cause__ 属性中。这为调试者提供了完整的上下文 :不仅知道发生了什么 (RuntimeError),还知道为什么会发生 (ZeroDivisionError)。
上下文管理器与 with 语句:自动化资源管理
with 语句是 Python 中一种优雅且健壮的语法,用于管理资源。它保证 无论代码块是正常执行完毕、发生异常还是提前 return,特定的"设置" (setup) 和"清理" (teardown) 操作都能被执行。最常见的例子就是文件处理:
传统方式 (容易出错):
python
f = open("myfile.txt", "w")
try:
f.write("Hello")
# 假设这里发生了一个错误,比如 1 / 0
finally:
# 无论是否发生错误,finally 块都会执行
f.close()
这种 try...finally 结构是必须的,因为您必须确保 f.close() 被调用,否则文件句柄可能会被泄漏。但这很冗长。
with 语句 (现代、安全的方式):
python
with open("myfile.txt", "w") as f:
f.write("Hello")
# 假设这里发生错误 1 / 0
# 当代码块退出时(无论何种原因),f.close() 会被 *自动调用*
with 语句极大地简化了代码,并消除了忘记 close() 的风险。让我们来拆解这个示例,它完美地展示了如何使用内置的上下文管理器。
python
"""
File processor
Reads a text file, prints the longest line length, and logs results.
"""
file_path = input("Enter file path: ")
log_file = "process.log"
try:
# 1. 核心:with 语句 + 异常处理
with open(file_path, encoding="utf-8") as f:
max_len = 0
longest_line = ""
for line in f:
# 2. 现代语法:海象运算符 (:=)
if (l := len(line.st
rip())) > max_len:
max_len = l
longest_line = line.strip()
print(f"Longest line ({max_len} chars): {longest_line}")
# 3. 嵌套的 with 语句 (打开日志文件)
with open(log_file, "a", encoding="utf-8") as log:
log.write(f"{file_path}: {max_len}\n")
except FileNotFoundError as e:
# 4. 健壮性:处理特定异常
print(f"File not found: {e}")
如何让我们自定义的类支持With语句
with 语句之所以能工作,是因为它遵循一个协议,该协议依赖于两个"魔术方法":__enter__ 和 __exit__
python
import time
class timer:
def __init__(self, name):
self.name = name
def __enter__(self):
"""(1) 进入 with 块时调用"""
print(f"[Timer '{self.name}' starting...]")
self.start = time.time()
# (2) a. return 的值会赋给 'as' 后面的变量
return self
def __exit__(self, exc_type, exc_value, traceback):
"""(3) 退出 with 块时调用"""
end = time.time()
print(f"[Timer '{self.name}' finished] Took: {end - self.start:.4f}s")
# (4) 异常处理
if exc_type:
print(f" L-> Exited with an exception: {exc_type.__name__}")
# (2) b. 返回 False (或 None) 会重新抛出异常
# 返回 True 则会"吞噬"异常
return False
__enter__(self):
- 这是"设置"阶段。在您的示例中,它记录了开始时间。
return self的意义: 当您编写with timer("Query") as t:时,变量t将被赋值为__enter__方法的返回值 。在这里返回self允许您在with块内部与timer实例交互(尽管您的示例没有这样做)。
__exit__(self, exc_type, exc_value, traceback):
- 这是"清理"阶段。它总是会被调用。
- 这三个参数至关重要:
- 如果
with块正常完成 :exc_type,exc_value,traceback全部为None。 - 如果
with块发生异常 :这三个参数会接收到异常的类型、值和堆栈跟踪信息(与sys.exc_info()相同)。
- 如果
- 异常处理能力:
- 在
__exit__方法内部,您可以检查exc_type是否为None来判断是否发生了错误。 - 返回
True: 如果__exit__返回True,它告诉 Python:"我已经处理了这个异常,请不要将它传播出去。" 异常被"吞噬"了。 - 返回
False(或None): 如果__exit__返回False或None(默认),它告诉 Python:"我没有处理这个异常(或者我只是记录了它),请在__exit__执行完毕后,继续将它抛出。"
- 在
演示 __exit__ 的异常处理:
python
with timer("Test Exception"):
print(" L-> Inside block, about to raise error...")
x = 1 / 0 # 故意制造一个错误
print(" L-> This line will not be printed.")
print("\n--- Program continues (because __exit__ handled it) ---")
上述代码(使用我们修改后的 timer)的输出:
[Timer 'Test Exception' starting...]
L-> Inside block, about to raise error...
[Timer 'Test Exception' finished] Took: 0.0001s
L-> Exited with an exception: ZeroDivisionError
Traceback (most recent call last):
File "...", line X, in <module>
x = 1 / 0
ZeroDivisionError: division by zero
注意:计时器仍然正确打印了它的结束信息和错误信息,然后异常被重新抛出 (因为 __exit__ 返回了 False)。
更简单的方式:@contextmanager
对于像 timer 这样简单的上下文管理器,Python 在 contextlib 模块中提供了一个更简单的创建方式:使用 @contextmanager 装饰器。这允许您将一个生成器 (generator) 转换为上下文管理器:
python
from contextlib import contextmanager
import time
@contextmanager
def timer_generator(name):
# --- 这部分是 __enter__ ---
print(f"[GenTimer '{name}' starting...]")
start = time.time()
try:
# yield 将控制权交回给 with 块
yield
finally:
# --- 这部分是 __exit__ (在 finally 中保证执行) ---
end = time.time()
print(f"[GenTimer '{name}' finished] Took: {end - start:.4f}s")
# 使用方法完全相同!
with timer_generator("Sleeping"):
time.sleep(1)
yield之前的代码是__enter__。yield之后的代码(放在try...finally中)是__exit__。- 这种方式更简洁,并且自动处理了异常的传播。
总结表:现代 Python 核心技巧
| 技巧 | 说明 | 参考资料 |
|---|---|---|
| docstring & RST | 提供函数/模块文档 | PEP 257 |
| 长行折叠 | 使用括号换行,符合 PEP 8 | PEP 8 --- Maximum Line Length |
| if...elif 优化 | 字典映射或列表推导 | 官方 Tutorial --- More Control Flow Tools |
海象运算符 := |
在表达式中赋值 | PEP 572 |
| for-else | else 在循环自然结束时执行 | 官方 Tutorial |
| 异常顺序 | 子类异常先于父类 | 官方 Tutorial --- Errors and Exceptions |
| 安全捕获 | 避免 bare except | 官方 Tutorial |
| 异常链 | raise ... from ... 保留原异常 |
官方 Tutorial |
| with 语句 | 自动管理资源 | 官方 Reference |
Reference
参考资料