4、Python编程基础(下):函数定义与参数、面向对象编程(类与对象)、模块与包管理、文件读写操作、异常处理
好,咱们接着往下走。上一章我们把 Python 的基础语法和数据结构捋了一遍,现在该聊聊真正让代码“活起来”的东西了。函数、类、模块、文件、异常——这几个东西,说白了就是你把零散的代码组织成工程的核心手段。我在做机器人系统的时候,如果代码结构一塌糊涂,联调阶段简直就是噩梦。所以这一章,咱们把这些硬骨头啃下来。
4.1 函数定义与参数
函数是什么?就是一段可以重复调用的代码块。你想想看,如果你每次要计算电机转速都要把公式写一遍,那代码得多臃肿?
4.1.1 基本定义与调用
def calculate_speed(rpm, gear_ratio):
"""计算减速后的输出转速"""
output_speed = rpm / gear_ratio
return output_speed
# 调用
result = calculate_speed(3000, 10)
print(f"输出转速: {result} RPM")
嗯,这里要注意:def 关键字后面跟函数名,冒号结尾,缩进表示函数体。我个人习惯在函数开头写一个文档字符串(docstring),用三引号括起来。这样别人看代码的时候,或者你自己三个月后回来看,都能快速知道这个函数是干嘛的。
4.1.2 参数类型
Python 的参数机制挺灵活的,我刚开始用的时候也踩过坑。咱们一个一个看:
| 参数类型 | 定义方式 | 调用示例 | 说明 |
|---|---|---|---|
| 位置参数 | def f(a, b) |
f(1, 2) |
按顺序传参,少一个都不行 |
| 默认参数 | def f(a, b=10) |
f(1) 或 f(1, 20) |
不传就用默认值,但默认参数要放在后面 |
| 关键字参数 | def f(a, b) |
f(b=2, a=1) |
通过名字传参,顺序可以打乱 |
| 可变参数 | def f(*args) |
f(1, 2, 3) |
*args 接收元组,数量不限 |
| 关键字可变参数 | def f(**kwargs) |
f(name='a', val=10) |
**kwargs 接收字典 |
*args、**kwargs。
4.1.3 返回值与作用域
函数可以返回多个值,实际上返回的是一个元组:
def get_motor_status():
speed = 1500
temperature = 45.2
error_code = 0
return speed, temperature, error_code
s, t, e = get_motor_status()
print(f"转速: {s}, 温度: {t}, 错误码: {e}")
关于作用域,我建议你记住一个原则:函数内部定义的变量是局部变量,函数外部的是全局变量。如果你想在函数内部修改全局变量,需要用 global 关键字声明。但说实话,我很少用 global,因为它会让代码变得难以调试。更好的做法是把需要共享的数据通过参数传进去,或者用后面要讲的类来封装。
4.2 面向对象编程(类与对象)
面向对象编程,说白了就是把数据和操作数据的方法打包在一起。在机器人系统里,每个硬件设备——电机、传感器、控制器——都可以抽象成一个对象。我刚开始做机器人开发时,用面向过程的方式写代码,结果一个电机控制逻辑散落在十几个函数里,改一个地方就要改一堆。后来用类来封装,清爽多了。
4.2.1 类的定义与实例化
class Motor:
"""电机控制类"""
def __init__(self, name, max_rpm):
self.name = name
self.max_rpm = max_rpm
self.current_rpm = 0
self._temperature = 25.0 # 私有属性,约定用下划线开头
def start(self):
print(f"{self.name} 电机启动")
self.current_rpm = 100
def stop(self):
print(f"{self.name} 电机停止")
self.current_rpm = 0
def set_speed(self, target_rpm):
if target_rpm > self.max_rpm:
print(f"警告:目标转速 {target_rpm} 超过最大值 {self.max_rpm}")
self.current_rpm = self.max_rpm
else:
self.current_rpm = target_rpm
# 创建对象
left_motor = Motor("左轮", 3000)
right_motor = Motor("右轮", 3000)
left_motor.start()
left_motor.set_speed(2500)
print(f"当前转速: {left_motor.current_rpm}")
__init__ 是构造方法,在创建对象时自动调用。self 代表实例本身,所有实例方法第一个参数都是 self。我个人习惯把需要外部访问的属性直接写出来,把内部使用的属性用下划线开头,比如 _temperature。虽然 Python 没有真正的私有属性,但这个约定大家都会遵守。
4.2.2 继承与多态
继承是面向对象的核心特性之一。比如你有多种类型的电机——直流电机、步进电机、伺服电机——它们都有共同的行为(启动、停止、调速),但实现方式不同。这时候就可以用继承:
class BaseMotor:
def __init__(self, name):
self.name = name
def start(self):
raise NotImplementedError("子类必须实现 start 方法")
def stop(self):
raise NotImplementedError("子类必须实现 stop 方法")
class DCMotor(BaseMotor):
def start(self):
print(f"{self.name} 直流电机:通电启动")
def stop(self):
print(f"{self.name} 直流电机:断电停止")
class StepperMotor(BaseMotor):
def start(self):
print(f"{self.name} 步进电机:发送脉冲序列启动")
def stop(self):
print(f"{self.name} 步进电机:停止脉冲输出")
# 多态:同一个接口,不同实现
motors = [DCMotor("驱动电机"), StepperMotor("定位电机")]
for m in motors:
m.start()
4.2.3 类方法与静态方法
class MotorUtils:
motor_count = 0 # 类变量
def __init__(self):
MotorUtils.motor_count += 1
@classmethod
def get_count(cls):
"""类方法:可以访问类变量"""
return cls.motor_count
@staticmethod
def rpm_to_rads(rpm):
"""静态方法:与类无关的工具函数"""
return rpm * 2 * 3.14159 / 60
类方法用 @classmethod 装饰,第一个参数是 cls(类本身)。静态方法用 @staticmethod 装饰,不需要 self 或 cls,就是一个普通的工具函数,只是放在类的命名空间里方便管理。
4.3 模块与包管理
当你的代码越来越多,把所有东西写在一个文件里显然不现实。模块就是一个 .py 文件,包就是一个包含 __init__.py 文件的目录。我在做机器人系统时,通常这样组织代码:
robot_project/
├── __init__.py
├── main.py
├── hardware/
│ ├── __init__.py
│ ├── motor.py
│ ├── sensor.py
│ └── controller.py
├── algorithms/
│ ├── __init__.py
│ ├── kinematics.py
│ └── path_planning.py
└── utils/
├── __init__.py
├── logger.py
└── config.py
4.3.1 导入方式
# 方式一:导入整个模块
import hardware.motor
motor = hardware.motor.DCMotor("驱动电机")
# 方式二:导入特定类或函数
from hardware.motor import DCMotor
motor = DCMotor("驱动电机")
# 方式三:导入包
from hardware import motor
motor.DCMotor("驱动电机")
# 方式四:别名
from algorithms import kinematics as kin
result = kin.forward_kinematics(joint_angles)
from module import *,结果两个模块里都有同名的 calculate() 函数,后导入的把前一个覆盖了,查了半天才找到原因。我的建议是:永远不要用 import *,显式导入你需要的名字。
4.3.2 __init__.py 的作用
这个文件可以为空,但它的存在告诉 Python 这个目录是一个包。你也可以在 __init__.py 里做一些初始化工作,比如:
# hardware/__init__.py
from .motor import DCMotor, StepperMotor
from .sensor import Lidar, Camera
__all__ = ['DCMotor', 'StepperMotor', 'Lidar', 'Camera']
这样,外部只需要 from hardware import DCMotor 就能直接使用,不需要知道具体的模块路径。
4.4 文件读写操作
机器人系统离不开文件操作——读取配置文件、记录日志、保存传感器数据、加载地图等等。Python 的文件操作其实很简单,但有几个细节需要注意。
4.4.1 基本读写
# 写入文件
with open('sensor_data.txt', 'w', encoding='utf-8') as f:
f.write("时间戳: 2024-01-15 10:30:00\n")
f.write("温度: 25.3°C\n")
f.write("湿度: 60%\n")
# 读取文件
with open('sensor_data.txt', 'r', encoding='utf-8') as f:
content = f.read()
print(content)
# 逐行读取
with open('sensor_data.txt', 'r', encoding='utf-8') as f:
for line in f:
print(line.strip())
这里的关键是 with 语句。它会在代码块执行完毕后自动关闭文件,即使发生异常也会关闭。我刚开始学的时候经常忘记 f.close(),导致文件句柄泄漏。用 with 就再也不用担心这个问题了。
4.4.2 文件模式
| 模式 | 说明 | 文件存在时 | 文件不存在时 |
|---|---|---|---|
'r' |
只读 | 正常读取 | 报错 |
'w' |
写入(覆盖) | 清空后写入 | 创建新文件 |
'a' |
追加 | 在末尾追加 | 创建新文件 |
'r+' |
读写 | 正常读写 | 报错 |
'wb' |
二进制写入 | 清空后写入 | 创建新文件 |
4.4.3 处理 CSV 和 JSON
在机器人项目中,CSV 常用于记录日志,JSON 常用于配置文件:
import csv
import json
# CSV 写入
with open('motor_log.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['时间', '转速', '温度'])
writer.writerow(['10:00', 1500, 45.2])
writer.writerow(['10:01', 1520, 45.5])
# CSV 读取
with open('motor_log.csv', 'r') as f:
reader = csv.reader(f)
for row in reader:
print(row)
# JSON 写入
config = {
'motor': {'max_rpm': 3000, 'acceleration': 500},
'sensor': {'lidar_range': 10, 'camera_resolution': '1080p'}
}
with open('config.json', 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4, ensure_ascii=False)
# JSON 读取
with open('config.json', 'r', encoding='utf-8') as f:
loaded_config = json.load(f)
print(loaded_config['motor']['max_rpm'])
4.5 异常处理
代码总会出错,这不是你写得不好,而是现实世界太复杂了。文件可能不存在、网络可能断开、传感器可能返回非法值。异常处理就是让你优雅地应对这些意外情况。
4.5.1 try-except 结构
try:
# 可能出错的代码
with open('config.json', 'r') as f:
config = json.load(f)
motor_speed = config['motor']['speed']
print(f"电机转速: {motor_speed}")
except FileNotFoundError:
print("配置文件不存在,使用默认配置")
motor_speed = 1000
except KeyError as e:
print(f"配置文件中缺少键: {e}")
motor_speed = 1000
except Exception as e:
print(f"未知错误: {e}")
motor_speed = 1000
else:
# 没有异常时执行
print("配置加载成功")
finally:
# 无论是否异常都执行
print("清理资源")
except Exception。比如文件操作捕获 FileNotFoundError 和 PermissionError,网络操作捕获 ConnectionError 和 TimeoutError。这样每个异常都有对应的处理逻辑,而不是一刀切。
4.5.2 自定义异常
有时候内置的异常类型不够用,你可以自己定义:
class MotorOverheatError(Exception):
"""电机过热异常"""
def __init__(self, motor_name, temperature, threshold):
self.motor_name = motor_name
self.temperature = temperature
self.threshold = threshold
self.message = f"{motor_name} 温度 {temperature}°C 超过阈值 {threshold}°C"
super().__init__(self.message)
def check_motor_temperature(motor):
if motor.temperature > motor.max_temperature:
raise MotorOverheatError(motor.name, motor.temperature, motor.max_temperature)
try:
check_motor_temperature(left_motor)
except MotorOverheatError as e:
print(f"紧急停机: {e.message}")
# 执行紧急停机逻辑
4.5.3 异常处理的最佳实践
- 不要吞掉异常:
except: pass是最糟糕的写法,错误被隐藏了,后面出了问题根本找不到原因。 - 异常粒度要适中:太粗(一个 try 包整个程序)和太细(每行代码一个 try)都不好。一般以“一个操作单元”为单位。
- 记录异常信息:在生产环境中,用
logging模块记录异常,而不是只打印到控制台。 - 清理资源:用
finally或with语句确保文件、网络连接等资源被正确释放。
核心要点回顾:
- 函数是代码复用的基础,参数类型要灵活运用
- 面向对象把数据和操作封装在一起,适合模拟现实世界的实体
- 模块和包帮你组织大型项目,导入时要显式、清晰
- 文件操作记得用
with语句,自动管理资源 - 异常处理不是可有可无,而是健壮系统的必备要素