欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

可读性与可扩展性:一个完美的函数 #P010#

程序员文章站 2022-04-18 14:30:39
...

在这一篇文章中,我们会在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函数遍历目录,并找到目录下的所有图片。下面再来看几个更加实际的需求:

  1. 找到某个目录及子目录下最大的十个文件
  2. 找到某个目录及子目录下最老的十个文件
  3. 找到某个目录及子目录下,所有文件名中包含"mysql-bin"的文件
  4. 找到某个目录及子目录下,排除.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 系统管理与自动化运维》一书。

相关标签: 高质量Python编程