3. Python基础速通(下):函数与模块、文件读写、面向对象与异常处理

好,咱们接着聊。上一章我们把Python的变量、流程控制这些基本功过了一遍。这一章,才是真正让你能写出“能用”的代码的关键。说白了,前面那些是砖块,这一章教你怎么用砖块盖房子。

我个人习惯把Python编程分为两个阶段:能写脚本能写工程。这一章的内容,就是帮你从“能写脚本”跨入“能写工程”的门槛。咱们一个一个来。

Python基础速通(下)知识体系 函数与模块 def / import / 作用域 文件读写 CSV / JSON / with 面向对象 class / 继承 / 封装 异常处理 try / except / finally 材料科学应用:数据处理 → 自动化分析 → 结果输出 核心目标:让Python代码可复用、可维护、可容错 代码复用 数据持久化 结构建模 健壮性

3.1 函数与模块:别再写重复代码了

我在材料数据处理项目里见过最头疼的事,就是有人把同一段计算代码复制粘贴了二十遍。改一个参数,得改二十个地方。嗯,这就是典型的“还没学会函数”。

函数,说白了就是把一段逻辑打包,起个名字,随用随调。Python里用def关键字定义函数。

def calculate_density(mass, volume):
    """计算材料密度(单位:g/cm³)"""
    if volume <= 0:
        return None  # 体积不能为零或负数
    return mass / volume

# 调用
density = calculate_density(12.5, 4.2)
print(f"密度为:{density:.2f} g/cm³")
我的习惯:每个函数都写docstring(就是那个三引号注释)。别嫌麻烦,三个月后你回头看自己的代码,会感谢现在的自己。

参数传递这块有个坑。Python的参数传递是“传对象引用”。对于可变对象(比如列表),函数内部修改会影响到外部。

def add_element(data, element):
    data.append(element)  # 这会修改原列表

my_list = [1, 2, 3]
add_element(my_list, 4)
print(my_list)  # 输出 [1, 2, 3, 4] —— 原列表被改了!
我曾经踩过的坑:在函数里直接修改了传入的DataFrame,结果主程序的数据全乱了。后来我养成了习惯——如果不想修改原数据,在函数第一行先copy一下:data = data.copy()

模块就是多个函数的集合文件。一个.py文件就是一个模块。用import导入。

# 假设我们有一个文件 materials_utils.py
# 里面定义了 calculate_density, calculate_stress 等函数

# 在另一个文件中使用
import materials_utils

d = materials_utils.calculate_density(10, 2.5)

# 或者只导入特定函数
from materials_utils import calculate_density

你想想看,把常用的材料计算函数整理成一个模块,每次新项目直接import,是不是省事多了?

3.2 文件读写:CSV和JSON是材料科学的好朋友

做材料科学的,谁没跟实验数据打过交道?CSV和JSON就是最常见的两种数据格式。CSV适合表格数据,JSON适合结构化数据。

读写CSV,我推荐用Python内置的csv模块,简单直接。

import csv

# 写入CSV
data = [
    ["样品编号", "成分", "密度(g/cm³)", "硬度(HRC)"],
    ["S001", "Fe-0.2C", 7.85, 22],
    ["S002", "Fe-0.4C", 7.82, 35],
    ["S003", "Fe-0.6C", 7.80, 48]
]

with open("steel_samples.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(data)

# 读取CSV
with open("steel_samples.csv", "r", encoding="utf-8") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)
重点:一定要用with open(...) as f这种写法。它会自动关闭文件,哪怕中间出错了也会关。我见过有人手动写f.close(),结果异常时没执行到,文件句柄泄露了。

JSON在材料科学里常用于存储复杂的配置信息或嵌套数据。比如一个材料的多层结构描述。

import json

# 一个复合材料的结构描述
composite = {
    "name": "碳纤维增强环氧树脂",
    "layers": [
        {"material": "碳纤维", "thickness_mm": 0.2, "orientation": 0},
        {"material": "环氧树脂", "thickness_mm": 0.1, "orientation": None},
        {"material": "碳纤维", "thickness_mm": 0.2, "orientation": 90}
    ],
    "properties": {
        "density": 1.6,
        "tensile_strength": 1200
    }
}

# 写入JSON
with open("composite.json", "w", encoding="utf-8") as f:
    json.dump(composite, f, indent=2, ensure_ascii=False)

# 读取JSON
with open("composite.json", "r", encoding="utf-8") as f:
    data = json.load(f)
    print(data["name"])

