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 本章知识体系
下面这张图帮你理清本章的知识脉络:
好了,这一章的内容就这些。面向对象让你把解析逻辑组织得井井有条,正则帮你从文本中精准提取信息,文件I/O是数据进出的通道,推导式则让代码更简洁。这四个工具组合起来,写一个HDL解析器的基础就有了。
记住,写代码时多想想「这个场景用哪个工具最合适」。别硬套,也别偷懒。嗯,就这些,去练练手吧。