现代 Python 学习笔记:Statements & Syntax

现代 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
)

参考PEP 8 --- Maximum Line Length


多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:

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 是什么? eZeroDivisionError 类的一个实例

  • 它有什么用?

    1. 打印信息: print(e) 会调用该对象的 __str__ 方法,通常会给出一个人类可读的错误信息(例如:"division by zero")。

    2. 获取参数: e.args 是一个包含传递给异常构造函数参数的元组。

    3. 日志记录: 在生产环境中,您会使用 logging 模块记录完整的异常信息:

      python 复制代码
      import 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
  1. Python 清楚地告诉您,原始错误是 ZeroDivisionError: division by zero
  2. 然后它说:"在处理上述异常时,发生了另一个异常"。
  3. 最后它显示了您抛出的新异常 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__ 返回 FalseNone(默认),它告诉 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

参考资料

相关推荐
晓梦.16 分钟前
Vue3学习笔记
笔记·学习
路边草随风22 分钟前
milvus向量数据库使用尝试
人工智能·python·milvus
newobut40 分钟前
vscode远程调试python程序,基于debugpy库
vscode·python·调试·debugpy
思成不止于此1 小时前
【MySQL 零基础入门】DQL 核心语法(二):表条件查询与分组查询篇
android·数据库·笔记·学习·mysql
APIshop1 小时前
用 Python 把“API 接口”当数据源——从找口子到落库的全流程实战
开发语言·python
SadSunset1 小时前
(15)抽象工厂模式(了解)
java·笔记·后端·spring·抽象工厂模式
一点晖光2 小时前
Docker 作图咒语生成器搭建指南
python·docker
smj2302_796826522 小时前
解决leetcode第3768题.固定长度子数组中的最小逆序对数目
python·算法·leetcode
木头左2 小时前
位置编码增强法在量化交易策略中的应用基于短期记忆敏感度提升
python
Acc1oFl4g2 小时前
详解Java反射
java·开发语言·python