Python目录操作全解析:从os.path到pathlib,实战场景与性能优化

📅 2026/7/25 6:33:22 👁️ 阅读次数 📝 编程学习
Python目录操作全解析:从os.path到pathlib,实战场景与性能优化

1. 项目概述:为什么目录操作是Python开发的基石

在Python开发的日常里,无论你是写一个简单的数据清洗脚本,还是构建一个复杂的Web应用,几乎都绕不开一个最基础却又最核心的操作:和文件系统打交道。而文件系统的入口,就是目录。很多人觉得目录操作不就是os.listdir列个表、os.mkdir建个文件夹吗?但真正踩过坑的开发者都知道,这里面藏着不少“暗礁”。比如,跨平台路径分隔符的兼容性问题、处理海量文件时的性能瓶颈、递归遍历时可能遇到的符号链接循环,还有权限不足导致的静默失败。这些细节处理不好,轻则脚本报错,重则可能误删数据。

我见过不少新手写的脚本,在Windows上跑得好好的,一到Linux服务器上就“罢工”,多半是路径字符串硬编码了反斜杠\。也见过一些数据处理脚本,因为递归遍历目录时没做深度限制,不小心进入了软链接的循环,直接把CPU跑满。所以,深入理解Python的目录操作,远不止记住几个API那么简单,它关乎你代码的健壮性、可移植性和效率。这篇文章,我就结合自己这些年踩过的坑和积累的经验,带你从“会用”到“精通”,把Python目录操作的方方面面都掰开揉碎了讲清楚。无论你是刚入门Python,想写点自动化脚本提高效率,还是已经有一定经验,想优化现有代码的健壮性,相信都能在这里找到你需要的东西。

2. 核心模块与工具选型:不止于ospathlib

提到目录操作,大家第一时间想到的肯定是Python内置的osos.path模块。它们确实是元老,功能强大且直接与操作系统交互。但随着Python 3.4的发布,pathlib模块横空出世,它提供了一种更面向对象、更符合现代Python风格的路径操作方式。那么,在实际项目中我们该如何选择呢?我的原则是:新项目优先使用pathlib,维护旧代码或需要极细粒度控制时使用os模块

2.1osos.path:稳定可靠的老兵

os模块是Python与操作系统交互的接口,目录操作只是其功能的一部分。它的优势在于直接、底层,你能获得最接近系统调用的控制力。

import os # 经典用法:拼接路径 base_dir = '/home/user/projects' data_dir = os.path.join(base_dir, 'data', '2023') print(data_dir) # 输出: /home/user/projects/data/2023 # 判断路径是否存在 if os.path.exists(data_dir): print("目录已存在") else: print("目录不存在")

os.path子模块则专门处理路径字符串,它提供的是函数式编程风格。这里有一个非常重要的点:os.path的函数处理的是字符串,它本身不访问文件系统。比如os.path.exists(path),它背后的原理是尝试用os.stat()系统调用获取文件状态,如果失败(比如文件不存在或无权访问),则返回False。这意味着它的性能开销相对较小,但也要注意,在多线程或多进程环境下,从exists()检查到实际操作(如open())之间,文件状态可能已经改变,这就是经典的“竞态条件”(TOCTOU)。对于安全性要求高的操作,更推荐尝试执行并捕获异常(EAFP风格),而不是先检查再执行(LBYL风格)。

2.2pathlib:面向对象的现代路径管家

pathlib将路径抽象为Path对象,让路径操作变得像操作普通对象一样直观。它的最大优点是代码更清晰、更安全

from pathlib import Path # 创建Path对象 base_path = Path('/home/user/projects') data_path = base_path / 'data' / '2023' # 使用 / 运算符拼接,非常直观 print(data_path) # 输出: /home/user/projects/data/2023 # 判断并创建目录(exist_ok参数避免目录已存在的错误) data_path.mkdir(parents=True, exist_ok=True) # 遍历目录 for item in data_path.iterdir(): print(item.name, item.is_file())

为什么我推荐新项目用pathlib

  1. 表达式清晰path / 'subdir' / 'file.txtos.path.join(path, 'subdir', 'file.txt')更易读。
  2. 方法链式调用:你可以Path('.').resolve().parent / 'config.ini'这样流畅地操作。
  3. 自动处理系统差异Path对象在输出时会自动转换为当前系统的路径格式(Windows用\,Unix用/)。
  4. 功能集成:很多常用操作如read_text(),write_bytes(),glob()都直接集成在对象上。

