3、Python语法速查:面向对象编程(类与继承)、正则表达式基础、文件I/O操作、列表与字典推导式

各位同学,欢迎来到第三章。说实话,这一章的内容有点「杂」,但别小看它。我在写HDL解析器的时候,这几个知识点几乎每天都在用。类用来封装解析逻辑,正则用来匹配信号名,文件I/O用来读写.v文件,推导式用来快速处理列表——缺一不可。

咱们一个一个来,先讲最核心的。

3.1 面向对象编程:类与继承

面向对象编程,说白了就是把数据和操作数据的方法打包在一起。你想想看,一个Verilog模块有端口、有信号、有赋值语句,用类来建模再合适不过了。

核心概念:类(class)是模板,对象(instance)是实例。继承(inheritance)让子类复用父类的代码。

3.1.1 定义一个简单的类

class Module:
    """表示一个Verilog模块"""
    def __init__(self, name, ports):
        self.name = name
        self.ports = ports
        self.signals = []
    
    def add_signal(self, signal_name):
        self.signals.append(signal_name)
    
    def describe(self):
        return f"模块 {self.name},端口数:{len(self.ports)},信号数:{len(self.signals)}"

这里有个细节:__init__是构造方法,每次创建对象时自动调用。self指向当前实例,必须作为第一个参数。我个人习惯把属性都写在__init__里,这样一眼就能看清这个类有哪些数据。

3.1.2 继承:让子类复用父类

我在项目中遇到过这样的情况:解析Verilog时,模块和实例化语句有很多共同属性。用继承就很优雅。

class Module:
    def __init__(self, name):
        self.name = name
    
    def parse(self):
        raise NotImplementedError("子类必须实现parse方法")

class AlwaysBlock(Module):
    """always块,继承自Module"""
    def __init__(self, name, sensitivity_list):
        super().__init__(name)  # 调用父类构造
        self.sensitivity_list = sensitivity_list
    
    def parse(self):
        return f"解析always块:{self.name},敏感列表:{self.sensitivity_list}"

我的经验:super()调用父类方法时,记得传参。我曾经漏掉super().__init__(),结果子类里self.name一直是None,排查了半天。

3.1.3 避坑指南:类变量 vs 实例变量

class Parser:
    # 类变量,所有实例共享
    supported_formats = ["verilog", "vhdl"]
    
    def __init__(self, file_path):
        # 实例变量,每个实例独有
        self.file_path = file_path
        self.lines = []

嗯,这里要注意:类变量如果被修改,会影响所有实例。我建议能用实例变量就别用类变量,除非你真的需要共享数据。

3.2 正则表达式基础

正则表达式,说白了就是「模式匹配」。在HDL解析中,你要从一行文本里提取端口名、信号宽度、注释内容——正则就是干这个的。

核心模块:re模块,Python自带,无需安装。

3.2.1 常用函数

函数 作用 示例
re.match() 从字符串开头匹配 re.match(r'\d+', '123abc')
re.search() 搜索整个字符串 re.search(r'\d+', 'abc123')
re.findall() 返回所有匹配项 re.findall(r'\w+', 'a b c')
re.sub() 替换匹配内容 re.sub(r'\s+', ' ', text)

3.2.2 实战:提取Verilog端口

import re

line = "input wire [7:0] data_in,"
pattern = r'(input|output|inout)\s+(wire|reg)?\s*(\[\d+:\d+\])?\s*(\w+)'

match = re.search(pattern, line)
if match:
    direction = match.group(1)   # input
    data_type = match.group(2)   # wire
    width = match.group(3)       # [7:0]
    name = match.group(4)        # data_in
    print(f"端口名:{name},方向:{direction},宽度:{width}")

注意:正则中的?表示「可选」,*表示「零次或多次」。写正则时一定要考虑边界情况——比如端口没有宽度声明时,group(3)会是None

3.2.3 避坑指南:贪婪匹配

正则默认是贪婪的,会尽可能多地匹配。比如.*会匹配到行尾。想让它「见好就收」,加个?变成.*?。我曾经在解析注释时踩过这个坑,匹配结果把整个文件都吞了。

3.3 文件I/O操作

