1. 文件操作在Python学习路线中的位置:为什么这里最容易懵
如果你正跟着教程一路学到文件操作,我猜你现在的心情大概率是:前面的列表、字典、循环、函数都还好好的,一碰到open()就开始各种花式报错——文件找不到、中文乱码、写入的内容莫名其妙消失、程序跑完文件还是空的……别急,这些坑我当年全踩过。
作为Python基础系列的第6篇,这篇内容我会把文件操作的底层逻辑和实操细节一次性掰开揉碎。你不需要死记硬背API,只需要理解几件核心的事:文件是如何被打开的、读写过程中数据往哪走、程序结束之后文件为什么要"关闭"。把这几个点想通,后面遇到任何文件读写需求——处理配置文件、分析日志、批量整理数据、爬虫落地保存——都能顺手写出来。
这篇内容适合刚学完Python基础语法、正准备做小项目的读者。如果你对with open(...) as f只有模糊印象,或者经常写完文件却不知道怎么去检查结果,这篇文章就是写给你的。
先说一个很多人没意识到的事实:文件操作是Python学习路上的一个分水岭。在这之前,你写的程序运行完数据就没了——变量、列表、字典都在内存里,程序一退出全都清空。文件操作是你第一次真正让数据"留下来"的能力。学会它之后,你的小程序才算真正具备实用价值,能记录用户输入、能保存配置、能生成报告,你的Python不再只是"算数玩具",而是一个能完成真实任务的工具。这也是为什么几乎所有面试、项目练习、自动化脚本都绕不开文件读写。
既然是系列教程里的第6篇,我默认你已经掌握了变量、数据类型、条件判断、循环和函数的基本用法。如果还没学到函数,也不影响本文的主体阅读,但第6章实战部分用到了函数封装,建议你至少知道def怎么用。
2. 打开文件的底层逻辑:模式、编码与资源释放
2.1 open()函数的几个关键参数,每个都有讲究
几乎所有文件操作的起点都是open(),但很多人只记住它的第一个参数是文件名。实际上,一个完整的open()调用往往需要关注三个东西:文件路径、打开模式、编码方式。
# 最常见的三种写法 f = open('data.txt', 'r', encoding='utf-8') # r:只读,默认模式 f = open('data.txt') # 带完整参数的写法 f = open('/path/to/data.txt', 'r+', encoding='utf-8')第一个参数是文件路径,第二个是打开模式,第三个是编码。路径问题我会在第5章专门讲,这里先聚焦模式和编码。
打开模式是很多人一开始就搞混的地方。我见过不少初学者用'w'模式打开一个只读需求的文件,结果文件内容被清空了,当场心态爆炸。为了不重蹈覆辙,下面这几个模式你必须刻在脑子里:
| 模式 | 含义 | 文件不存在时 | 文件存在时 | 常用场景 |
|---|---|---|---|---|
'r' | 只读 | 报错 | 正常打开,指针在开头 | 读取数据 |
'w' | 写入,覆盖 | 新建 | 清空内容再写 | 覆盖保存 |
'a' | 追加 | 新建 | 指针移到末尾 | 日志记录 |
'x' | 独占创建 | 新建 | 报错 | 防止覆盖 |
'r+' | 读写 | 报错 | 不截断,指针在开头 | 需要边读边改 |
'w+' | 读写,覆盖 | 新建 | 清空内容 | 频繁读写的临时文件 |
'a+' | 读写,追加 | 新建 | 指针在末尾 | 追加后需要读取 |
注意'r+'和'w+'的区别:一个不截断、一个截断。我曾经用'w+'处理一个重要数据文件,只因为想同时读和写,结果文件内容被清空重写,幸好有备份。从那以后,凡是涉及修改已有文件的场景,我默认用'r+',只有明确要覆盖时才用'w'系列。
2.2 编码从来不是小事:UTF-8 和 GBK 的恩怨
编码问题是文件操作里最容易让人崩溃的一类错误。报错信息UnicodeDecodeError: 'gbk' codec can't decode byte 0x...几乎是每个Python初学者的噩梦。原因很简单:open()在Windows系统上默认编码是GBK,而大多数现代文件、脚本、数据交换用的都是UTF-8。两边对不上,自然乱码。
解决方式很简单,就在open()里显式指定编码:
# 显式指定UTF-8编码 with open('data.txt', 'r', encoding='utf-8') as f: content = f.read()养成"所有open()都写encoding='utf-8'"的习惯,能避开80%的乱码问题。写文件时也一样:
# 写入时也指定UTF-8 with open('output.txt', 'w', encoding='utf-8') as f: f.write('中文内容')还有个细节很多人不知道:在某些编辑器里保存UTF-8文件时会自动加上BOM(字节序标记),而utf-8编码读取时会把这个隐藏字符带进第一个字符串。遇到这种情况,把编码改成'utf-8-sig'即可。我第一次读从Excel导出的CSV时就吃过这个亏——第一列的表头莫名其妙多了一个字符,排查了半天才发现是BOM在作怪。
2.3 为什么必须用with语句:资源释放的底层逻辑
几乎每篇教程都会推荐用with open(...) as f,但很多人不理解为什么。我先说不这么做的后果。
直接调用f = open('data.txt')之后,如果不手动f.close(),文件句柄会一直占用系统资源。在脚本里运行次数少影响不大,但如果你是循环里反复打开文件,又一直不关闭,很快就会遇到OSError: [Errno 24] Too many open files。我见过一个数据处理脚本跑了几万次循环之后崩溃,就是因为在循环里open()没有关。
with语法是一个上下文管理器,它在代码块结束时会自动调用f.close(),即使代码中间抛出异常也能保证资源被释放。这个特性让异常处理变得极其安全。
# 推荐:with写法,自动关闭 with open('data.txt', 'r', encoding='utf-8') as f: content = f.read() # 到这里文件已经自动关闭了如果你出于某种原因必须手动管理,那请记得用try-finally兜底:
f = open('data.txt', 'r', encoding='utf-8') try: content = f.read() finally: f.close()但说实话,日常开发中90%的场景用with就够了,没必要手写try-finally。
3. 文本文件实操:读取与写入的完整实践
3.1 三种读取方式各有适用场景,别只盯着read()
文本读取最基础的是下面这四种方式,它们针对不同体量的文件各有优势。
with open('data.txt', 'r', encoding='utf-8') as f: # 方式一:一次性读取全部内容,返回字符串 content = f.read() # 方式二:读取一行,包括换行符 line = f.readline() # 方式三:读取所有行,返回列表,每行是一个元素 lines = f.readlines() # 方式四:迭代读取,逐行处理(推荐用于大文件) for line in f: process(line)表面上看都是读文件,区别在于内存占用的差异。read()会把整个文件内容一次性加载到内存里,适合小文件。readlines()类似,也是全量加载,只是结果变成了列表。这两个方法处理几十MB的文件没问题,但如果是几百MB甚至数个GB的大日志文件,内存很容易爆掉。
我处理过最大的一个CSV文件有4GB,用readlines()直接内存报错,换成for line in f迭代逐行读取后内存占用稳定在几十MB左右。原理在于,for line in f不会一次性把所有行加载进内存,而是从文件对象中按需读取下一行。这种惰性加载机制是处理大文件的关键。
另外readline()的使用频率其实不高,它适合那种需要手动控制读几行、边读边做判断的场景。比如读取一个有格式头的文件:
with open('config.ini', 'r', encoding='utf-8') as f: header = f.readline() # 先读表头 version = f.readline() # 再读版本号 for line in f: # 继续处理剩余行 pass3.2 写入与追加:内容到底什么时候落到磁盘
写入文件的方式不多,最常用的是write()和writelines()。
with open('output.txt', 'w', encoding='utf-8') as f: f.write('第一行\n') f.write('第二行\n') with open('output.txt', 'a', encoding='utf-8') as f: f.write('这行追加在末尾\n')write()的参数是字符串,一次写一段。writelines()接收一个可迭代对象(如列表),把每个元素依次写入:
lines = ['甲\n', '乙\n', '丙\n'] with open('output.txt', 'w', encoding='utf-8') as f: f.writelines(lines)注意writelines()不会自动给你加换行符,列表里的'\n'是手动加的。我见过很多人以为它和readlines()是对称操作,应该有自动换行,结果写出来的文件全挤在一行了。
还有一个隐藏知识点:文件写入不是实时的。f.write()调用后,数据先进缓冲区,只有缓冲区写满或调用f.flush()、f.close()时才会真正写入磁盘。程序正常退出时with会自动关闭文件,但如果你在程序运行中途去查看输出文件,可能会发现内容不全。遇到这种情况,可以在关键位置手动调用f.flush()强制落盘。这在写日志、写进度文件时很实用。
另外,很多人在循环里用write()写几千行数据时发现速度极慢,原因就是每写一行都强制flush。正确做法是攒一批再写,或者写完统一关闭文件,让缓冲区自动批量落盘。
3.3 实战小例:统计日志文件里某个关键词的出现次数
这里我把读取和写入串起来,写一个实用性很高的小工具——统计日志中某个关键词出现的次数并输出报告。
keyword = input('要统计的关键词:') source_file = 'access.log' report_file = 'report.txt' count = 0 with open(source_file, 'r', encoding='utf-8') as f: for line in f: count += line.count(keyword) with open(report_file, 'w', encoding='utf-8') as f: f.write(f'关键词"{keyword}"共出现{count}次\n') print(f'统计完成,结果已写入{report_file}')这个脚本虽然短,但完整展示了逐行读取避免内存占用、文件写入落盘、异常自动关闭这几个关键点。把它扩展到统计多个关键词、输出每个关键词的行号,就是一个小型日志分析工具。
4. 二进制文件实战:从复制图片到处理大文件
4.1 文本模式和二进制模式的本质区别
前面讲的所有操作默认都是文本模式,也就是open()不指定'b'参数的情况。文本模式下,Python会对内容做编码解码——磁盘上的字节序列被解码成字符串。而二进制模式'b'下,Python不做任何解码,直接读取原始字节,把内容表示为bytes类型。
# 二进制模式读取图片 with open('photo.jpg', 'rb') as f: data = f.read()为什么需要二进制模式?因为图片、音频、视频、压缩包这些文件本质就是字节序列,没有编码和字符串的概念。如果你用文本模式去打开图片,要么报编码错误,要么得到一堆无关的乱码;而用二进制模式就能原样复制、逐块处理。
判断"该用文本还是二进制"的简单标准:文件里存的是不是给人看的内容?是则用文本模式,否则用二进制模式。配置文件、日志、CSV用文本;图片、音频、数据库文件、序列化对象用二进制。这个判断标准在99%的场景下不会出错。
4.2 文件指针与seek/tell:读写位置的精确控制
文件对象内部其实维护着一个"指针",指向当前读写的位置。每次read()或write()都会移动这个指针。tell()返回当前指针位置,seek()则把它移动到指定位置。
with open('data.txt', 'r', encoding='utf-8') as f: print(f.tell()) # 0,初始在开头 f.read(5) # 读5个字符 print(f.tell()) # 5,指针移到第5个字符后 f.seek(0) # 跳回开头 print(f.tell()) # 0在文本模式下,seek()的单位是字符偏移,处理中文时容易混乱,因为一个中文字符在UTF-8编码下占3个字节;但在二进制模式下,seek()和tell()的单位是字节,逻辑更直观,这也是二进制处理大文件时的核心工具。
# 显示文件大小的两种方式 import os print(os.path.getsize('photo.jpg')) with open('photo.jpg', 'rb') as f: print(f.seek(0, 2)) # 移动到文件末尾 print(f.tell()) # 返回总字节数seek()的第二个参数是关键:0表示从开头偏移,1表示从当前位置偏移,2表示从末尾偏移。第二个参数为2时,配合偏移量0就能快速定位到文件末尾,拿到文件总大小。
4.3 分块复制大文件:一次完整代码
使用read()一次性读取所有二进制数据的做法,对大文件同样有内存问题。处理大文件的分块读取是文件操作中的高频需求。下面是复制一个大型二进制文件(比如视频)的完整实现。
import os def copy_file(src, dst, chunk_size=64 * 1024): """分块复制文件,chunk_size默认64KB""" total_size = os.path.getsize(src) copied_size = 0 with open(src, 'rb') as src_f: with open(dst, 'wb') as dst_f: while True: chunk = src_f.read(chunk_size) if not chunk: break dst_f.write(chunk) copied_size += len(chunk) progress = copied_size / total_size * 100 print(f'\r复制进度:{progress:.1f}%', end='') print('\n复制完成')几个细节值得说明。chunk_size选择64KB是一个经验值,太小的分块会导致频繁IO调用拖慢速度,太大的分块则可能让单块占用内存过多。64 * 1024即是64KB,这个数值在绝大多数场景下性能都很平衡。读取到文件尾部时,最后一块可能不足64KB,read()返回多少就写多少,直接用if not chunk来判断是否到了末尾——因为当读到末尾时,read()返回空字节串b'',布尔运算下为False,正好跳出循环。
\r和end=''的组合是进度条打印的常用技巧,让输出始终刷新在同一行,不过度刷屏。
复制文件其实有更省事的shutil.copy(),但我还是建议你至少手写一次分块复制逻辑,这样面对超大文件、断点续传、自定义进度显示等复杂需求时,你都能自己实现而不是去找现成轮子。
5. 路径与目录管理:从os模块到pathlib的现代实践
5.1 相对路径、绝对路径与工作目录:一个反直觉的坑
文件操作报错里最常见的,大概就是FileNotFoundError。很多人的第一反应是"文件明明存在啊",但问题往往出在路径上。
with open('data.txt', 'r') as f: content = f.read()这段代码中的'data.txt'是一个相对路径,Python会相对于当前工作目录(Current Working Directory)去查找。当前工作目录不是脚本所在的目录,而是你运行Python命令时所在的目录。很多人用PyCharm或VS Code点运行按钮,觉得工作目录就是项目目录,实际上各种IDE的默认工作目录设置并不一样,同一个脚本在不同环境下运行,查找的位置可能完全不同。
排查思路是这样。第一步,先打印当前工作目录确认基础:
import os print(os.getcwd()) # 返回当前工作目录的绝对路径第二步,判断要读取的文件是否真的在打印出的目录下。第三种方式,也是最稳的做法,直接用绝对路径,或者用下面要讲的os.path和pathlib把路径拼接完整。
5.2 os和os.path的常用操作
os模块提供了大量与操作系统交互的函数,其中一些是文件操作绕不开的依赖。
import os # 判断路径是否存在 os.path.exists('data.txt') # 判断是不是文件 / 目录 os.path.isfile('data.txt') os.path.isdir('data.txt') # 拼接路径(自动处理分隔符) path = os.path.join('folder', 'sub', 'data.txt') # 获取文件大小(字节) os.path.getsize('data.txt') # 创建目录 os.mkdir('new_folder') # 单层 os.makedirs('a/b/c', exist_ok=True) # 多层,exist_ok避免报错 # 重命名 / 删除 os.rename('old.txt', 'new.txt') os.remove('temp.txt') # 列出目录下的文件 os.listdir('.')os.path.join()是跨平台处理路径分隔符的关键。Windows用反斜杠\,macOS和Linux用正斜杠/,直接拼接字符串很容易在不同系统上出问题。用os.path.join()自动生成当前系统正确的分隔符,从源头上杜绝路径分隔符错误。
5.3 pathlib:更优雅、更现代的文件路径操作
os.path用了几十年,但Python 3.4引入的pathlib正逐渐成为官方推荐。pathlib把路径封装成Path对象,支持直接用/运算符拼接路径,代码读起来直观很多。
from pathlib import Path # 创建路径对象 p = Path('folder/sub/data.txt') print(p) # folder/sub/data.txt # 拼接路径 q = Path('folder') / 'sub' / 'data.txt' print(q) # folder/sub/data.txt # 获取父目录、文件名、后缀 print(p.parent) # folder/sub print(p.name) # data.txt print(p.stem) # data print(p.suffix) # .txt # 判断是否存在、是不是文件 print(p.exists()) print(p.is_file()) # 读取和写入一键完成 text = p.read_text(encoding='utf-8') p.write_text('hello', encoding='utf-8') # 批量遍历目录 for file_path in Path('.').glob('*.txt'): print(file_path)我最喜欢pathlib的点是,它把"路径的操作"和"文件的读写"统一到同一个对象上。以前要写os.path.join(os.path.dirname(__file__), 'data.txt')之类冗长的代码,现在一个Path对象加一个运算符就搞定了。
如果你现在刚开始学,强烈建议直接用pathlib;如果你在维护老项目、熟悉os.path,也不需要急着替换,两者官方都支持。但新代码我推荐pathlib,它是面向对象编程思路在文件操作上的体现,长期来看可读性和可维护性都更好。
5.4 批量重命名文件:调动os和pathlib的完整案例
这里用一个小案例把路径和目录操作串起来:批量把一个文件夹里的所有图片按顺序重命名,顺便验证pathlib的便利性。
from pathlib import Path folder = Path('photos') if not folder.exists(): raise SystemExit(f'目录不存在: {folder}') # 收集所有png和jpg文件 images = list(folder.glob('*.png')) + list(folder.glob('*.jpg')) images.sort() # 排序,保证处理顺序稳定 for idx, img in enumerate(images, start=1): new_name = f'photo_{idx:03d}{img.suffix}' img.rename(folder / new_name) print(f'共重命名 {len(images)} 个文件')这里f'photo_{idx:03d}'的格式化写法会让编号保持三位对齐,最终得到photo_001.jpg、photo_002.jpg这种风格的命名。这个脚本看起来很简短,但它综合运用了目录遍历、路径对象拼接、格式化字符串、文件重命名这几个核心能力。很多初学者的第一个自动化工具有可能就是这类批量整理脚本。
6. 实战案例:写一个配置文件读写小工具
学到这里,我建议你把前面的知识点拼起来做一个小工具,否则容易"看懂了但手不会"。下面这个例子是我自己早期练习时经常写的类型——一个简单的配置文件读写工具。它不算复杂,但完整覆盖了路径处理、文本读写、异常处理、数据解析、数据保存这五个环节。
6.1 需求与设计
假设我们要管理一个简单的应用配置,格式是key = value,例如:
# app.cfg host = 127.0.0.1 port = 8080 debug = true需求有两个:读取配置时把内容解析成字典;修改配置后能写回文件,同时保留文件里的注释和键值顺序。
保留注释和顺序是一个容易被忽略的细节。如果只想着"读进来改掉再重写",用字典存数据后写回去,那么注释会丢、顺序可能变。这个设计点促使我们写一个稍复杂的实现。
6.2 代码实现
from pathlib import Path from typing import Dict, List class ConfigManager: def __init__(self, path: str): self.path = Path(path) self.lines: List[str] = [] self.config: Dict[str, str] = {} def load(self): """读取配置文件,解析键值对,并保留原始行""" if not self.path.exists(): raise FileNotFoundError(f'配置文件不存在:{self.path}') self.lines = self.path.read_text(encoding='utf-8').splitlines() for line in self.lines: stripped = line.strip() # 跳过空行和注释行 if not stripped or stripped.startswith('#'): continue if '=' not in stripped: continue key, value = stripped.split('=', 1) self.config[key.strip()] = value.strip() return self.config def save(self): """将配置写回文件,保留注释和键值顺序""" output_lines = [] used_keys = set(self.config.keys()) for line in self.lines: stripped = line.strip() if stripped and not stripped.startswith('#') and '=' in stripped: key = stripped.split('=', 1)[0].strip() if key in self.config: output_lines.append(f'{key} = {self.config[key]}') used_keys.discard(key) continue output_lines.append(line) # 附加新增的配置项 for key in self.config: if key in used_keys: output_lines.append(f'{key} = {self.config[key]}') self.path.write_text('\n'.join(output_lines) + '\n', encoding='utf-8') def get(self, key: str, default: str = None) -> str: return self.config.get(key, default) def set(self, key: str, value: str): self.config[key] = str(value) # 使用示例 cm = ConfigManager('app.cfg') cfg = cm.load() print('当前端口:', cfg.get('port')) cm.set('port', '9090') cm.set('debug', 'false') cm.save() print('配置已保存')6.3 这个工具设计上的几个关键点
第一,load()里用split('=', 1)而不是split('='),是因为值本身可能包含=符号,比如password = a=b=c。指定分割次数为1,只在第一个等号处切开,值部分原样保留。这个细节很典型,处理真实数据时经常遇到。
第二,save()里维护used_keys集合,用于识别哪些键是新增的,需要追加到文件末尾;哪些键是原有的,需要原地替换。没有这个集合,新增键会丢失。
第三,所有文件操作都基于pathlib.Path的read_text和write_text,代码非常简洁,也少写了几行open()样板代码。
这个小工具足够用于真实项目场景,你可以把它扩展成支持JSON格式、支持分段(section)配置、支持环境变量覆盖等更高级的功能。我当时的扩展方向是加入一个getint()和getbool()方法,做类型转换,这样调用方不需要每次手动int()。
7. 文件操作最常见的5个坑及排查思路
最后这章是整套内容里最值钱的部分。下面这几个坑都是我在真实项目里踩过、也帮别人排查过的,按出现频率排序。
7.1 UnicodeDecodeError:编码不匹配
报错信息长这样:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 0: invalid continuation byte排查链路是这样的:先确认文件的实际编码。Windows上很多老旧软件(比如记事本默认保存的ANSI、Excel导出的GBK)生成了非UTF-8文件,而你用encoding='utf-8'去读,就会报这个错。
解决方案是按实际情况调整编码参数:
# 如果确定是GBK编码 with open('data.txt', 'r', encoding='gbk') as f: content = f.read()更稳的做法是用errors='ignore'参数跳过无法解码的字节,但我不推荐这个方案——它可能让数据悄悄丢失。更好的思路是用chardet库自动检测编码,网上有很多现成例子,但最可靠的其实是:从源头统一——生成文件时就统一指定UTF-8编码,读取时也显式指定。
7.2 FileNotFoundError:路径和工作目录的错位
报错信息不复杂,但极容易反复出现。如果确认文件存在却依然报错,按下面的顺序排查:
- 打印当前工作目录,确认Python进程实际位置
- 打印目标文件的绝对路径,确认能否构成完整路径
- 检查文件名拼写、扩展名是否隐藏(Windows默认隐藏扩展名是经典陷阱)
如果你想在任何环境下都稳定定位脚本旁边的文件,推荐用__file__来定位脚本所在目录:
from pathlib import Path base_dir = Path(__file__).resolve().parent target = base_dir / 'data.txt'__file__是脚本文件的绝对路径,resolve()会解析掉符号链接和相对路径,parent取到脚本所在目录。用这种方式拼接路径,不管你的代码在哪个目录下被执行,都能找到正确位置。
我会在做部署、定时任务时有意识地检查这个问题,因为定时任务的工作目录经常是系统目录而不是脚本目录。
7.3 PermissionError:文件被占用或没有权限
Windows上最常见的场景:用Excel打开了某个CSV,再运行Python脚本尝试写入同一文件,就会报PermissionError: [Errno 13] Permission denied。Linux上则常见于尝试写入没有写权限的目录。
排查思路是:先确认是否真的被其他程序锁定,关闭占用文件的程序再运行。确认是否有写权限,用os.access(path, os.W_OK)检查。如果是日志文件经常被占用的场景,可以考虑写入临时文件再替换,或者改用追加模式避免覆盖整个文件。
7.4 大文件导致的内存溢出
这个问题在3.1节提过,这里再补充一个诊断方法。如果脚本执行过程中内存占用持续飙升直到崩溃,多半是用了read()或readlines()读取超大文件。解决办法分几种:
- 逐行处理:
for line in f: ...,适合文本逐行读取 - 分块处理:
f.read(chunk_size),适合二进制文件 - 流式处理:不一次性加载,边读边处理边释放
你不需要一开始就把代码写成最复杂的形式,但如果预估文件可能很大,直接在最初就按"逐行"或"分块"来写,会避免中途返工。
7.5 数据丢失:忘记关闭文件或缓冲区未刷新
这个问题最隐蔽,因为它未必会报错。表现是:程序看起来正常结束,但文件里没有内容,或者内容不完整。
原因有两类。一类是open()之后忘记close(),另一类是程序在缓冲区落盘前崩溃了。还有一种很常见的情况是:程序运行还没结束,你手动打开文件查看结果,发现是空文件或内容不完整,就以为代码写错了——其实只是缓冲区没有落盘。
对策很简单:所有文件读写都用with语法,别裸调open()。如果程序运行中途需要确认写入结果,可以调用f.flush()强制落盘。
排查链路说起来都很简单,难的是形成习惯。我的建议是——真正把"文件对象需要释放、文件内容不是实时的、路径并不总是你以为的那个路径"这三件事内化成直觉。做到了这三点,文件操作这一关你就真正迈过去了。
另外还有个偏好:我在自己的项目里,能一句话用pathlib完成的操作,就绝不写5行os.path代码。不是性能问题,而是可读性和维护性。等你去维护一套几个月前的旧代码时,你会感谢当时那个用Path把路径写清楚的自己。