注意pathlib在底层最终还是调用了os模块的功能。对于超高性能要求的场景(如遍历包含数十万个文件的目录),直接使用os.scandir()可能比Path.iterdir()有微小的性能优势,因为scandir()在遍历时能提供更多的文件信息(如inode、文件类型),减少了额外的系统调用。但对于99%的应用场景,pathlib的便利性远胜于那一点性能差异。

2.3 第三方库的用武之地

当内置库无法满足需求时,我们可以看向第三方库。例如:

  • watchdog:监控目录中文件的变化(创建、修改、删除),常用于实现自动重新加载、实时同步等功能。
  • send2trash:将文件发送到系统回收站,而不是永久删除,提供了更安全的删除操作。
  • pyfilesystem2 (fs):提供了一个统一的文件系统抽象层,让你可以用相同的代码操作本地文件、内存文件、甚至FTP、S3等资源。

对于绝大多数目录操作任务,Python标准库已经足够强大。第三方库是在你有特定、复杂需求时的扩展,不要为了用而用。

3. 核心操作详解:从创建、遍历到高级管理

掌握了工具,我们来逐一攻克目录操作的各个核心场景。我会按照一个典型的“生命周期”来组织:从创建目录开始,到遍历和搜索内容,再到复制、移动、删除,最后是获取和修改属性。

3.1 目录的创建与删除:细节决定成败

创建目录听起来简单,但里面有几个关键参数和异常处理需要特别注意。

使用pathlib创建目录:

from pathlib import Path target_dir = Path('./logs/app/2023-10-01') # 方法一:逐级创建,如果中间目录不存在会报错 try: target_dir.mkdir() # 默认 parents=False, exist_ok=False except FileNotFoundError: print("父目录不存在,创建失败!") except FileExistsError: print("目录已存在!") # 方法二:一键创建所有不存在的父目录(推荐) target_dir.mkdir(parents=True, exist_ok=True) # parents=True: 自动创建所有不存在的父目录 # exist_ok=True: 如果目录已存在,不抛出异常,静默跳过

使用os模块创建目录:

import os target_path = './logs/app/2023-10-01' # 创建单级目录 os.mkdir(target_path) # 等价于 mkdir(parents=False, exist_ok=False) # 创建多级目录(递归创建) os.makedirs(target_path, exist_ok=True) # 等价于 mkdir(parents=True, exist_ok=True)

实操心得永远使用exist_ok=True。这是一个防御性编程的好习惯。你的脚本可能被多次执行,或者在其他地方目录已经被创建。设置exist_ok=True可以避免因目录已存在而导致的FileExistsError,让脚本更具健壮性。除非你的业务逻辑严格要求目录必须由本次调用创建。

目录删除操作:删除比创建风险更高,务必谨慎。

from pathlib import Path import shutil dir_to_remove = Path('./temp_cache') # 删除空目录 try: dir_to_remove.rmdir() # 仅当目录为空时成功 except OSError as e: print(f"删除失败: {e}") # 目录非空或不存在时会抛出异常 # 递归删除整个目录树(危险操作!) shutil.rmtree(dir_to_remove) # 使用shutil模块 # 等效的pathlib方式(Python 3.12+): dir_to_remove.rmtree()

重要警告shutil.rmtree()不可逆的删除操作,它会直接删除目录及其下所有内容,不会送入回收站。在生产环境中使用前,务必进行双重确认,或者考虑先移动到临时位置。一个常见的保险做法是,在删除前先打印出将要删除的目录列表,让用户或日志确认。

3.2 目录遍历与文件搜索:效率与灵活性的平衡

遍历目录是数据分析、日志处理、项目构建等场景中的高频操作。根据需求不同,我们有多种遍历策略。

基础遍历:列出目录内容

from pathlib import Path project_root = Path('.') # 列出所有条目(文件和目录) print("--- 所有条目 ---") for item in project_root.iterdir(): print(f"{item.name} - {'目录' if item.is_dir() else '文件'}") # 仅列出文件 print("\n--- 仅文件 ---") files = [f for f in project_root.iterdir() if f.is_file()] for f in files: print(f.name) # 仅列出目录 print("\n--- 仅目录 ---") dirs = [d for d in project_root.iterdir() if d.is_dir()] for d in dirs: print(d.name)

