3. Python基础速览:列表、字典、函数、类、面向对象编程在工程中的应用
说句实在话,很多人刚开始学Python,觉得语法简单,刷刷题就过了。但一上手工程,就发现不对劲。列表怎么越用越慢?字典的键为什么不能是列表?函数参数传值还是传引用?类继承怎么搞得代码一团乱麻?
这些坑,我几乎都踩过。今天这一章,咱们就直奔主题。不谈考试,只谈工程。看看列表、字典、函数、类这些东西,究竟怎么玩才能让代码跑得又快又稳。你想想看,如果每次写循环都能快几毫秒,优化参数时少掉几个坑,累积起来的效率是很可观的。
3.1 列表:不只是“数组”这么简单
列表是Python中最常用的数据结构之一。很多人把它当数组用,其实它的设计精妙得多。
我个人习惯,把列表分为“用途”和“性能”两个维度来看。用途上,它是动态数组,能存任意类型。性能上,插入和删除的位置决定了效率,这个必须心里有数。
我在项目中遇到过一个问题:用列表存日志流水,每天上百万条,每次都从头部插入新数据。结果程序越来越慢,最后内存都炸了。
后来用deque,设置最大长度,自动丢弃旧数据。代码几行就解决了。
from collections import deque
log_queue = deque(maxlen=10000) # 最多保留最近10000条
log_queue.appendleft(new_log) # 头部插入,O(1)
log_queue.pop() # 尾部弹出,O(1)
# 不推荐
values = []
for x in raw_data:
if x > 0:
values.append(x * 2)
# 推荐
values = [x * 2 for x in raw_data if x > 0]
3.2 字典:工程优化的第一利器
字典,说白了就是哈希表。查找效率O(1),这在对接参数时简直是神器。你的参数名、特征值、配置项,全都能用字典来管理。
我曾经在调试一个参数协同优化的脚本时,发现每次从列表里找参数都要循环几十万次。改成字典后,速度提升了上百倍。
# 慢:列表查找O(n)
params_list = [{'name': 'lr', 'value': 0.01}, {'name': 'batch_size', 'value': 32}]
for param in params_list:
if param['name'] == 'lr':
print(param['value'])
# 快:字典查找O(1)
params_dict = {'lr': 0.01, 'batch_size': 32}
print(params_dict['lr'])
但这里有个大坑:字典的键必须是不可变类型。字符串、数字、元组可以。列表不行。为什么?因为哈希表要求键的哈希值不变,列表可变,哈希值就变了。
# 错误示范
d = {}
d[[1, 2, 3]] = 'test' # TypeError: unhashable type: 'list'
# 正确做法
d = {}
d[(1, 2, 3)] = 'test' # 元组键是允许的
3.3 函数:把逻辑拆成小块
函数不只是“一段可重用的代码”。在工程中,函数是组织逻辑的最小单元。我建议每个函数只做一件事,并且把它做好。
举个例子,参数预处理、特征计算、结果输出,分别写不同的函数。这样调试时,你只用看一个函数,不用满屏幕找逻辑。
def preprocess_params(raw_params):
"""清洗和校验参数"""
processed = {}
for key, value in raw_params.items():
if value is not None and value > 0:
processed[key] = float(value)
return processed
def compute_feature(input_data, config):
"""基于配置计算特征"""
feature = input_data * config['scale'] + config['offset']
return feature
def save_result(feature, path):
"""保存结果到文件"""
with open(path, 'w') as f:
f.write(str(feature))
def normalize(data: list[float], mean: float, std: float) -> list[float]:
"""数据归一化"""
return [(x - mean) / std for x in data]
3.4 类与面向对象编程:工程化的基石
我见过不少人,觉得面向对象就是“为了写类而写类”。其实不然。在工程中,类是把数据和方法捆绑在一起的高级抽象。当你处理多个同类的对象时,类能让代码结构清晰十倍。
比如参数协同优化,每个参数都有自己的范围、类型、优化状态。用字典当然可以存,但当你需要为每个参数增加方法时,类就显出优势了。
class Parameter:
"""一个可优化的参数"""
def __init__(self, name: str, value: float, min_val: float, max_val: float):
self.name = name
self.value = value
self.min_val = min_val
self.max_val = max_val
def clamp(self):
"""将值限制在范围内"""
self.value = max(self.min_val, min(self.max_val, self.value))
def perturb(self, delta: float):
"""在一定范围内扰动"""
self.value += delta
self.clamp()
def __repr__(self):
return f"Param({self.name}={self.value:.4f})"
有了类,你就可以批量创建参数、统一操作,这在优化循环中特别好用。
# 创建参数集合
params = [
Parameter('learning_rate', 0.01, 1e-5, 1.0),
Parameter('batch_size', 32, 1, 512),
Parameter('dropout', 0.5, 0.0, 1.0)
]
# 统一扰动
for p in params:
p.perturb(0.1)
print(params)
这里还要提一下__repr__和__str__。在调试时,打印出优雅的对象信息能节省大把时间。我习惯给每个类都加上__repr__,这样打印列表时,每个参数一目了然。
3.5 面向对象编程在工程中的应用模式
在参数协同优化这个场景里,我常用的模式是:策略模式 + 组合。每种优化算法(贝叶斯优化、随机搜索、网格搜索)都实现相同的接口,然后在主程序中切换使用。
class OptimizerStrategy:
"""优化策略基类"""
def optimize(self, params: list[Parameter]) -> list[Parameter]:
raise NotImplementedError
class RandomSearch(OptimizerStrategy):
def optimize(self, params):
for p in params:
p.perturb(random.uniform(-0.5, 0.5))
return params
class GridSearch(OptimizerStrategy):
def __init__(self, steps: int = 10):
self.steps = steps
def optimize(self, params):
# 网格搜索具体实现
pass
这样,如果你想换一种优化方法,只需换一个策略类,其他代码不用动。这叫“开闭原则”——对扩展开放,对修改关闭。
嗯,我这里整理了一个对比,帮你快速理清楚不同数据结构在工程中的定位:
| 结构 | 核心特性 | 工程适用场景 | 性能注意 |
|---|---|---|---|
| 列表(list) | 有序、可重复、可变 | 日志序列、按顺序处理的数据 | 尾部操作快,头部操作慢 |
| 字典(dict) | 键值对、哈希表 | 参数表、配置映射、缓存 | 查找O(1),键必须不可变 |
| 函数(function) | 可调用、可传递 | 数据预处理、计算单元、回调 | 建议单一职责、带类型提示 |
| 类(class) | 数据+方法、封装、继承 | 参数对象、优化器、策略模式 | 组合优于继承,继承层数≤3 |
为了更直观地展示这些基础概念之间的工程关系,我画了一张结构图。它大致反映了从数据到逻辑再到模块的演化路径。
说实话,很多人学完Python基础知识后,还是不知道怎么把它们串起来用。这一章我特意用工程的视角,把列表、字典、函数、类和面向对象编程串成一条线。你想想看,一个参数协同优化的项目,底层是字典管理参数名和值,中间用列表批量处理参数对象,用函数封装每一步计算逻辑,用类把策略抽象成模块。
这套思路,是我在多个项目里反复验证过的。你照着这个模式写代码,至少不会在工程上犯基础错误。至于更复杂的用法——比如装饰器、迭代器、上下文管理器,那是后面章节的事了。