您当前位置: 首页 ‣ 深入 Python 3 ‣
难度级别: ♦♦♢♢♢
❝ 我们的想象力被扩展到极致,不是像在虚构中那样去想象那些不存在的东西,而是去理解那些存在的东西。 ❞
— 理查德·费曼
每种编程语言都有一个特性,一个刻意简化的复杂事物。如果您来自其他语言,您可能会很容易错过它,因为您的旧语言没有简化那个事物(因为它正忙着简化其他事物)。本章将向您介绍列表推导、字典推导和集合推导:围绕一种非常强大的技术的三个相关概念。但首先,我想先绕个弯,介绍两个模块,它们将帮助您浏览本地文件系统。
⁂
Python 3 带有一个名为 os
的模块,它代表“操作系统”。os
模块包含大量函数,用于获取有关本地目录、文件、进程和环境变量的信息,在某些情况下,还可以操作这些信息。Python 尽其所能为所有支持的操作系统提供统一的 API,以便您的程序可以在任何计算机上运行,尽可能减少平台特定的代码。
当您刚开始使用 Python 时,您将在 Python Shell 中花费大量时间。在本书中,您将看到如下示例
examples
文件夹 中的其中一个模块如果您不了解当前工作目录,步骤 1 可能会因 ImportError
而失败。为什么?因为 Python 会在 导入搜索路径 中查找示例模块,但它不会找到它,因为 examples
文件夹不是搜索路径中的目录之一。要解决这个问题,您可以执行以下两种操作之一
examples
文件夹添加到导入搜索路径examples
文件夹当前工作目录是 Python 始终在内存中持有的一个不可见属性。始终存在一个当前工作目录,无论您是在 Python Shell 中,从命令行运行自己的 Python 脚本,还是在某个 Web 服务器上运行 Python CGI 脚本。
os
模块包含两个用于处理当前工作目录的函数。
>>> import os ① >>> print(os.getcwd()) ② C:\Python31 >>> os.chdir('/Users/pilgrim/diveintopython3/examples') ③ >>> print(os.getcwd()) ④ C:\Users\pilgrim\diveintopython3\examples
os
模块随 Python 提供;您可以随时随地导入它。os.getcwd()
函数获取当前工作目录。当您运行图形化 Python Shell 时,当前工作目录从 Python Shell 可执行文件所在的目录开始。在 Windows 上,这取决于您安装 Python 的位置;默认目录为 c:\Python31
。如果您从命令行运行 Python Shell,则当前工作目录从您运行 python3
时所在的目录开始。os.chdir()
函数更改当前工作目录。os.chdir()
函数时,我使用了 Linux 样式的路径名(正斜杠,没有驱动器号),即使我在 Windows 上。这是 Python 试图掩盖不同操作系统之间差异的地方之一。说到目录,我想指出 os.path
模块。os.path
包含用于操作文件名和目录名的函数。
>>> import os >>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py')) ① /Users/pilgrim/diveintopython3/examples/humansize.py >>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py')) ② /Users/pilgrim/diveintopython3/examples\humansize.py >>> print(os.path.expanduser('~')) ③ c:\Users\pilgrim >>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py')) ④ c:\Users\pilgrim\diveintopython3\examples\humansize.py
os.path.join()
函数使用一个或多个部分路径名构造路径名。在本例中,它只是简单地连接字符串。os.path.join()
函数将在路径名之前添加一个额外的斜杠,然后将其与文件名连接。它是一个反斜杠而不是正斜杠,因为我在 Windows 上构建了这个示例。如果您在 Linux 或 Mac OS X 上复制此示例,您将看到一个正斜杠。不要纠结于斜杠;始终使用 os.path.join()
,让 Python 做正确的事情。os.path.expanduser()
函数将扩展使用 ~
表示当前用户主目录的路径名。这适用于所有用户都有主目录的平台,包括 Linux、Mac OS X 和 Windows。返回的路径没有尾部斜杠,但 os.path.join()
函数不介意。os.path.join()
函数可以接受任意数量的参数。当我发现这一点时,我欣喜若狂,因为 addSlashIfNecessary()
是我在新语言中构建工具箱时总是需要编写的那些愚蠢的小函数之一。不要在 Python 中编写这个愚蠢的小函数;聪明的人已经为你解决了这个问题。os.path
还包含用于将完整路径名、目录名和文件名拆分为其组成部分的函数。
>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py' >>> os.path.split(pathname) ① ('/Users/pilgrim/diveintopython3/examples', 'humansize.py') >>> (dirname, filename) = os.path.split(pathname) ② >>> dirname ③ '/Users/pilgrim/diveintopython3/examples' >>> filename ④ 'humansize.py' >>> (shortname, extension) = os.path.splitext(filename) ⑤ >>> shortname 'humansize' >>> extension '.py'
split
函数拆分完整路径名,并返回包含路径和文件名的元组。os.path.split()
函数正是这样做的。您将 split
函数的返回值分配给两个变量的元组。每个变量接收返回的元组的相应元素的值。dirname
接收从 os.path.split()
函数返回的元组的第一个元素的值,即文件路径。filename
接收从 os.path.split()
函数返回的元组的第二个元素的值,即文件名。os.path
还包含 os.path.splitext()
函数,该函数拆分文件名并返回包含文件名和文件扩展名的元组。您使用相同的技术将它们分别分配给不同的变量。glob
模块是 Python 标准库中的另一个工具。它是一种通过编程方式获取目录内容的简单方法,它使用您可能已经从命令行工作中熟悉的通配符。
>>> os.chdir('/Users/pilgrim/diveintopython3/') >>> import glob >>> glob.glob('examples/*.xml') ① ['examples\\feed-broken.xml', 'examples\\feed-ns0.xml', 'examples\\feed.xml'] >>> os.chdir('examples/') ② >>> glob.glob('*test*.py') ③ ['alphameticstest.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest3.py', 'pluraltest4.py', 'pluraltest5.py', 'pluraltest6.py', 'romantest1.py', 'romantest10.py', 'romantest2.py', 'romantest3.py', 'romantest4.py', 'romantest5.py', 'romantest6.py', 'romantest7.py', 'romantest8.py', 'romantest9.py']
glob
模块接受一个通配符,并返回与通配符匹配的所有文件和目录的路径。在此示例中,通配符是一个目录路径加上“*.xml
”,它将匹配 examples
子目录中的所有 .xml
文件。examples
子目录。os.chdir()
函数可以接受相对路径名。.py
扩展名结尾且文件名中包含 test
的所有文件。每个现代文件系统都存储有关每个文件的元数据:创建日期、最后修改日期、文件大小等。Python 提供了一个单独的 API 来访问这些元数据。您无需打开文件;您只需要文件名。
>>> import os >>> print(os.getcwd()) ① c:\Users\pilgrim\diveintopython3\examples >>> metadata = os.stat('feed.xml') ② >>> metadata.st_mtime ③ 1247520344.9537716 >>> import time ④ >>> time.localtime(metadata.st_mtime) ⑤ time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17, tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)
examples
文件夹。feed.xml
是 examples
文件夹中的一个文件。调用 os.stat()
函数返回一个对象,其中包含有关该文件的几种不同类型的元数据。st_mtime
是修改时间,但它使用的是一种不太实用的格式。(从技术上讲,它是自纪元以来的秒数,纪元定义为 1970 年 1 月 1 日的第一秒。真的。)time
模块是 Python 标准库的一部分。它包含用于在不同时间表示之间进行转换、将时间值格式化为字符串以及处理时区的函数。time.localtime()
函数将时间值从秒数转换为纪元(来自 os.stat()
函数返回的 st_mtime
属性)转换为更实用的结构,例如年、月、日、时、分、秒等。该文件最后修改时间为 2009 年 7 月 13 日下午 5:25 左右。# continued from the previous example >>> metadata.st_size ① 3070 >>> import humansize >>> humansize.approximate_size(metadata.st_size) ② '3.0 KiB'
os.stat()
函数还返回文件的尺寸,在 st_size
属性中。feed.xml
文件大小为 3070
字节。st_size
属性传递给 approximate_size()
函数。在 上一节 中,glob.glob()
函数返回了一系列相对路径名。第一个示例的路径名类似于 'examples\feed.xml'
,第二个示例的相对路径名甚至更短,例如 'romantest1.py'
。只要您保留在同一个当前工作目录中,这些相对路径名就可以用于打开文件或获取文件元数据。但是,如果您要构造一个绝对路径名,即包含所有目录名直至根目录或驱动器号的路径名,那么您将需要使用 os.path.realpath()
函数。
>>> import os >>> print(os.getcwd()) c:\Users\pilgrim\diveintopython3\examples >>> print(os.path.realpath('feed.xml')) c:\Users\pilgrim\diveintopython3\examples\feed.xml
⁂
一个 列表推导 提供了一种紧凑的方式,通过对列表的每个元素应用函数将一个列表映射到另一个列表。
>>> a_list = [1, 9, 8, 4] >>> [elem * 2 for elem in a_list] ① [2, 18, 16, 8] >>> a_list ② [1, 9, 8, 4] >>> a_list = [elem * 2 for elem in a_list] ③ >>> a_list [2, 18, 16, 8]
a_list
是您要映射的列表。Python 解释器逐个遍历 a_list
,并将每个元素的值暂时分配给变量 elem
。然后,Python 应用函数 elem * 2
并将该结果追加到返回的列表中。您可以在列表推导中使用任何 Python 表达式,包括 os
模块中用于操作文件和目录的函数。
>>> import os, glob >>> glob.glob('*.xml') ① ['feed-broken.xml', 'feed-ns0.xml', 'feed.xml'] >>> [os.path.realpath(f) for f in glob.glob('*.xml')] ② ['c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml', 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml', 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml']
.xml
文件的列表。.xml
文件列表,并将其转换为完整路径名的列表。列表推导还可以过滤项目,生成的结果可能小于原始列表。
>>> import os, glob
>>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000] ①
['pluraltest6.py',
'romantest10.py',
'romantest6.py',
'romantest7.py',
'romantest8.py',
'romantest9.py']
if
子句。if
关键字后的表达式将针对列表中的每个项目进行评估。如果表达式计算结果为 True
,则该项目将包含在输出中。此列表推导查看当前目录中所有 .py
文件的列表,并且 if
表达式通过测试每个文件的大小是否大于 6000
字节来过滤该列表。有六个这样的文件,因此列表推导返回一个包含六个文件名的列表。到目前为止,所有关于列表推导的示例都包含简单的表达式,例如将数字乘以一个常数、调用一个函数或简单地返回原始列表项(在过滤之后)。但是,列表推导的复杂程度没有限制。
>>> import os, glob >>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')] ① [(3074, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml'), (3386, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml'), (3070, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml')] >>> import humansize >>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')] ② [('3.0 KiB', 'feed-broken.xml'), ('3.3 KiB', 'feed-ns0.xml'), ('3.0 KiB', 'feed.xml')]
.xml
为扩展名的文件,获取每个文件的大小(通过调用 os.stat()
函数),并构造一个包含文件大小和每个文件绝对路径的元组(通过调用 os.path.realpath()
函数)。approximate_size()
函数,传入每个 .xml
文件的文件大小。⁂
字典推导式类似于列表推导式,但它会构建一个字典而不是列表。
>>> import os, glob >>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')] ① >>> metadata[0] ② ('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0, st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344, st_mtime=1247520344, st_ctime=1247520344)) >>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')} ③ >>> type(metadata_dict) ④ <class 'dict'> >>> list(metadata_dict.keys()) ⑤ ['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py', 'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py', 'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py', 'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py', 'pluraltest4.py'] >>> metadata_dict['alphameticstest.py'].st_size ⑥ 2509
test
的 .py
文件,然后构建一个包含文件名和文件元数据(来自调用 os.stat()
函数)的元组。f
)是字典键;冒号之后的表达式(本例中的 os.stat(f)
)是值。glob.glob('*test*.py')
返回的文件名。os.stat()
函数的返回值。这意味着我们可以通过名称在该字典中“查找”一个文件以获取其文件元数据。元数据之一是 st_size
,即文件大小。文件 alphameticstest.py
长度为 2509
字节。与列表推导式一样,您可以在字典推导式中包含一个 if
子句,以根据对每个项目求值的表达式来筛选输入序列。
>>> import os, glob, humansize >>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')} ① >>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \ ... for f, meta in metadata_dict.items() if meta.st_size > 6000} ② >>> list(humansize_dict.keys()) ③ ['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6'] >>> humansize_dict['romantest9'] ④ '6.5 KiB'
glob.glob('*')
),获取每个文件的文件元数据(os.stat(f)
),并构建一个字典,其键为文件名,其值为每个文件的文件元数据。6000
字节的文件(if meta.st_size > 6000
),并使用该筛选后的列表构建一个字典,其键为文件名减去扩展名(os.path.splitext(f)[0]
),其值为每个文件的近似大小(humansize.approximate_size(meta.st_size)
)。approximate_size()
函数返回的字符串。以下是用字典推导式的一个技巧,您可能有一天会用得上:交换字典的键和值。
>>> a_dict = {'a': 1, 'b': 2, 'c': 3} >>> {value:key for key, value in a_dict.items()} {1: 'a', 2: 'b', 3: 'c'}
当然,这只有在字典的值是不可变的时才有效,例如字符串或元组。如果尝试对包含列表的字典执行此操作,它将以最糟糕的方式失败。
>>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5} >>> {value:key for key, value in a_dict.items()} Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <dictcomp> TypeError: unhashable type: 'list'
⁂
集合也不甘落后,它们也有自己的推导式语法。它与字典推导式的语法非常相似。唯一的区别是集合只有值,而不是键:值对。
>>> a_set = set(range(10)) >>> a_set {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} >>> {x ** 2 for x in a_set} ① {0, 1, 4, 81, 64, 9, 16, 49, 25, 36} >>> {x for x in a_set if x % 2 == 0} ② {0, 8, 2, 4, 6} >>> {2**x for x in range(10)} ③ {32, 1, 2, 4, 8, 64, 128, 256, 16, 512}
9
的数字集合的平方。if
子句,以筛选每个项目,然后再将其返回到结果集中。⁂
os
模块os
— 可移植访问特定于操作系统的功能os.path
模块os.path
— 与平台无关的文件名操作glob
模块glob
— 文件名模式匹配time
模块time
— 用于操作时钟时间的函数© 2001–11 马克·皮尔格里姆