递归遍历:处理嵌套结构当需要处理目录树中的所有文件时,就需要递归。

from pathlib import Path def collect_py_files(root_dir: Path): """收集目录树下所有的.py文件""" py_files = [] for item in root_dir.rglob('*.py'): # rglob 递归通配符匹配 if item.is_file(): py_files.append(item) return py_files # 或者使用 glob def collect_py_files_glob(root_dir: Path): py_files = list(root_dir.glob('**/*.py')) # ** 表示递归所有子目录 return [f for f in py_files if f.is_file()] # glob会返回目录,需要过滤

使用os.walk()进行精细控制os.walk()是一个生成器,每次迭代返回一个三元组(当前目录路径, 子目录名列表, 文件名列表)。它给你更多的控制权,比如可以在遍历过程中动态修改子目录名列表来跳过某些目录。

import os for root, dirs, files in os.walk('.', topdown=True): # 跳过所有名为 __pycache__ 的目录 if '__pycache__' in dirs: dirs.remove('__pycache__') # 从dirs中移除,os.walk就不会再进入它 print(f"当前目录: {root}") print(f"子目录: {dirs}") print(f"文件: {files}") print("-" * 20)

性能提示:对于包含海量文件(例如超过10万个)的目录,os.scandir()Path.iterdir()配合递归逻辑,通常比os.walk()rglob()有更好的性能,因为它们返回的是os.DirEntryPath对象,在遍历时就能获取文件类型等信息,减少了额外的stat系统调用。你可以自己实现一个基于scandir的递归遍历器来处理超大型目录树。

基于条件的文件搜索除了简单的通配符,我们经常需要根据文件属性(大小、修改时间)或内容来搜索。

from pathlib import Path import time def find_recent_files(directory: Path, days=7): """查找最近N天内修改过的文件""" cutoff_time = time.time() - (days * 24 * 60 * 60) recent_files = [] for file_path in directory.rglob('*'): if file_path.is_file(): # 获取文件的修改时间(时间戳) mtime = file_path.stat().st_mtime if mtime > cutoff_time: recent_files.append((file_path, time.ctime(mtime))) return recent_files def find_large_files(directory: Path, size_mb=100): """查找大于指定大小的文件""" size_limit = size_mb * 1024 * 1024 # 转换为字节 large_files = [] for file_path in directory.rglob('*'): if file_path.is_file(): file_size = file_path.stat().st_size if file_size > size_limit: large_files.append((file_path, file_size / (1024*1024))) # 转换为MB return large_files

3.3 目录与文件的复制、移动与重命名

这些操作涉及到数据的迁移,需要特别注意路径处理和错误捕获。

复制整个目录树

import shutil from pathlib import Path source = Path('/path/to/source_project') destination = Path('/path/to/backup_project') # 最简单的方式,复制整个目录 try: # dirs_exist_ok参数确保如果目标目录已存在,内容会被合并而非报错 shutil.copytree(source, destination, dirs_exist_ok=True) print("目录复制成功") except shutil.Error as e: print(f"复制过程中发生错误: {e}") except OSError as e: print(f"系统错误: {e}")

shutil.copytree还有一些有用的参数:

  • ignore: 接受一个函数,用于过滤不需要复制的文件或目录。
  • ignore_dangling_symlinks: 处理无效符号链接。
  • copy_function: 可以指定自定义的复制函数,例如你想用shutil.copy2来保留元数据。

移动与重命名在文件系统层面,移动(move)和重命名(rename)本质上是同一个操作。

from pathlib import Path old_path = Path('old_name.txt') new_path = Path('new_name.txt') # 重命名文件 if old_path.exists(): old_path.rename(new_path) print("重命名完成") # 移动文件到另一个目录 source_file = Path('data/raw.txt') target_dir = Path('archive/2023/') target_dir.mkdir(parents=True, exist_ok=True) # 确保目标目录存在 destination = target_dir / 'raw.txt' if source_file.exists(): source_file.rename(destination) # 或者使用 replace() 方法,如果目标存在则替换 # source_file.replace(destination)

