Python模块文件&面向对象
[TOC]
1. 模块 (Module) 与包 (Package)
1.1 为什么需要模块化
当代码量持续增长,若所有逻辑都堆砌在单个 .py 文件中,会暴露一系列核心问题:
-
代码维护难:函数、变量、业务逻辑混杂,定位特定功能如同 “大海捞针”
-
复用性差:其他项目想使用你的工具函数,只能复制粘贴,易出错且无法统一维护
-
命名冲突:不同功能的同名变量 / 函数极易 “撞车”,引发难以排查的逻辑错误
-
协作效率低:多人修改同一文件时,代码冲突频繁,合并成本高
模块化的核心逻辑:按 “职责划分” 将代码拆分到不同文件,再通过 import(导入)语句按需组合使用 —— 这和 Java 中按功能拆分不同类文件、通过import导入的设计思路完全一致。
1.2 模块是什么
在 Python 中,一个 .py 文件就是一个独立模块(类比 Java 中一个.java文件对应一个类)。
例如:utils.py、math_tools.py 都是合法模块名(导入时无需写 .py 后缀)。
模块的核心价值:
-
导入(Import):使用其他模块的变量、函数、类
-
导出(Export):控制本模块对外暴露的成员(变量、函数、类)
1.3 模块的导入与导出
1.3.1 基础导入:导入整个模块
import math # 导入Python标准库的数学模块(类比Java import java.lang.Math;)
# 使用模块内的属性/方法,需加“模块名.”前缀(模块导出的所有成员都可通过此方式访问)
print(math.pi) # 输出圆周率:3.141592653589793(pi是math模块导出的常量)
print(math.sqrt(16)) # 计算平方根:4.0(sqrt是math模块导出的函数)
1.3.2 精准导入:导入模块的指定成员
from math import pi, sqrt # 只导入需要的成员,减少命名空间占用
# 等价于:从math模块中“导入”pi和sqrt两个导出成员
# 直接使用导入的成员,无需前缀
print(pi) # 3.141592653589793
print(sqrt(16)) # 4.0
1.3.3 别名导入:为模块 / 成员起别名
import datetime as dt # 为长模块名起别名(类比Java import java.util.Date as MyDate;)
from math import pi as PI # 为成员起别名,避免命名冲突
# 通过别名使用模块/成员
now = dt.datetime.now() # 获取当前时间(dt是datetime模块的别名)
print(now)
print(PI) # 3.141592653589793(PI是pi的别名)
1.3.4 批量导入:from 模块 import *(导入所有导出成员)
语法说明:
from 模块名 import * # 导入模块中“所有对外导出”的成员
示例 1:导入标准库所有成员
from math import * # 导入math模块的所有导出成员(pi、sqrt、sin、cos等)
# 直接使用所有导入的成员,无需前缀
print(pi) # 3.141592653589793
print(sqrt(25)) # 5.0
print(sin(0)) # 0.0(sin是math模块的导出函数)
示例 2:自定义模块的批量导入与导出控制
第一步:定义模块 calc_utils.py(控制导出内容)
"""计算工具模块:演示导出控制"""
# 1. 公开成员(计划对外导出的)
def add(a, b):
"""加法函数:公开导出"""
return a + b
def mul(a, b):
"""乘法函数:公开导出"""
return a * b
PI = 3.1415926 # 常量:公开导出
# 2. 私有成员(仅模块内部使用,不建议对外导出)
# Python中以下划线_开头的成员,约定为“私有成员” [并非真的私有化,面向对象会讲解具体私有化]
def _check_number(num):
"""内部校验函数:私有,不对外导出"""
return isinstance(num, (int, float))
# 3. 显式指定模块的导出列表(核心:控制*能导入的成员)
# __all__ 是模块的内置变量,定义了使用 from 模块 import * 时能导入的成员列表
__all__ = ["add", "mul", "PI"]
# 模块自测代码:仅直接运行本文件时执行
if __name__ == "__main__":
print("自测加法:", add(1, 2)) # 输出 3
print("自测乘法:", mul(3, 4)) # 输出 12
print("内部校验:", _check_number(5)) # 输出 True(内部可使用私有函数)
Python 中 _单下划线 是约定俗成的私有成员(仅提醒开发者 “不建议外部使用”,并非真正限制访问),而 __双下划线 才是强制私有成员(通过名称修饰机制阻止外部直接访问)
第二步:在主程序 **main.py **中批量导入
# 方式1:批量导入(仅能导入__all__中定义的成员)
from calc_utils import *
# 可正常使用__all__中的成员
print("10 + 20 =", add(10, 20)) # 输出 30
print("3 × 5 =", mul(3, 5)) # 输出 15
print("圆周率:", PI) # 输出 3.1415926
# 以下代码会报错:_check_number不在__all__中,无法通过*导入
# print(_check_number(10)) # NameError: name '_check_number' is not defined
# 方式2:强制导入私有成员(不推荐,破坏封装)
# 即使有__all__,仍可通过精准导入方式获取私有成员,但不符合Python规范
from calc_utils import _check_number
print("强制导入私有函数:", _check_number(10)) # 输出 True(不建议这么用)
1.3.5 导入时机与性能说明
-
规范写法:import 语句统一写在文件顶部(类比 Java 的 import 语句位置),提升代码可读性
-
性能特点:同一个模块在程序运行期间仅初始化一次,后续import会直接复用缓存,无需担心重复导入的性能损耗
-
核心原则:少量导入不会成为性能瓶颈,优先保证代码清晰易读
Java 对比:
-
Python 的import是 “加载并执行模块初始化代码”,后续复用缓存;
-
Java 的类加载有 “加载 / 链接 / 初始化” 流程,类对象也会被 ClassLoader 缓存;
-
Python 的sys.path 作用类似 Java 的classpath(都是指定模块 / 类的搜索路径)。
1.4 模块搜索路径:sys.path
Python 执行import xxx时,会按顺序在sys.path列表的路径中查找xxx.py文件或包。可通过以下代码查看搜索路径:
import sys
# 打印所有模块搜索路径(优先级从高到低)
for idx, path in enumerate(sys.path):
print(f"{idx+1}. {path}")
sys.path 通常包含三类核心路径:
-
当前运行目录:执行脚本所在的项目目录
-
标准库目录:Python 内置模块的存放路径
-
第三方库目录:site-packages(pip 安装的库都在这里,类比 Java 的 maven 仓库)
1.5 高频坑:文件名与库名冲突
1.5.1 问题核心
在 Python 项目开发中,切勿在项目根目录(或程序运行目录)创建与 Python 标准库、第三方库同名的 .py 文件(如 json.py、requests.py、os.py 等)。这是新手极易踩的高频错误,会直接导致模块导入异常、核心功能失效。
1.5.2 错误案例演示
场景复现
假设你在项目根目录创建了一个名为 json.py 的文件,文件内容如下:
# json.py(本地错误文件)
print("这是我自己的 json.py 文件")
接着在同目录下创建 main.py,尝试使用标准库 json 的核心功能:
# main.py
import json
# 尝试使用 json 标准库的 dumps 方法
data = {"name": "Python", "version": 3.11}
print(json.dumps(data))
执行结果与报错
运行 main.py 会触发如下报错(核心报错片段):
这是我自己的 json.py 文件
AttributeError: module 'json' has no attribute 'dumps'
报错原因:Python 执行 import json 时,优先导入了本地的 json.py,而非系统标准库的 json 模块;而你本地的 json.py 中没有 dumps 方法,因此报错。
1.5.3 底层原因解析
Python 的模块导入遵循「就近原则」,其查找模块的顺序为:
- 当前执行脚本所在目录(项目根目录);
- 环境变量
PYTHONPATH配置的目录; - Python 安装目录下的标准库路径;
- 第三方库安装路径(如
site-packages)。
简单来说:当本地文件名与系统库名一致时,Python 会 “认错” 模块 —— 把你的本地文件当成系统库导入,自然找不到库的核心功能。
1.5.4 解决方案
-
重命名本地文件(最直接):将冲突的文件名修改为非库名格式,比如把
json.py改为my_json_utils.py、json_handler.py等; -
清理缓存文件:若已运行过错误代码,目录下会生成
__pycache__文件夹(编译后的缓存),需删除该文件夹后重新运行程序; -
验证导入路径
(可选):通过以下代码确认导入的模块路径,排查是否导入了本地文件:
import json # 打印导入的 json 模块实际路径 print(json.__file__)- 正确结果:指向 Python 安装目录的
json.py(如C:\Python311\Lib\json\__init__.py); - 错误结果:指向你项目目录下的
json.py。
- 正确结果:指向 Python 安装目录的
1.5.5 避坑技巧
- 项目文件命名遵循「自定义前缀 + 功能」规则,比如
proj_json_parse.py、test_requests_api.py,避免直接使用json.py、requests.py等纯库名; - 常用易冲突库名清单(需重点规避):
- 标准库:
json.py、os.py、sys.py、time.py、random.py、csv.py; - 第三方库:
requests.py、pandas.py、numpy.py、flask.py、django.py;
- 标准库:
- 大型项目建议使用「包结构」管理代码:将功能代码放在子包中(如
src/utils/json_handler.py),而非直接放在项目根目录,从结构上避免冲突。
1.6 name 与程序入口
每个 Python 模块都有内置变量__name__,用于标识模块的运行状态:
- 直接运行模块:
__name__="__main__"(类比 Java 的public static void main(String[] args)入口方法) - 被其他模块导入:
__name__= 模块名(如calc_utils)
核心用途:将测试 / 演示代码放入入口保护中,避免被导入时自动执行。
import keyword
# 1. 打印Python所有关键字列表
print(keyword.kwlist)
# 2. 打印列表对象的所有可用方法/属性(和关键字无关,只是演示dir()用法)
print(dir(keyword.kwlist))
# 3. 演示Python数字的下划线分隔写法(提升可读性,不影响数值)
num1 = 1_000_000_000_000_00_0
print(num1) # 输出:1000000000000000
# 4. 主程序入口判断
if __name__ == '__main__':
print(num1 , end=" ")
这段代码主要做了三件事:展示 Python 关键字、演示数字下划线写法、包含了一个 __name__ == '__main__' 语句块!
为什么需要 __name__ == '__main__'?
你觉得 “直接输出也能打印,为啥还要加这个判断”,核心原因是:这个判断不是为了 “能输出”,而是为了 “控制代码的执行场景”。
第一:先理解 __name__ 是什么?
__name__ 是 Python 的内置变量,它的值完全取决于当前脚本的运行方式,核心规则如下:
- 当你直接运行这个脚本(比如
python demo.py),__name__的值是固定字符串'__main__'; - 当你把这个脚本作为模块导入到其他文件时(比如在另一个文件写
import demo),__name__的值会变成该脚本的模块名(即文件名,比如脚本叫calc_utils.py,则__name__ = 'calc_utils';脚本叫demo.py,则__name__ = 'demo')这样就可以控制主动运行执行,别人调用不执行!
简单说:__name__ 就像脚本的 “身份标识”—— 自己直接运行时是 “主程序”,被别人导入时是 “普通模块”。
第二:对比两种场景,一目了然
假设你的代码文件名叫 calc_utils.py(对应你提到的 calc_utils 模块名),我们用具体例子看差异:
场景 1:直接运行 demo.py
python demo.py
此时 __name__ = '__main__',所以:
- 所有非缩进的代码(打印关键字、打印 num1 等)都会执行;
if __name__ == '__main__':条件成立,里面的打印代码也会执行;- 最终会看到多次打印 num1 的结果。
场景 2:把 demo.py 作为模块导入到另一个文件
新建 main.py,内容如下:
# main.py
import demo # 导入你的demo.py文件(此时demo的__name__ = 'demo')
print("我是main.py的代码")
运行 main.py,你会发现:
demo.py里所有非缩进的代码(打印关键字、print (num1) 等)都会被执行(这是你不想要的);- 但
if __name__ == '__main__':条件不成立(因为demo里的__name__ = 'demo',不等于'__main__'),所以这个块里的代码不会执行; - 如果把
demo.py里的打印逻辑都放进if块,导入时就只会加载变量 / 函数,不会打印任何内容。
- 改造后的示例(更合理的写法)
把 “仅在直接运行时需要执行的代码” 放进 if 块,“供其他模块使用的内容” 放在外面:
# demo.py
import keyword
# 供其他模块使用的变量/函数(对外暴露的内容)
num1 = 1_000_000_000_000_00_0
def print_keywords():
"""打印Python关键字列表(供其他模块调用)"""
print(keyword.kwlist)
# 仅直接运行本脚本时才执行的代码(测试/演示逻辑)
if __name__ == '__main__':
# 测试变量
print(num1) # 1000000000000000
# 测试函数
print_keywords()
# 多次打印演示
print(num1 , end=" ")
此时:
- 直接运行
demo.py:__name__ = '__main__',if块里的所有测试代码都会执行; - 导入
demo(import demo):__name__ = 'demo',if块代码不执行,只会加载num1变量和print_keywords函数,避免干扰主程序。
1.7 包 (Package) 与目录组织
当项目中模块(.py 文件)数量增多时,零散的文件会导致代码难以维护。Python 提供包(Package) 机制按目录层级组织模块,其设计思路与 Java 按 com.company.utils 这类包结构管理类文件的逻辑完全一致,仅物理实现略有差异。
1.7.1 包的核心定义
Python 中,一个目录要成为合法的包,需满足:
- 包含
__init__.py文件(可空,也可编写包级初始化逻辑,如批量导出模块、定义包版本等); - 现代 Python 支持无
__init__.py的 “命名空间包”,但入门阶段建议严格遵循 “目录 +__init__.py” 的规范(类比 Java 必须按包名创建对应目录结构)。
1.7.2 典型项目结构
Python 包结构(物理目录)
project/
├── main.py # 主程序(入口文件)
└── mypkg/ # 自定义包
├── __init__.py # 包初始化文件(必填)
├── tools.py # 工具模块
└── io_utils.py # IO操作模块
Java 包结构(物理目录,对比参考)
project/
├── Main.java # 主程序(入口类)
└── com/
└── company/
└── mypkg/
├── Tools.java # 工具类
└── IOUtils.java # IO操作类
核心对比说明 :
- Python 用 “目录 + .py 文件” 组织模块,Java 用 “目录 + .java 文件” 组织类;
- 两者均按 “功能职责” 分层,目录结构与包名 / 模块名一一对应;
- Python 需通过
__init__.py标识包,Java 无需额外文件,仅通过package com.company.mypkg;声明包名。
1.7.3 包的导入方式
方式 1:导入包中的指定模块
Python 代码:
# 导入mypkg包下的tools模块
from mypkg import tools
# 调用模块中的函数
print(tools.some_func())
Java 等效代码:
// 导入com.company.mypkg包下的Tools类
import com.company.mypkg.Tools;
// 调用类中的静态方法
System.out.println(Tools.someFunc());
方式 2:导入模块中的指定成员
Python 代码:
# 直接导入io_utils模块的load_json函数
from mypkg.io_utils import load_json
data = load_json("data.json")
Java 等效代码:
// 静态导入IOUtils类的loadJson方法
import static com.company.mypkg.IOUtils.loadJson;
String data = loadJson("data.json");
方式 3:导入模块并自定义别名
Python 代码:
# 简化长模块名(常用技巧)
from mypkg import io_utils as io
data = io.load_json("data.json")
Java 等效代码:
// Java无直接别名语法,间接简化
import com.company.mypkg.IOUtils;
IOUtils io = new IOUtils();
String data = io.loadJson("data.json");
1.7.4 Java 对比总结
- Python 包是 “物理目录 +
__init__.py” 的组合,导入时按 “目录。模块” 路径查找; - Java 包是 “命名空间 + 物理目录” 的映射,导入时按 “包名。类名” 查找;
- 两者核心目的一致:按功能拆分代码,降低耦合,提升可读性。
1.8 循环导入:模块化高频报错
循环导入是 Python 模块化开发中最易踩的坑之一,其本质与 Spring 中 “两个组件互相依赖” 导致的编译 / 运行异常完全相同,仅报错形式略有差异。
1.8.1 问题定义
当模块 A 导入模块 B 的成员,同时模块 B 又导入模块 A 的成员时,会触发循环导入(Circular Import)。
核心原因:Python/Java 加载模块 / 类时,会按 “自上而下” 的顺序初始化,循环依赖会导致 “被依赖的成员还未定义就被引用”。
运行代码时会触发以下报错(新手易误判为 “代码写错”,实则是依赖关系问题):
Python:ImportError: cannot import name 'XXX' from 'YYY' 或 AttributeError: module 'XXX' has no attribute 'YYY';
1.8.2 示例:循环导入的问题代码
# A模块导入B模块的函数(触发循环第一步)
from b import b_func
def a_func():
return "A模块函数"
# 调用B模块函数
print(b_func())
# B模块导入A模块的函数(形成循环依赖)
from a import a_func
def b_func():
return "B模块函数"
# 调用A模块函数
print(a_func())
运行 python a.py 报错:ImportError: cannot import name 'b_func' from 'b'。
1.8.3 解决思路(优先级从高到低)
1.8.3.1 方案 1:抽离公共代码(推荐)
-
操作步骤:将 A、B 共用的逻辑抽离到第三个独立模块(如
common.py),A、B 均导入该模块,切断循环依赖。 -
优化后 Python 结构:
project/ ├── a.py ├── b.py └── common.py # 抽离公共函数/变量 -
Java 类比方案:将两个类的公共逻辑抽离为
CommonUtils.java,A、B 均导入该工具类,不再互相依赖。
1.8.3.2 方案 2:调整依赖方向
- 操作步骤:梳理模块的 “层级关系”,让底层模块(如工具模块)不依赖上层模块(如业务模块),仅保留 “上层依赖底层” 的单向依赖。
- Java 类比方案:调整类的依赖关系,确保工具类(底层)不依赖业务类(上层),仅业务类依赖工具类。
1.8.3.3 方案 3:局部导入(临时方案)
-
操作步骤:将
import语句移到函数内部(仅在需要时导入),避免模块加载阶段的循环依赖。 -
Python 优化示例(a.py):
def a_func(): return "A模块函数" def call_b_func(): # 函数内部导入,避开模块初始化阶段的循环 from b import b_func print(b_func()) if __name__ == "__main__": call_b_func() -
Java 类比方案:Java 无 “局部导入” 语法,需通过 “延迟初始化”(如在方法内创建对象)临时规避,但不推荐长期使用。
2. 文件操作&JSON格式 (File I/O)
2.1 为什么要学文件操作
程序运行时,变量、列表、字典等数据都存储在内存中,程序结束后会立即丢失;而文件操作可以将数据持久化到磁盘中,下一次运行程序时仍然可以读取、使用这些数据。
日常开发中,常见的文件类型分为两大类:
-
文本文件:纯字符编码,可直接用文本编辑器打开
- 普通文本:
.txt、.log - 结构化文本:
.csv(表格)、.json(键值对)、.xml
- 普通文本:
-
二进制文件:按字节存储,不可直接用文本编辑器解析
- 多媒体文件:图片(
.jpg/.png)、音频(.mp3)、视频(.mp4) - 压缩包:
.zip/.rar、可执行文件(.exe)
- 多媒体文件:图片(
2.2 open 与 with:核心写法
Python 中操作文件的基础函数是 open(),它用于打开一个文件并返回文件句柄(类似 Java 的 FileInputStream/FileOutputStream)。
推荐用法:使用 with 语句管理文件资源。它的核心优势是:代码块执行完毕后,会自动调用 close() 方法关闭文件,无需手动编写关闭逻辑,彻底避免因忘记关闭文件导致的资源泄漏问题。
graph TD
A["with open(...)"] --> B["打开文件引用"]
B --> C["读/写操作"]
C --> D["离开 with 代码块"]
D --> E["自动 close 释放资源"]
Java 对比:
Python 的
with open(...) as f:语法,完全等价于 Java 7 引入的 try-with-resources 语法。两者的设计目标一致:自动管理资源,无需手动关闭。
- Python 示例:
with open("data.txt", "r") as f:- Java 等效示例:
try (FileReader fr = new FileReader("data.txt")) { ... }
2.2.1 读取文本文件(一次性读取)
适用于小文件(文件大小远小于内存),一次性将文件全部内容读取为一个字符串。
# 定义文件路径(相对路径:相对于当前程序运行的目录;绝对路径:完整路径,如 C:/data.txt)
path = "data.txt"
# with 语句:自动关闭文件
# 参数说明:"r" = 只读模式;encoding="utf-8" = 指定字符编码,读写中文必须显式指定
with open(path, mode="r", encoding="utf-8") as f:
# read():一次性读取文件的全部内容
full_text = f.read()
# 打印读取到的内容
print(full_text)
2.2.2 按行读取文本文件(逐行遍历)
适用于大文件(如日志文件、数据量巨大的文本),逐行读取并处理,不会一次性占用大量内存,是处理大文件的标准写法。
path = "data.txt"
with open(path, mode="r", encoding="utf-8") as f:
# 直接遍历文件句柄 f,会自动按行读取
for line in f:
# 注意:每行的末尾会自带一个换行符 \n
# rstrip("\n"):移除行尾的换行符,避免打印时出现多余的空行
cleaned_line = line.rstrip("\n")
print(cleaned_line)
2.2.3 写入文本文件
写入文件有两种核心模式:覆盖写入("w")和追加写入("a")。
模式 1:覆盖写入("w")
如果文件不存在,会自动创建;如果文件已存在,会清空原文件的所有内容,然后写入新内容。
path = "out.txt"
with open(path, mode="w", encoding="utf-8") as f:
# write() 方法:写入字符串
# 注意:write() 不会自动添加换行符,需要手动在字符串末尾加 \n
f.write("Hello, Python!\n")
f.write("文件操作的核心是 with 语句\n")
模式 2:追加写入("a")
如果文件不存在,会自动创建;如果文件已存在,会在文件的末尾追加新内容,原文件内容不会被修改。
path = "out.txt"
with open(path, mode="a", encoding="utf-8") as f:
# 追加一行内容到文件末尾
f.write("这是追加的内容,不会覆盖原文件\n")
2.3 常用模式与核心参数
open() 函数的第二个参数 mode 是必选的,用于指定文件的打开模式。下表是开发中最常用的几种模式:
| 模式 | 中文含义 | 核心说明 | 适用场景 |
|---|---|---|---|
"r" |
只读(文本) | 默认模式;文件不存在则报错;只能读,不能写 | 读取文本文件 |
"w" |
写入(文本) | 文件不存在则创建;文件存在则清空;只能写,不能读 | 覆盖写入文本文件 |
"a" |
追加(文本) | 文件不存在则创建;文件存在则追加;只能写,不能读 | 日志记录、追加文本内容 |
"rb" |
只读(二进制) | 以字节流形式读取;文件不存在则报错 | 读取图片、音频、压缩包 |
"wb" |
写入(二进制) | 以字节流形式写入;文件不存在则创建;存在则清空 | 写入图片、保存二进制数据 |
核心参数补充:
encoding="utf-8":读写中文文本文件时,必须显式指定该参数!如果不指定,Python 会使用系统默认的字符编码(Windows 下为 GBK,Linux/Mac 下为 UTF-8),极易导致中文乱码。- 二进制模式(
"rb"/"wb"):不能指定encoding参数,因为二进制文件是按字节存储的,不存在字符编码的概念。
2.4 小练习:统计文本文件的行数与关键字出现次数
结合按行读取和字符串操作,实现一个实用的小功能:统计文本文件的总行数,以及指定关键字在文件中出现的总次数(忽略大小写)。
# 配置参数:文件路径 + 要统计的关键字
file_path = "data.txt"
target_keyword = "python"
# 初始化统计变量
total_lines = 0 # 总行数
total_keyword_hits = 0 # 关键字出现的总次数
# 按行读取文件,逐行处理
with open(file_path, mode="r", encoding="utf-8") as f:
for line in f:
total_lines += 1 # 每读取一行,行数加 1
# 统一转换为小写:实现忽略大小写的统计(如 Python、PYTHON 都算)
# count() 方法:统计子字符串在当前字符串中出现的次数
keyword_count_in_line = line.lower().count(target_keyword.lower())
total_keyword_hits += keyword_count_in_line # 累加到总次数中
# 打印统计结果
print(f"文件总行数:{total_lines}")
print(f"关键字「{target_keyword}」出现的总次数:{total_keyword_hits}")
2.5 JSON:最常用的数据存储格式之一
JSON(JavaScript Object Notation)是开发中最主流的结构化数据格式,也是 Python 与其他语言(Java/JS/Go)、前后端之间传输数据的首选格式。
JSON 核心特点:
- 本质是文本文件:可直接用文本编辑器打开,可读性强;
- 结构清晰:仅包含两种核心结构 ——
对象(对应 Python 的 dict)和数组(对应 Python 的 list); - 跨语言通用:Python、Java、JavaScript、Go 等所有主流语言都内置 / 提供了 JSON 解析库,数据互通无壁垒;
- 体积小:相比 XML,JSON 语法更简洁,传输 / 存储效率更高。
Java 对比:
- Python:标准库
json模块原生支持 JSON 序列化 / 反序列化,无需额外依赖;- Java:需借助第三方库(如 Jackson、Gson、FastJSON),核心思想完全一致:
- Python:
json.dumps(对象)→ JSON 字符串;json.loads(JSON串)→ Python 对象;- Java(Jackson):
new ObjectMapper().writeValueAsString(对象)→ JSON 字符串;new ObjectMapper().readValue(JSON串, 类名.class)→ Java 对象。
2.5.1 json.dumps/json.loads(字符串级别:内存操作)
dumps(dump string):将 Python 对象序列化为 JSON 格式的字符串(仅在内存中操作,不涉及文件);
loads(load string):将 JSON 格式的字符串反序列化回 Python 对象(dict/list 等)。
import json # 导入Python内置的json标准库
# 1. 定义一个Python字典(待序列化的对象)
python_data = {
"name": "Tom",
"age": 18,
"is_student": True,
"tags": ["python", "oop", "json"],
"score": {"math": 95, "english": 88}
}
# 2. 序列化:Python对象 → JSON字符串
# 关键参数:ensure_ascii=False → 保留中文等非ASCII字符(不转成\uXXXX编码)
json_str = json.dumps(python_data, ensure_ascii=False)
print("JSON字符串:")
print(json_str)
print("数据类型:", type(json_str)) # 输出:<class 'str'>
# 3. 反序列化:JSON字符串 → Python对象
python_obj = json.loads(json_str)
print("\n反序列化后的Python对象:")
print(python_obj)
print("获取tags字段:", python_obj["tags"]) # 输出:['python', 'oop', 'json']
print("数据类型:", type(python_obj)) # 输出:<class 'dict'>
2.5.2 json.dump/json.load(文件级别:持久化操作)
dump:将 Python 对象序列化后直接写入文件(一步完成 “序列化 + 写文件”);
load:从 JSON 文件中读取内容,并反序列化为 Python 对象(一步完成 “读文件 + 反序列化”)。
import json
# 1. 定义要写入文件的Python数据(列表+字典)
user_list = [
{"name": "Tom", "age": 18, "city": "北京"},
{"name": "Jerry", "age": 20, "city": "上海"},
{"name": "Alice", "age": 19, "city": "广州"}
]
# 2. 序列化并写入JSON文件
file_path = "users.json"
with open(file_path, "w", encoding="utf-8") as f:
# 关键参数:
# - ensure_ascii=False:保留中文
# - indent=2:格式化输出(缩进2个空格),便于阅读/调试(生产环境可省略,减少文件体积)
json.dump(user_list, f, ensure_ascii=False, indent=2)
# 3. 从JSON文件读取并反序列化
with open(file_path, "r", encoding="utf-8") as f:
loaded_data = json.load(f)
print("从文件读取的Python对象:")
print(loaded_data)
print("第一个用户的姓名:", loaded_data[0]["name"]) # 输出:Tom
print("数据类型:", type(loaded_data)) # 输出:<class 'list'>
2.5.3 常见坑:JSON 仅支持 “可序列化类型”
JSON 是一种轻量级数据格式,仅支持以下基础类型,非内置类型直接序列化会报错:
| JSON 类型 | Python 对应类型 | 说明 |
|---|---|---|
| 字符串 | str | 必须用双引号包裹 |
| 数字 | int/float | 无大小限制 |
| 布尔 | bool | JSON 中是 true/false(小写),Python 中是 True/False |
| 空值 | None | JSON 中是 null,Python 中是 None |
| 数组 | list/tuple | 序列化后统一为 JSON 数组 |
| 对象 | dict | 键必须是字符串 |
错误示例(非内置类型序列化)
import json
from datetime import datetime # 导入Python的日期时间类型
# 定义包含非JSON内置类型的数据(datetime不是JSON支持的类型)
data = {
"name": "Tom",
"register_time": datetime.now() # datetime对象
}
try:
# 直接序列化会触发TypeError
json_str = json.dumps(data, ensure_ascii=False)
except TypeError as e:
print("JSON序列化失败:", e)
# 输出:JSON序列化失败:Object of type datetime is not JSON serializable
# 解决方案:自定义序列化函数(入门版)
def json_serialize(obj):
# 如果是datetime对象,转换为字符串
if isinstance(obj, datetime):
return obj.strftime("%Y-%m-%d %H:%M:%S")
# 其他类型抛出原异常
raise TypeError(f"类型 {type(obj)} 不支持JSON序列化")
# 使用default参数指定自定义序列化函数
json_str = json.dumps(data, ensure_ascii=False, default=json_serialize)
print("\n自定义序列化后的JSON字符串:")
print(json_str)
# 输出示例:{"name":"Tom","register_time":"2026-01-06 15:30:00"}
总结(JSON 核心要点)
- JSON 是跨语言的结构化文本格式,Python 用
json标准库操作,Java 需借助 Jackson/Gson 等库; - 内存操作用
dumps/loads,文件操作用dump/load,ensure_ascii=False是读写中文的必加参数; - JSON 仅支持基础类型,非内置类型(如 datetime)需自定义序列化逻辑;
indent=2用于格式化输出(调试用),生产环境建议省略以减小文件体积。
3. 异常处理 (Exception)
3.1 为什么需要异常处理
程序运行时会遇到各种“不可控问题”:文件不存在、网络超时、用户输入不合法等。
异常处理的目标:
- 防止程序直接崩溃
- 给用户明确的错误提示
- 做好兜底处理(默认值、重试、回滚、释放资源等)
3.2 try / except / else / finally
基本语法
try:
# 可能出错的代码放在 try 里
x = int("123")
except ValueError:
# 只处理你“预期会发生”的异常
print("转换失败:不是合法数字")
else:
# 没异常才会执行
print("转换成功:", x)
finally:
# 无论是否异常都会执行(常用于资源清理)
print("不管成功失败都会执行到这里")
3.2.1 捕获多个异常
try:
a = int("1")
b = 10 / 0
except (ValueError, ZeroDivisionError) as e:
print("发生错误:", e)
3.2.2 不建议:裸 except
裸except:会把所有异常都吞掉,容易隐藏真实问题。入门阶段建议尽量写具体异常类型。
假设你写了一段 “读取文件并转换数字” 的代码,预期可能出现「文件不存在」「内容不是数字」两种错误,但代码可能还会触发其他你没预料到的错误(比如变量名写错):
场景 1:用具体异常(推荐)
try:
# 步骤1:读文件
with open("data.txt", "r") as f:
content = f.read()
# 步骤2:转数字(假设文件内容是字符串)
num = int(content)
# 步骤3:这里故意写错变量名(你没预料到的错误)
print(numm) # 变量名错了,会触发 NameError
except FileNotFoundError:
# 只捕获“文件不存在”的异常
print("错误:文件找不到")
except ValueError:
# 只捕获“转换数字失败”的异常
print("错误:文件内容不是合法数字")
运行结果:
NameError: name 'numm' is not defined
好处:你能清晰看到「变量名写错」这个未预期的错误,立刻知道代码哪里写错了,能快速修复。
场景 2:用裸 except(危险)
把上面的代码改成裸 except:
try:
with open("data.txt", "r") as f:
content = f.read()
num = int(content)
print(numm) # 变量名错误
except: # 裸 except:捕捉所有异常
print("出错了,但不知道为啥")
运行结果:
出错了,但不知道为啥
问题来了:
- 你只知道 “出错了”,但完全不知道是「文件不存在」「内容不是数字」还是「变量名写错」;
- 这个 “变量名写错” 是语法级的低级错误,本应该立刻发现并修复,但被裸 except 掩盖了;
- 更严重的是:哪怕是系统级的致命异常(比如用户按
Ctrl+C终止程序的KeyboardInterrupt),也会被裸 except 吞掉,导致程序无法正常退出。
3.3 raise:主动抛出异常
当你检测到“业务规则不满足”时,可以主动抛异常中断流程:
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("取款金额必须大于 0")
if amount > balance:
raise ValueError("余额不足")
return balance - amount
print(withdraw(100, 30))
Java 对比:
- 关键字:Python 用
raise抛出异常,Java 用throw;- 异常类型:
- Java 分为 受检异常(Checked Exception)(必须 try-catch 或 throws)和 非受检异常(Unchecked Exception);
- Python 所有异常都是非受检的,解释器不强制要求捕获,给予开发者最大的灵活性(但也需更自律)。
3.4 自定义异常:表达更清晰的业务错误
Python 中所有异常都是「类」—— 内置异常(如 ValueError)是 Python 提前定义好的类,自定义异常就是继承 Exception 类创建的专属业务异常类。
- 必须继承
Exception(而非BaseException):BaseException包含系统级异常(如KeyboardInterrupt),自定义业务异常继承Exception能避免捕获到系统级错误; - 自定义异常的核心价值:让异常语义更贴合业务场景(比如
LoginError一眼就知道是登录相关错误,而ValueError是通用错误)。
3.4.1 自定义异常的基础用法
# 1. 定义自定义异常类(继承 Exception)
class LoginError(Exception):
"""
自定义登录异常类
用于表达所有登录相关的业务错误(用户名不存在、密码错误、验证码过期等)
"""
# pass 表示暂时无自定义逻辑,仅继承父类功能
pass
# 2. 业务函数中抛出自定义异常
def login(username, password):
"""模拟登录校验"""
# 模拟数据库中的正确账号密码
valid_username = "admin"
valid_password = "123456"
# 校验失败时,主动抛出 LoginError
if username != valid_username or password != valid_password:
# raise + 异常类实例(括号内是错误提示信息)
raise LoginError("用户名或密码错误")
# 校验通过,返回登录成功
return True
# 3. 捕获并处理自定义异常
try:
# 调用登录函数(传入错误的账号密码)
login("tom", "111")
except LoginError as e:
# e 是异常实例,直接打印会输出错误提示信息
print("登录失败:", e) # 输出:登录失败:用户名或密码错误
3.4.2 自定义异常的进阶用法(增强错误信息)
基础版仅能传递字符串提示,进阶版可自定义属性(如错误码、错误详情),让异常信息更完整(便于日志记录、前端展示):
class LoginError(Exception):
"""
增强版登录异常类
:param message: 错误提示信息
:param error_code: 自定义错误码(如1001=用户名错,1002=密码错)
:param detail: 错误详情(可选,如请求ID、时间戳)
"""
def __init__(self, message, error_code, detail=None):
# 调用父类 __init__ 方法,保证基础异常功能
super().__init__(message)
# 新增自定义属性
self.error_code = error_code
self.detail = detail
# 业务函数中按不同错误类型抛异常
def login(username, password):
valid_username = "admin"
valid_password = "123456"
if username != valid_username:
# 用户名错误:错误码1001,附带详情
raise LoginError(
message="用户名不存在",
error_code=1001,
detail=f"尝试登录的用户名:{username}"
)
if password != valid_password:
# 密码错误:错误码1002
raise LoginError(
message="密码错误",
error_code=1002,
detail=f"用户名 {username} 输入了错误密码"
)
return True
# 捕获异常时,获取自定义属性
try:
login("tom", "111") # 用户名错误
except LoginError as e:
print(f"登录失败:{e}") # 输出:登录失败:用户名不存在
print(f"错误码:{e.error_code}") # 输出:错误码:1001
print(f"错误详情:{e.detail}") # 输出:错误详情:尝试登录的用户名:tom
try:
login("admin", "654321") # 密码错误
except LoginError as e:
print(f"登录失败:{e}") # 输出:登录失败:密码错误
print(f"错误码:{e.error_code}") # 输出:错误码:1002
3.4.3 自定义异常的适用场景
- 业务专属错误:登录失败、订单超时、权限不足、库存不足等(用自定义异常比通用异常更清晰);
- 多错误类型区分:同一业务场景下有多种错误(如登录的 “用户名错”“密码错”“验证码错”);
- 错误信息标准化:需要统一的错误码、错误格式(如接口返回给前端的异常信息)。
3.4.4 Python 常用内置异常类型
Python 内置了大量异常类,覆盖开发中常见的错误场景,以下是按 “使用频率” 整理的核心清单:
- 基础类型错误(最常用)
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
ValueError |
传入的值类型正确,但内容不合法 | int("abc")(字符串无法转数字) |
TypeError |
传入的参数类型错误 | 1 + "2"(数字和字符串无法相加) |
NameError |
使用未定义的变量 / 函数名 | print(numm)(变量名写错) |
IndexError |
列表 / 元组索引超出范围 | [1,2,3][5](索引 5 不存在) |
KeyError |
字典中访问不存在的键 | {"name":"Tom"}["age"](键 age 不存在) |
ZeroDivisionError |
除数为 0 | 10 / 0 |
AttributeError |
访问对象不存在的属性 / 方法 | "abc".append(1)(字符串无 append 方法) |
- 文件 / IO 相关异常
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
FileNotFoundError |
打开不存在的文件(只读模式) | open("不存在的文件.txt", "r") |
PermissionError |
没有文件操作权限(如写只读文件) | open("/root/test.txt", "w")(无权限) |
IOError |
通用 IO 错误(如文件读写失败) | 网络文件读写中断 |
- 数据结构 / 迭代相关异常
| 异常类型 | 触发场景 | 示例 |
|---|---|---|
StopIteration |
迭代器遍历完毕后继续取值 | it = iter([1,2]); next(it); next(it); next(it) |
KeyError |
字典 / 集合访问不存在的键 / 元素 | set([1,2]).remove(3)(移除不存在元素) |
AssertionError |
assert 断言失败(用于逻辑校验) | assert 1>2, "逻辑错误" |
- 系统 / 运行时异常
| 异常类型 | 触发场景 | 说明 |
|---|---|---|
RuntimeError |
通用运行时错误(无具体分类的错误) | 如递归深度超出限制 |
MemoryError |
内存不足(如创建超大列表) | [1]*10000000000(超出内存限制) |
KeyboardInterrupt |
用户按下 Ctrl+C 终止程序 | 系统级异常,不建议捕获 |
ImportError |
导入模块失败(如模块不存在) | import non_exist_module |
ModuleNotFoundError |
ImportError 的子类,模块不存在(Python3.6+) | 同上 |
- 异常层级关系(核心)
所有内置异常都继承自 BaseException,核心层级如下(简化版):
BaseException(顶层)
├── Exception(业务异常的父类)
│ ├── ValueError/TypeError/IndexError(基础错误)
│ ├── FileNotFoundError/IOError(IO错误)
│ ├── 自定义异常(继承Exception)
├── KeyboardInterrupt(用户中断)
├── SystemExit(程序退出)
4. 面向对象编程 (OOP)
4.1 面向过程和面向对象编程
面向过程编程(Procedural Programming)和面向对象编程(OOP)是两种核心编程范式,Python 作为混合型语言,同时支持这两种编程方式。
- 面向过程:以 “步骤 / 过程” 为核心,将问题拆解为一系列函数 / 步骤,按顺序执行;
- 面向对象:以 “对象” 为核心,将数据(属性)和操作(方法)封装到对象中,关注对象间的交互。
4.1.1 面向过程示例
面向过程的核心是 “步骤化”,适合简单、线性的任务,示例(做饭):
# 面向过程:拆解为独立步骤函数
def buy():
"""购买食材"""
print("去超市购买食材。")
def wash():
"""清洗食材"""
print("清洗蔬菜。")
def cut():
"""切菜"""
print("切菜。")
def cook():
"""烹饪"""
print("开始烹饪。")
def serve():
"""上菜"""
print("上菜啦!")
# 按顺序执行步骤
buy()
wash()
cut()
cook()
serve()
缺点:代码复用性差,新增菜品 / 修改步骤时需大幅改动代码,难以维护。
4.1.2 面向对象示例
面向对象的核心是 “封装、继承、多态”,示例(做饭):
"""
面向对象实现做饭功能:封装+继承+多态
"""
# 父类:菜品模板(抽象共性)
class Dish:
def __init__(self, name):
self.name = name # 实例属性:菜品名称
def prepare(self):
"""准备菜品(子类重写实现具体逻辑)"""
pass
# 子类1:沙拉(继承Dish,重写prepare)
class Salad(Dish):
def prepare(self):
print(f"1. 为【{self.name}】购买蔬菜")
print(f"2. 清洗【{self.name}】的蔬菜")
print(f"3. 切【{self.name}】的蔬菜")
print(f"4. 将【{self.name}】装盘")
# 子类2:炖肉(继承Dish,重写prepare)
class Stew(Dish):
def prepare(self):
print(f"1. 为【{self.name}】购买肉类")
print(f"2. 清洗【{self.name}】的肉")
print(f"3. 切【{self.name}】的肉")
print(f"4. 烹饪【{self.name}】")
print(f"5. 将【{self.name}】装盘")
# 子类3:汤品(继承Dish,重写prepare)
class Soup(Dish):
def prepare(self):
print(f"1. 为【{self.name}】购买食材")
print(f"2. 煮【{self.name}】")
# 实例化并调用(多态:不同子类调用同一方法,表现不同行为)
salad = Salad("蔬菜沙拉")
stew = Stew("土豆炖肉")
soup = Soup("紫菜鸡蛋汤")
salad.prepare()
print() # 空行分隔
stew.prepare()
print()
soup.prepare()
优势:
- 封装:菜品名称和制作逻辑封装在类中,数据和操作绑定;
- 继承:子类复用父类的
__init__方法,无需重复定义; - 多态:不同子类的
prepare方法实现不同逻辑,新增菜品只需加子类,不修改原有代码。
Java 对比:
- Python 无 “抽象类 / 接口” 的强制关键字(如 Java 的
abstract class/interface),但可通过空方法模拟抽象逻辑;- Java 需显式声明继承(
extends),Python 直接在类定义后加父类名即可。
4.2 类和对象
4.2.1 类(Class)和对象(Object)
| 概念 | 核心定义 |
|---|---|
| 类 | 对对象共性的抽象,是创建对象的 “模板”,定义了对象的属性(是什么)和方法(能做什么); |
| 对象 | 类的具体实例,拥有类定义的属性和方法,是客观存在的具体实体; |
4.2.2 定义类
语法格式:
class 类名: # 类名用大驼峰命名法(如 Person、Student)
"""类说明文档(可选)"""
# 类体:类属性、方法、实例属性等
示例:定义 Person 类
class Person:
"""人的类:演示类属性、实例属性、方法"""
# 类属性(所有实例共享)
home = "earth"
# 初始化方法:创建对象时自动调用,初始化实例属性
def __init__(self):
self.age = 0 # 实例属性(每个实例独立)
# 实例方法:操作实例数据
def eat(self):
print("eating...")
def drink(self):
print("drinking...")
Java 对比:
- Python 的
__init__对应 Java 的构造方法(public Person() {});- Java 需显式声明属性类型(如
private String home = "earth";),Python 无需声明类型。
4.2.3 类的操作之一:成员引用
语法:类名.成员名(访问类属性、方法、说明文档)
# 访问类属性
print(Person.home) # 输出:earth
# 访问类方法(返回函数对象)
print(Person.eat) # 输出:<function Person.eat at 0x...>
# 访问类说明文档
print(Person.__doc__) # 输出:人的类:演示类属性、实例属性、方法
4.2.4 类的操作之二:实例化
创建对象的过程称为 “实例化”,语法:变量名 = 类名(实参列表)
# 实例化 Person 类
p = Person()
# 访问实例属性/类属性
print(p.age) # 实例属性,输出:0
print(p.home) # 类属性,输出:earth
# 调用实例方法
p.eat() # 输出:eating...
p.drink() # 输出:drinking...
4.3 __init__() 方法
__init__() 是 Python 的构造方法,创建对象时自动调用,用于初始化实例属性。
核心特点 :
self:约定俗成的参数名,代表实例对象本身,通过self可访问实例属性 / 方法;- 非必需:类中未定义
__init__时,Python 会使用默认空构造方法; - 返回值:只能返回
None,返回其他值会触发TypeError。
示例:
class Person:
"""人的类:带参数的构造方法"""
def __init__(self, name, age):
"""
构造方法:初始化姓名和年龄
:param name: 姓名(字符串)
:param age: 年龄(整数)
"""
self.name = name # 实例属性:姓名
self.age = age # 实例属性:年龄
def print_info(self):
"""打印实例信息"""
print(f"姓名:{self.name},年龄:{self.age}")
# 实例化(传入构造方法参数)
person = Person("张三", 18)
person.print_info() # 输出:姓名:张三,年龄:18
Java 对比:
- Java 构造方法名必须与类名相同(
public Person(String name, int age));- Python 构造方法固定为
__init__,参数无需声明类型。
4.4 self
self 是实例方法的第一个参数,代表实例对象本身,核心作用:
- 在类内部:通过
self访问 / 修改实例属性、调用实例方法; - 调用方法时:实例对象会自动作为
self传入,无需手动传参。
示例
class Person:
home = "earth"
def __init__(self, name):
self.name = name # 实例属性
def eat(self):
print(f"{self.name} is eating...")
def drink(self):
print(f"{self.name} is drinking...")
def eat_and_drink(self):
# 内部调用实例方法(通过self)
self.eat()
self.drink()
# 实例化
p = Person("张三")
# 两种调用方式(效果一致)
p.eat() # 推荐:自动传入self
Person.eat(p) # 等效:手动传入实例对象
# 调用组合方法
p.eat_and_drink()
Java 对比:
- Java 中无需显式声明
self(对应this),直接用this.name访问实例属性;- Python 必须显式写
self作为第一个参数,否则会报错。
4.5 属性
Python 中属性分为类属性和实例属性,核心区别是 “是否共享”。
4.5.1 类属性
- 定义:类中、方法外的属性(所有实例共享);
- 访问:推荐
类名.类属性,也可实例名.类属性; - 修改:仅推荐
类名.类属性 = 值(实例修改会新增同名实例属性,而非修改类属性)。
示例:
class Person:
home = "earth" # 类属性
# 访问类属性
print(Person.home) # 输出:earth
p1 = Person()
p2 = Person()
print(p1.home, p2.home) # 输出:earth earth
# 修改类属性(所有实例同步变化)
Person.home = "mars"
print(Person.home, p1.home, p2.home) # 输出:mars mars mars
# 实例“修改”类属性(实际新增实例属性)
p1.home = "venus"
print(Person.home) # 类属性不变:mars
print(p1.home) # 实例属性:venus
print(p2.home) # 其他实例仍用类属性:mars
4.5.2 实例属性
- 定义:
__init__中通过self.属性名定义(每个实例独立); - 访问:仅能
实例名.实例属性,不能类名.实例属性; - 修改:
实例名.实例属性 = 值(仅影响当前实例)。
示例:
class Person:
def __init__(self, name, age):
self.name = name # 实例属性
self.age = age # 实例属性
# 实例化
p1 = Person("张三", 18)
p2 = Person("李四", 20)
# 访问实例属性
print(p1.name, p1.age) # 输出:张三 18
print(p2.name, p2.age) # 输出:李四 20
# 修改实例属性(互不影响)
p1.age = 19
print(p1.age) # 输出:19
print(p2.age) # 输出:20
# print(Person.name) # 报错:类无法访问实例属性
Java 对比:
- Java 类属性需加
static(如static String home = "earth";),实例属性不加;- Python 无需
static,通过定义位置区分类属性 / 实例属性。
4.6 方法
Python 类中有三种方法:实例方法、类方法、静态方法。
4.6.1 类方法
- 定义:
@classmethod装饰器,第一个参数cls(代表类本身); - 访问权限:仅能访问类属性 / 类方法,不能访问实例属性 / 方法;
- 调用:推荐
类名.类方法(),也可实例名.类方法()。
示例:
class Person:
home = "earth" # 类属性
def __init__(self, name):
self.name = name # 实例属性
@classmethod
def print_home(cls):
"""类方法:访问类属性"""
print(f"家庭住址:{cls.home}")
# print(self.name) # 报错:类方法无法访问实例属性
# 调用类方法
Person.print_home() # 推荐:输出“家庭住址:earth”
p = Person("张三")
p.print_home() # 不推荐,但允许
Java 对比:
- Python 类方法对应 Java 的
static方法(public static void printHome());- Java 静态方法中用
类名.属性访问,Python 用cls.属性。
4.6.2 实例方法
- 定义:无装饰器,第一个参数
self(代表实例); - 访问权限:可访问类属性 / 类方法、实例属性 / 实例方法;
- 调用:推荐
实例名.方法名(),也可类名.方法名(实例)。
示例:
class Person:
home = "earth" # 类属性
def __init__(self, name):
self.name = name # 实例属性
@classmethod
def print_home(cls):
print(f"家庭住址:{cls.home}")
def eat(self):
"""实例方法:访问类属性+实例属性"""
print(f"{self.name} is eating in {Person.home}")
def print_info(self):
"""实例方法:调用其他方法"""
self.eat() # 调用实例方法
Person.print_home() # 调用类方法
# 实例化并调用
p = Person("张三")
p.print_info()
# 等效调用:Person.print_info(p)
4.6.3 静态方法
- 定义:
@staticmethod装饰器,无self/cls特殊参数; - 作用:工具函数(与类 / 实例无关),仅为代码组织放在类中;
- 调用:
类名.静态方法()或实例名.静态方法()。
示例:
class Person:
home = "earth"
def __init__(self, name):
self.name = name
@staticmethod
def say(word):
"""静态方法:无类/实例依赖"""
print(f"说:{word}")
# 不推荐:静态方法中访问类属性
# print(Person.home)
# 调用静态方法
Person.say("hello") # 推荐:输出“说:hello”
p = Person("张三")
p.say("hi") # 允许:输出“说:hi”
4.6.4 三种方法对比
| 特性 | 实例方法 | 类方法 | 静态方法 |
|---|---|---|---|
| 装饰器 | 无 | @classmethod |
@staticmethod |
| 第一个参数 | self(实例) |
cls(类) |
无 |
| 访问权限 | 类 + 实例属性 / 方法 | 仅类属性 / 方法 | 无(可手动访问类属性) |
| 推荐调用方式 | 实例名.方法() |
类名.方法() |
类名.方法() |
| 使用场景 | 操作实例数据 | 操作类数据 / 工厂方法 | 独立工具函数 |
4.6.5 特殊方法 / 魔法方法
方法名以 __ 开头和结尾的方法(如 __init__),特定操作时自动调用。
常用特殊方法示例:
class DemoClass:
def __new__(cls, name):
"""1. 创建实例(第一个调用)"""
print(f"调用 __new__ 创建 {cls.__name__} 实例")
instance = super().__new__(cls)
return instance
def __init__(self, name):
"""2. 初始化实例"""
print("调用 __init__ 初始化属性")
self.name = name
def __str__(self):
"""3. 调用 str(实例) 时执行"""
return f"DemoClass: name={self.name}"
def __del__(self):
"""4. 实例销毁时执行"""
print(f"调用 __del__ 销毁 {self.name} 实例")
# 演示流程
obj = DemoClass("test") # 触发 __new__ + __init__
print(str(obj)) # 触发 __str__
del obj # 触发 __del__(引用计数为0时)
Java 对比:
- Python 的
__str__对应 Java 的toString()方法;- Python 无析构方法的严格触发时机(依赖垃圾回收),Java 有
finalize()(类似__del__)。
4.6.6 动态增删属性和方法
Python 支持运行时动态修改类 / 实例的属性和方法(Java 需反射,Python 原生支持)。
4.6.6.1 动态添加类属性 / 方法
class Person:
def __init__(self, name):
self.name = name
# 动态添加类属性
Person.home = "earth"
# 动态添加类方法
@classmethod
def come_from(cls):
print(f"I come from {cls.home}")
Person.come_from = come_from
# 验证
print(Person.home) # 输出:earth
Person.come_from() # 输出:I come from earth
4.6.6.2 动态添加实例属性 / 方法
import types
# 实例化
p1 = Person("张三")
# 动态添加实例属性
p1.age = 18
# 动态添加实例方法
def eat(self):
print(f"{self.name} 在吃饭")
p1.eat = types.MethodType(eat, p1)
# 验证
print(p1.age) # 输出:18
p1.eat() # 输出:张三 在吃饭
4.6.6.3 __slots__ 限制动态添加
__slots__ 可限制实例能添加的属性 / 方法(仅对当前类生效,子类无效):
class Person:
__slots__ = ("name", "age") # 仅允许添加这两个实例属性
p = Person()
p.name = "张三" # 允许
p.age = 18 # 允许
# p.weight = 60 # 报错:weight 不在 __slots__ 中
5. 面向对象编程三大基本特征
5.1 封装
封装是面向对象的核心特性之一,指将类的属性和方法包裹在类内部,隐藏实现细节,仅对外暴露必要的访问接口,既保证数据安全,又简化外部调用逻辑。
5.1.1 私有化机制
Python 通过命名约定实现 “私有化”,限制属性 / 方法的访问范围,核心分为两种方式:
| 私有化方式 | 命名规则 | 特性 |
|---|---|---|
| 单下划线 | _属性名/_方法名 |
约定俗成的 “非公开 API”,无强制限制,仅提醒开发者 “不要外部访问”; |
| 双下划线 | __属性名/__方法名 |
名称改写(Name Mangling):Python 自动将其转为 _类名__属性名,类外无法直接访问; |
5.1.2 私有属性和方法示例
"""
演示私有属性/方法的定义与访问规则
核心:双下划线成员会被名称改写,类外需通过 _类名__成员名 访问(不推荐)
"""
class Person:
# 私有类属性:自动改写为 _Person__home
__home = "earth"
def __init__(self, name):
self.__name = name # 私有实例属性:_Person__name
self.__age = 0 # 私有实例属性:_Person__age
self._sex = "male" # 单下划线:约定非公开,无强制限制
# 私有方法:自动改写为 _Person__check_age
def __check_age(self, age):
print(f"检查年龄{age}的合法性...")
if age < 0 or age > 100:
raise ValueError("年龄超出0-100范围")
# 公开接口:获取私有属性
def get_name(self):
return self.__name
# 公开接口:设置私有属性(带校验逻辑)
def set_age(self, age):
self.__check_age(age) # 类内可直接调用私有方法
self.__age = age
# 公开接口:获取私有属性
def get_age(self):
return self.__age
# 实例化测试
p = Person("张三")
# 1. 单下划线成员:可外部访问(仅为约定)
print(f"单下划线成员访问:p._sex = {p._sex}") # 输出:male
# 2. 双下划线成员:直接访问报错,需通过改写后的名称(不推荐)
# print(p.__name) # 报错:AttributeError
print(f"私有属性改写后访问:p._Person__name = {p._Person__name}") # 输出:张三
print(f"通过公开接口访问:p.get_name() = {p.get_name()}") # 推荐方式:输出张三
# 3. 私有方法/属性的合法操作
p.set_age(18) # 通过公开接口设置私有属性(自动触发校验)
print(f"私有属性age:p.get_age() = {p.get_age()}") # 输出:18
# p.__check_age(20) # 报错:AttributeError
p._Person__check_age(20) # 强制访问改写后的私有方法(不推荐)
Java 对比:
- Java 通过
private/public关键字强制私有化,Python 无关键字,仅靠命名规则;- Java 私有成员完全无法外部访问,Python 可通过名称改写 “绕过”(违背封装原则,禁止使用)。
5.1.3 @property 装饰器:属性化方法
@property 可将方法转为 “属性式访问”(调用无需加 ()),常用来封装私有属性的读写逻辑,替代传统的 getter/setter 方法,让代码更简洁。
核心用法
@property:定义 getter 方法(读取私有属性);@方法名.setter:定义 setter 方法(修改私有属性);- 注意:方法名不能与属性名重名,否则会触发无限递归。
示例代码
"""
@property 核心场景:封装私有属性的读写逻辑
"""
class Person:
def __init__(self, name=None):
self.__name = name # 私有属性
# 1. 无返回值的属性方法(仅执行逻辑)
@property
def eat(self):
print(f"{self.__name} is eating")
# 2. getter方法:读取私有属性
@property
def name(self):
return self.__name
# 3. setter方法:修改私有属性(需与getter方法名一致)
@name.setter
def name(self, new_name):
if not isinstance(new_name, str):
raise TypeError("姓名必须是字符串")
self.__name = new_name
# 测试
p = Person("张三")
p.eat # 属性式调用:无需加(),输出“张三 is eating”
p.name = "李四" # 调用setter方法,修改私有属性
print(p.name) # 调用getter方法,输出“李四”
# 错误示例:递归调用(注释掉避免报错)
# @property
# def age(self):
# return self.age # 无限递归:self.age 会再次调用 @property 装饰的age方法
5.2 继承
继承允许子类(派生类)复用父类(基类)的属性和方法,同时可扩展子类特有功能或重写父类方法,大幅减少代码冗余,是实现代码复用的核心手段。
5.2.1 单继承
语法格式
class 子类名(父类名):
子类体 # 可新增属性/方法,或重写父类方法
示例代码
"""
单继承核心:
1. 子类通过 super() 调用父类构造方法;
2. 子类无法直接访问父类私有成员(可改写名称访问,不推荐);
"""
# 父类:通用的Person类
class Person:
def __init__(self, name, age):
self.name = name
self.__age = age # 私有属性:_Person__age
print("父类Person构造方法执行")
def eat(self):
print(f"{self.name} 吃东西....")
# 子类1:Student(继承Person,新增score属性)
class Student(Person):
def __init__(self, name, age, score):
# 调用父类构造方法(初始化name/__age)
super().__init__(name, age)
self.score = score # 子类特有属性
print("子类Student构造方法执行")
def study(self):
# 不推荐:通过改写名称访问父类私有属性
print(f"{self.name} 学习,年龄:{self._Person__age},分数:{self.score}")
# 子类2:Teacher(继承Person,新增title属性)
class Teacher(Person):
def __init__(self, name, age, title):
super().__init__(name, age)
self.title = title # 子类特有属性
print("子类Teacher构造方法执行")
def teach(self):
print(f"{self.name} 老师,教授 {self.title}")
# 测试
s = Student("张三", 18, 90)
s.eat() # 复用父类方法
s.study() # 子类特有方法
t = Teacher("王五", 28, "Python编程")
t.eat() # 复用父类方法
t.teach() # 子类特有方法
5.2.2 多继承
多继承允许子类同时继承多个父类的属性和方法,核心规则:
- 属性 / 方法重名时,按 “从左到右” 顺序覆盖;
- 方法查找时,先查子类,再按继承顺序查父类;
- 常用 “混入类(Mixin)” 模式:将单一功能封装为 Mixin 类,通过多继承组合功能。
多继承示例(Mixin 模式)
"""
Mixin模式核心:
1. 每个Mixin类封装单一功能(如飞行、游泳),不单独实例化;
2. 主类通过多继承组合多个Mixin的功能,实现“能力复用”;
"""
# Mixin类1:飞行能力
class Flyable:
def fly(self):
print(f"{self.name} 正在飞行")
# Mixin类2:游泳能力
class Swimming:
def swim(self):
print(f"{self.name} 正在游泳")
# Mixin类3:行走能力
class Walkable:
def walk(self):
print(f"{self.name} 正在行走")
# 基础类:鸟类(提供核心属性)
class Bird:
def __init__(self, name):
self.name = name # 所有Mixin类依赖的核心属性
def make_sound(self):
print(f"{self.name} 在鸣叫")
# 子类:鸭子(组合基础类+多个Mixin)
class Duck(Bird, Flyable, Swimming, Walkable):
def __init__(self, name):
super().__init__(name) # 初始化基础类属性
# 子类特有方法
def catch_fish(self):
print(f"{self.name} 正在抓鱼")
# 测试:鸭子拥有所有组合能力
duck = Duck("小黄鸭")
duck.fly() # 来自Flyable
duck.swim() # 来自Swimming
duck.walk() # 来自Walkable
duck.make_sound() # 来自Bird
duck.catch_fish() # 子类特有
Java 对比:
- Java 不支持多继承,需通过接口(Interface)实现类似 Mixin 的功能;
- Python 多继承更灵活,但需注意 “钻石继承” 问题(通过 MRO 解决)。
5.2.3 子类复用 / 重写父类方法
子类可通过两种方式复用父类方法,也可重写父类方法实现自定义逻辑:
- 复用:
super().方法名()或父类名.方法名(self, 参数); - 重写:子类定义与父类同名的方法,覆盖原有逻辑。
示例代码
"""
子类复用/重写父类方法
核心:super() 自动适配继承关系,推荐优先使用
"""
class Person:
def __init__(self, name):
self.name = name
print("父类构造方法执行")
def __str__(self):
return f"{self.name} 是一个人"
def eat(self):
print(f"{self.name} 吃东西....")
# 子类:Student
class Student(Person):
# 重写父类构造方法
def __init__(self, name, score):
super().__init__(name) # 复用父类构造方法
self.score = score
print("子类构造方法执行")
# 复用父类方法
def show(self):
# 方式1:super()(推荐,适配多继承)
super().eat()
# 方式2:父类名.方法名(需手动传self)
# Person.eat(self)
# 重写父类__str__方法
def __str__(self):
# 复用父类__str__结果 + 子类扩展
return super().__str__() + f",也是学生,分数:{self.score}"
# 测试
s = Student("张三", 90)
s.show() # 复用父类eat方法
print(s) # 调用重写后的__str__:输出“张三是一个人,也是学生,分数:90”
5.2.4 方法解析顺序(MRO)
MRO(Method Resolution Order)是 Python 解决多继承方法查找的规则,核心是 “深度优先 + 左到右”,可通过 类名.__mro__ 或 类名.mro() 查看继承链。
核心规则
- 方法查找时,按 MRO 列表顺序依次查找,找到即执行;
super()并非仅调用 “直接父类”,而是按 MRO 列表调用下一个类的方法;- 多继承构造方法需通过
super()传递参数,让 MRO 自动处理初始化逻辑。
示例代码
"""
MRO核心演示:
1. 查看类的继承链;
2. super() 按MRO顺序调用父类方法;
"""
# 顶层父类:Animal
class Animal:
def __init__(self, name, **kwargs):
self.name = name
print("Animal 构造方法执行")
def eat(self):
print("Animal 吃东西...")
# 父类1:Fish(继承Animal)
class Fish(Animal):
def __init__(self, name=None, **kwargs):
print("Fish 构造方法执行")
super().__init__(name, **kwargs) # 传递剩余参数给MRO下一个类
def eat(self):
print("Fish 吃东西...")
super().eat() # 调用MRO下一个类的eat方法
# 父类2:Person(继承Animal)
class Person(Animal):
def __init__(self, name=None, gender=None, **kwargs):
print("Person 构造方法执行")
self.gender = gender
super().__init__(name, **kwargs)
def eat(self):
print("Person 吃东西...")
super().eat()
# 子类:美人鱼(多继承 Fish, Person)
class Mermaids(Fish, Person):
def __init__(self, name, gender, age):
print("Mermaids 子类构造方法执行")
# 仅调用一次super(),MRO自动处理所有父类初始化
super().__init__(name=name, gender=gender, age=age)
def eat(self):
print("Mermaids 吃东西...")
super().eat() # 按MRO调用Fish的eat
# 1. 查看MRO列表
print("Mermaids MRO:", Mermaids.__mro__)
# 输出:(Mermaids, Fish, Person, Animal, object)
# 2. 实例化测试(按MRO顺序执行构造方法)
m = Mermaids("小海燕", "女", 500)
# 输出顺序:Mermaids → Fish → Person → Animal
# 3. 调用eat方法(按MRO顺序执行)
m.eat()
# 输出:Mermaids吃东西... → Fish吃东西... → Person吃东西... → Animal吃东西...
5.3 多态
多态指 “同一方法在不同子类中呈现不同行为”,核心是 “父类定义方法,子类重写实现”,无需关注具体子类类型,仅需调用统一接口,大幅提升代码灵活性。
5.3.1 多态示例
"""
多态核心:
1. 父类定义统一接口(如go方法);
2. 子类重写接口实现不同逻辑;
3. 外部调用时,仅需传入子类实例,自动执行对应逻辑;
"""
# 父类:定义统一接口
class Animal:
def go(self):
"""所有动物的移动行为(父类统一接口)"""
pass
# 子类1:狗(重写go方法)
class Dog(Animal):
def go(self):
print("狗:四条腿跑")
# 子类2:鱼(重写go方法)
class Fish(Animal):
def go(self):
print("鱼:在水里游")
# 子类3:鸟(重写go方法)
class Bird(Animal):
def go(self):
print("鸟:张开翅膀飞")
# 通用调用函数(无需关注具体子类)
def move(animal):
"""接收Animal子类实例,调用统一接口"""
animal.go()
# 测试:传入不同子类,执行不同行为
dog = Dog()
fish = Fish()
bird = Bird()
move(dog) # 输出:狗:四条腿跑
move(fish) # 输出:鱼:在水里游
move(bird) # 输出:鸟:张开翅膀飞
Java 对比:
- Java 多态需基于 “继承 / 接口”,且方法需为非静态 / 非私有;
- Python 是动态类型语言,无需显式继承,只要实例有对应方法即可(“鸭子类型”),更灵活。
5.4 总结
- 封装:通过私有化隐藏细节,
@property简化私有属性访问,核心是 “数据安全 + 接口简化”; - 继承:单继承复用父类逻辑,多继承通过 Mixin 模式组合功能,MRO 解决多继承方法查找问题;
- 多态:父类定义统一接口,子类重写实现不同行为,核心是 “接口统一,实现多样”。
6. 综合小案例:JSON 通讯录
实现一个可持久化的通讯录管理器,数据存储在 contacts.json 文件中,支持联系人的新增、查询、列表展示,且通过 JSON 完成数据的保存与加载,结合类封装、文件操作、异常处理三大核心知识点。
业务流程:
Java 对比:
- Python 用
list[dict]存储联系人,Java 中更常用List<Contact>(自定义 Contact 类建模);- Python 的
try/except对应 Java 的try/catch,with open对应 Java 的try-with-resources(自动关闭流);- JSON 序列化 / 反序列化逻辑一致,Python 用
json模块,Java 常用Jackson/Gson库。
6.1 数据格式设计
contacts.json 文件采用 JSON 列表格式存储,每个元素是一个联系人字典(包含姓名、手机号),结构清晰且易读:
[
{"name": "Tom", "phone": "13800000000"},
{"name": "Jerry", "phone": "13900000000"}
]
6.2 核心类封装(ContactBook)
将通讯录的核心逻辑封装为 ContactBook 类,职责单一、接口清晰,便于维护和扩展:
import json
class ContactBook:
"""
通讯录管理器类
核心功能:加载/保存数据、新增/查询/列出/删除联系人
"""
def __init__(self, file_path="contacts.json"):
"""
初始化通讯录
:param file_path: 数据文件路径,默认 contacts.json
"""
self.file_path = file_path # 数据文件路径
self.contacts = [] # 内存中的联系人列表(list[dict])
def load(self):
"""
从 JSON 文件加载联系人数据
异常处理:文件不存在/JSON 格式错误时,初始化空列表
:return: 联系人列表(list[dict])
"""
try:
with open(self.file_path, "r", encoding="utf-8") as f:
self.contacts = json.load(f) # 反序列化 JSON 到列表
except (FileNotFoundError, json.JSONDecodeError):
# FileNotFoundError:首次运行无文件;JSONDecodeError:文件内容损坏/空文件
self.contacts = []
return self.contacts
def save(self):
"""
将内存中的联系人数据保存到 JSON 文件
注意:ensure_ascii=False 支持中文,indent=2 格式化输出
"""
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self.contacts, f, ensure_ascii=False, indent=2)
def _validate_phone(self, phone):
"""
私有方法:校验手机号格式(11位纯数字)
:param phone: 待校验的手机号
:raise ValueError: 格式不合法时抛出异常
"""
if not (len(phone) == 11 and phone.isdigit()):
raise ValueError("手机号必须是11位纯数字")
def add(self, name, phone):
"""
新增联系人(带参数校验)
:param name: 联系人姓名(非空)
:param phone: 联系人手机号(11位纯数字)
:raise ValueError: 姓名/手机号为空、联系人已存在、手机号格式错误
"""
# 基础非空校验
if not name.strip():
raise ValueError("联系人姓名不能为空")
if not phone.strip():
raise ValueError("联系人手机号不能为空")
# 手机号格式校验
self._validate_phone(phone.strip())
# 重复校验(大小写不敏感)
if self.find(name):
raise ValueError(f"联系人【{name}】已存在,无法重复添加")
# 新增联系人(去除首尾空格,保证数据整洁)
self.contacts.append({
"name": name.strip(),
"phone": phone.strip()
})
def find(self, name):
"""
查询联系人(大小写不敏感)
:param name: 要查询的姓名(支持模糊匹配,如 tom → Tom)
:return: 找到则返回联系人字典,未找到返回 None
"""
target_name = name.strip().lower() # 统一转为小写,实现大小写不敏感
for contact in self.contacts:
if contact["name"].lower() == target_name:
return contact
return None
def remove(self, name):
"""
删除联系人(大小写不敏感)
:param name: 要删除的姓名
:raise ValueError: 联系人不存在时抛出异常
"""
contact = self.find(name)
if not contact:
raise ValueError(f"联系人【{name}】不存在,无法删除")
self.contacts.remove(contact)
def all(self):
"""
获取所有联系人(返回列表副本,避免外部修改内部数据)
:return: 联系人列表副本(list[dict])
"""
return list(self.contacts)
6.3 模块拆分与交互入口
将代码拆分为两个模块,符合 “单一职责” 原则,便于管理和复用:
6.3.1 模块 1:contact_book.py(核心类)
将上述 ContactBook 类完整保存到 contact_book.py 文件中,作为通讯录核心模块。
6.3.2 模块 2:main.py(交互入口)
"""
通讯录交互入口
依赖 contact_book.py 中的 ContactBook 类
"""
from contact_book import ContactBook
def main():
"""主函数:演示通讯录基本操作"""
# 1. 初始化通讯录并加载数据
book = ContactBook("contacts.json")
book.load()
print("=== 初始化通讯录,加载已有数据 ===")
print(f"当前联系人列表:{book.all()}\n")
try:
# 2. 新增联系人(带校验)
book.add("Tom", "13800000000")
book.add("Jerry", "13900000000")
book.add("张三", "13712345678") # 测试中文
print("=== 新增联系人后 ===")
print(f"当前联系人列表:{book.all()}\n")
# 3. 查询联系人(大小写不敏感)
print("=== 查询联系人 ===")
print(f"查询 Tom:{book.find('tom')}") # 小写匹配
print(f"查询 李四:{book.find('李四')}\n") # 未找到返回 None
# 4. 删除联系人
book.remove("Jerry")
print("=== 删除 Jerry 后 ===")
print(f"当前联系人列表:{book.all()}\n")
# 5. 保存修改到文件
book.save()
print("=== 所有修改已保存到 contacts.json ===")
except ValueError as e:
# 捕获参数校验/业务逻辑异常,友好提示
print(f"操作失败:{e}")
if __name__ == "__main__":
main()
6.4 运行效果示例
=== 初始化通讯录,加载已有数据 ===
当前联系人列表:[]
=== 新增联系人后 ===
当前联系人列表:[{'name': 'Tom', 'phone': '13800000000'}, {'name': 'Jerry', 'phone': '13900000000'}, {'name': '张三', 'phone': '13712345678'}]
=== 查询联系人 ===
查询 Tom:{'name': 'Tom', 'phone': '13800000000'}
查询 李四:None
=== 删除 Jerry 后 ===
当前联系人列表:[{'name': 'Tom', 'phone': '13800000000'}, {'name': '张三', 'phone': '13712345678'}]
=== 所有修改已保存到 contacts.json ===
评论