文件操作是解析器的基本功。你要读入.v文件,处理完再写出去。Python的文件操作很直观,但有几个细节要注意。

3.3.1 读取文件

# 推荐方式:使用with语句,自动关闭文件
with open('top_module.v', 'r', encoding='utf-8') as f:
    content = f.read()        # 一次性读取全部
    # 或者按行读取
    for line in f:
        print(line.strip())

我的习惯:永远指定encoding='utf-8'。Verilog文件可能是ASCII,也可能是UTF-8,不指定编码在某些系统上会报错。

3.3.2 写入文件

with open('output.v', 'w', encoding='utf-8') as f:
    f.write('// Generated by HDL Parser\n')
    f.write(f'module {module_name} (\n')
    for port in ports:
        f.write(f'    input {port},\n')
    f.write(');\n')

3.3.3 避坑指南:文件路径

Windows用反斜杠\,Linux用正斜杠/。我建议用os.path.join()pathlib来拼接路径,跨平台无烦恼。

from pathlib import Path

base_dir = Path('./verilog_files')
file_path = base_dir / 'top_module.v'  # 自动处理斜杠
with open(file_path, 'r') as f:
    ...

3.4 列表与字典推导式

推导式是Python的「语法糖」,让你用一行代码搞定循环+条件判断。写解析器时,经常需要从一堆信号里筛选出符合条件的,推导式就是干这个的。

3.4.1 列表推导式

# 基础语法:[表达式 for 变量 in 可迭代对象 if 条件]

# 提取所有以"clk"开头的信号
signals = ['clk', 'rst_n', 'data_in', 'clk_div', 'addr']
clk_signals = [s for s in signals if s.startswith('clk')]
print(clk_signals)  # ['clk', 'clk_div']

# 带条件的推导式:只保留宽度大于8的端口
ports = [('data', 32), ('addr', 16), ('flag', 1)]
wide_ports = [name for name, width in ports if width > 8]
print(wide_ports)  # ['data', 'addr']

3.4.2 字典推导式

# 基础语法:{键:值 for 变量 in 可迭代对象 if 条件}

# 将信号列表转为字典:信号名 -> 长度
signals = ['clk', 'rst_n', 'data_in']
signal_dict = {s: len(s) for s in signals}
print(signal_dict)  # {'clk': 3, 'rst_n': 5, 'data_in': 7}

# 实战:从端口列表构建查找表
ports = [('clk', 'input'), ('data', 'inout'), ('addr', 'input')]
port_dir = {name: direction for name, direction in ports if direction == 'input'}
print(port_dir)  # {'clk': 'input', 'addr': 'input'}

核心技巧:推导式虽然简洁,但不要嵌套太深。超过两层嵌套,可读性会急剧下降。我一般只用在简单场景,复杂逻辑还是老老实实写for循环。

3.4.3 避坑指南:推导式中的副作用

推导式里不要做文件I/O、打印等有副作用的操作。它本质上是表达式,不是语句。我曾经在推导式里写print()调试,结果列表生成完了,打印了一堆None。

3.5 本章知识体系

下面这张图帮你理清本章的知识脉络:

Python语法速查(HDL解析器实战) 面向对象编程 类定义(class) 构造方法 __init__ 继承与 super() 类变量 vs 实例变量 正则表达式 re.match / search re.findall / sub 分组提取 group() 贪婪 vs 非贪婪 文件I/O操作 with open() 读取 写入与追加模式 编码指定 utf-8 pathlib 跨平台路径 列表/字典推导式 列表推导式 [x for] 字典推导式 {k:v} 条件过滤 if 避免副作用 核心:类封装逻辑 → 正则匹配文本 → 文件读写数据 → 推导式快速处理 应用场景:Verilog端口解析、信号列表筛选、模块代码生成

好了,这一章的内容就这些。面向对象让你把解析逻辑组织得井井有条,正则帮你从文本中精准提取信息,文件I/O是数据进出的通道,推导式则让代码更简洁。这四个工具组合起来,写一个HDL解析器的基础就有了。

记住,写代码时多想想「这个场景用哪个工具最合适」。别硬套,也别偷懒。嗯,就这些,去练练手吧。


专注资料整理