file获取文件后缀_Python 工匠:高效操作文件的三个建议

dad1cd696119cf10ed854a8861ce2839.png

前言

在这个世界上,人们每天都在用 Python 完成着不同的工作。而文件操作,则是大家最常需要解决的任务之一。使用 Python,你可以轻松为他人生成精美的报表,也可以用短短几行代码快速解析、整理上万份数据文件。当我们编写与文件相关的代码时,通常会关注这些事情:我的代码是不是足够快?我的代码有没有事半功倍的完成任务? 在这篇文章中,我会与你分享与之相关的几个编程建议。我会向你推荐一个被低估的 Python 标准库模块、演示一个读取大文件的最佳方式、最后再分享我对函数设计的一点思考。下面,让我们进入第一个“模块安利”时间吧。

建议一

如果你需要在 Python 里进行文件处理,那么标准库中的 osos.path 兄弟俩一定是你无法避开的两个模块。在这两个模块里,有着非常多与文件路径处理、文件读写、文件状态查看相关的工具函数。让我用一个例子来展示一下它们的使用场景。有一个目录里装了很多数据文件,但是它们的后缀名并不统一,既有 .txt,又有 .csv。我们需要把其中以 .txt 结尾的文件都修改为 .csv 后缀名。我们可以写出这样一个函数:

import os
import os.pathdef unify_ext_with_os_path(path):"""统一目录下的 .txt 文件名后缀为 .csv"""for filename in os.listdir(path):basename, ext = os.path.splitext(filename)if ext == '.txt':abs_filepath = os.path.join(path, filename)os.rename(abs_filepath, os.path.join(path, f'{basename}.csv')) 

让我们看看,上面的代码一共用到了哪些与文件处理相关的函数:

  • os.listdir(path):列出 path 目录下的所有文件(含文件夹)
  • os.path.splitext(filename):切分文件名里面的基础名称和后缀部分
  • os.path.join(path,filename):组合需要操作的文件名为绝对路径
  • os.rename(...):重命名某个文件

上面的函数虽然可以完成需求,但说句实话,即使在写了很多年 Python 代码后,我依然觉得:这些函数不光很难记,而且最终的成品代码也不怎么讨人喜欢。

使用 pathlib 模块改写代码

为了让文件处理变得更简单,Python 在 3.4 版本引入了一个新的标准库模块:pathlib。它基于面向对象思想设计,封装了非常多与文件操作相关的功能。如果使用它来改写上面的代码,结果会大不相同。

使用 pathlib 模块后的代码:

from pathlib import Pathdef unify_ext_with_pathlib(path):for fpath in Path(path).glob('*.txt'):fpath.rename(fpath.with_suffix('.csv')) 

和旧代码相比,新函数只需要两行代码就完成了工作。而这两行代码主要做了这么几件事:

  1. 首先使用 Path(path) 将字符串路径转换为 Path 对象
  2. 调用 .glob('*.txt') 对路径下所有内容进行模式匹配并以生成器方式返回,结果仍然是 Path 对象,所以我们可以接着做后面的操作
  3. 使用 .with_suffix('.csv') 直接获取使用新后缀名的文件全路径
  4. 调用 .rename(target) 完成重命名

相比 osos.path,引入 pathlib 模块后的代码明显更精简,也更有整体统一感。所有文件相关的操作都是一站式完成。

其他用法

除此之外,pathlib 模块还提供了很多有趣的用法。比如使用 / 运算符来组合文件路径:

#   旧朋友:使用 os.path 模块
>>> import os.path
>>> os.path.join('/tmp', 'foo.txt')
'/tmp/foo.txt'# ✨ 新潮流:使用 / 运算符
>>> from pathlib import Path
>>> Path('/tmp') / 
'foo.txt'PosixPath('/tmp/foo.txt') 

或者使用 .read_text() 来快速读取文件内容:

# 标准做法,使用 with open(...) 打开文件
>>> with open('foo.txt') as file:
... print(file.read())
...
foo# 使用 pathlib 可以让这件事情变得更简单
>>> from pathlib import Path
>>> print(Path('foo.txt').read_text())
foo 

