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

参考资料

相关推荐
ha20428941943 小时前
Linux操作系统学习之---基于环形队列的生产者消费者模型(毛坯版)
linux·c++·学习
dxnb223 小时前
Datawhale25年10月组队学习:math for AI+Task5解析几何
人工智能·学习
哲Zheᗜe༘3 小时前
了解学习Redis主从复制
数据库·redis·学习
井队Tell5 小时前
打造高清3D虚拟世界|零基础学习Unity HDRP高清渲染管线(第九天)
学习·3d·unity
麦麦大数据5 小时前
F036 vue+flask中医热性药知识图谱可视化系统vue+flask+echarts+mysql
vue.js·python·mysql·flask·可视化·中医中药
移远通信5 小时前
MQTT协议:物联网时代的通信革命
python·物联网·网络协议
Amo Xiang6 小时前
JavaScript逆向与爬虫实战——基础篇(css反爬之动态字体实现原理及绕过)
爬虫·python·js逆向·动态字体
编程让世界美好6 小时前
选手评分问题(python)
python
java1234_小锋6 小时前
PyTorch2 Python深度学习 - PyTorch2安装与环境配置
开发语言·python·深度学习·pytorch2