注意事项:跨设备(例如从C盘到D盘,或从本地磁盘到网络驱动器)的rename()操作可能会失败,因为底层系统调用rename通常要求源和目标在同一文件系统上。跨设备移动需要先复制再删除原文件。shutil.move()函数更智能,它会先尝试os.rename(),如果失败(因为跨设备),则会回退到“复制+删除”的策略。

3.4 路径信息获取与属性修改

获取路径的各个组成部分和文件属性,是进行逻辑判断的基础。

分解与构建路径

from pathlib import Path p = Path('/home/user/projects/myapp/src/utils/helper.py') # 获取路径的各个部分 print(f"完整路径: {p}") print(f"锚点 (Anchor): {p.anchor}") # 根目录,如 '/' 或 'C:\\' print(f"父目录 (Parent): {p.parent}") # /home/user/projects/myapp/src/utils print(f"父目录的父目录: {p.parent.parent}") print(f"文件名 (包含后缀): {p.name}") # helper.py print(f"主干 (Stem): {p.stem}") # helper print(f"后缀 (Suffix): {p.suffix}") # .py print(f"所有后缀: {p.suffixes}") # ['.py'],对于.tar.gz会是['.tar', '.gz'] print(f"是否是绝对路径: {p.is_absolute()}") # 构建新路径 new_p = p.with_name('config.ini') # 替换文件名 print(new_p) # /home/user/projects/myapp/src/utils/config.ini new_p2 = p.with_suffix('.json') # 替换后缀 print(new_p2) # /home/user/projects/myapp/src/utils/helper.json

获取与修改文件属性

from pathlib import Path import os import time p = Path('some_file.txt') stat_info = p.stat() # 获取文件状态信息 print(f"文件大小: {stat_info.st_size} 字节") print(f"最后修改时间: {time.ctime(stat_info.st_mtime)}") print(f"最后访问时间: {time.ctime(stat_info.st_atime)}") print(f"创建时间: {time.ctime(stat_info.st_ctime)}") # 注意:Unix上可能是元数据修改时间 print(f"文件模式: {oct(stat_info.st_mode)}") # 修改文件时间戳(访问时间和修改时间) new_timestamp = time.time() # 当前时间 os.utime(p, (new_timestamp, new_timestamp)) # (访问时间, 修改时间) # 修改文件权限 (仅限Unix-like系统) p.chmod(0o755) # 设置为 rwxr-xr-x

踩坑记录stat()返回的时间戳是浮点数形式的秒数(从1970年1月1日UTC开始)。st_ctime在Unix系统上表示“元数据最后修改时间”(如权限变更),而不是文件的“创建时间”。真正的创建时间在某些文件系统上可能无法获取。Windows上的st_ctime才更接近创建时间。处理时间时,务必考虑时区问题,time.ctime()显示的是本地时间。

4. 实战场景与高级技巧:解决真实世界的问题

了解了基础操作后,我们来看几个综合性的实战场景,这些场景融合了多个操作,并且包含了一些提升效率和健壮性的高级技巧。

4.1 场景一:自动化清理临时文件与日志

这是一个非常常见的运维或开发任务:定期清理过期的临时文件或日志,防止磁盘被占满。

from pathlib import Path import time import shutil def cleanup_old_files(directory: Path, days_old: int = 30, extensions: list = None, dry_run: bool = True): """ 清理指定目录中超过一定天数的文件。 参数: directory: 要清理的根目录 days_old: 文件保留天数 extensions: 指定要清理的文件后缀列表,如 ['.log', '.tmp']。None表示所有文件。 dry_run: 模拟运行模式,只打印不删除。正式运行时设为False。 """ cutoff_time = time.time() - (days_old * 86400) deleted_count = 0 freed_space = 0 # 字节 print(f"开始清理目录: {directory}") print(f"删除 {days_old} 天前的文件" + (f",后缀为 {extensions}" if extensions else "")) print(f"模拟运行: {dry_run}") print("-" * 50) for item in directory.rglob('*'): if not item.is_file(): continue # 按后缀过滤 if extensions and item.suffix.lower() not in extensions: continue # 按修改时间过滤 if item.stat().st_mtime > cutoff_time: continue # 执行删除 file_size = item.stat().st_size if dry_run: print(f"[模拟] 将删除: {item} (大小: {file_size/1024:.1f} KB, 修改于: {time.ctime(item.stat().st_mtime)})") else: try: item.unlink() # 删除文件 print(f"[已删除] {item} (大小: {file_size/1024:.1f} KB)") except OSError as e: print(f"[失败] 无法删除 {item}: {e}") continue deleted_count += 1 freed_space += file_size print("-" * 50) print(f"总计: 标记 {deleted_count} 个文件待删除,可释放 {freed_space / (1024**3):.2f} GB 空间") if dry_run: print("注意:以上为模拟运行,未实际删除文件。") return deleted_count, freed_space # 使用示例 log_dir = Path('/var/log/myapp') # 先模拟运行,确认要删除的文件 cleanup_old_files(log_dir, days_old=7, extensions=['.log', '.gz'], dry_run=True) # 确认无误后,实际运行 # cleanup_old_files(log_dir, days_old=7, extensions=['.log', '.gz'], dry_run=False)