除了我在文章里介绍的这些,pathlib 模块还提供了非常多有用的方法,强烈建议去 官方文档 详细了解一下。

如果上面这些都不足以让你动心,那么我再多给你一个使用 pathlib 的理由:PEP-519 里定义了一个专门用于“文件路径”的新对象协议,这意味着从该 PEP 生效后的 Python 3.6 版本起,pathlib 里的 Path 对象,可以和以前绝大多数只接受字符串路径的标准库函数兼容使用:

>>> p = Path('/tmp')
# 可以直接对 Path 类型对象 p 进行 join
>>> os.path.join(p, 'foo.txt')
'/tmp/foo.txt' 

所以,无需犹豫,赶紧把 pathlib 模块用起来吧。

建议二

几乎所有人都知道,在 Python 里读取文件有一种“标准做法”:首先使用 withopen(fine_name) 上下文管理器的方式获得一个文件对象,然后使用 for 循环迭代它,逐行获取文件里的内容。

下面是一个使用这种“标准做法”的简单示例函数:

def count_nine(fname):"""计算文件里包含多少个数字 '9'"""count = 0with open(fname) as file:for line in file:count += line.count('9')return count 

假如我们有一个文件 small_file.txt,那么使用这个函数可以轻松计算出 9 的数量。

# small_file.txtfeiowe9322nasd9233rlaoeijfiowejf8322kaf9a# OUTPUT: 3print(count_nine('small_file.txt')) 

为什么这种文件读取方式会成为标准?这是因为它有两个好处:

  1. with 上下文管理器会自动关闭打开的文件描述符
  2. 在迭代文件对象时,内容是一行一行返回的,不会占用太多内存

标准做法的缺点

但这套标准做法并非没有缺点。如果被读取的文件里,根本就没有任何换行符,那么上面的第二个好处就不成立了。当代码执行到 forlineinfile 时,line 将会变成一个非常巨大的字符串对象,消耗掉非常可观的内存。让我们来做个试验:有一个 5GB 大的文件 big_file.txt,它里面装满了和 small_file.txt 一样的随机字符串。只不过它存储内容的方式稍有不同,所有的文本都被放在了同一行里:

# FILE: big_file.txt
df2if283rkwefh... <剩余 5GB 大小> ...

如果我们继续使用前面的 count_nine 函数去统计这个大文件里 9 的个数。那么在我的笔记本上,这个过程会足足花掉 65 秒,并在执行过程中吃掉机器 2GB 内存。

为了解决这个问题,我们需要暂时把这个“标准做法”放到一边,使用更底层的 file.read() 方法。与直接循环迭代文件对象不同,每次调用 file.read(chunk_size) 会直接返回从当前位置往后读取 chunk_size 大小的文件内容,不必等待任何换行符出现。所以,如果使用 file.read() 方法,我们的函数可以改写成这样:

def count_nine_v2(fname):"""计算文件里包含多少个数字 '9',每次读取 8kb"""count = 0block_size = 1024 * 8with open(fname) as fp:while True:chunk = fp.read(block_size)# 当文件没有更多内容时,read 调用将会返回空字符串 ''if not chunk:breakcount += chunk.count('9')return count

在新函数中,我们使用了一个 while 循环来读取文件内容,每次最多读取 8kb 大小,这样可以避免之前需要拼接一个巨大字符串的过程,把内存占用降低非常多。

利用生成器解耦代码

假如我们在讨论的不是 Python,而是其他编程语言。那么可以说上面的代码已经很好了。但是如果你认真分析一下 count_nine_v2 函数,你会发现在循环体内部,存在着两个独立的逻辑:数据生成(read 调用与 chunk 判断)数据消费。而这两个独立逻辑被耦合在了一起。正如我在《编写地道循环》里所提到的,为了提升复用能力,我们可以定义一个新的 chunked_file_reader 生成器函数,由它来负责所有与“数据生成”相关的逻辑。这样 count_nine_v3 里面的主循环就只需要负责计数即可。