我个人习惯用indent=2让JSON文件可读性好一点。调试的时候直接打开看,一目了然。

3.3 面向对象编程基础:把材料当成“对象”来思考

面向对象编程(OOP)听起来高大上,其实核心思想很简单:把数据和操作数据的方法打包在一起。在材料科学里,这太自然了——一个材料样品,既有属性(成分、密度、硬度),又有行为(计算强度、模拟热处理)。

class SteelSample:
    """钢材样品类"""
    
    def __init__(self, sample_id, composition, density):
        self.sample_id = sample_id
        self.composition = composition
        self.density = density
        self.hardness = None  # 初始未知
    
    def set_hardness(self, value):
        """设置硬度值"""
        if value < 0:
            raise ValueError("硬度不能为负数")
        self.hardness = value
    
    def calculate_stress(self, force, area):
        """计算应力 σ = F/A"""
        if area <= 0:
            return None
        return force / area
    
    def info(self):
        """返回样品信息字符串"""
        h = f"{self.hardness} HRC" if self.hardness else "未测量"
        return f"样品{self.sample_id}:{self.composition},密度{self.density}g/cm³,硬度{h}"

# 使用
s1 = SteelSample("S001", "Fe-0.2C", 7.85)
s1.set_hardness(22)
print(s1.info())

继承是OOP的另一个核心。比如我们有SteelSample,还可以有CeramicSample,它们都继承自一个基类MaterialSample

class MaterialSample:
    def __init__(self, sample_id):
        self.sample_id = sample_id
    
    def info(self):
        return f"样品编号:{self.sample_id}"

class SteelSample(MaterialSample):
    def __init__(self, sample_id, composition):
        super().__init__(sample_id)  # 调用父类构造
        self.composition = composition
    
    def info(self):
        # 重写父类方法
        return f"{super().info()},成分:{self.composition}"
我的建议:刚开始学OOP,别想得太复杂。你就记住一句话:把相关的数据和函数装在一个盒子里。这个盒子就是类。等你写多了,自然就理解封装、继承、多态这些概念了。

3.4 异常处理:让程序学会“扛揍”

程序跑着跑着就崩了,这种事谁没遇到过?异常处理就是给程序穿上防弹衣。不是不让它出错,而是出错时知道怎么处理。

def read_experiment_data(filename):
    """安全地读取实验数据文件"""
    try:
        with open(filename, "r", encoding="utf-8") as f:
            data = f.read()
        # 尝试解析数据
        values = [float(x) for x in data.split(",")]
        return values
    except FileNotFoundError:
        print(f"错误:文件 {filename} 不存在")
        return []
    except ValueError:
        print(f"错误:文件 {filename} 中包含非数字数据")
        return []
    except Exception as e:
        print(f"未知错误:{e}")
        return []
    finally:
        print("数据读取操作完成")  # 不管有没有异常,都会执行

为什么会这样?因为实验数据文件经常会有各种意外——格式不对、编码问题、数据缺失。如果不做异常处理,一个文件读不了,整个程序就停了。

我曾经犯过的错:一开始写代码,所有异常都用except Exception一把抓。结果程序不崩了,但数据错了也不知道。后来我改成except FileNotFoundErrorexcept ValueError这样分开处理,该报错的报错,该重试的重试,程序才真正“健壮”起来。

自定义异常也很有用。比如在材料数据处理中,可以定义InvalidSampleErrorDataRangeError等,让错误信息更有业务含义。

class InvalidSampleError(Exception):
    """样品数据无效时抛出的异常"""
    pass

def validate_sample(density, hardness):
    if density <= 0:
        raise InvalidSampleError(f"密度值 {density} 无效")
    if hardness < 0 or hardness > 100:
        raise InvalidSampleError(f"硬度值 {hardness} 超出合理范围")
    return True

好了,这一章的内容就这些。函数和模块让你告别重复代码,文件读写让你能和实验数据打交道,面向对象让代码结构更清晰,异常处理让程序更稳定。这四个技能组合起来,你已经能写出像模像样的材料数据处理工具了。

记住,写代码和做实验一样——多练,多踩坑,多总结。下次遇到问题,先想想能不能用函数封装,数据要不要存成CSV,能不能抽象成一个类,会不会有异常需要处理。慢慢来,这些都会变成你的肌肉记忆。


公众号:蓝海资料掘金营,微信deep3321