简短的答案
要判断文件是否存在,就用标准库中的 os.path.isfile()。当路径指向一个存在的普通文件时,它返回 True,否则返回 False。路径不存在时,它也永远不会报错。
路径是相对于程序运行时所在的文件夹(当前工作目录)而言的,除非你传入绝对路径,比如 /home/ada/notes.txt 或 C:\Users\Ada\notes.txt。
本页其余部分会介绍提出这个问题的其他方法、它们的区别,以及根本不该提出这个问题的情况。
os.path.exists、isfile 和 isdir 的区别
os.path 模块中有三个相关的判断函数,它们回答的问题略有不同:
| 函数 | 路径是以下哪种时返回 True |
|---|---|
os.path.exists(p) | 任何存在的东西:文件、目录或有效的符号链接 |
os.path.isfile(p) | 存在的普通文件 |
os.path.isdir(p) | 存在的目录 |
一涉及目录,区别就显现出来了:
输出:
reports True False True
reports/summary.txt True True False
reports/old.txt False False False
即使 reports 是一个文件夹,os.path.exists("reports") 也是 True。如果你的代码接下来要把这个路径当作文件来读,exists 就是错误的判断:目录也能通过它,然后 open("reports") 就会失败(在 Linux 和 macOS 上抛出 IsADirectoryError,在 Windows 上抛出 PermissionError)。你指的是文件就用 isfile,指的是文件夹就用 isdir。
关于这三个 os.path 函数,还有两个细节:
- 符号链接会被跟随。指向真实文件的链接算作文件;失效的链接(目标已经不存在)在这三个函数中都返回
False。 - 如果 Python 没有权限查看某个文件夹的内容,判断会返回
False,而不是抛出PermissionError。False的意思是“找不到或访问不到”,并不严格等于“不存在”。
用 pathlib 判断
pathlib 是标准库中面向对象的路径 API,大多数新代码都在用它。Path 对象以方法的形式提供了同样的三种判断:
Path.exists()、Path.is_file() 和 Path.is_dir() 的行为与 os.path.exists、isfile 和 isdir 一样。/ 运算符会用适合当前操作系统的分隔符拼接路径的各部分,所以 folder / "summary.txt" 在 Windows、macOS 和 Linux 上都能用。通过 Path 对象读写文件,请看文件操作。
该用哪一个?如果你的其余代码已经在传递 Path 对象,就继续用 pathlib。如果你用的是普通字符串和 os 函数,os.path.isfile 同样正确。它们给出的答案是一样的。
为什么先判断可能是错误的做法
判断文件是否存在,最常见的原因是接下来要打开它:
import os
if os.path.isfile("config.txt"):
with open("config.txt") as f: # the file might be gone by now
settings = f.read()
else:
settings = ""
这里有一个漏洞。在调用 isfile() 和调用 open() 之间,另一个程序、另一个线程或者用户都可能删除或重命名这个文件。判断结果是 True,打开却仍然失败,你的程序照样会因为 FileNotFoundError 崩溃。这个漏洞叫作竞态条件,也叫“检查时间到使用时间”(TOCTOU)问题。在你自己的笔记本电脑上这种情况很少见,但在多个进程共享一个文件夹的繁忙服务器上就没那么少见了。
解决办法是去掉单独的判断,让 open() 本身来做判断。文件不存在时,Python 会抛出 FileNotFoundError,你在 except 中处理它:
输出:
theme=dark
(no settings file, using defaults)
现在只有一个操作,而不是两个,所以不存在竞争。这种风格在 Python 社区里有个名字:EAFP,即“请求原谅比请求许可更容易”。相反的风格,也就是先判断,叫作 LBYL,即“三思而后行”。在文件操作上,Python 更倾向于 EAFP,因为在你打开文件的那一刻,只有操作系统知道答案。
可能出错的不只有 FileNotFoundError。路径是目录时会抛出 IsADirectoryError(在 Windows 上是 PermissionError),没有读取权限的文件会抛出 PermissionError。这三个都是 OSError 的子类,所以 except OSError: 能一次捕获所有“打不开”的情况。异常一页讲解了如何在捕获某个具体错误和捕获一整类错误之间做选择。
什么时候单纯判断就可以
先判断并非在所有情况下都是错的。当你只需要知道答案,而下一行并不打算操作这个文件时,它就是合适的工具:
- 决定显示哪条消息(“找到 3 个存档”)
- 在开始一项耗时的任务之前,验证用户输入的路径
- 跳过已经做完的工作,即使答案过时,代价也只是重做一遍
经验法则:如果你要打开、读取、写入或删除这个文件,就在 try 中做。如果你只是想知道,用 isfile() 或 Path.is_file() 就可以。
文件不存在时才创建
反过来的问题也会遇到:“写入这个文件,但绝不覆盖已有的文件”。显而易见的写法有和前面一样的竞态问题:
if not os.path.exists("report.txt"):
with open("report.txt", "w") as f: # another process may create it first
f.write("new report\n")
模式 "x"(独占创建)把两步合成一步。它会创建文件,如果这个名字已经被占用,就抛出 FileExistsError:
输出:
attempt 1: created report.txt
attempt 2: report.txt already exists, left it alone
对于文件夹,os.makedirs(path, exist_ok=True) 和 Path(path).mkdir(parents=True, exist_ok=True) 会创建目录以及所有缺失的上级目录,如果目录已经存在,就什么也不做。不需要先调用 isdir()。
常见错误
- 想判断文件却用了
exists()。 同名的目录也能通过判断。请用isfile()或Path.is_file()。 - 忘了相对路径从哪里开始。
"data.txt"是在当前工作目录中查找的,也就是你启动程序的位置,不一定是脚本所在的文件夹。要找脚本旁边的文件,请从Path(__file__).parent构建路径。 - 先判断,再打开。 文件可能在两者之间发生变化。请在
try中打开,并处理FileNotFoundError。 - 捕获所有异常。 在
open()外面写except Exception:,也会把读取文件的代码中的拼写错误和 bug 一起掩盖。请捕获FileNotFoundError或OSError。
常见问题
在 Python 中怎么判断文件是否存在?
导入 os,然后调用 os.path.isfile("data.txt"),只有当它是一个存在的普通文件时才返回 True。用 pathlib 的话就是 Path("data.txt").is_file()。如果同名的目录也应该算数,就用 os.path.exists() 或 Path.exists()。
os.path.exists 和 os.path.isfile 有什么区别?
只要那个路径上有东西,os.path.exists(p) 就是 True:文件、目录,或者指向真实目标的符号链接都算。os.path.isfile(p) 只对普通文件为 True,所以遇到目录时它返回 False。对应目录的函数是 os.path.isdir(p)。
打开文件之前应该先判断它是否存在吗?
通常不需要。在判断和调用 open() 之间,文件可能被删除或被创建,所以这个判断什么也保证不了。直接在 try 中打开文件,在 except 中处理 FileNotFoundError。只有当你只需要知道答案时,比如决定显示哪条消息,单独判断是否存在才是正确的做法。
怎么在文件不存在时才创建它?
用模式 "x" 打开它:open("report.txt", "x")。Python 会创建这个文件,如果已经有同名文件,就抛出 FileExistsError。判断和创建在一步内完成,所以其他程序没法插到两者中间。