def chunked_file_reader(fp, block_size=1024 * 8):"""生成器函数:分块读取文件内容"""while True:chunk = fp.read(block_size)# 当文件没有更多内容时,read 调用将会返回空字符串 ''if not chunk:breakyield chunkdef count_nine_v3(fname):count = 0with open(fname) as fp:for chunk in chunked_file_reader(fp):count += chunk.count('9')return count

进行到这一步,代码似乎已经没有优化的空间了,但其实不然。iter(iterable) 是一个用来构造迭代器的内建函数,但它还有一个更少人知道的用法。当我们使用 iter(callable,sentinel) 的方式调用它时,会返回一个特殊的对象,迭代它将不断产生可调用对象 callable 的调用结果,直到结果为 setinel 时,迭代终止。

def chunked_file_reader(file, block_size=1024 * 8):"""生成器函数:分块读取文件内容,使用 iter 函数 """ # 首先使用 partial(fp.read, block_size) 构造一个新的无需参数的函数# 循环将不断返回 fp.read(block_size) 调用结果,直到其为 '' 时终止for chunk in iter(partial(file.read, block_size), ''):yield chunk

最终,只需要两行代码,我们就完成了一个可复用的分块文件读取函数。那么,这个函数在性能方面的表现如何呢?和一开始的 2GB 内存/耗时 65 秒 相比,使用生成器的版本只需要 7MB 内存 / 12 秒 就能完成计算。效率提升了接近 4 倍,内存占用更是不到原来的 1%。

建议三

统计完文件里的 “9” 之后,让我们换一个需求。现在,我想要统计每个文件里出现了多少个英文元音字母(aeiou)。只要对之前的代码稍作调整,很快就可以写出新函数 count_vowels

):"""统计某个文件中,包含元音字母(aeiou)的数量"""VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}count = 0with open(filename, 'r') as fp:for line in fp:for char in line:if char.lower() in VOWELS_LETTERS:count += 1return count# OUTPUT: 16
print(count_vowels('small_file.txt'))

和之前“统计 9”的函数相比,新函数变得稍微复杂了一些。为了保证程序的正确性,我需要为它写一些单元测试。但当我准备写测试时,却发现这件事情非常麻烦,主要问题点如下:

  1. 函数接收文件路径作为参数,所以我们需要传递一个实际存在的文件
  2. 为了准备测试用例,我要么提供几个样板文件,要么写一些临时文件
  3. 而文件是否能被正常打开、读取,也成了我们需要测试的边界情况

如果,你发现你的函数难以编写单元测试,那通常意味着你应该改进它的设计。上面的函数应该如何改进呢?答案是:让函数依赖“文件对象”而不是文件路径。修改后的函数代码如下:

def count_vowels_v2(fp):"""统计某个文件中,包含元音字母(aeiou)的数量"""VOWELS_LETTERS = {'a', 'e', 'i', 'o', 'u'}count = 0for line in fp:for char in line:if char.lower() in VOWELS_LETTERS:count += 1return count# 修改函数后,打开文件的职责被移交给了上层函数调用者
with open('small_file.txt') as fp:print(count_vowels_v2(fp))

这个改动带来的主要变化,在于它提升了函数的适用面。因为 Python 是“鸭子类型”的,虽然函数需要接受文件对象,但其实我们可以把任何实现了文件协议的 “类文件对象(file-like object)” 传入 count_vowels_v2 函数中。

而 Python 中有着非常多“类文件对象”。比如 io 模块内的 StringIO 对象就是其中之一。它是一种基于内存的特殊对象,拥有和文件对象几乎一致的接口设计。利用 StringIO,我们可以非常方便的为函数编写单元测试。

