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()
我的经验: 在机器人项目中,我经常用抽象基类(ABC)来定义接口规范。这样团队里不同人开发不同模块时,只要遵循同一个接口,联调就不会出问题。你想想看,如果每个人对“启动电机”的理解都不一样,那代码根本没法合在一起跑。

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 语句,自动管理资源
  • 异常处理不是可有可无,而是健壮系统的必备要素
Python 编程基础(下)知识体系 Python 编程基础(下) 函数定义与参数 面向对象编程 模块与包管理 文件读写操作 异常处理 位置/默认/可变参数 返回值与作用域 类定义与实例化 继承、多态、封装 导入方式与别名 __init__.py 与包结构 文本/二进制/CSV/JSON with 语句与文件模式 try-except-finally 自定义异常与最佳实践 核心原则:代码复用 + 封装抽象 + 模块化组织 + 健壮性 每个知识点都对应机器人系统中的实际应用场景