这个脚本的亮点与技巧:

  1. dry_run参数:这是生产环境脚本的“金科玉律”。任何删除操作都必须先模拟运行,确认输出无误后再实际执行。可以避免灾难性错误。
  2. 递归遍历rglob('*'):确保能清理到所有子目录下的文件。
  3. 双重过滤:先通过is_file()suffix快速过滤,再通过stat().st_mtime进行精确的时间判断,效率更高。
  4. 异常捕获:删除文件时可能遇到权限不足、文件被占用等情况,必须用try...except包裹,避免整个脚本因单个文件失败而中断。
  5. 空间统计:计算并显示可释放的空间,让操作结果更直观。

4.2 场景二:构建项目目录结构生成器

在启动新项目、创建数据流水线或搭建测试环境时,我们经常需要快速生成一套标准的目录结构。

from pathlib import Path import json def create_project_skeleton(base_path: Path, structure_config: dict): """ 根据配置字典创建项目目录结构。 参数: base_path: 项目根目录 structure_config: 定义结构的字典。 示例: { "src": { "main.py": None, # None 或空字符串表示创建空文件 "utils": { "__init__.py": "", "helpers.py": "# Some helper functions\n", }, }, "tests": { "__init__.py": "", "test_main.py": "import pytest\n", }, "docs": {}, "data": { "raw": {}, "processed": {}, }, "README.md": "# Project Title\n", ".gitignore": "*.pyc\n__pycache__/\n", } """ def _create(path: Path, config: dict): for name, content in config.items(): item_path = path / name if content is None or isinstance(content, str): # 这是一个文件 item_path.parent.mkdir(parents=True, exist_ok=True) if not item_path.exists(): item_path.touch() # 创建空文件 if content: # 如果有内容,则写入 item_path.write_text(content, encoding='utf-8') print(f"创建文件: {item_path}") else: print(f"文件已存在,跳过: {item_path}") elif isinstance(content, dict): # 这是一个目录 item_path.mkdir(parents=True, exist_ok=True) print(f"创建目录: {item_path}") # 递归创建子目录和文件 _create(item_path, content) else: raise ValueError(f"配置项 '{name}' 的值类型无效: {type(content)}") print(f"正在项目根目录 '{base_path}' 下创建结构...") base_path.mkdir(parents=True, exist_ok=True) _create(base_path, structure_config) print("项目骨架创建完成!") # 从JSON文件加载配置 def create_from_json(config_file: Path, target_dir: Path): with open(config_file, 'r', encoding='utf-8') as f: config = json.load(f) create_project_skeleton(target_dir, config) # 使用示例 if __name__ == "__main__": project_root = Path('./my_new_project') # 直接定义结构 skeleton = { "src": { "__init__.py": "", "main.py": "#!/usr/bin/env python3\n\nprint('Hello, World!')\n", "utils": { "__init__.py": "", "file_utils.py": "# File operation utilities\n", "data_utils.py": "", }, }, "tests": { "__init__.py": "", "test_utils.py": "import pytest\n\n# Test cases here\n", }, "data": { "raw": {".gitkeep": ""}, # 用.gitkeep占位,让git跟踪空目录 "processed": {".gitkeep": ""}, }, "configs": {"config.yaml": "# Configuration\n"}, "logs": {}, # 空目录 "requirements.txt": "", "README.md": "# My New Project\n\n## Description\n", ".gitignore": "*.pyc\n__pycache__/\n*.log\n.data/\n", } create_project_skeleton(project_root, skeleton)