# 注意:以下测试函数需要使用 pytest 执行
import pytest
from io import StringIO@pytest.mark.parametrize("content,vowels_count", [# 使用 pytest 提供的参数化测试工具,定义测试参数列表# (文件内容, 期待结果)('', 0),('Hello World!', 3),('HELLO WORLD!', 3),('你好,世界', 0),]
)
def test_count_vowels_v2(content, vowels_count):# 利用 StringIO 构造类文件对象 "file"file = StringIO(content)assert count_vowels_v2(file) == vowels_count

使用 pytest 运行测试可以发现,函数可以通过所有的用例:

❯ pytest vowels_counter.py
====== test session starts ======
collected 4 itemsvowels_counter.py ... [100%]====== 4 passed in 0.06 seconds ======

而让编写单元测试变得更简单,并非修改函数依赖后的唯一好处。除了 StringIO 外,subprocess 模块调用系统命令时用来存储标准输出的 PIPE 对象,也是一种“类文件对象”。这意味着我们可以直接把某个命令的输出传递给 count_vowels_v2 函数来计算元音字母数:

import subprocess# 统计 /tmp 下面所有一级子文件名(目录名)有多少元音字母
p = subprocess.Popen(['ls', '/tmp'], stdout=subprocess.PIPE, encoding='utf-8')# p.stdout 是一个流式类文件对象,可以直接传入函数
# OUTPUT: 42
print(count_vowels_v2(p.stdout))

正如之前所说,将函数参数修改为“文件对象”,最大的好处是提高了函数的 适用面可组合性。通过依赖更为抽象的“类文件对象”而非文件路径,给函数的使用方式开启了更多可能,StringIO、PIPE 以及任何其他满足协议的对象都可以成为函数的客户。

不过,这样的改造并非毫无缺点,它也会给调用方带来一些不便。假如调用方就是想要使用文件路径,那么就必须得自行处理文件的打开操作。

如何编写兼容二者的函数

有没有办法即拥有“接受文件对象”的灵活性,又能让传递文件路径的调用方更方便?答案是:有,而且标准库中就有这样的例子。打开标准库里的 xml.etree.ElementTree 模块,翻开里面的 ElementTree.parse 方法。你会发现这个方法即可以使用文件对象调用,也接受字符串的文件路径。而它实现这一点的手法也非常简单易懂:

def parse(self, source, parser=None):"""*source* is a file name or file object, *parser* is an optional parser"""close_source = False# 通过判断 source 是否有 "read" 属性来判定它是不是“类文件对象”# 如果不是,那么调用 open 函数打开它并负担起在函数末尾关闭它的责任if not hasattr(source, "read"):source = open(source, "rb")close_source = True

使用这种基于“鸭子类型”的灵活检测方式, count_vowels_v2 函数也同样可以被改造得更方便,我在这里就不再重复啦。

总结

文件操作我们在日常工作中经常需要接触的领域,使用更方便的模块、利用生成器节约内存以及编写适用面更广的函数,可以让我们编写出更高效的代码。让我们最后再总结一下吧:

  • 使用 pathlib 模块可以简化文件和目录相关的操作,并让代码更直观
  • PEP-519 定义了表示“文件路径”的标准协议,Path 对象实现了这个协议
  • 通过定义生成器函数来分块读取大文件可以节约内存
  • 使用 iter(callable,sentinel) 可以在一些特定场景简化代码
  • 难以编写测试的代码,通常也是需要改进的代码
  • 让函数依赖“类文件对象”可以提升函数的适用面和可组合性

原文作者:piglei

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/559856.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

lte核心网由哪些设备组成_电气设备安装工程由哪些部分组成?

电气设备安装工程的组成通用电气设备安装项目是一个工程系统&#xff0c;通过接收电能&#xff0c;转换和分配电能&#xff0c;使用电能或从接收电能到分配给电气设备组成。根据其主要功能&#xff0c;它们分为电气照明系统&#xff0c;电源系统和配电系统。这种主要基于电能的…

guid主分区表损坏怎么办_抹盘提示进程失败,导致Win 10无法正常开机怎么办?...

点击上方「蓝字」关注我哦一般来说&#xff0c;ESP分区不足200MB会导致抹盘进程失败&#xff0c;并且损坏Win 10引导&#xff0c;需要进入PE模式将其修复。本教程所使用的PE为WePE。0、条件电脑进入PE系统。1、开始找到桌面上的“Dism”打开运行。打开程序后可能会提示临时目录…

bundle 安装_超级小白使用pip安装第三方库的正确姿势

为了感谢大家对“Python客栈”的关注与支持&#xff0c;即日起&#xff0c;我们每天会在留言中随机抽取三位粉丝发放6.6元小红包。快来参与吧&#xff01;11.pip是什么pip是python的包管理工具&#xff0c;python2.7/python3.4以上的版本都已经集成了该工具&#xff0c;我们可以…

arcgis js 地图打印_Arcgis在国土空间规划编制中的应用

规划行业经过多年的转变&#xff0c;从原来的城市规划到现在的国土空间规划&#xff0c;理论和技术应用都随之更新。新的国土空间规划在编制成果上&#xff0c;注重建立全国同一的国土空间基础信息平台。目前&#xff0c;单就国土空间规划的编制而言&#xff0c;已从传统规划的…

.network 中文文档_以太坊链下支付网络Raiden API中文文档

雷电网络(Raiden Network)是以太坊区块链的链下扩容方案&#xff0c;Raiden节点的Restful API官方文档中文版由汇智网提供&#xff0c;访问地址&#xff1a;http://cw.hubwiz.com/card/c/raiden-api/Raiden API访问端结点URL中通常包含有版本信息&#xff0c;以便支持对不同版本…

python write 写多行_如何用 Python 执行单行命令

一般来说&#xff0c;面对日常处理的一些小任务&#xff0c;直接用 sed,grep 之类的就可以搞定&#xff0c;更复杂一点的就会考虑 awk 或者用一些现成的轮子&#xff0c;要是 awk 搞不定我就只好用 Python 了。但有些时候&#xff0c;我仅仅只是想写一个一次性脚本&#xff0c;…

小红书点赞收藏有什么用_橱柜门用什么材料好?老师傅开口说话了!听进去算你的,收藏备用...

想做好一个橱柜&#xff0c;橱柜门的材质要选好&#xff0c;而一旦涉及到材质&#xff0c;那些专业生僻的叫法让人眼花缭乱&#xff0c;云里雾里的&#xff0c;难以理解。其实橱柜门的材质大概分为6大类&#xff0c;市面上主流的材质大致有3类。今天FUN姐就跟大家聊聊橱柜门到底…

ddr4服务器内存和普通内存_服务器内存介绍及其供电规范

内存(Memory)也称内存储器或主存储器&#xff0c;与CPU进行高速的数据沟通&#xff0c;用于暂时存放CPU的运算数据。见上图&#xff0c;内存紧挨CPU两侧布局&#xff0c;内存数量和容量根据系统需求和CPU能力决定&#xff0c;内存性能对服务器整体运行快慢起着至关重要的影响。…

小程序新闻列表页面布局代码_论坛小程序·“我的”页面布局

这次让我们来看看界面是怎么布局的吧&#xff01;比如打开模拟器里“我的”页面&#xff0c;找到其所对应的me文件夹里的文件打开me.wxml查看第一段代码&#xff0c;发现其中包含五个view类&#xff0c;他们的属性标签分别是&#xff1a;classamountBg、classimg、classaccount…

准考证打印系统关闭怎么办_2019年执业药师准考证无法正常打印,怎么办?

2019年各地区执业药师准考证打印入口陆续开通&#xff0c;但是很多考生会遇到准考证打印的问题&#xff0c;现小编整理了2019年执业药师准考证无法正常打印情况&#xff0c;供各位考生参考。一、打印准考证必须使用IE浏览器6.0或以上版本。二、禁用或卸载有关拦截工具。(Window…

安卓机更新系统会卡吗_安卓机可以用AirPods Pro吗?告诉你真实体验

[PConline评测]安卓系统可以使用AirPods Pro吗&#xff1f;从AirPods Pro发布起&#xff0c;这问题一直备受关注&#xff0c;毕竟苹果官方宣称AirPods Pro兼容多种苹果设备&#xff0c;可没说兼容安卓机。我们知道其他AirPods产品是可以用在安卓机上的&#xff0c;那么AirPods …

# 解析bt文件_BT、磁力链这些词语是什么意思?

“知其然知其所以然”。我们经常在下载资料的时候能看到BT、磁力链等词语&#xff0c;这些词语到底是什么意思呢&#xff1f;下载都会用&#xff0c;但是你了解吗&#xff1f;BT下载传统的下载模式是每个客户端从服务器拷贝文件&#xff0c;跟校园内常用的FTP一样。因为服务器宽…

怎么修改ppt的虚线间隔_还有一小时下班,领导交给我一份ppt,做不完不许走!...

相信在职场中的好多人&#xff0c;都会遇到领导突然给任务的时候&#xff0c;遇到这种情况该怎么办呢&#xff1f;还能怎么办&#xff1f;听话&#xff01;干活&#xff01;就是这份ppt&#xff0c;就是它&#xff0c;决定了我的下班时间&#xff01;ppt水平差&#xff0c;时间…

kitti pkl可视化_KITTI 3D Lidar 数据可视化

前言Lilikili&#xff1a;在尝试做Lidar方面的研究的时候&#xff0c;发现对于可视化大家的脑洞还不是很够&#xff0c;所以提供一些思路让大家蛤一下。数据集本次使用的数据集是KITTI 3D object detection条目下面的data_object_velodyne.zip&#xff0c;里面存储了二进制存储…

php mysql 排名_mysql中如何实现排名

mysql中实现排名的方法&#xff1a;使用demo函数&#xff0c;语法为【SELECT banji,avg(score) as AvgS FROM table_test GROUP BY banji ORDER BY AvgS DESC】。mysql中实现排名的方法&#xff1a;可用Demo实现注意里面的A&#xff0c;属于嵌套查询&#xff0c;所以排名会正确…

figtree如何编辑进化树_iTOL快速绘制颜值最高的进化树

iTOL简介大家在看高分文章时&#xff0c;总会惊叹于&#xff0c;为什么人家能做出那么好看而且高大上的系统发育树&#xff0c;而且好看的图也能直接提升文章的档次&#xff0c;冲击高分文章。人家的树不管是从配色还是各种注释信息都让人无可挑剔&#xff0c;而你每次花了半个…

java怎么碰到异常跳过继续执行_Java异常处理很难吗?BAT大厂的架构师是怎么处理Java异常的?...

欢迎关注专栏《Java架构筑基》——专注于Java技术的研究与分享&#xff01;Java架构筑基​zhuanlan.zhihu.comJava架构筑基——专注于Java技术的研究与分享&#xff01;后续文章将首发此专栏&#xff01;欢迎各位Java工程师朋友投稿和关注# 链接 Java程序员福利"常用资料分…

mysql 回滚之后抛出异常_在PHP中 开始事务后,程序抛出异常 没有执行commit也没有执行rollback mysql事务会回滚吗?...

网上有人说开启事务后 sql语句执行错误或程序出错 没有执行rollback的情况下&#xff0c;下一次运行就会自动commit程序出错不会回滚rollback但是也有人说抛出异常等 程序如果MySQL存在没有提交的事务&#xff0c;那么这时候 AUTOCOMMIT 自动提交的参数应该是为 0 的。不过如果…

mygeneration mysql_利用Mygeneration 自动生成maping文件和entity类文件

利用Mygeneration 自动生成maping文件和entity类文件Mygeneration下载要下载1.3.0.3版本&#xff0c; 默认支持mysql了&#xff0c;不用再安装mysql的驱动了。Template Code : 主要使用的模板代码&#xff0c;会实时展示在Template Source选项卡中Interface Code : 根据Templat…

mysql schema命名规范_数据库设计 命名及编码规范

http://blog.csdn.net/seusoftware/archive/2010/04/24/5524414.aspx引用一、综述命名和编码过程中&#xff0c;定义有意义的名称&#xff0c;以易于理解、方便书写为原则。(1)避免使用中文&#xff0c;尽量使用全拼音或全英文&#xff0c;以方便国际化&#xff1b;(2)避免拼音…