1.4 Python语言基础要素
既然你已经学会了如何创建和运行Python脚本,那么以前需要手动实现的业务过程,现在完全可以通过编写Python脚本来自动化和规模化地完成。后面几章会详细介绍如何使用Python脚本来自动化和规模化地完成任务,但是在进行下一部分内容之前,还需要掌握更多的Python语言基础要素。通过掌握更多的基础要素,你会对Python有更深入的理解,在后面的章节中就可以综合运用这些知识来完成具体的数据处理任务。首先,本节介绍Python中最常用的数据类型,然后讨论使用if语句和函数来处理数据的方法。在此之后,从实际出发,介绍如何使用Python读写文本文件和CSV文件。
1.4.1 数值
Python有好几种内置数值类型。数值类型非常有用,因为很多商业应用需要对数值进行分析和处理。Python中最主要的4种数值类型是整数、浮点数、长整数和复数。这里只介绍整数和浮点数,因为它们在商业应用中是最常用的。你可以把下面处理整数和浮点数的示例添加到first_script.py中,放在现有的代码下面,然后重新运行脚本,在屏幕上检查输出。
1.整数
下面直接看几个带有整数的示例:
x = 9 print("Output #4: {0}".format(x)) print("Output #5: {0}".format(3**4)) print("Output #6: {0}".format(int(8.3)/int(2.7)))
Output #4展示了如何将一个整数(数字9)赋给变量x,然后将变量x打印出来。Output #5说明了如何得到3的4次方(等于81)并将结果打印出来。Output #6演示了将数值转换成整数并进行除法运算的方法。数值通过内置的int函数转换成整数,所以算式变成了8除以2,结果为4.0。
2.浮点数
和整数一样,浮点数(即带小数点的数)对很多商业应用来说也是非常重要的。下面是几个带有浮点数的示例:
print("Output #7: {0:.3f}".format(8.3/2.7)) y = 2.5*4.8 print("Output #8: {0:.1f}".format(y)) r = 8/float(3) print("Output #9: {0:.2f}".format(r)) print("Output #10: {0:.4f}".format(8.0/3))
Output #7和Output #6非常相似,除了将两个相除的数保留为浮点数,这样算式就是8.3除以2.7,大约等于3.074。这个示例中print语句的语法,"{0:.3f}".format(floating_point_number/floating_point_number),说明了如何设置print语句中的小数位数。在这个示例中,.3f设定了打印的输出值应该有3位小数。
Output #8表示用2.5乘以4.8,将结果赋给变量y,然后将结果打印出来,带有一位小数。这两个浮点数相乘的结果是12,所以打印出的值是12.0。Output #9和Output #10表示以两种方式计算8除以3,结果都是一个浮点数,大约等于2.667。
type函数
Python提供一个名为type的函数,你可以对所有对象调用这个函数,来获得关于Python如何处理这个对象的更多信息。如果你对一个数值变量调用这个函数,它会告诉你这个数值是整数还是浮点数,还会告诉你这个数值是否能当作字符串进行处理。函数的语法非常简单:type(varible)会返回Python中的数据类型。此外,因为Python是一种“面向对象”的语言,所以你可以对Python中所有命名对象调用type函数,不仅是变量,还有函数、语句等。如果你的代码出现了意外的错误,调用type函数可以帮助你进行错误诊断。
在Python中进行数值处理时,需要知道的非常重要的一点是,你可以使用几种标准库模块和内置函数与模块来进行常见的数学计算。你已经使用了两个内置函数来处理数值,分别是int和float。另一个有用的标准模块是math。
Python标准模块随着Python基本程序一起安装在你的计算机中,但是当你新建一个脚本时,计算机只加载一些非常基本的操作(这就是Python启动非常快的一个原因)。要想使用math模块中的一些函数,只需在脚本开头shebang行的下方添加from math import[function name]。例如,可以将下面的代码行添加到first_script.py中,在shebang行下面:
#!/usr/bin/env python3 from math import exp, log, sqrt
如果在first_script.py中添加了这一行,你就有3个有用的数学函数可以任意使用了。函数exp、log和sqrt分别表示e的乘方、自然对数和平方根。下面是使用math模块中的函数进行计算的几个示例:
print("Output #11: {0:.4f}".format(exp(3))) print("Output #12: {0:.2f}".format(log(4))) print("Output #13: {0:.1f}".format(sqrt(81)))
这3种数学函数的计算结果是浮点数,分别约为20.0855、1.39和9.0。
这只是math模块中一点微小的功能。Python中还有很多有用的数学函数和模块,用于商业、科学、统计和其他应用,本书还会对此进行更多的讨论。关于数学模块和其他标准模块以及内置函数的更多信-息,可以参考Python标准库(https://docs.python.org/3/library/index.html)。
1.4.2 字符串
字符串是Python中的另一种基本数据类型。它通常是指人类可以阅读的文本,你可以这么理解。但更广泛地说,它是一个字符序列,并且字符只有在组成这个序列时才有意义。很多商业应用中都有字符串类型的数据,比如供应商和客户的名字及地址、评价和反馈数据、事件日志和文档记录。一些对象看上去是整数,但实际上是字符串,比如邮政编码。邮政编码01111(马萨诸塞州斯普林菲尔德)和整数1111是不一样的,你不能对邮政编码做加减乘除,所以最好在代码中将邮政编码作为字符串来处理。这一节将介绍用于字符串管理的一些模块、函数和操作。
字符串可以包含在单引号、双引号、3个单引号或3个双引号之间。下面是字符串的几个示例:
print("Output #14: {0:s}".format('I\'m enjoying learning Python.')) print("Output #15: {0:s}".format("This is a long string. Without the backslash\ it would run off of the page on the right in the text editor and be very\ difficult to read and edit. By using the backslash you can split the long\ string into smaller strings on separate lines so that the whole string is easy\ to view in the text editor.")) print("Output #16: {0:s}".format('''You can use triple single quotes for multi-line comment strings.''')) print("Output #17: {0:s}".format("""You can also use triple double quotes for multi-line comment strings."""))
Output #14和本章开头的示例非常像。它展示了一个包含在单引号之间的简单字符串。这个print语句的结果是"I'm enjoying learning Python."。请记住,如果用双引号来包含这个字符串的话,就不需要在"I'm"的单引号前面使用反斜杠了。
Output #15展示了如何使用反斜杠将一个非常长的字符串分段显示在多个行中,以使它易于理解和编辑。尽管这个字符串分布在脚本的多个行中,它还是一个字符串,并作为一个完整的字符串被打印出来。在这种将长字符串分成多行的方法中,要注意的是反斜杠必须是每行的最后一个字符。如果你意外地按了一下空格键,反斜杠后面就会出现一个看不见的空格,脚本就会抛出一个语法错误,不能正常运行。因此,使用3个单引号或3个双引号来创建多行字符串更稳妥一些。
Output #16和Output #17展示了如何使用3个单引号和3个双引号来创建多行字符串。这两个示例的输出如下:
Output #16: You can use triple single quotes for multi-line comment strings. Output #17: You can also use triple double quotes
for multi-line comment strings.
当你使用3个单引号或3个双引号时,不需要在前面一行的末尾加上反斜杠。还有,请注意一下Output #15和Output #16以及Output #17在打印到屏幕之后的区别。Output #15使用在行尾添加反斜杠的方式将字符串分成多行,使每行代码更短并且更易于理解,但还是作为一行长文本打印到屏幕上。与之相反,Output #16和Output #17使用3个单引号和3个双引号创建多行字符串,打印到屏幕上后也是分行的。
和数值类型一样,也有很多标准模块、内置函数和操作符可以用来管理字符串。常用的操作符和函数包括+、*和len。下面就是几个使用这些操作符处理字符串的示例:
string1 = "This is a " string2 = "short string." sentence = string1 + string2 print("Output #18: {0:s}".format(sentence)) print("Output #19: {0:s} {1:s}{2:s}".format("She is", "very "*4, "beautiful.")) m = len(sentence) print("Output #20: {0:d}".format(m))
Output #18展示了如何使用+操作符来将两个字符串相加。这个print语句的输出结果是This is a short string。+操作符将两个字符串按照原样相加,所以如果你想在结果字符串中留出空格的话,就必须在原字符串中加上空格(例如:在Output #18中,要在字母“a”后面加空格)或者在原字符串之间加上空格(例如:在Output #19中,要在“very”后面加上空格)。
Output #19展示了如何使用*操作符将字符串重复一定的次数。在这个示例中,结果字符串包含了字符串“very”(就是very后面跟着一个空格)的4个副本。
Output #20展示了如何使用内置函数len来确定字符串中字符的数量。函数len将空格与标点符号也计入字符串长度。所以,在Output #20中,字符串This is a short string.的长度是23个字符。
处理字符串的一个常用标准库模块是string。在string模块中,你可以使用多个函数来有效管理字符串。下面用示例说明了使用这些字符串函数的方法。
1.split
下面的两个示例展示了如何使用split函数来将一个字符串拆分成一个子字符串列表,列表中的子字符串正好可以构成原字符串。(列表是Python中的另一种内置数据类型,本章后面将会讨论。)split函数可以在括号中使用两个附加参数。第一个附加参数表示使用哪个字符进行拆分。第二个附加参数表示进行拆分的次数(例如:进行两次拆分,可以得到3个子字符串):
string1 = "My deliverable is due in May" string1_list1 = string1.split() string1_list2 = string1.split(" ",2) print("Output #21: {0}".format(string1_list1)) print("Output #22: FIRST PIECE:{0} SECOND PIECE:{1} THIRD PIECE:{2}"\ .format(string1_list2[0], string1_list2[1], string1_list2[2])) string2 = "Your,deliverable,is,due,in,June" string2_list = string2.split(',') print("Output #23: {0}".format(string2_list)) print("Output #24: {0} {1} {2}".format(string2_list[1], string2_list[5],\ string2_list[-1]))
在Output #21中,括号中没有附加参数,所以split函数使用空格字符(默认值)对字符串进行拆分。因为这个字符串中有5个空格,所以字符串被拆分成具有6个子字符串的列表。新生成的列表为['My', 'deliverable', 'is', 'due', 'in', 'May']。
Output #22明确地在split函数中包含了两个附加参数。第一个附加参数是" ",说明想用空格来拆分字符串。第二个附加参数是2,说明只想使用前两个空格进行拆分。因为设定了拆分两次,所以会生成一个带有3个元素的列表。第二个附加参数会在你解析数据的时候派上用场。举例来说,你可能会解析一个日志文件,文件中包含时间戳、错误代码和由空格分隔的错误信息。在这种情况下,你应该使用前两个空格进行拆分,解析出时间戳和错误代码,但是不使用剩下的空格进行拆分,以便完整无缺地保留错误信息。
在Output #23和Output #24中,括号中的附加参数是个逗号。在这种情况下,split函数在出现逗号的位置拆分字符串。结果列表为['Your', 'deliverable', 'is', 'due', 'in', 'June']。
2.join
下面的示例展示了如何使用join函数将列表中的子字符串组合成一个字符串。join函数将一个参数放在join前面,表示使用这个字符(或字符串)在子字符串之间进行组合:
print("Output #25: {0}".format(','.join(string2_list)))
在这个示例中,附加参数为一个逗号,位于圆括号中。所以join函数将子字符串组合成一个字符串,子字符串之间为逗号。因为列表中有6个子字符串,所以子字符串被组合成一个字符串,子字符串之间有5个逗号。新生成的字符串是Your,deliverable,is,due,in,June。
3.strip
下面两组示例展示了如何使用strip、lstrip和rstrip函数从字符串两端删除不想要的字符。这3个函数都可以在括号中使用一个附加参数来设定要从字符串两端删除的字符(或字符串)。
第一组示例展示了如何使用lstrip、rstrip和strip函数分别从字符串的左侧、右侧和两侧删除空格、制表符和换行符:
string3 = " Remove unwanted characters from this string.\t\t \n" print("Output #26: string3: {0:s}".format(string3)) string3_lstrip = string3.lstrip() print("Output #27: lstrip: {0:s}".format(string3_lstrip)) string3_rstrip = string3.rstrip() print("Output #28: rstrip: {0:s}".format(string3_rstrip)) string3_strip = string3.strip() print("Output #29: strip: {0:s}".format(string3_strip))
string3的左侧含有几个空格。此外,右侧包含制表符(\t)、几个空格和换行符(\n)。如果以前你没有见过\t和\n,那么现在知道了这是计算机中表示制表符和换行符的方法。
在Output #26中,你会看到句子前面有空白字符;在句子下面有一个空行,因为有换行符;在句子后面你看不到制表符和空格,但它们确实在那儿。Output #27、Output #28和Output #29分别展示了从字符串左侧、右侧和两侧删除空格、制表符和换行符的方法。{0:s}中的s表示传入print语句的值应该格式化为一个字符串。
第二组示例展示了从字符串两端删除其他字符的方法,将这些字符作为strip函数的附加参数即可。
string4 = "$$Here's another string that has unwanted characters.__---++" print("Output #30: {0:s}".format(string4)) string4 = "$$The unwanted characters have been removed.__---++" string4_strip = string4.strip('$_-+') print("Output #31: {0:s}".format(string4_strip))
在这组示例中,美元符号($)、下划线(_)、短划线(-)和加号(+)需要从字符串两端删除。通过将这些字符作为附加参数,可以通知程序从字符串两端删除它们。在Output #31中,结果字符串为The unwanted characters have been removed.。
4.replace
下面两个示例展示了如何使用replace函数将字符串中的一个或一组字符替换为另一个或另一组字符。这个函数在括号中使用两个附加参数,第一个参数是要在字符串中查找替换的字符或一组字符,第二个参数是要用来替换掉第一个参数的字符或一组字符:
string5 = "Let's replace the spaces in this sentence with other characters." string5_replace = string5.replace(" ", "!@!") print("Output #32 (with !@!): {0:s}".format(string5_replace)) string5_replace = string5.replace(" ", ",") print("Output #33 (with commas): {0:s}".format(string5_replace))
Output #32展示了如何使用replace函数将字符串中的空格替换为!@!。结果字符串为Let's!@!replace!@!the!@!spaces !@!in!@!this!@!sentence!@!with!@!other!@!characters.。
Output #33展示了如何使用逗号替换字符串中的空格。结果字符串为Let's,replace,the, spaces,in,this,sentence,with,other,characters.。
5.lower、upper、capitalize
最后3个示例展示了如何使用lower、upper和capitalize函数。lower和upper函数分别用来将字符串中的字母转换为小写和大写。capitalize函数对字符串中的第一个字母应用upper函数,对其余的字母应用lower函数:
string6 = "Here's WHAT Happens WHEN You Use lower." print("Output #34: {0:s}".format(string6.lower())) string7 = "Here's what Happens when You Use UPPER." print("Output #35: {0:s}".format(string7.upper())) string5 = "here's WHAT Happens WHEN you use Capitalize." print("Output #36: {0:s}".format(string5.capitalize())) string5_list = string5.split() print("Output #37 (on each word):") for word in string5_list: print("{0:s}".format(word.capitalize()))
Output #34和Output #35是对lower和upper函数最直接的应用。在字符串上应用了这些函数之后,string6中的所有字母都是小写,string7中的所有字母都是大写。
Output #36和Output #37演示了capitalize函数。Output #36说明了capitalize函数对字符串中的第一个字母应用upper函数,对其余的字母应用lower函数。Output #37将capitalize函数放在一个for循环中。for循环是一个控制流结构,将在后面详细讨论,现在不妨先看一下。
代码for word in string5_list的意义是这样的:“对于string5_list这个列表中的每个元素,需要做点事情。”下面的代码print word.capitalize()说明了对于列表中的每个元素要做的事情。这两行代码放在一起的意义就是:“对于string5_list这个列表中的每个元素,先应用capitalize函数,然后再打印出来。”代码执行的结果就是列表中的每个单词首字母大写,其余字母小写。
Python中还有更多管理字符串的模块和函数。和内置函数math一样,你可以参考Python标准库(https://docs.python.org/3/library/index.html)来获取更多信息。
1.4.3 正则表达式与模式匹配
很多商业分析都依赖模式匹配,也称为正则表达式(regular expression)。举例来说,你可能需要分析一下来自某个州(比如马里兰州)的所有订单。在这种情况下,你需要识别的模式就是Maryland这个单词。同样,你还可能需要分析一下来自某个供应商(比如StaplesRUs)的商品质量,那么你要识别的模式就是StaplesRUs。
Python包含了re模块,它提供了在文本中搜索特定模式(也就是正则表达式)的强大功能。要在脚本中使用re模块提供的功能,需要在脚本上方加入import re这行代码,放在上一个import语句之后。现在first_script.py的上方应该是这样的:
#!/usr/bin/env python3 from math import exp, log, sqrt import re
通过导入re模块,你可以使用一大波函数和元字符来创建和搜索任意复杂的模式。元字符(metacharacter)是正则表达式中具有特殊意义的字符。每个元字符都有特殊意义,它们使正则表达式能够匹配特定的字符串。常用的元字符包括|、()、[]、.、*、+、?、^、$和(?P<name>)。如果你在正则表达式中见到这些字符,要知道程序不是要搜索这些字符本身,而是要搜索它们描述的东西。你可以在Python标准库中的“Regular Expression Operations”一节中获得关于元字符的更多信息(https://docs.python.org/3/library/re.html)。
re模块中还包括很多有用的函数,用于创建和搜索特定的模式(本节要介绍的函数包括re.compile、re.search、re.sub、re.ignorecase和re.I)。来看一下示例代码:
# 计算字符串中模式出现的次数 string = "The quick brown fox jumps over the lazy dog." string_list = string.split() pattern = re.compile(r"The", re.I) count = 0 for word in string_list: if pattern.search(word): count += 1 print("Output #38: {0:d}".format(count))
第一行将字符串变量string赋值为The quick brown fox jumps over the lazy dog.。下一行将字符串拆分列表,列表中的每个元素都是一个单词。
再下一行使用re.compile和re.I函数以及用r表示的原始字符串,创建一个名为pattern的正则表达式。re.compile函数将文本形式的模式编译成为编译后的正则表达式。正则表达式不是必须编译的,但是编译是个好习惯,因为这样可以显著地提高程序运行速度。re.I函数确保模式是不区分大小写的,能同时在字符串中匹配“The”和“the”。原始字符串标r可以确保Python不处理字符串中的转义字符,比如\、\t或\n。这样在进行模式匹配时,字符串中的转义字符和正则表达式中的元字符就不会有意外的冲突。在上面的例子中,字符串中没有转义字符,所以r不是必需的,但是在正则表达式中使用原始字符串标志是一个好习惯。接下来的一行代码创建了一个变量count来保存字符串中模式出现的次数,初始值设为0。
再下一行是个for循环语句,在列表变量string_list的各个元素之间进行迭代。它取出的第一个元素是“The”这个单词,取出的第二个元素是“quick”这个单词,以此类推,直到取出列表中所有的单词。接下来的一行使用re.search函数将列表中的每个单词与正则表达式进行比较。如果这个单词与正则表达式相匹配,函数就返回True,否则就返回None或False。所以if语句的意义就是:如果单词与正则表达式匹配,那么count的值就加1。
最后,print语句打印出正则表达式在字符串中找到模式“The”(不区分大小写)的次数,在本例中,找到了两次。
太可怕了!
正则表达式在进行搜索时的确功能强大,但是非常难以读懂(它曾被称为“只用来写的语言”),所以当你第一次没有看懂的时候,不用太在意,连专家都不一定能看懂!
当你逐渐熟悉了正则表达式后,使用它们得到你想要的结果就简单了。要想愉快地学习正则表达式,你可以参考一下Google研发总监Peter Norvig的工作,他创建了一个正则表达式,这个表达式可以匹配美国总统的名字,并且可以筛掉失败的总统竞选人,网址为https://www.oreilly.com/learning/regex-golf-with-peter-norvig。
再看另一个示例:
# 在字符串中每次找到模式时将其打印出来 string = "The quick brown fox jumps over the lazy dog." string_list = string.split() pattern = re.compile(r"(?P<match_word>The)", re.I)</match_word> print("Output #39:") for word in string_list: if pattern.search(word): print("{:s}".format(pattern.search(word).group('match_word')
第二个示例与第一个示例的区别在于,这个示例是想在屏幕上打印出每个匹配的字符串,而不是匹配的次数。要想得到匹配的字符串,将它们打印到屏幕上或存储在文件中,需要使用(?P<name>)元字符和group函数。这个示例中的多数代码和前一个示例中讨论过的代码是一样的,所以这里重点解释新的部分。
第一个新代码片段是(?P<name>),这是一个出现在re.compile函数中的元字符。这个元字符使匹配的字符串可以在后面的程序中通过组名符号<name>来引用。在这个示例中,这个组被称为<match_word>。
最后一个新代码片段出现在if语句中。这个代码片段的意义是:“如果结果为True(也就是说,如果单词与模式匹配),那么就在search函数返回的数据结构中找出match_word组中的值,并把这些值打印在屏幕上。”
下面是最后一个示例:
# 使用字母“a”替换字符串中的单词“the” string = "The quick brown fox jumps over the lazy dog." string_to_find = r"The" pattern = re.compile(string_to_find, re.I) print("Output #40: {:s}".format(pattern.sub("a", string)
最后一个示例展示了如何使用re.sub函数来在文本中用一种模式替换另一种模式。再说一遍,这个示例中的多数代码和前两个示例中讨论过的代码是一样的,所以这里重点解释新的部分。
第一个新代码片段将正则表达式赋给变量pattern,于是这个变量可以被传入到re.compile函数中。解释一下,在调用re.compile函数之前,将正则表达式赋给变量不是必需的;但是,如果你的正则表达式特别长而且复杂的话,将它赋给一个变量然后将变量传给re.compile函数这种做法可以使你的代码更利于理解。
最后一个新代码片段在最后一行。这个代码片段使用re.sub函数以不区分大小写的方式在变量string中寻找模式,然后将发现的每个模式替换成字母a。这次替换的最终结果是a quick brown fox jumps over a lazy dog.。
更多关于正则表达式函数的信息可以在Python标准库(https://docs.python.org/3/library/index.html)中找到,也可以参考MichaelFitzgerald的著作《学习正则表达式》。
1.4.4 日期
日期在大多数商业应用中都是必不可少的。你需要知道一个事件在何时发生,距离这件事发生还有多少时间,或者几个事件之间的时间间隔。因为日期是很多应用的核心,也因为日期是一种非常不寻常的数据,在处理时经常要乘以60或24,也有“差不多30分钟”和“几乎是365天和一个季度”这样的说法,所以在Python中对日期有特殊的处理方式。
Python中包含了datetime模块,它提供了非常强大的功能来处理日期和时间。要想在脚本中使用datetime模块提供的功能,需要在脚本上方加入from datetime import date, time, datetime, timedelta,放在之前的import语句下面。现在first_script.py的上方应该是这样的:
#!/usr/bin/env python3 from math import exp, log, sqrt import re from datetime import date, time, datetime, timedelta
导入datetime模块之后,就有各式各样的日期时间对象和函数供你随意使用了。常用的对象和函数包括today、year、month、day、timedelta、strftime和strptime。这些函数可以捕获具体的日期数据(例如:年、月、日)、进行日期和时间的加减运算、创建特定形式的日期字符串以及根据日期字符串创建datetime对象。下面是使用这些datetime对象和函数的几个示例。第一组示例演示了date对象和datetime对象之间的区别:
# 打印出今天的日期形式,以及年、月、日 today = date.today() print("Output #41: today: {0!s}".format(today)) print("Output #42: {0!s}".format(today.year)) print("Output #43: {0!s}".format(today.month)) print("Output #44: {0!s}".format(today.day)) current_datetime = datetime.today() print("Output #45: {0!s}".format(current_datetime))
通过使用date.today(),你可以创建一个date对象,其中包含了年、月、日,但不包含时间元素,比如时、分、秒。相反,通过datetime.today()创建的对象则包含时间元素。{0!s}中的!s表示传入到print语句中的值应该格式化为字符串,尽管它是个数值型数据。最后,你可以使用year、month和day来捕获具体的日期元素。
下一个示例演示了如何使用timedelta函数来对date对象进行时间的加减操作:
# 使用timedelta计算一个新日期 one_day = timedelta(days=-1) yesterday = today + one_day print("Output #46: yesterday: {0!s}".format(yesterday)) eight_hours = timedelta(hours=-8) print("Output #47: {0!s} {1!s}".format(eight_hours.days, eight_hours.seconds))
在这个示例中,使用timedelta函数从今天减去了1天。当然,还可以在括号中使用days=10、hours=-8或者weeks=2来创建变量,分别表示未来10天、以前8个小时或者未来2个星期。
在使用timedelta时需要注意的一点是,它将括号中的时间差以天、秒和毫秒的形式存储,然后将数值规范化后得到一个唯一的值。这说明分钟、小时和星期会被分别转换成60秒、3600秒和7天,然后规范化,就是生成天、秒和毫秒“列”(类似于小学数学中的个位、十位等等)。举例来说,hours=-8的输出是(-1 days, 57,600 seconds),不是更简单的(-28,800 seconds)。是这样计算的:86400秒(3600秒每小时*24小时每天)-28 800秒(3600秒每小时*8小时)= 57 600秒。正如你所见,对负值的规范化乍看上去很令人吃惊,特别是在进行取整和舍入时。
第三个示例展示了如何从一个date对象中减去另一个。相减的结果是个datetime对象,将所得的差以天、小时、分钟和秒来显示。例如,在这个示例中结果是“1 day, 0:00:00”:
# 计算出两个日期之间的天数 date_diff = today - yesterday print("Output #48: {0!s}".format(date_diff)) print("Output #49: {0!s}".format(str(date_diff).split()[0]))
在某些情况下,你可能只需要结果中的数值部分。举例来说,在这个示例中你只需要数值1。从结果中得到这个数值的一种方法是使用前面已经讨论过的字符串函数。str函数可以将结果转换成字符串;split函数可以使用空白字符将字符串拆分,并使每个子字符串成为列表的一个元素;[0]表示“取出列表中的第一个元素”,在本例中就是数值1。在1.4.5节中还会看到[0]这种语法,因为它是列表索引,可以用来从列表中取出特定的元素。
第四组示例展示了如何使用strftime函数根据一个date对象来创建具有特定格式的字符串:
# 根据一个日期对象创建具有特定格式的字符串 print("Output #50: {:s}".format(today.strftime('%m/%d/%Y'))) print("Output #51: {:s}".format(today.strftime('%b %d, %Y'))) print("Output #52: {:s}".format(today.strftime('%Y-%m-%d'))) print("Output #53: {:s}".format(today.strftime('%B %d, %Y')))
在我写这一章的时候,当天的4种打印形式如下:
01/28/2016 Jan 28, 2016 2016-01-28 January 28, 2016
这4个示例说明了如何使用格式符来创建不同格式的日期字符串,格式符包括%Y、%B、%b、%m和%d。你可以在Python标准库(https://docs.python.org/3/library/datetime.html)的“datetime—Basic date and time types”一节中找到datetime模块使用的其他格式符。
# 根据一个表示日期的字符串 # 创建一个带有特殊格式的datetime对象 date1 = today.strftime('%m/%d/%Y') date2 = today.strftime('%b %d, %Y') date3 = today.strftime('%Y-%m-%d') date4 = today.strftime('%B %d, %Y') # 基于4个具有不同日期格式的字符串 # 创建2个datetime对象和2个date对象 print("Output #54: {!s}".format(datetime.strptime(date1, '%m/%d/%Y'))) print("Output #55: {!s}".format(datetime.strptime(date2, '%b %d, %Y'))) # 仅显示日期部分 print("Output #56: {!s}".format(datetime.date(datetime.strptime\ (date3, '%Y-%m-%d')))) print("Output #57: {!s}".format(datetime.date(datetime.strptime\ (date4, '%B %d, %Y'))))
第五组示例展示了如何使用strptime函数根据具有特定形式的日期字符串来创建datetime对象。在这个示例中,date1、date2、date3和date4是字符串变量,以不同的形式表示今天。前两个print语句展示了将前两个字符串变量date1和date2转换成datetime对象的结果。要使它们正确工作,strptime函数中使用的形式需要和传入函数的字符串变量的形式相匹配。这两个print语句的输出结果是个datetime对象,2014-01-28 00:00:00。
有些时候,你可能只对datetime对象中的日期部分感兴趣。在这种情况下,你可以使用嵌套的函数(在最后两个print语句中,是date和strptime)将日期字符串变量转换为datetime对象,然后仅返回datetime对象的日期部分。这些print语句的结果是2014-01-28。当然,你不需要立刻打印出这个值。你可以将这个日期赋给一个新变量,然后使用这个变量进行计算,洞悉随着时间产生的业务数据。
1.4.5 列表
很多商业分析中都使用列表。你会维护各种客户列表、产品列表、资产列表、销售量列表,等等。但是Python中的列表(对象的可排序集合)更加灵活!上面那些列表中包含的都是相似的对象(例如:包含客户姓名的字符串或代表销售量的浮点数),但是Python中的列表可不止这么简单。它可以包含数值、字符串、其他列表、元组和字典(本章稍后介绍)的任意组合。因为列表在商业应用中使用广泛、灵活性高、作用突出,所以掌握如何在Python中操作列表是极其重要的。
如你所愿,Python提供了很多有用的函数和操作符来管理列表。以下演示了最常用的和最有效的列表函数和操作符的使用方法。
1.创建列表
# 使用方括号创建一个列表 # 用len()计算列表中元素的数量 # 用max()和min()找出最大值和最小值 # 用count()计算出列表中某个值出现的次数 a_list = [1, 2, 3] print("Output #58: {}".format(a_list)) print("Output #59: a_list has {} elements.".format(len(a_list))) print("Output #60: the maximum value in a_list is {}.".format(max(a_list))) print("Output #61: the minimum value in a_list is {}.".format(min(a_list))) another_list = ['printer', 5, ['star', 'circle', 9]] print("Output #62: {}".format(another_list)) print("Output #63: another_list also has {} elements.".format\ (len(another_list))) print("Output #64: 5 is in another_list {} time.".format(another_list.count(5)))
这个示例展示了如何创建两个简单列表,a_list和another_list。将元素放在方括号之间就可以创建列表。a_list包含数值1、2和3。another_list包含一个字符串printer、一个数值5以及一个包含了两个字符串和一个数值的列表。
这个示例还展示了如何使用4种列表函数:len、min、max和count。len返回列表中元素的个数。min和max分别返回列表中的最小值和最大值。count返回列表中某个元素出现的次数。
2.索引值
# 使用索引值访问列表中的特定元素 # [0]是第1个元素,[-1]是最后一个元素 print("Output #65: {}".format(a_list[0])) print("Output #66: {}".format(a_list[1])) print("Output #67: {}".format(a_list[2])) print("Output #68: {}".format(a_list[-1])) print("Output #69: {}".format(a_list[-2])) print("Output #70: {}".format(a_list[-3])) print("Output #71: {}".format(another_list[2])) print("Output #72: {}".format(another_list[-1]))
这个示例说明了如何使用索引值来引用列表中的特定元素。列表索引值从0开始,所以你可以通过在列表名称后面的方括号中放入0来引用列表中的第一个元素。示例中的第一个print语句print a_list[0]打印出a_list中的第一个元素,即数值1。a_list[1]引用的是列表中的第二个元素,a_list[2]引用的是列表中的第三个元素,以此类推直到列表结束。
这个示例还说明了你可以使用负索引值从列表尾部引用列表元素。列表尾部的索引值从-1开始,所以你可以通过在列表名称后面的方括号中放入-1来引用列表中的最后一个元素。第四个print语句print a_list[-1]打印出a_list中的最后一个元素,即数值3。a_list[-2]打印出列表中倒数第二个元素,a_list[-3]打印出列表中倒数第三个元素,以此类推直到列表开头。
3.列表切片
# 使用列表切片访问列表元素的一个子集 # 从开头开始切片,可以省略第1个索引值 # 一直切片到末尾,可以省略第2个索引值 print("Output #73: {}".format(a_list[0:2])) print("Output #74: {}".format(another_list[:2])) print("Output #75: {}".format(a_list[1:3])) print("Output #76: {}".format(another_list[1:]
这个示例展示了如何使用列表切片引用列表元素的一个子集。在列表名称后面的方括号中放入由冒号隔开的两个索引,就可以创建一个列表切片。列表切片引用的是列表中从第一个索引值到第二个索引值的前一个元素。举例来说,第一个print语句print a_list[0:2]的意义就是:“打印出a_list中索引值为0和1的元素”。这个print语句打印出[1, 2],因为这是列表中的前两个元素。
这个示例还说明,如果从列表开头开始切片,就可以省略第一个索引值,如果一直切片到列表末尾,就可以省略第二个索引值。举例来说,最后一个print语句print another_list[1:]的意义就是:“从列表中第二个元素开始,打印出another_list中其余所有的元素。”这个print语句打印出[5,['star', 'circle', 9]],因为这是列表中最后两个元素。
4.列表复制
# 使用[:]复制一个列表 a_new_list = a_list[:] print("Output #77: {}".format(a_new_list))
这个示例展示了如何复制一个列表。如果你需要对列表进行某种操作,比如添加或删除元素,或对列表进行排序,但你还希望原始列表保持不变,这时这个功能就非常重要了。要复制一个列表,在列表名称后面的方括号中放入一个冒号,然后将其赋给一个新的变量即可。在这个示例中,a_new_list是a_list的一个完美复制,所以你可以对a_new_list添加或删除元素,也可以对a_new_list进行排序,而不会影响a_list。
5.列表连接
# 使用+将两个或更多个列表连接起来 a_longer_list = a_list + another_list print("Output #78: {}".format(a_longer_list))
这个示例展示了如何将两个或更多个列表连接在一起。当你必须分别引用多个具有相似信息的列表,但希望将它们组合起来进行分析的时候,这个功能就非常重要了。举例来说,由于数据存储方式的原因,你可能需要生成一个销售量列表,其中包括来自于一个数据源和另一个数据源的销售量列表。要将两个销售量列表连接在一起进行分析,可以将两个列表名称用+操作符相加,然后赋给一个新变量。在这个示例中,a_long_list包含a_list和another_list中的所有元素,将它们连接在一起形成一个更长的列表。
6.使用in和not in
# 使用in和not in来检查列表中是否有特定元素 a = 2 in a_list print("Output #79: {}".format(a)) if 2 in a_list: print("Output #80: 2 is in {}.".format(a_list)) b = 6 not in a_list print("Output #81: {}".format(b)) if 6 not in a_list: print("Output #82: 6 is not in {}.".format(a_list))
这个示例展示了如何使用in和not in来检查列表中是否存在某个特定元素。这些表达式的结果是True或False,取决于表达式为真还是假。这个功能在商业应用中非常重要,因为你可以使用它在程序中添加有意义的业务逻辑。举例来说,它们经常应用于if语句,比如“如果SupplierY在SupplierList中,那么做些什么事,否则做些其他事。”本章后面的内容中将会介绍更多if语句和其他控制流表达式的示例。
7.追加、删除和弹出元素
# 使用append()向列表末尾追加一个新元素 # 使用remove()从列表中删除一个特定元素 # 使用pop()从列表末尾删除一个元素 a_list.append(4) a_list.append(5) a_list.append(6) print("Output #83: {}".format(a_list)) a_list.remove(5) print("Output #84: {}".format(a_list)) a_list.pop() a_list.pop() print("Output #85: {}".format(a_list))
这个示例展示了向列表中添加元素和从列表中删除元素的方法。append方法将一个新元素追加到列表末尾。你可以使用该方法按照具体业务规则创建列表。举例来说,要建立一个关于CustomerX的采购量的列表,可以先创建一个名为CustomerX的空列表,然后在记录所有客户采购量的主列表中扫描,如果在主列表中发现了CustomerX,就可以将这个采购量数据追加到列表CustomerX中。
remove方法可以删除列表中的任意元素。你可以使用该方法删除列表中的错误元素和输入错误,还可以按照具体业务规则删除列表中的元素。在这个示例中,remove方法从a_list中删除了数值5。
pop方法删除列表中的最后一个元素。和remove方法相似,你可以使用pop方法删除列表末尾的错误元素和输入错误,还可以按照具体的业务规则从列表末尾删除元素。在这个示例中,对pop方法的两次调用从a_list中分别删除了数值6和数值4。
8.列表反转
# 使用reverse()原地反转一个列表会修改原列表 # 要想反转列表同时又不修改原列表,可以先复制列表 a_list.reverse() print("Output #86: {}".format(a_list)) a_list.reverse() print("Output #87: {}".format(a_list))
这个示例展示了使用reverse函数以in-place方式对列表进行反转的方法(原地反转)。“in-place”表示反转操作将原列表修改为顺序颠倒的新列表。举例来说,示例第一次调用reverse函数将a_list改变为[3, 2, 1]。第二次调用reverse函数则将a_list恢复到初始顺序。要想使用列表的反转形式而不修改原列表,可以先复制列表,然后对列表副本进行reverse操作。
9.列表排序
# 使用sort()对列表进行原地排序会修改原列表 # 要想对列表进行排序同时又不修改原列表,可以先复制列表 unordered_list = [3, 5, 1, 7, 2, 8, 4, 9, 0, 6] print("Output #88: {}".format(unordered_list)) list_copy = unordered_list[:] list_copy.sort() print("Output #89: {}".format(list_copy)) print("Output #90: {}".format(unordered_list))
这个示例展示了使用sort函数以in-place方式对列表进行排序的方法。和reverse函数一样,这种原地排序将原列表修改为排好顺序的新列表。要想使用排好顺序的列表而不修改原列表,可以先复制列表,然后对列表副本进行sort操作。
10.sorted排序函数
# 使用sorted()对一个列表集合按照列表中某个位置的元素进行排序 my_lists = [[1,2,3,4], [4,3,2,1], [2,4,1,3]] my_lists_sorted_by_index_3 = sorted(my_lists, key=lambda index_value:\ index_value[3]) print("Output #91: {}".format(my_lists_sorted_by_index_3))
这个示例展示了如何使用sorted函数以及关键字函数,对一个列表集合按照每个列表中特定索引位置的值进行排序。关键字函数设置用于列表排序的关键字。在这个示例中,关键字是一个lambda函数,表示使用索引位置为3的值(也就是列表中的第四个元素)对列表进行排序。(后续章节将会对lambda函数做更多讨论。)使用每个列表中的第四个元素作为排序关键字,应用sorted函数之后,第二个列表[4, 3, 2, 1]成为了第一个列表,第三个列表[2, 4, 1, 3]成为了第二个列表,第一个列表[1, 2, 3, 4]成为了第三个列表。另外,你应该知道sorted函数的排序与sort函数的in-place原地排序方式不同,sort函数改变了原列表的元素顺序,sorted函数则返回一个新的排好序的列表,并不改变原列表的元素顺序。
下一个排序示例使用operator标准模块,这个模块提供的功能可以使用多个关键字对列表、元组和字典进行排序。为了在脚本中使用operator模块中的itemgetter函数,在脚本上方添加from operator import itemgetter:
#!/usr/bin/env python3 from math import exp, log, sqrt import re from datetime import date, time, datetime, timedelta from operator import itemgetter
导入operator模块中的itemgetter函数后,你可以使用每个列表中多个索引位置的值对列表集合进行排序:
# 使用itemgetter()对一个列表集合按照两个索引位置来排序 my_lists = [[123,2,2,444], [22,6,6,444], [354,4,4,678], [236,5,5,678], \ [578,1,1,290], [461,1,1,290]] my_lists_sorted_by_index_3_and_0 = sorted(my_lists, key=itemgetter(3,0)) print("Output #92: {}".format(my_lists_sorted_by_index_3_and_0))
这个示例展示了如何使用sorted()函数和itemgetter函数按照每个列表中多个索引位置的值对列表集合进行排序。关键字函数设置用于列表排序的关键字。在这个示例中,关键字是包含两个索引值(3和0)的itemgetter函数。这个语句的意义是:“先按照索引位置3中的值对列表进行排序,然后,在这个排序基础上,按照索引位置0中的值对列表进一步排序。”
这种通过多个元素对列表和其他数据容器进行排序的方法非常重要,因为你经常需要按照多个值对数据进行排序。例如,如果要处理每天销售交易数据,你需要先按照日期再按照每天的交易量大小进行排序。或者,在处理供应商数据时,你会先按照供应商姓名再按照每个供应商的供货发票日期来排序。sorted函数和itemgetter函数可以实现这样的功能。
如果想获得列表函数的更多信息,可以参考Python标准库(https://docs.python.org/3/library/index.html)。
1.4.6 元组
元组除了不能被修改之外,其余特点与列表非常相似。正因为元组不能被修改,所以没有元组修改函数。你可能会感到奇怪,为什么要设计这两种如此相似的数据结构。这是因为元组具有可修改的列表无法实现的重要作用,例如作为字典键值。元组不如列表使用广泛,所以这里只是简略地介绍一下。
1.创建元组
# 使用圆括号创建元组 my_tuple = ('x', 'y', 'z') print("Output #93: {}".format(my_tuple)) print("Output #94: my_tuple has {} elements".format(len(my_tuple))) print("Output #95: {}".format(my_tuple[1])) longer_tuple = my_tuple + my_tuple print("Output #96: {}".format(longer_tuple))
这个示例展示了创建元组的方法。将元素放在括号中间,就可以创建一个元组。此示例还说明,前面讨论过的很多应用于列表的函数和操作符,也同样适用于元组。例如,len函数返回元组中元素的个数,元组索引和元组切片可以引用元组中特定的元素,+操作符可以连接多个元组。
2.元组解包
# 使用赋值操作符左侧的变量对元组进行解包 one, two, three = my_tuple print("Output #97: {0} {1} {2}".format(one, two, three)) var1 = 'red' var2 = 'robin' print("Output #98: {} {}".format(var1, var2)) # 在变量之间交换彼此的值 var1, var2 = var2, var1 print("Output #99: {} {}".format(var1, var2))
这个示例展示了元组的一个很有意思的操作——解包。可以将元组中的元素解包成为变量,在赋值操作符的左侧放上相应的变量就可以了。在这个示例中,字符串x、y和z被解包成为变量one、two和three的值。这个功能可以用来在变量之间交换变量值。在示例的最后一部分,var2的值被赋给var1,var1的值被赋给var2。Python会同时对元组的各个部分求值。这样,red robin变成了robin red。
3.元组转换成列表(及列表转换成元组)
# 将元组转换成列表,列表转换成元组 my_list = [1, 2, 3] my_tuple = ('x', 'y', 'z') print("Output #100: {}".format(tuple(my_list))) print("Output #101: {}".format(list(my_tuple)))
最后,可以将元组转换成列表,也可以将列表转换成元组。这个功能和可以将一个元素转换成字符串的str函数很相似。要将一个列表转换成元组,将列表名称放在tuple()函数中即可。同样,要将一个元组转换成列表,将元组名称放在list()函数中即可。
如果想获得元组的更多信息,可以参考Python标准库(https://docs.python.org/3/library/index.html)。
1.4.7 字典
Python中的字典本质上是包含各种带有唯一标识符的成对信息的列表。和列表一样,字典也广泛应用于各种商业分析。在商业分析中,可以用字典表示客户(以客户编码为键值),也可以用字典表示产品(以序列号或产品编号为键值),还可以用字典表示资产、销售量等。
在Python中,这样的数据结构称为字典,在其他编程语言中则称为关联数组、键-值存储和散列值。在商业分析中,列表和字典都是非常重要的数据结构,但是它们之间还存在着重要的区别,要想有效地使用字典,必须清楚这些区别。
在列表中,你可以使用被称为索引或索引值的连续整数来引用某个列表值。在字典中,要引用一个字典值,则可以使用整数、字符串或其他Python对象,这些统称为字典键。在唯一键值比连续整数更能反映出变量值含义的情况下,这个特点使字典比列表更实用。
在列表中,列表值是隐式排序的,因为索引是连续整数。在字典中,字典值则没有排序,因为索引不仅仅只是数值。你可以为字典中的项目定义排序操作,但是字典确实没有内置排序。
在列表中,为一个不存在的位置(索引)赋值是非法的。在字典中,则可以在必要的时候创建新的位置(键)。
因为没有排序,所以当你进行搜索或添加新值时,字典的响应时间更快(当你插入一个新项目时,计算机不需要重新分配索引值)。当处理的数据越来越多时,这是一个重要的考虑因素。
因为字典在商业应用中使用广泛、灵活性高、作用突出,所以掌握如何在Python中使用字典是极其重要的。下面的示例代码演示了最常用的和最有效的用于处理字典的函数和操作符的使用方法。
1.创建字典
# 使用花括号创建字典 # 用冒号分隔键-值对 # 用len()计算出字典中键-值对的数量 empty_dict = { } a_dict = {'one':1, 'two':2, 'three':3} print("Output #102: {}".format(a_dict)) print("Output #103: a_dict has {!s} elements".format(len(a_dict))) another_dict = {'x':'printer', 'y':5, 'z':['star', 'circle', 9]} print("Output #104: {}".format(another_dict)) print("Output #105: another_dict also has {!s} elements"\ .format(len(another_dict)))
这个示例展示了创建字典的方法。要创建一个空字典,将字典名称放在等号左侧,在等号右侧放上一对花括号即可。
示例中的第二个字典a_dict演示了向字典中添加键和值的一种方法。a_dict说明键和值是用冒号隔开的,花括号之间的键-值对则是用逗号隔开的。字典键是用单引号或双引号围住的字符串,字典值可以是字符串、数值、列表、其他字典或其他Python对象。在a_dict中,字典值是整数,但是在another_dict中,字典值是字符串、数值和列表。最后,这个示例说明了len函数返回的是字典中键-值对的个数。
2.引用字典中的值
# 使用键来引用字典中特定的值 print("Output #106: {}".format(a_dict['two'])) print("Output #107: {}".format(another_dict['z']
要引用字典中一个特定的值,需要使用字典名称、一对方括号和一个特定的键值(一个字符串)。在这个示例中,a_dict['two']的结果是整数2,another_dict['z']的结果是列表['star', 'circle', 9]。
3.复制
# 使用copy()复制一个字典 a_new_dict = a_dict.copy() print("Output #108: {}".format(a_new_dict))
要复制一个字典,先在字典名称后面加上copy函数,然后将这个表达式赋给一个新的字典即可。在这个示例中,a_new_dict是字典a_dict的一个副本。
4.键、值和项目
# 使用keys()、values()和items() # 分别引用字典中的键、值和键-值对 print("Output #109: {}".format(a_dict.keys())) a_dict_keys = a_dict.keys() print("Output #110: {}".format(a_dict_keys)) print("Output #111: {}".format(a_dict.values())) print("Output #112: {}".format(a_dict.items()))
要引用字典的键值,在字典名称后面加上keys函数即可。这个表达式的结果是包含字典键值的一个列表。要引用字典值,在字典名称后面加上values函数即可。这个表达式的结果是包含字典值的一个列表。
要想同时引用字典的键和值,在字典名称后面加上items函数即可。结果是一个列表,其中包含的是键-值对形式的元组。例如,a_dict.items()的结果是[('three', 3), ('two', 2), ('one', 1)]。在1.4.8节中会介绍如何使用for循环来对字典中的所有键和值进行解包和引用。
5.使用in、not in和get
if 'y' in another_dict: print("Output #114: y is a key in another_dict: {}."\ .format(another_dict.keys())) if 'c' not in another_dict: print("Output #115: c is not a key in another_dict: {}."\ .format(another_dict.keys())) print("Output #116: {!s}".format(a_dict.get('three'))) print("Output #117: {!s}".format(a_dict.get('four'))) print("Output #118: {!s}".format(a_dict.get('four', 'Not in dict')))
这个示例展示了测试字典中是否存在某个键值的两种方法。第一种方法是使用if语句、in或not in以及字典名称。使用in、if语句来测试y是否是another_dict中的一个键值。如果语句结果为真(也就是说如果y是another_dict中的一个键值),那么就执行print语句;否则,就不执行。这种if in和if not in语句经常用于测试是否存在某个键值,和其他语句一起使用时,还可以为字典添加新的键值。本书后续章节会有添加键值的示例。
缩进的作用
你应该注意到if语句后面的行是缩进的。Python使用缩进来表示一个语句是否属于一个逻辑上的“块”,也就是说,如果if语句判断为True,那么if语句后面所有缩进的代码都要被执行,然后Python解释器再继续下一步工作。你会发现这种缩进还会在随后讨论的其他逻辑块中出现,现在你需要记住的是,在Python中缩进是有明确意义的,你必须遵循这个原则。如果你使用IDE或者像Sublime Text这样的编辑器,软件会帮助你设置每次使用制表符都相当于按了一定次数的空格键;如果你使用像Notepad一样的普通文本编辑器,那么就要注意使用同样数目的空格表示同一级别的缩进(一般使用4个空格)。
最后需要注意的是,有时候文本编辑器会使用制表符代替空格,这样程序看上去没有问题,但还是会收到一条错误信息(像“Unexpected indent in line 37”),这就会令程序员非常抓狂。尽管有这个问题,一般来说,Python使用缩进还是会使代码更加清晰易读,因为你可以轻松地理解程序的逻辑过程。
第二种测试具体键值的方式是使用get函数,这个函数也可以按照键值取得相应的字典值。和前一种测试键值的方式不同,如果字典中存在这个键,get函数就返回键值对应的字典值;如果字典中不存在这个键,则返回None。此外,get函数还可以使用第二个参数,表示如果字典中不存在键值时函数的返回值。通过这种方式,如果字典中不存在该键值,可以返回除None之外的一些其他内容。
6.排序
# 使用sorted()对字典进行排序 # 要想对字典排序的同时不修改原字典 # 先复制字典 print("Output #119: {}".format(a_dict)) dict_copy = a_dict.copy() ordered_dict1 = sorted(dict_copy.items(), key=lambda item: item[ print("Output #120 (order by keys): {}".format(ordered_dict1)) ordered_dict2 = sorted(dict_copy.items(), key=lambda item: item[1]) print("Output #121 (order by values): {}".format(ordered_dict2)) ordered_dict3 = sorted(dict_copy.items(), key=lambda x: x[1], reverse=True) print("Output #122 (order by values, descending): {}".format(ordered_dict3)) ordered_dict4 = sorted(dict_copy.items(), key=lambda x: x[1], reverse=False) print("Output #123 (order by values, ascending): {}".format(ordered_dict4))
这个示例展示了如何使用不同方式对字典进行排序。本节开头就说过,字典没有隐含排序,但是,你可以使用前面的代码片段对一个字典对象进行排序。可以按照字典的键或字典值来排序,如果这些值是数值型的,排序方式可以是升序,也可以是降序。
这个示例中使用了copy函数来为字典a_dict制作一个副本,副本的名称为dict_copy。为字典制作副本确保了原字典a_dict不会被修改。下一行代码中包含了sorted函数、一个由items函数生成的元组列表和一个作为sorted函数关键字的lambda函数。
这行代码比较复杂,可以先将它分解一下。这行代码的目的是对items函数生成的键-值元组列表按照某种规则进行排序。这种规则就是key,它相当于一个简单的lambda函数。(lambda函数是一个简单函数,在运行时返回一个表达式。)在这个lambda函数中,item是唯一的参数,表示由items函数返回的每个键-值元组。冒号后面是要返回的表达式,这个表达式是item[0],即返回元组中的第一个元素(也就是字典键值),用作sorted函数的关键字。简而言之,这行代码的意义是:将字典中的键-值对按照字典键值升序排序。下一个sorted函数使用item[1]而不是item[0],所以这行代码按照字典值对键-值对进行升序排序。
最后两行代码中的sorted函数与它们前面一行代码中的sorted函数很相似,因为这3个sorted函数都使用字典值作为排序关键字。又因为这个字典的值是数值型的,所以可以按照升序或降序对其进行排序。最后两种排序方式展示了如何使用sorted函数的reverse参数来设定排序结果是升序还是降序。reverse=True对应降序,所以键-值对按照字典值以降序排序。
要想获得更多关于字典的信息,请参考Python标准库(https://docs.python.org/3/library/index.html)。
1.4.8 控制流
控制流元素非常重要,因为可以在程序中包含有意义的业务逻辑。很多商务处理和分析依赖于业务逻辑,例如“如果客户的花费超过一个具体值,那么就怎样怎样”或“如果销售额属于A类,则编码为X,如果销售额属于B类,则编码为Y,否则编码为Z。”这些逻辑语句在代码中可以用控制流元素来表示。
Python提供了若干种控制流元素,包括if-elif-else语句、for循环、range函数和while循环。正如它们的名字所示,if-else语句提供的逻辑为“如果这样,那么就做那个,否则做些别的事情”。else代码块并不是必需的,但可以使你的代码更加清楚。for循环可以使你在一系列值之间进行迭代,这些值可以是列表、元组或字符串。你可以使用range函数与len函数一起作用于列表,生成一个索引序列,然后用在for循环中。最后,只要while条件为真,while循环会一直执行内部的代码。
1.if-else
# if-else语句 x = 5 if x > 4 or x != 9: print("Output #124: {}".format(x)) else: print("Output #124: x is not greater than 4")
第一个示例展示了一个简单的if-else语句。if条件判断x是否大于4或者x是否不等于9(!=操作法表示“不等于”)。通过使用or操作符,在找到一个表达式为True时就停止判断。在这个例子中,x等于5,5大于4,所以x != 9是不用判断的。第一个条件x > 4为真,所以print x被执行,打印结果为数值5。如果if代码块中没有一个条件为真,那么就执行else代码块中的print语句。
2.if-elif-else
# if-elif-else语句 if x > 6: print("Output #125: x is greater than six") elif x > 4 and x == 5: print("Output #125: {}".format(x*x)) else: print("Output #125: x is not greater than 4")
第二个示例展示了一个稍微复杂一点的if-elif-else语句。同前一个示例一样,if代码块测试x是否大于6。如果这个条件为真,那么停止判断,执行相应的print语句。实际上,5不大于6,所以使用下面的elif语句继续判断。这个语句测试x是否大于4并且x是否等于5。使用and操作符进行的判断在找到一个表达式为False时就停止。在这个例子中, x等于5,5大于4,并且求的x值是5,所以执行print x*x,打印结果为数值25。因为这里已经使用了等号作为对象赋值操作符,所以用两个等号(==)判断是否相等。如果if和elif代码块都不为真,那么就执行else代码块中的print语句。
3.for循环
y = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', \ 'Nov', 'Dec'] z = ['Annie', 'Betty', 'Claire', 'Daphne', 'Ellie', 'Franchesca', 'Greta', \ 'Holly', 'Isabel', 'Jenny'] print("Output #126:") for month in y: print("{!s}".format(month)) print("Output #127: (index value: name in list)") for i in range(len(z)): print("{0!s}: {1:s}".format(i, z[i])) print("Output #128: (access elements in y with z's index values)") for j in range(len(z)): if y[j].startswith('J'): print("{!s}".format(y[j])) print("Output #129:") for key, value in another_dict.items(): print("{0:s}, {1}".format(key, value))
这4个for循环示例演示了如何使用for循环在序列中迭代。在本书后面的章节,以及一般的商业应用中,这种功能都非常重要。第一个for循环示例展示了基本语法,即for variable in sequence,做某事。variable是一个临时占位符,表示序列中的各个值,并且只在for循环中有意义。在这个示例中,变量名为month。sequence是你要进行迭代的序列的名称。同样在这个示例中,序列名为y,是一个月份列表。因此,这个示例的意义是:“对于y中的每个值,打印出这个值。”
第二个for循环示例展示了如何使用range函数和len函数的组合生成一个可以在for循环中使用的索引值序列。为了弄清楚复合函数之间的相互作用,可以仔细地分析一下。len函数返回列表z中元素的个数,这里的个数是10。然后range函数生成了一系列整数,是从0开始直到比len函数的结果少1的整数,在这个例子中,就是0~9的整数。因此,for循环的意义就是:“对于0~9的整数序列中的一个整数i,打印出整数i,再打印一个空格,然后打印出列表z中索引值为i的元素值。”在本书的很多示例中,你都会看到range函数和len函数的组合用在for循环中,因为这种组合在很多商业应用中是非常有用的。
第三个for循环示例展示了如何使用从一个序列中生成的索引值来引用另一个序列中具有同样索引值的元素,也说明了如何在for循环中包含if语句来说明业务逻辑。这个示例又一次使用range函数和len函数生成了列表z的索引值。然后,if语句测试列表y中具有这些索引值的元素(y[0]='Jan', y[1]='Feb', …, y[9]='Oct')是否以大写字母J开头。
最后一个for循环展示了在字典的键和值之间进行迭代和引用的方法。在for循环的第一行中,items函数返回字典的键-值元组。for循环中的key和value变量依次捕获这些值。 for循环中的print语句在每一行打印出一个键-值对,键和值之间以逗号隔开。
4.简化for循环:列表、集合与字典生成式
列表、集合与字典生成式是Python中一种简化的for循环写法。列表生成式出现在方括号内,集合生成式与字典生成式则出现在花括号内。所有的生成式都包括条件逻辑(例如:if-else语句)。
列表生成式。下面的示例展示了如何使用列表生成式来从一个列表集合中筛选出符合特定条件的列表子集:
# 使用列表生成式选择特定的行 my_data = [[1,2,3], [4,5,6], [7,8,9]] rows_to_keep = [row for row in my_data if row[2] > 5] print("Output #130 (list comprehension): {}".format(rows_to_keep))
在这个示例中,列表生成式的意义是:对于my_data中的每一行,如果这行中索引位置2的值(即第三个值)大于5,则保留这一行。因为6和9都大于5,所以rows_to_keep中的列表子集为[4, 5, 6]和[7, 8, 9]。
集合生成式。下面的示例展示了如何使用集合生成式来从一个元组列表中选择出特定的元组集合:
# 使用集合生成式在列表中选择出一组唯一的元组 my_data = [(1,2,3), (4,5,6), (7,8,9), (7,8,9)] set_of_tuples1 = {x for x in my_data} print("Output #131 (set comprehension): {}".format(set_of_tuples1)) set_of_tuples2 = set(my_data) print("Output #132 (set function): {}".format(set_of_tuples2))
在这个示例中,集合生成式的意义是:对于my_data中的每个元组,如果它是一个唯一的元组,则保留这个元组。你可以称这个表达式为集合生成式而不是列表生成式,因为表达式中是花括号,不是方括号,而且它也不是字典生成式,因为它没有使用键-值对这样的语法。
这个示例中的第二个print语句说明你可以通过Python内置的set函数达到和集合生成式同样的效果。在这个例子中,使用内置的set函数更好,因为它比集合生成式更精炼,而且更易读。
字典生成式。下面的示例展示了如何使用字典生成式来从一个字典中筛选出满足特定条件的键-值对子集:
# 使用字典生成式选择特定的键-值对 my_dictionary = {'customer1': 7, 'customer2': 9, 'customer3': 11} my_results = {key : value for key, value in my_dictionary.items() if \ value > 10} print("Output #133 (dictionary comprehension): {}".format(my_results))
在这个例子中,字典生成式的意义为:对于my_dictionay中的每个键-值对,如果值大于10,则保留这个键-值对。因为值11大于10,所以保留在my_results中的键-值对为{'customer3':11}。
5.while循环
print("Output #134:") x = 0 while x < 11: print("{!s}".format(x)) x += 1
这个示例展示了如何使用while循环来打印0~10的整数。x = 0将变量x初始化为0。然后while循环判断x是否小于11。因为x小于11,所以while循环在一行中打印出x值,然后将x的值增加1。接着while循环继续判断x(此时为1)是否小于11。因为确实小于11,所以继续执行while循环内部的语句。这个过程一直以这种方式继续,直到x从10增加到11。这时,while循环继续判断x是否小于11,表达式结果为假,while循环内部语句不再被执行。
while循环适合于知道内部语句会被执行多少次的情况。更多时候,你不太确定内部语句需要执行多少次,这时就应该使用for循环。
6.函数
在一些情况下,你会发现自己编写函数比使用Python内置函数和安装别人开发的模块更方便有效。举例来说,如果你发现总是在不断重复地书写同样的代码片段,那么就应该考虑将这个代码片段转换为函数。某些情况下,函数可能已经存在于Python基础模块或“可导入”的模块中了。如果函数已经存在,就应该使用这些开发好并已经通过了大量测试的函数。但是,有些情况下,你需要的函数不存在或不可用,这时就需要你自己创建函数。
要在Python中创建函数,需要使用def关键字,并在后面加上函数名称和一对圆括号,然后再加上一个冒号。组成函数主体的代码需要缩进。最后,如果函数需要返回一个或多个值,可以使用return关键字来返回函数结果供程序使用。下面的示例展示了在Python中创建和使用函数的方法:
# 计算一系列数值的均值 def getMean(numericValues): return sum(numericValues)/len(numericValues) if len(numericValues) > 0 else float('nan') my_list = [2, 2, 4, 4, 6, 6, 8, 8] print("Output #135 (mean): {!s}".format(getMean(my_list))
这个示例展示了如何创建函数来计算一系列数值的均值。函数名为getMean。圆括号之间的短语表示要传入函数中的数值序列,这是一个仅在函数作用范围内有意义的变量。在函数内部,由序列的总和除以序列中数值的个数计算出序列的均值。此外,还可以使用if-else语句来检验序列中是否包含数值。如果确实包含数值,函数返回序列均值。如果不包含数值,函数返回nan(即:非数值)。如果省略了if-else语句,而且序列中正好没有任何数值的话,程序就会抛出一个除数为0的错误。最后,使用return关键字返回函数结果供程序使用。
在这个示例中,my_list包含了8个数值。my_list被传入getMean()函数中。8个数值的总和为40,40除以8等于5。所以,print语句打印出整数5。
就像你猜测的那样,mean函数已经存在了。例如,NumPy中就有一个mean函数。所以,你可以通过导入NumPy,使用它里面的mean函数得到同样的结果:
import numpy as np print np.mean(my_list)
再说一次,如果在Python基础程序或可导入模块中,已经存在你需要的函数,就应该使用这些开发好的并已经通过了大量测试的函数。使用Google或Bing来搜索“<你需要的功能描述> Python function”可以帮助你找到想要的Python函数。但是,如果你想完成业务过程中特有的任务,知道如何去创建函数还是非常重要的。
7.异常
编写一个强壮稳健的程序的一个重要方面就是有效地处理错误和异常。在编写程序时,你可能会隐含地假设程序要处理的数据类型和数据结构,如果有数据违反了你的假设,就会使程序抛出错误。
Python中包含了若干种内置的异常对象。常用的异常包括IOError、IndexError、KeyError、NameError、SyntaxError、TypeError、UnicodeError和ValueError。你可以在网上获得更多的异常信息,参见Python标准库中的“Built-in Exceptions”那一节(http://docs.python.org/3/library/exceptions.html)。你可以使用try-except来构筑处理错误信息的第一道防线,即使数据不匹配,你的程序还可以继续运行。
下面展示了两种使用try-except代码块来有效地捕获和处理异常的方法(一种比较短,另一种比较长)。这两个示例修改了上一节的函数示例,来说明如何使用try-except代码块代替if语句处理空列表的情况。
8.try-except
# 计算一系列数值的均值 def getMean(numericValues): return sum(numericValues)/len(numericValues) my_list2 = [ ] # 简单形式 try: print("Output #138: {}".format(getMean(my_list2))) except ZeroDivisionError as detail: print("Output #138 (Error): {}".format(float('nan'))) print("Output #138 (Error): {}".format(detail))
在这种处理异常的方法中,函数getMean()中没有检验序列是否包含数值的if语句。如果序列是空的,就像列表my_list2一样,那么调用这个函数会导致一个异常ZeroDivisionError。
要想使用try-except代码块,需要将你要执行的代码放在try代码块中,然后,使用except代码块来处理潜在的错误并打印出错误信息来帮助你理解程序错误。在某些情况下,异常具有特定的值。你可以通过在except行中加上as短语来引用异常值,然后打印出你为异常指定的变量。因为my_list2中不包含任何数值,所以执行except代码块,打印出nan和Error: float division by zero。
9.try-except-else-finally
# 完整形式 try: result = getMean(my_list2) except ZeroDivisionError as detail: print "Output #142 (Error): " + str(float('nan')) print "Output #142 (Error):", detail else: print "Output #142 (The mean is):", result finally: print "Output #142 (Finally): The finally block is executed every time"
这个完整形式的异常处理方法除了try和except代码块,还包含else和finally代码块。如果try代码块成功执行,则会接着执行else代码块。因此,如果传递给try代码块中的getMean()函数的数值序列中包含任意数值,那么这些数值的均值就会被赋给try代码块中的变量result,然后接着执行else代码块。例如,如果这里使用程序代码+my_list1+,就会打印出The mean is: 5.0。因为my_list2不包含任何数值,所以执行except代码块,打印出nan和Error: float division by zero。然后,总是执行finally模块,打印出The finally block is executed every time。