可读性与可扩展性:一个完美的函数 #P010#
在这一篇文章中,我们会在os.walk函数的基础上,封装一个查找文件的函数。如果读者是一位编程经验还不太丰富的工程师,相信可以从这篇文章中受到一些启发,并在以后的工作中,更加能够编写出扩展性强、可读性强、好维护的代码。
1 使用os.walk遍历目录树
为了避免读者不了解os.walk函数,从而无法理解这篇文章在就讲什么。因此,在正式介绍今天的主题之前,我们先来看一下os.walk函数的使用。
os.walk函数用以查找某个目录下的文件和目录,相对于os.listdir,os.walk不但会列出当前目录下的文件和目录,还会递归地遍历目录下的子目录。例如,查找某个目录及其子目录下所有的图片文件。对于这个查找图片的需求,可以使用os.walk函数。os.walk函数遍历某个目录及其子目录,对于每一个目录,walk返回一个三元组(dirpath, dirnames, filenames)。其中,dirpath保存的是当前目录,dirnames是当前目录下的子目录列表,filenames是当前目录下的文件列表。
下面的代码演示了os.walk函数的用法,使用os.walk函数遍历lmx这个用户的HOME目录及其子目录,并找到所有的图片文件:
#!/usr/bin/python
#-*- coding: UTF-8 -*-
import os
import fnmatch
images = ['*.jpg', '*.jpeg', '*.png', '*.tif', '*.tiff']
matches = []
for root, dirnames, filenames in os.walk(os.path.expanduser("~lmx")):
for extensions in images:
for filename in fnmatch.filter(filenames, extensions):
matches.append(os.path.join(root, filename))
print(matches)
在这段代码中,我们查找lmx这个用户的HOME目录及其子目录,然后通过fnmatch进行文件扩展名匹配。如果文件匹配了某一个扩展名,则会将该文件保存到结果列表(matches)中。
基于os.walk函数,我们还可以实现一个功能,那就是在遍历目录及其子目录时,如果想要忽略掉某一个子目录,可以直接修改三元组中的dirnames。即从dirnames这个列表中移除需要忽略掉的目录,就实现了查找文件时排除某个目录的目的。如下所示:
for root, dirnames, filenames in os.walk(os.path.expanduser("~lmx/t")):
......
if 'exclude_dir' in dirnames:
dirnames.remove('exclude_dir')
2 封装成函数
在前面的例子中,我们演示了os.walk函数的用法。现在,大家都知道这段程序的作用,是找到某个目录下所有的图片文件。但是,假设读者没有阅读到前面的说明文字,仅仅是阅读了代码,是否能够快速地知道这段程序的作用呢?或者说,读者下次再来看这段代码,能否快速的知道这段代码的作用呢?
这就涉及到一个代码编程规范:在编程时,我们应该尽可能将代码封装到函数中,并为函数取一个顾名思义的名字,以提高程序的可读性。我们一起来尝试一下,为上面的代码取一个顾名思义的名字:
#!/usr/bin/python
#-*- coding: UTF-8 -*-
import os
import fnmatch
def find_all_images(path):
images = ['*.jpg', '*.jpeg', '*.png', '*.tif', '*.tiff']
matches = []
for root, dirnames, filenames in os.walk(os.path.expanduser("~root/web")):
for extensions in images:
for filename in fnmatch.filter(filenames, extensions):
matches.append(os.path.join(root, filename))
print(matches)
if __name__ == '__main__':
find_all_images("~lmx")
我们做的事情非常简单,仅仅是将第一节的代码封装到一个函数中,并为函数取了一个好记的名字。现在,哪怕是第一次看这段代码的工程师,也可以快速的知道,这段程序的作用是找到某个目录下所有的图片文件。
3 提高程序的可扩展性
前面使用os.walk函数遍历目录,并找到目录下的所有图片。下面再来看几个更加实际的需求:
- 找到某个目录及子目录下最大的十个文件
- 找到某个目录及子目录下最老的十个文件
- 找到某个目录及子目录下,所有文件名中包含"mysql-bin"的文件
- 找到某个目录及子目录下,排除.git子目录以后,所有的Python源文件
看到这里的需求,最简单的想法,就是参考前面查找图片的例子,对每一个需求提供一个程序。如果读者是一名在校大学生,或者是刚毕业的应届生,问题不会很大。如果是一名已经工作的工程师,对每一个需求提供一个程序,恐怕是不合格的。这里的几个需求,虽然表面上看完全不一样,但是,它们都有一个共同的需求,即找到某个目录及其子目录下的某种文件。更加通用的需求是,找到某个目录树中,除部分特殊目录以外,其他目录中的某一些文件。
当我们提炼出这个通用的需求以后,就可以先实现这个通用的需求,将这个通用的需求抽象成一个函数,再通过调用这个函数来实现其他需求,这样就可以减少代码冗余了。如下所示:
#!/usr/bin/python
#-*- coding: UTF-8 -*-
import os
import fnmatch
def is_file_match(filename, patterns):
for pattern in patterns:
if fnmatch.fnmatch(filename, pattern):
return True
return False
def find_specific_files(root, patterns, exclude_dirs):
result = []
for root, dirnames, filenames in os.walk(root):
for filename in filenames:
if is_file_match(filename, patterns):
result.append(os.path.join(root, filename))
for d in exclude_dirs:
if d in dirnames:
dirnames.remove(d)
return result
def main():
result = find_specific_files('~root', ['*.txt', '*.md'], ['.git', '.pyenv'])
print(result)
if __name__ == '__main__':
main()
在这段程序中,我们查找图片的例子进行了大幅的改造。改造得更加的通用。
4 使用默认参数提高程序的易用性
#!/usr/bin/python
#-*- coding: UTF-8 -*-
import os
import fnmatch
def is_file_match(filename, patterns):
for pattern in patterns:
if fnmatch.fnmatch(filename, pattern):
return True
return False
def find_specific_files(root, patterns=['*'], exclude_dirs=[]):
result = []
for root, dirnames, filenames in os.walk(root):
for filename in filenames:
if is_file_match(filename, patterns):
result.append(os.path.join(root, filename))
for d in exclude_dirs:
if d in dirnames:
dirnames.remove(d)
return result
def main():
result = find_specific_files('web', ['*.txt', '*.md'])
result = find_specific_files('web')
print(result)
if __name__ == '__main__':
main()
这里定义了一个find_specific_files函数,该函数接受三个参数,分别是查找的根路径,匹配的文件模式列表和需要排除的目录列表。其中,匹配模式列表和排除的目录列表都有默认值,默认情况下,找到根路径下的所有文件。
5 使用生成器提高程序的可读性
#!/usr/bin/python
#-*- coding: UTF-8 -*-
import os
import fnmatch
def is_file_match(filename, patterns):
for pattern in patterns:
if fnmatch.fnmatch(filename, pattern):
return True
return False
def find_specific_files(root, patterns=['*'], exclude_dirs=[]):
for root, dirnames, filenames in os.walk(root):
for filename in filenames:
if is_file_match(filename, patterns):
yield os.path.join(root, filename)
for d in exclude_dirs:
if d in dirnames:
dirnames.remove(d)
6 应用
有了find_specific_files函数以后,实现任何查找类的需求,都非常简单,只需要很少的代码就能够实现。例如:
-
查到目录下的所有文件
for item in find_specific_files("."):
print(item)
-
查找目录下的所有图片
patterns= ['*.jpg', '*.jpeg', '*.png', '*.tif', '*.tiff']
for item in find_specific_files(".", patterns):
print(item) -
查找目录树中,除dir2目录以外,其他目录下的所有图片
patterns= ['*.jpg', '*.jpeg', '*.png', '*.tif', '*.tiff']
exclude_dirs = [‘dir2’]
for item in find_specific_files(".", patterns, exclude_dirs):
print(item)
有了find_specific_files这个辅助函数以后,再来看前面的需求,就比较简单。例如,对于找到某个目录及子目录下最大的十个文件,现在已经能够通过find_specific_files 找到某个目录下的所有文件。接下来要做的就是,获取文件的大小,并按大小排序。排序以后输出最大的十个文件即可。如下所示:
files = {name: os.path.getsize(name) for name in find_specific_files('.')}
result = sorted(files.items(), key=lambda d: d[1], reverse=True)[:10]
for i, t in enumerate(result, 1):
print(i, t[0], t[1])
在这个例子中,首先通过字典推导创建了一个字典,字典的key是找到的文件,字典的value是文件的大小。构建出字典以后,使用Python内置的sorted函数对字典进行逆序排序。排序完成以后获取最大的十个文件。笔者在MySQL源码的里面运行,得到的结果如下:
1 ./.git/objects/pack/pack-6f40c5050007871f17368c349f1db15ae59af33b.pack 1073524236
2 ./.git/objects/pack/pack-6f40c5050007871f17368c349f1db15ae59af33b.idx 34002592
3 ./strings/ctype-eucjpms.c 3639754
4 ./strings/ctype-ujis.c 3629335
5 ./mysql-test/suite/parts/r/partition_alter4_myisam.result 3327484
6 ./mysql-test/suite/parts/r/partition_alter4_innodb.result 3266444
7 ./mysql-test/suite/engines/iuds/r/strings_update_delete.result 3187152
8 ./mysql-test/suite/engines/iuds/r/type_bit_iuds.result 2972348
9 ./mysql-test/suite/sys_vars/r/max_binlog_cache_size_func.result 2241976
10 ./strings/ctype-cp932.c 1846215
也可以指定参数,排除.git目录,如下所示:
files = {name: os.path.getsize(name) for name in find_specific_files('.', exclude_dirs=['.git'])}
result = sorted(files.items(), key=lambda d: d[1], reverse=True)[:10]
for i, t in enumerate(result, 1):
print(i, t[0], t[1])
可以看到,有了find_specific_files这个辅助函数以后,要实现查找类的功能非常简单,下面再看几个例子。
-
找到某个目录及子目录下最老的十个文件
files = {name: os.path.getmtime(name)for name in find_specific_files('.')} result = sorted(files.items(), key=lambda d: d[1])[:10] for i, t in enumerate(result, 1): print(i, t[0], time.ctime(t[1]))
-
找到某个目录及子目录下,所有文件名中包含"mysql-bin"的文件
files = [name for name in find_specific_files('.', patterns=['*mysql-bin*'])] for i, name in enumerate(files, 1): print(i, name)
在这个例子中,除了传递目录以外,还传递了相应的匹配模式。为了支持多种匹配模式,模式匹配这个参数以列表的形式表示。虽然这个例子中,只有一个匹配的模式,也必须使用列表的形式传递匹配的模式。通过列表推导获取所有的文件,然后直接输出即可。
-
找到某个目录及子目录下,排除.git子目录以后,所有的Python源文件
files = [name for name in find_specific_files(’.’, patterns=[’*.py’], exclude_dirs=[’.git’])]
for i, name in enumerate(files, 1):
print(i, name) -
删除某个目录及其子目录下的所有pyc文件
files = [name for name in find_specific_files('.', patterns=['*.pyc'])] for name in files: os.remove(name)
7 总结
坦诚的说,除非功能非常简单,否则,我们并不能一次就编写出一个完美的函数。并且,最开始的时候,可能需求本身就很简单,那么就没有必要写如此通用的函数。但是,随着功能越来越复杂,我们要能够发现代码中的冗余,并将相同需求的功能提升为一个通用的函数。最重要的是,我们既要能够区分好的代码与坏的代码,还要持续不断的去改进、去优化我们已有的代码。在下一篇文章中,我们将讨论什么是好的代码,什么是不好的代码。
作者介绍
赖明星,架构师、作家。现就职于腾讯,参与并主导下一代金融级数据库平台研发。有多年的 Python 开发经验和一线互联网实战经验,擅长 C、Python、Java、MySQL、Linux 等主流技术。国内知名的 Python 技术专家和 Python 技术的积极推广者,著有《Python Linux 系统管理与自动化运维》一书。