设计思路与技巧:

  1. 递归函数_create:这是处理树形结构的经典模式。函数根据配置项的值类型(字符串/None表示文件,字典表示目录)来决定创建文件还是目录,并递归处理子目录。
  2. 配置驱动:将目录结构定义在字典或JSON文件中,使得结构可以版本化、可复用。你可以为不同类型的项目(如Flask Web应用、数据分析项目、Python包)准备不同的模板。
  3. parents=True, exist_ok=True:创建目录时的黄金搭档,确保路径上的所有目录都存在,且不会因目录已存在而报错。
  4. .gitkeep技巧:Git默认不跟踪空目录。在希望Git跟踪的空目录中放一个空的.gitkeep文件是一个常见的做法。
  5. 文件内容初始化:脚本不仅创建空文件,还能写入初始内容(如Python文件的shebang、基础注释、配置文件模板),极大提升效率。

4.3 场景三:实现一个简单的文件同步工具(单向)

这个场景综合了遍历、比较、复制、删除等操作,是一个很好的综合练习。

from pathlib import Path import shutil import filecmp import hashlib class SimpleFileSyncer: """一个简单的单向文件同步器(将源目录同步到目标目录)""" def __init__(self, source: Path, destination: Path): self.source = source.resolve() # 解析为绝对路径 self.destination = destination.resolve() self.actions = {'copy': [], 'update': [], 'delete': []} self.dry_run = True # 默认模拟运行 def _calculate_file_hash(self, filepath: Path) -> str: """计算文件的MD5哈希值,用于精确比较内容""" hash_md5 = hashlib.md5() try: with open(filepath, "rb") as f: for chunk in iter(lambda: f.read(4096), b""): hash_md5.update(chunk) except (IOError, OSError): return None # 文件无法读取 return hash_md5.hexdigest() def _scan_and_compare(self, src_path: Path, dst_path: Path): """递归扫描并比较两个目录""" # 确保目标目录存在 dst_path.mkdir(parents=True, exist_ok=True) # 获取源目录内容 src_items = {item.name: item for item in src_path.iterdir() if item.name != '.syncignore'} # 可忽略特定文件 dst_items = {item.name: item for item in dst_path.iterdir() if item.name != '.syncignore'} # 处理需要复制或更新的文件/目录 for name, src_item in src_items.items(): dst_item = dst_path / name if src_item.is_file(): if name not in dst_items: # 目标不存在,需要复制 self.actions['copy'].append((src_item, dst_item)) else: # 目标存在,比较内容 if dst_items[name].is_file(): # 比较修改时间(快速检查)和文件大小 src_stat = src_item.stat() dst_stat = dst_items[name].stat() # 如果大小或修改时间不同,则进行更精确的哈希比较 if (src_stat.st_size != dst_stat.st_size or src_stat.st_mtime > dst_stat.st_mtime): src_hash = self._calculate_file_hash(src_item) dst_hash = self._calculate_file_hash(dst_items[name]) if src_hash != dst_hash: self.actions['update'].append((src_item, dst_item)) else: # 目标存在但是目录,需要删除目录并复制文件(根据需求,这里选择覆盖) self.actions['delete'].append(dst_items[name]) self.actions['copy'].append((src_item, dst_item)) elif src_item.is_dir(): if name not in dst_items: # 目标目录不存在,需要创建并递归处理 self.actions['copy'].append((src_item, dst_item)) self._scan_and_compare(src_item, dst_item) else: if dst_items[name].is_dir(): # 都是目录,递归比较 self._scan_and_compare(src_item, dst_items[name]) else: # 目标是文件,需要删除文件并创建目录 self.actions['delete'].append(dst_items[name]) self.actions['copy'].append((src_item, dst_item)) self._scan_and_compare(src_item, dst_item) # 处理需要删除的项目(在目标中存在但在源中不存在) for name, dst_item in dst_items.items(): if name not in src_items: self.actions['delete'].append(dst_item) def analyze(self): """分析差异,生成同步计划""" print(f"分析同步计划: {self.source} -> {self.destination}") self.actions = {'copy': [], 'update': [], 'delete': []} self._scan_and_compare(self.source, self.destination) print(f"需要复制的项目: {len(self.actions['copy'])}") print(f"需要更新的文件: {len(self.actions['update'])}") print(f)