Python-秘籍手册-全-
Python 秘籍手册(全)
一、字符串和文本
自从计算的早期以来,计算中使用的数据存储在基本的文本文件中。任何编写过 shell 脚本的人都非常清楚,Unix 系统及其工具是基于这样一种假设构建的,即处理文本将是程序的大部分工作。Python 也不例外;它提供了几个编程元素来帮助完成基本的文本处理任务。
首先,让我们注意一下 Python 是如何存储字符串的。字符串是不可变的列表,所以它们不能被改变。对字符串内容的任何更改都需要复制到内存中的新位置。当您试图优化代码的任何文本处理部分时,您必须始终牢记这一点。
1-1.串联字符串
问题
你想用一些更小的字符串来构建字符串。这个过程称为串联。
解决办法
构建字符串最简单的方法是使用+
操作符。在这种情况下,字符串被放在一起,并返回完整的新字符串。
它是如何工作的
清单 1-1 展示了一个例子。
>>> new_str = "hello " + "world"
>>> print(new_str)
hello world
Listing 1-1.Basic Concatenation
这段代码返回字符串“hello world”。如果希望两个单词之间有一个空格,需要显式地给其中一个字符串添加一个空格。
您也可以从一个较小字符串的多个副本创建字符串。这是通过使用*
操作符并乘以您想要的副本数来完成的。参见清单 1-2 中的示例。
>>> new_str = "Hello" * 3
>>> print(new_str)
HelloHelloHello
Listing 1-2.
Multiplicative Concatenation
这将返回字符串“HelloHelloHello”。
当您只处理字符串时,这两个操作符工作得很好。如果您想使用其他数据类型,您可以使用上面的例子,首先将您的数据传递到函数str()
中。通过这种方式,您可以将数字添加到构造的字符串中。清单 1-3 中显示了一个例子。
>>> New_str = "Count = " + str(42)
>>> print(New_str)
Count = 42
Listing 1-3.Concatenating Non-Strings
1-2.比较字符串
问题
您想要比较字符串,检查两个字符串是否具有相同的值,或者检查两个名称是否指向同一个 string 对象。
解决办法
有两种方法进行比较,使用is
或使用==
。第一种是测试两个变量名是否引用同一个对象的方法,第二种是比较每个变量的实际值的方法。
它是如何工作的
要测试相同的文本是否存储在两个独立的字符串中,使用类似清单 1-4 中的代码。
str1 = "Hello"
str2 = "World"
if str1 == str2:
print("The strings are equal")
else:
print("The strings are not equal")
Listing 1-4.Comparing Strings
此代码返回“字符串不相等”。您可以使用任何常用的比较运算符,如!=
、<
或>.
Note
当进行大于或小于比较时,字符串是逐字母比较的。另外,Python 对待大写字母和小写字母的方式不同。大写字母在小写字母之前,所以 Z 在 a 之前。
1-3.搜索子字符串
问题
你想在一个字符串对象中搜索一个子串。
解决办法
您可以使用in
操作符来检测子字符串是否存在。您可以通过使用 string 对象的find()
方法来定位该子字符串的起始索引。
它是如何工作的
在 Python 中,有一个名为in
的多态操作符,可以用来查看一个数据元素是否存在于一个更大的数据元素集合中。当您需要查看一个子字符串是否存在于另一个 string 对象中时,这也是可行的。清单 1-5 中给出了用法。
>>> Str1 = 'This is a string'
>>> 'is' in Str1
True
>>> 'me' in Str1
False
Listing 1-5.Looking for a Substring
如您所见,它返回一个布尔值,告诉您是否找到了子串。如果需要查找子字符串的位置,请使用 string 对象的 find 方法。使用上面的代码,您可以使用
>>> Str1.find('is')
2
>>> Str1.find('me')
-1
Listing 1-6.Finding the Index of a Substring
这段代码返回子字符串第一个实例的索引。如果要查找其他实例,可以包含一个开始和/或结束索引值,以便进行搜索。因此,要找到第二个实例is
,使用清单 1-7 中的代码。
>>> Str1.find('is', 3)
5
Listing 1-7.Finding a Substring Beyond the First One
1-4.获取子字符串
问题
你需要从一个字符串对象中获取一个子串。
解决办法
一旦找到子字符串的索引,就可以通过复制原始字符串的一部分来获得它。这是通过使用切片符号从字符串中获取元素来实现的。
它是如何工作的
切片由开始索引和结束索引定义。要获取字符串中的第一个单词,您可以使用清单 1-8 中显示的任一选项。
>>> Str2 = 'One two three'
>>> Str2[0:2]
On
>>> Str2[:2]
On
>>> Str2[8:]
three
Listing 1-8.Slice Notation
Note
切片适用于 Python 中的所有列表。您还可以使用负索引值来向后计数,而不是向前计数。
1-5.替换文本匹配
问题
你需要用新的内容替换字符串的一部分。
解决办法
因为 Python 字符串是不可变的,所以替换子字符串需要将原始字符串切碎,然后将所有内容重新连接成一个新字符串。
它是如何工作的
让我们使用上述切片和拼接的例子,并将它们放在一起,如清单 1-9 所示。
>>> str1 = "Here are a string"
>>> corrected_str1 = str1[:5] + "is" + str1[7:]
>>> print(corrected_str1)
Here is a string
Listing 1-9.Manual String Replacement
string 对象包含一个名为replace()
的方法,该方法也可以用来替换一个或多个子字符串实例。您需要提交旧的子串、要替换的新子串以及要替换的实例数。参见清单 1-10 。
>>> corrected_str1 = str1.replace("are", "is", 1)
Listing 1-10.Using the replace() Function
如果省略 count,此方法将替换字符串中的每个实例。
1-6.反转字符串
问题
你需要反转一个字符串对象的内容。
解决办法
反转字符串可以通过使用切片符号以相反的顺序选择出单个字符来完成。
它是如何工作的
Python 包含了扩展切片的概念,它包含第三个参数来定义在列表中移动时的步幅长度。如果这个步幅长度是负的,您是在告诉 Python 向后遍历列表。反转字符串的一行程序类似于清单 1-11 中的代码。
>>> str1 = "Hello World"
>>> str2 = str1[::-1]
>>> print(str2)
dlrow olleH
Listing 1-11.Reversing a String with Slices
因为这是一种特殊类型的切片,所以可以用它来获取一个子串,并在一个命令中全部反转。
1-7.修剪空白
问题
您需要从用户输入中删除空白。
解决办法
string 对象的strip()
方法可以用来从字符串中删除任何多余的空白字符。
它是如何工作的
当您的代码需要接受来自用户的文本输入时,您通常需要能够修剪掉可能存在于字符串开头或结尾的任何空白字符。如果您希望简单地删除字符串开头和结尾的所有空白字符,您可以使用strip()
方法。参见清单 1-12 。
>>> str1 = "Space"
>>> str2 = str1.strip()
>>> print(str2)
Space
Listing 1-12.Stripping Whitespace
如果您想从字符串的开头和结尾去掉某个特定的字符,您可以将它作为参数传递给方法。您可以分别使用方法lstrip()
或rstrip()
从开头或结尾去掉不需要的字符。
1-8.改变大小写
问题
您需要将字符的大小写设置为全部大写或全部小写。
解决办法
方法可以对内容执行大小写更改。
它是如何工作的
处理用户输入时出现的另一个问题是,您可能需要将所有字符设置为大写或小写。这通常是对放入数据库的信息进行的,以简化两个值之间的比较。这样可以避免前面提到的大写和小写字符被区别对待的问题。在这两种情况下,这都是通过 string 对象提供的方法来完成的。参见清单 1-13 。
>>> str1 = "Hello World"
>>> print(str1.lower())
hello world
>>> print(str1.upper())
HELLO WORLD
Listing 1-13.Changing the Case of a String
Note
你可以用方法capitalize()
将一个字符串大写。
1-9.转换为数字
问题
您需要将用户输入的数字转换为数字数据类型。
解决办法
有一些转换函数可以将字符串转换成其他数据类型。对于数字,有int()
、float()
、long()
和complex()
函数。
它是如何工作的
这些函数接受一个字符串,并返回您所请求类型的数字。该字符串应该与直接在 Python 中输入的数字文字具有相同的形式。如果字符串与请求的类型转换的预期格式不匹配,将会出现错误。
默认的基数是 10。您可以输入不同的基数,以便创建不同的数字。例如,如果您正在输入十六进制数字,您可以使用清单 1-14 中的代码。
>>> hex1 = int("ae", 16)
>>> hex1
174
Listing 1-14.Type Cast Functions
可能的基数是 2 到 36。
1-10.迭代字符串中的字符
问题
您需要遍历字符串对象的字符,并对每个字符应用一些处理。
解决办法
您可以使用一个for
循环迭代每个字符。
它是如何工作的
如果您需要处理给定字符串的每个字符,那么您需要能够遍历该字符串。您可以构建一个简单的循环,使用索引并从列表中提取每个元素。参见清单 1-15 。
str1 = "0123456789"
for i in range(10):
print(str1[i], " and ")
Listing 1-15.Iterating Over a String
此代码返回文本“0 和 1 以及 2 和 3”。更 Pythonic 化的方法是使用迭代器。令人高兴的是,列表自动支持迭代器方法。代码可以更改为清单 1-16 中的代码。
for i in str1:
print(i, " and ")
Listing 1-16.Using an Iterator
1-11.文本统计
问题
您需要找到字符串对象的统计数据。
解决办法
如果您对查看给定字符串的总体统计信息感兴趣,可以使用几种不同的方法和函数来收集这类信息。
它是如何工作的
最基本的统计是字符串中的字符数,由函数len()
给出。您可以使用函数min()
和max()
找到最小和最大字符值。要获得更详细的信息,您可以使用count()
方法来查看某些字符在字符串中出现了多少次。见清单 1-17 。
>>> str1 = "Hello world"
>>> len(str1)
11
>>> min(str1)
' '
>>> max(str1)
'w'
>>> str1.count('o')
2
Listing 1-17.String Statistics
1-12.编码 Unicode
问题
您需要将字符串编码为 Unicode。
解决办法
Python 曾经有一个unicode
数据类型用于存储 Unicode 编码的字符串。从版本 3 开始,Python 字符串文字默认存储为 Unicode 字符串。
它是如何工作的
如果您使用的是 Python 3,标准字符串对象已经存储为 Unicode。如果您使用的是 Python 2,您可以通过使用构造函数或定义 Unicode 文字来创建它们。参见清单 1-18 。
>>> ustr1 = unicode("Hello")
>>> ustr2 = u’Hello’
>>> ustr1 == ustr2
True
Listing 1-18.Using Unicode
Unicode 值实际上存储为整数。您可以将这些值编码成特定的编码字符串。举个例子,
ustr1.encode("utf-8")
给你一个编码为 UTF 8 的新字符串。您还可以使用decode()
方法将这个字符串解码回普通的 Unicode 字符串。
1-13.翻译
问题
你需要翻译字符串的内容。
解决办法
您可以将翻译映射应用于整个字符串。如果你想完成几个替换,这是很方便的。
它是如何工作的
您可以使用字符串的translate()
方法来应用这种映射。虽然您可以手动创建转换表,但是 string 数据类型包含一个名为maketrans()
的帮助函数,它创建一个转换表,将第一个参数中的每个字符映射到第二个参数中相同位置的字符。如清单 1-19 所示。
>>> str1 = "Hello world"
>>> translate_table = str1.maketrans("abcd", "efgh")
>>> str1.translate(translate_table)
Hello worlh
Listing 1-19.
Translating Strings
translate
方法可选地接受第二个参数,该参数包含一组要从给定字符串中删除并执行翻译的字符。如果您希望能够从字符串中删除一组字符,可以通过将转换表设置为 None 来实现。这样,你就可以用清单 1-20 中的代码去掉字符 l 和 w。
>>> str1.translate(str.maketrans({'l':None,'W':None}))
Heo ord
Listing 1-20.Using translate to Delete Characters
二、数字、日期和时间
电子计算机的最初目的是计算物理问题的答案。这取决于数字的有效使用和正确处理。这仍然消耗了普通计算机程序的大部分 CPU 周期。除了处理数字,在很多情况下,您还需要能够处理日期和时间。当您需要比较不同时区或闰日的年份时,这就有点麻烦了。
2-1.创建整数
问题
你需要创建整数。
解决办法
创建整数有几种方法。最简单的是简单地写一个整数。还有一些创建函数,允许你用任意的基数来创建任意的整数。在 Python 中,整数是无限范围内的数字,受限于可用的内存量。
它是如何工作的
清单 2-1 展示了一个例子。
>>> a = 123456789
>>> a.bit_length()
27
Listing 2-1.Integer Literals
这段代码创建了一个新的 integer 对象,通过变量a
来访问。您可以使用方法bit_length()
来查看使用了多少位来存储这个整数。整数是不可变的,所以如果你试图通过数学运算来改变它们,你最终会得到一个新的整数。
在清单 2-2 中,您可以看到如何使用基数 10 以外的东西来创建整数。
>>> b = int('110',2)
>>> b
6
Listing 2-2.Integer Object Instantiation
2-2.创建浮点
问题
你需要创建浮点数。
解决办法
与整数一样,您可以创建浮点文字,也可以使用函数float()
。
它是如何工作的
使用浮点文字时,需要包含小数点或指数。参见清单 2-3 中的一些例子。
>>> b = 1.
>>> b
1.0
>>> c = 1e2
>>> c
100.0
Listing 2-3.Using Float Literals
浮点的实际存储表示依赖于实现,并且在不同的架构上是不同的。
2-3.将浮点数舍入为整数
问题
你想把浮点数转换成整数。
解决办法
有两种方法可以将浮点数转换成整数。您可以将数字截断到小数点之前的部分,也可以将数字四舍五入到最接近的整数值。
它是如何工作的
可以使用和上面提到的相同的int()
函数。当您将一个浮点作为输入传递给它时,它会将数字向 0 截断。参见清单 2-4 。
>>> c = 1234.567
>>> int(c)
1234
Listing 2-4.Truncating Floating Point Numbers
在math
模块中有一个函数可以将浮点数截断成整数部分。它叫做math.trunc()
,清单 2-5 显示了如何使用它。
>>> import math
>>> math.trunc(c)
1234
Listing 2-5.Using math.trunc()
如你所见,它基本上去掉了小数部分,给你剩下的部分。如果您需要得到一个取整的值,查看清单 2-6 。
>>> round(c)
1235
Listing 2-6.Rounding Floating Point Numbers
如果有必要,您可以四舍五入到某个固定的小数位数,如清单 2-7 所示。
>>> round(c, 1)
1234.6
Listing 2-7.Rounding to One Decimal Place
2-4.格式化数字
问题
你需要得到一个数字的字符串表示。
解决办法
内置函数format()
可以用来输出数字的各种字符串表示。
它是如何工作的
format
函数采用一种格式规范,定义如何将数字转换成字符串。这种字符串的一般格式如清单 2-8 所示。
[sign][width][.precision][type]
Listing 2-8.Format Specification
如果符号是+
,你的数字会一直有一个前导符号。如果是-
,只有数字是负数才会加前导符号。如果您使用空格,如果数字是负数,您将得到一个前导减号,如果数字是正数,您将得到一个前导空格。
宽度值定义了总数的最小大小。如果宽度大于数字的大小,您将获得前导空格来填充到正确的大小。精度定义了在显示数字时应该使用多少个小数位。当然,这只对浮点数有效。
该类型可以是几种不同的可能性之一:
| 类型 | 意义 | | --- | --- | | `B` | 二进制格式 | | `D` | 十进制整数 | | `O` | 八进制整数 | | `x` | 十六进制格式,a-f 使用小写字母 | | `X` | 十六进制格式,A-F 使用大写字母 | | `n` | 这与`d`相同 | | 没有人 | 这与`d`相同 |对于浮点数,有一个完整的其他类型可供选择:
| 类型 | 意义 | | --- | --- | | `e` | 指数格式,用`e`标记指数。默认精度为 6。 | | `E` | 指数格式,用`E`标记指数。默认精度为 6。 | | `f` | 默认精度为 6 的定点。 | | `F` | 与`f`相同,只是 nan 转换为 NAN,inf 转换为 INF。 | | `g` | 常规格式,其中数字被舍入到给定的精度。如果数字太大,则以指数形式显示,用`e`标记指数。默认精度为 6。 | | `G` | 这与`g`相同,只是指数标有`E`。 | | `n` | 号码。这与`g`相同,只是它使用当前的区域设置来获取正确的分隔符。 | | `%` | 百分比,其中数字乘以 100,以`f`格式显示,带有百分比符号。 | | 没有人 | 这个和`g`一样。 |示例如清单 2-9 所示。
>>> format(c)
'1234.567'
>>> format(c, '+.2f')
'+1234.57'
>>> format(.25, '%')
'25.000000%'
Listing 2-9.Formatting Numbers
2-5.使用任意精度的数字
问题
您需要处理任意大小和精度的浮点数。
解决办法
Python 提供了一个名为decimal
的模块来处理用户定义的精度数。
它是如何工作的
decimal
模块提供了一个名为Decimal
的新对象,它将浮点数的存储和操作从底层架构中抽象出来。见清单 2-10 。
>>> from decimal import *
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
>>> getcontext().prec = 10
Listing 2-10.Importing the Decimal Object and Setting Precision
清单 2-10 显示了如何设置decimal
模块中使用的精度。清单 2-11 展示了如何使用Decimal
类。
>>> a = Decimal(10.5)
>>> a
Decimal('10.5')
>>> 2*a
Decimal('21.0')
Listing 2-11.Using the Decimal Class
2-6.生成随机数
问题
为了在代码中引入随机化,您需要生成随机数。
解决办法
Python 中的random
模块提供了为各种分布生成伪随机数的函数。
它是如何工作的
在没有非常专业的硬件的情况下,所有随机数都是使用一类称为伪随机数生成器(PRNGs)的函数来生成的。可用的更好的算法之一是称为 Mersenne twisters 的函数类。Python random
模块使用 Mersenne twisters 来生成随机数。使用当前系统时间作为种子来初始化生成器,参见清单 2-12 。
>>> import random
>>> random.seed()
Listing 2-12.Initializing the Random Module
您可以通过调用random.random()
获得 0.0 到 1.0 之间的下一个随机浮点数,如清单 2-13 所示。
>>> random.random()
0.35060766413719124
Listing 2-13.Getting the Next Random Number
您还可以通过使用random.choice()
函数在许多可能性中进行随机选择,如清单 2-14 所示。
>>> items = [1, 2, 3, 4]
>>> random.choice(items)
3
Listing 2-14.Making a Random Choice
2-7.获取当前日期和时间
问题
您需要从系统中获取当前日期和时间。
解决办法
Python 提供了一个名为datetime
的模块,该模块提供了处理日期、时间和时区的对象。
它是如何工作的
datetime
模块提供了一个datetime
类,包含所有的日期和时间信息,以及任何时区信息。清单 2-15 展示了如何获取当前日期和时间。
>>> import datetime
>>> curr_datetime = datetime.datetime.now()
>>> curr_datetime.year
2016
>>> curr_datetime.weekday()
2
Listing 2-15.Getting the Current Date and Time
在datetime
对象中有许多可用的帮助函数和属性,允许您以任何需要的方式解析日期和时间信息。
如果您只需要日期或时间部分,那么可以使用date
和time
类。有datetime
类的帮助器方法,即date()
和time()
,它们返回适当类型的对象。清单 2-16 展示了如何获取当前日期和时间的time
和date
对象。
>>> import datetime
>>> curr_datetime = datetime.datetime.now()
>>> curr_date = curr_datetime.date()
>>> curr_time = curr_datetime.time()
Listing 2-16.Getting Objects for the Current Date and Time
2-8.计算日期/时间差异
问题
您需要能够找出两个时间和/或两个日期之间的差异。
解决办法
Python 提供了作为datetime
模块一部分的timedelta
对象。
它是如何工作的
您可以创建一个代表某个日期或时间跨度的timedelta
对象。如果你有两个datetime
对象,你可以将它们相减得到一个timedelta
对象,如清单 2-17 所示。
>>> time1 = datetime.datetime.now()
>>> time2 = datetime.datetime.now()
>>> timediff = time2 - time1
>>> timediff.days
0
>>> timediff.seconds
7
>>> timediff.total_seconds()
7.532031
Listing 2-17.Finding the Difference Between Two Datetimes
如果你想创建一个固定的timedelta
,比方说一周,你想添加到一个给定的datetime
对象,你可以创建它并使用它,如清单 2-18 所示。
>>> timediff = datetime.timedelta(days=7)
>>> time1 = datetime.datetime.now()
>>> time1.day
10
>>> time2 = time1 + timediff
>>> time2.day
17
Listing 2-18.Creating Timedeltas
2-9.格式化日期和时间
问题
出于显示目的,您需要生成datetime
对象的字符串表示。
解决办法
datetime
对象有一个字符串格式方法,可以生成一个由规范字符串定义的字符串。
它是如何工作的
方法strftime()
获取一个规范字符串,并返回一个字符串,其中替换了相关的值。清单 2-19 中显示了一个基本示例。
>>> time3 = datetime.datetime.now()
>>> time3.strftime("%A %d. %B %Y %I:%M%p")
'Wednesday, 10\. February 2016 09:39AM'
Listing 2-19.Formatting a Datetime String
可能的格式选项如下:
| 管理的 | 意义 | | --- | --- | | `%a` | 作为区域缩写名称的工作日 | | `%A` | 作为区域全名的工作日 | | `%w` | 以 0 到 6 之间的数字表示的星期几 | | `%d` | 以零填充数字表示的月份中的某一天 | | `%b` | 作为区域缩写名称的月份 | | `%B` | 月份作为区域设置全名 | | `%m` | 以零填充数字表示的月份 | | `%y` | 以零填充的两位数表示的年份 | | `%Y` | 以零填充的四位数表示的年份 | | `%H` | 小时(24 小时制)作为零填充数字 | | `%I` | 以零填充数字表示的小时(12 小时制) | | `%p` | 等同于 AM 或 PM 的区域设置 | | `%M` | 以零填充数字表示的分钟 | | `%S` | 秒作为零填充数字 | | `%f` | 微秒作为零填充数字 | | `%z` | UTC 偏移量,形式为+HHMM 或–HHMM | | `%Z` | 时区名称 | | `%j` | 以零填充的三位数表示的一年中的某一天 | | `%U` | 一年中的第一周,将星期日作为一周的第一天 | | `%W` | 一年中的第一周,使用星期一作为一周的第一天 | | `%c` | 日期和时间的区域设置的适当表示 | | `%x` | 区域设置对日期的适当表示 | | `%X` | 区域设置对时间的适当表示 | | `%%` | 文字% |2-10.从字符串中读取日期和时间
问题
您需要获取用户输入并将其转换成一个datetime
对象。
解决办法
datetime
类可以基于输入字符串实例化一个新对象。
它是如何工作的
如果你分别接受日期和时间的每个元素,你可以在datetime
类中直接使用它们,如清单 2-20 所示。
>>> date4 = datetime.datetime(year=1999, month=9, day=21)
>>> date4.weekday()
1
Listing 2-20.Creating a Datetime Object
如果您有一个完整的字符串,使用您自己设计的格式,您可以在strptime()
函数中使用它以及一个格式规范字符串。清单 2-21 中给出了一个简单的例子。
>>> date5 = datetime.datetime.strptime("1999-09-21", "%Y-%m-%d")
>>> date5.weekday()
1
Listing 2-21.Using a Format String
格式规范字符串采用与上面的strftime()
函数相同的符号。
三、迭代器和生成器
通常,您需要处理来自某个来源的一些数据序列。在 Python 中实现这一点的方法是使用迭代器。标准 Python 中许多可用的数据类型都包含一个可以使用的 iterable 接口。对于那些没有的,您可以创建一个生成器,然后提供一个 iterable 接口。
3-1.遍历列表的内容
问题
你想遍历一个列表的内容。
解决办法
虽然列表是可迭代的,但是您需要使用iter
函数来访问相关的迭代器。
它是如何工作的
清单 3-1 展示了如何访问可迭代数据类型的相关迭代器。
>>> my_list = [1, 2, 3, 4]
>>> new_iter = iter(my_list)
>>> new_iter
<list_iterator at 0x1ffb0b8e470>
Listing 3-1.Accessing an Iterator
您可以使用传统的迭代器技术来使用这个新的iterator
对象,如清单 3-2 所示。
>>> next(new_iter)
1
>>> next(new_iter)
2
Listing 3-2.Using an Iterator
一旦清空了iterator
,就会引发一个异常,如清单 3-3 所示。
>>> next(new_iter)
4
>>> next(new_iter)
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-13-ed6082c80a14> in <module>()
----> 1 next(new_iter)
StopIteration:
Listing 3-3.Getting a StopIteration Exception
3-2.提取迭代器的内容
问题
您需要枚举一个迭代器来查看它包含了哪些元素。
解决办法
enumerate
内置函数可以接受一个iterable
对象,并返回一个包含计数和值的元组列表。
它是如何工作的
enumerate
内置函数将一个iterable
对象作为输入,返回由一个计数加一个值组成的元组。实际返回的enumerate
对象本身是可迭代的,所以你可以像使用其他迭代器一样使用它。清单 3-4 中显示了一个例子。
>>> pets = ['dogs', 'cats', 'lizards', 'pythons']
>>> pet_enum = enumerate(pets)
>>> next(pet_enum)
(0, 'dogs')
>>> next(pet_enum)
(1, 'cats')
Listing 3-4.Iterating Over an Enumerator
默认情况下,计数从 0 开始。您可以通过使用 start 参数来改变这一点,如清单 3-5 所示。
>>> pet_enum2 = enumerate(pets, start=5)
>>> next(pet_enum2)
(5, 'dogs')
Listing 3-5.Enumerating with a Different Count Start
如果您需要立即获得所有枚举值以进行进一步处理,您可以使用清单 3-6 中所示的代码创建一个元组列表。
>>> pet_list = list(enumerate(pets))
>>> pet_list
[(0, 'dogs'), (1, 'cats'), (2, 'lizards'), (3, 'pythons')]
Listing 3-6.Making an Enumerated List
3-3.过滤迭代器
问题
您只需要从迭代器中过滤出选定的项目。
解决办法
内置的filter
函数可以选择性地只返回那些对于某些过滤函数为真的元素。
它是如何工作的
内置的filter
函数以过滤函数为参数。这个过滤函数应该为您感兴趣的迭代器元素返回 true。在清单 3-7 中,您会看到一个返回从 0 到 9 的奇数的例子。
>>> odd_nums = filter(lambda x: x%2, range(10))
>>> next(odd_nums)
1
>>> next(odd_nums)
3
Listing 3-7.Getting the Odd Numbers Below 10
如您所见,filter
返回了一个迭代器,您可以以这种方式使用它。如果你一次需要所有的元素,你可以使用list
函数,如清单 3-8 所示。
>>> odd_list = list(filter(lambda x: x%2, range(10)))
>>> odd_list
[1, 3, 5, 7, 9]
Listing 3-8.Getting a List of Odd Numbers
如果要使用负片滤镜,需要使用itertools
包。它包括filterfalse
函数,该函数返回那些为某些过滤函数返回 false 的元素。清单 3-9 展示了如何使用它来获得所有的偶数。
>>> import itertools
>>> even_list = list(itertools.filterfalse(lambda x: x%2, range(10)))
>>> even_list
[0, 2, 4, 6, 8]
Listing 3-9.Getting a List of Even Numbers
对于这个例子,我们将忽略关于 0 是否是偶数的争论。
3-4.迭代文件的内容
问题
您需要遍历文件的内容进行处理。
解决办法
open
函数返回一个文件对象,可以逐行迭代进行处理。
它是如何工作的
从open
函数返回的文件对象是一个可迭代对象。迭代内容的通常方式是在一个for
循环中,如清单 3-10 所示。
>>> file1 = open('file.csv')
>>> for line in file1:
. . . : print(line)
. . ..:
1,one
2,two
3,three
4,four
Listing 3-10.Looping Over a File
然而,返回的 file 对象实际上是一个 iterable 函数,所以您可以像使用其他迭代器一样使用它。清单 3-11 显示了一个例子。
>>> file1 = open('file.csv')
>>> next(file1)
'1,one\n'
>>> next(file1)
'2,two\n'
Listing 3-11.Iterating Over a File
3-5.对没有迭代器的数据进行迭代
问题
您需要创建一个不可迭代的数据的可迭代版本。
解决办法
Python 的许多内置数据结构已经是可迭代的,因此对生成器的需求较少。但是,当您确实需要一个生成器时,通常没有其他可用的解决方案。
它是如何工作的
本质上,任何将控制权交还给调用它的部分的函数都是生成器。Python 理解您打算在使用yield
语句时创建一个生成器。它会自动保存函数在执行yield
语句时的状态,这样当next()
被调用时你就可以返回到该状态。清单 3-12 显示了一个生成方块序列的简单例子。
def squares(value=0):
while True:
value = value + 1
yield (value-1)*(value-1)
>>> generator = squares()
>>> next(generator)
0
>>> next(generator)
1
>>> next(generator)
4
>>> next(generator)
9
Listing 3-12.Generating the Sequence of Squares
Python 3.3 中的新特性是yield from
语句。这是创建生成器函数的一种方式,它使用其他迭代器来生成所需的值。例如,您可以创建一个生成器,它可以计数到某个值,然后再返回,如清单 3-13 所示。
def up_down(value=1):
yield from range(1, value, 1)
yield from range(value, 0, -1)
>>> list(up_down(3))
[1, 2, 3, 2, 1]
Listing 3-13.Generating a Count-Up Count-Down Function
3-6.创建迭代器的标准类
问题
在编程时,有几种情况下您可能有最好作为某种类型的迭代器实现的数据结构。换句话说,您需要创建迭代器的一个标准类。
解决办法
itertools
模块提供了大量常用迭代器类别的选择,您可以在许多情况下使用它们。
它是如何工作的
迭代器有几大类,在很多情况下非常有用。例如,清单 3-14 展示了如何制作一个累加器。
>>> import itertools
>>> accumulator = itertools.accumulate(range(10))
>>> next(accumulator)
0
>>> next(accumulator)
1
>>> next(accumulator)
3
>>> next(accumulator)
6
Listing 3-14.Creating an Accumulator
作为一个更复杂的例子,您可以用清单 3-15 中的代码得到两个小于 5 的数字的所有组合。
>>> list(itertools.combinations(range(5), 2))
[(0,1),
(0, 2),
(0, 3),
(0, 4),
(1, 2),
(1, 3),
(1, 4),
(2, 3),
(2, 4),
(3, 4)]
Listing 3-15.Generating Combinations of Pairs
四、文件和输入输出
对于任何类型的数据处理,文件是绝对必要的。很快就会出现信息太多而无法让用户手动输入的情况。您需要能够直接从各种类型的文件中导入信息。
在本章中,你将会看到为了读写文件你需要处理的最常见的任务。您还将看到如何处理其他形式的输入和输出。
4-1.复制文件
问题
您需要将文件从一个位置复制到另一个位置。
解决办法
shutil
模块中有两个函数可用于复制文件。
它是如何工作的
复制文件涉及文件的两个部分,所述文件的实际内容和描述该文件的元数据。清单 4-1 展示了如何只复制文件的内容。
>>> import shutil
>>> new_file_path = shutil.copy('file1.txt', 'file2.txt')
Listing 4-1.Copying File Contents
这会将file1.txt
复制到file2.txt
,将file2.txt
的路径返回到变量new_file_path
。如果目录名作为第二个参数给出,那么文件将被复制到这个目录中,并使用原始文件名作为新文件名。该功能还将保留文件的权限。如果file1.txt
是一个符号链接,这个函数调用将把file2.txt
创建为一个单独的文件。如果您想实际创建一个符号链接的副本,您可以使用清单 4-2 中的例子。
>>> shutil.copy('file1.txt', 'file2.txt', follow_symlinks=False)
Listing 4-2.Copying Symlinks
如果想保留更多与文件相关的元数据,可以使用copy2()
,如清单 4-3 所示。
>>> shutil.copy2('file1.txt', 'file2.txt')
Listing 4-3.Copying File Contents Plus Metadata
可以复制的元数据量因操作系统和平台而异。它会尽可能多的复制,最坏的情况是只能做和copy()
一样多的工作。注意copy2()
永远不会返回失败。它也接受follow_symlinks
参数,就像copy()
一样。
4-2.移动文件
问题
您需要将文件从一个位置移动到另一个位置。这也是重命名文件的方法,将文件从一个文件名移动到另一个文件名。
解决办法
因为重命名和移动文件对文件系统来说本质上是一样的,所以您可以使用shutils.move()
或os.rename()
来获得相同的结果。
它是如何工作的
使用move
功能时,根据源和目的地的位置有不同的行为。如果它们都在同一个文件系统上,则使用函数os.rename()
。否则,使用复制功能将文件复制到目标位置,然后删除源文件。默认的复制函数是copy2
,但是你可以提交一些其他的函数,如清单 4-4 所示。
>>> import shutil
>>> shutil.move('file1.txt', 'dir2', copy_function=copy)
Listing 4-4.Moving a File
复制函数需要接受一个源和一个目的地,以便被move
函数使用。
如果文件在同一个文件系统上,您可以直接使用rename
函数,如清单 4-5 所示。
>>> import os
>>> os.rename('file1.txt', 'dir2')
Listing 4-5.Renaming a File
需要注意的主要事项是,如果目标已经存在,并且您有权限写入它,它将被源自动替换。
4-3.读取和写入文本文件
问题
您需要打开、读取和写入文本文件。
解决办法
您可以使用提供的内置open
函数打开文件,然后使用read
和write
方法读取和写入文件。
它是如何工作的
在 Python 中,访问文件是通过文件描述符对象完成的。这是调用内置open
函数返回的对象,如清单 4-6 所示。
>>> fd1 = open('file1.txt')
>>> entire_file = fd1.read()
Listing 4-6.Opening a Text File for Reading
文件描述符的read
方法将读入文件的全部内容。这通常不是你想做的。为了读入文件的一部分,可以传递一个大小参数,如清单 4-7 所示。
>>> chunk1 = fd1.read(100)
Listing 4-7.Reading the First 100 Bytes of a File
逐行读入数据是如此常见,以至于提供了一种方法来做到这一点。在清单 4-8 中,您可以看到如何在一行中读取,或者如何遍历整个文件。
>>> line1 = fd1.readline()
OR
>>> for line in fd1:
>>> do_my_process(line)
Listing 4-8.Reading a File Line by Line
如果文件不太大,您可以将全部内容读入一个列表,其中每个元素都是文件中的一行。清单 4-9 提供了一个例子。
>>> file_list = fd1.readlines()
>>> first_line = file_list[0]
Listing 4-9.Reading a File into a List
编写一个文本文件只需要一些小的改动。当调用open
函数时,默认情况下你将打开只读文件。为了打开文件进行写入,您需要使用不同的模式。感兴趣的模式如下:
4-4.读取和写入 XML 文件
问题
您需要读入并处理 XML 文件,然后写出结果。
解决办法
Python 标准库包括一个 XML 解析器,它可以生成一个元素树,您可以使用它。
它是如何工作的
Python 标准库包括一个ElementTree
类,它提供了一种处理 XML 结构化数据的简化方法。首先,您需要用清单 4-10 中的代码打开一个 XML 文件。
>>> import xml.etree.ElementTree as ET
>>> my_tree = ET.parse(‘my_data.xml’)
>>> root = my_tree.getroot()
Listing 4-10.Opening an XML File
一旦有了根元素,就可以查询它并获得标签和属性值,如清单 4-11 所示。
>>> root.tag
'tag value'
>>> root.attrib
{ }
Listing 4-11.Looking at Element Attributes
您可以轻松地遍历任何给定元素的子元素。例如,清单 4-12 展示了如何遍历根元素的所有子元素。
for child in root:
# look at the tag
print(child.tag)
Listing 4-12.Iterating Through the Children of the Root Element
元素也可以作为列表来访问。这意味着您可以使用列表符号,如清单 4-13 所示。
>>> child1 = root[0]
Listing 4-13.Getting the First Child of an Element
使用ElementTree
类修改现有的 XML 文件或创建一个新文件也非常容易。可以直接改变元素的文本值,可以使用set()
方法设置元素属性。然后,您可以使用write()
方法保存更改。清单 4-14 展示了如何创建一个全新的 XML 文件。
>>> a = ET.Element(‘a’)
>>> b = ET.SubElement(a, ‘b’)
>>> c = ET.SubElement(a, ‘c’)
>>> a.write(‘new_file.xml’)
Listing 4-14.Creating a New XML File
4-5.创建目录
问题
您需要创建一个新目录来写出文件。
解决办法
Python 包含了一个名为pathlib
的新模块,它提供了一种面向对象的路径处理方式。
它是如何工作的
处理路径的核心类是Path
。您需要首先创建一个新的Path
对象并设置新的目录名,如清单 4-15 所示。然后可以调用mkdir()
方法在文件系统上创建实际的新目录。
>>> from pathlib import Path
>>> p = Path('.')
>>> p.joinpath('subdir1')
>>> p.mkdir()
Listing 4-15.Creating a New Subdirectory
4-6.监视目录的变化
问题
您需要监视一个目录,并在发生变化时进行注册,比如创建了一个新文件。
解决办法
Path
类包含了一个检查目录或文件详细属性的方法。
它是如何工作的
要检查当前目录中是否发生了任何更改,您需要用清单 4-16 中的代码获取当前状态。
>>> import pathlib
>>> p = pathlib.Path('.')
>>> modified_time = p.stat().st_mtime
Listing 4-16.Finding the Status of the Current Directory
然后,您可以循环查看这个最后修改时间是否已经更改。如果是这样,那么您知道目录已经更改。如果您需要知道变更是什么,那么您需要创建一个内容列表,并在变更注册前后进行比较。您可以使用glob
方法生成当前所有内容的列表,如清单 4-17 所示。
>>> import pathlib
>>> dir_list = sorted(pathlib.Path('.').glob('*'))
Listing 4-17.Getting the Contents of a Directory
4-7.遍历目录中的文件
问题
为了处理一组文件,您需要遍历目录的内容。
解决办法
Path
类包含一个迭代内容的函数,给你一个子Path
对象的列表。
它是如何工作的
如果想遍历当前目录的内容,可以使用清单 4-18 中的代码。
import pathlib
p = pathlib.Path('.')
for child in p.iterdir():
# Do something with the child object
my_func(child)
Listing 4-18.Iterating Over the Contents of the Current Directory
如果您只想处理文件,您需要包含一个检查,如清单 4-19 所示。
import pathlib
for child in pathlib.Path('.').iterdir():
if child.is_file():
my_func(child)
Listing 4-19.Iterating Over the Files in the Current Directory
4-8.保存数据对象
问题
您需要保存 Python 对象,以便将来在另一个 Python 程序运行中使用。
解决办法
Pickling objects 是序列化 Python 对象以便以后重用的标准方式。
它是如何工作的
Python 标准库包括模块pickle
。您需要用常用的open
函数打开一个文件,将其传递给 pickle 函数。当您打开文件时,您需要记住包括二进制标志。清单 4-20 展示了如何将一个 Python 对象打包到一个文件中。
>>> import pickle
>>> file1 = open('data1.pickle', 'wb')
>>> pickle.dump(data_obj1, file1)
Listing 4-20.Pickling a Python Object
为了以后重用这些数据,您可以使用清单 4-21 中的代码将其重新加载到 Python 中。
>>> file2 = open('data1.pickle', 'rb')
>>> data_reload = pickle.load(file2)
Listing 4-21.Loading a Pickled Object
完成后,不要忘记关闭文件句柄。
4-9.压缩文件
问题
你需要压缩文件以节省空间。
解决办法
标准库中有许多模块可以帮助您处理 zip、gzip、bzip2、lzma 和 tar 文件。
它是如何工作的
首先,让我们看看如何处理 zip 文件。处理压缩文件的第一步是打开它们。这类似于 Python 中的open
函数,如清单 4-22 所示。
>>> import zipfile
>>> my_zip = zipfile.ZipFile('my_file.zip', mode='r')
Listing 4-22.Opening a Zip File
该模式的使用方式与open
功能相同。要读取当前存在的 zip 文件,可以使用模式'r'
。如果你想创建一个新的 zip 文件,你可以使用模式'w'
。您也可以使用模式'a'
修改现有的 zip 文件。
您可以使用write
方法将文件添加到 zip 文件中,如清单 4-23 所示。
>>> import zipfile
>>> my_zip = zipfile.ZipFile(‘archive1.zip’, mode=’w’)
>>> my_zip.write(‘file1.txt’)
>>> my_zip.close()
Listing 4-23.Adding a File to a Zip Archive
要从现有的 zip 存档中提取文件,请使用清单 4-24 中所示的代码。
>>> import zipfile
>>> my_zip = zipfile.ZipFile(‘archive1.zip’, mode=’r’)
>>> my_zip.extract(‘file1.txt’)
>>> my_zip.close()
Listing 4-24.Extracting One File from a Zip Archive
你可以用方法extractall()
提取一切。如果您不知道给定的归档中有哪些文件,您可以使用方法namelist()
获得一个列表。
如果您需要直接处理 zip 存档的内容,您可以从存档中读取字节和向存档中写入字节。一旦您打开了 zip 文件,您就可以使用清单 4-25 中的代码来读写归档文件
>>> import zipfile
>>> my_zip = zipfile.ZipFile(‘file1.zip’, ‘a’)
>>> data_in = my_zip.read(‘data1.txt’)
>>> my_zip.write(‘data2.txt’, data_out)
>>> my_zip.close()
Listing 4-25.Reading and Writing Bytes from a Zip Archive
与处理多个文件的压缩存档相反,gzip
和bzip2
模块都处理单个文件的压缩。要打开任何一种类型,使用清单 4-26 中的样板代码。
>>> import gzip
>>> my_gzip = gzip.GzipFile(‘data1.txt.gz’)
>>> import bz2
>>> my_bzip = bz2.BZ2File(‘data2.txt.bz’)
Listing 4-26.Opening Gzip or Bzip2 Files
在这两种情况下,您都会得到一个实现BufferedIOBase
的对象。然后,您可以读写和操作压缩文件中的数据。创建新的 gzip 或 bzip2 对象时,请记住使用适当的模式。
五、Pandas 的 Python 数据分析
Python 真正大的增长领域之一是在科学领域,其中数据分析是一个巨大的组成部分。令人高兴的是,Python 包含了一个用于数据分析的瑞士军队工具,即pandas
包,它可以通过 pip 从 PyPi 存储库中安装。pandas
提供了很多数据处理和数据处理工具,如果你来自 R 背景,你可能已经习惯了。引入了几种新的数据类型,以及以非常高效的方式处理实际数据的附加功能。它通过构建由numpy
包提供的功能来实现这种效率。
5-1.使用 1D 数据
问题
您需要处理一维数据,如数组。
解决办法
pandas 包含了一种新的数据类型,称为Series
,用于一维数据。
它是如何工作的
一旦导入了pandas
包,就可以使用Series
构造函数获取一个已经存在的数据对象,比如一个列表,并将其转换成pandas
可以处理的格式。清单 5-1 展示了如何转换一个基本列表。
>>> import pandas as pd
>>> data = [1,2,3,4]
>>> data_pd = pd.Series(data)
Listing 5-1.Converting a List into a Series
您可以选择包含一个索引数组来索引值。在清单 5-1 的例子中,索引只是数据数组中的数字索引。此外,默认情况下,数据留在原处。如果在创建Series
对象时需要创建数据的新副本,可以包含参数copy=True. pandas
来推断新Series
对象使用的数据类型。可以用dtype
参数显式设置。可能的值来自numpy
。清单 5-2 展示了如何将清单 5-1 中的数据视为浮点。
>>> data_pd.dtype
dtype('int64')
>>> import numpy as np
>>> data_pd2 = pd.Series(data, dtype=np.float64)
>>> data_pd2.dtype
dtype('float64')
Listing 5-2.Explicitly Setting the dtype
所有常用的操作符都被重载,以便与Series
对象一起使用,如清单 5-3 所示。
>>> 2 * data_pd
0 2
1 4
2 6
3 8
dtype: int64
Listing 5-3.Basic Arithmetic with Series
访问元素使用与列表相同的语法,如清单 5-4 所示。
>>> data_pd[2]
3
>>> data_pd[3] = 9
Listing 5-4.Accessing Series Data
5-2.使用 2D 数据
问题
你需要处理二维数据。
解决办法
pandas
包括一个名为DataFrame
的优化对象,支持二维数据集。
它是如何工作的
pandas
包含一个名为DataFrame
的新对象,它为 2D 数据集创建一个对象。您可以从列表词典或词典列表中创建新的DataFrame
。清单 5-5 展示了如何从一个列表字典中创建一个新的DataFrame
。
>>> d1 = {'one' : [1,2,3,4], 'two' : [9,8,7,6]}
>>> df1 = pd.DataFrame(d1)
>>> df1
One two
0 1 9
1 2 8
2 3 7
3 4 6
Listing 5-5.Creating a DataFrame
同样,标准算术运算符被重载,以便与DataFrames
一起使用。既然有了两个维度,访问元素就有点复杂了。默认情况下,寻址是基于列的。这意味着您可以使用适当的标签直接访问给定的列,如清单 5-6 所示。
>>> df1['one']
0 1
1 2
2 3
3 4
Name: one, dtype: int64
>>> df1['one'][2]
3
Listing 5-6.Accessing DataFrame
Columns
如果想按行访问数据,需要使用DataFrame
的iloc
或loc
属性,如清单 5-7 所示。
>>> df1.loc[1]
one 2
two 8
Name: 1, dtype: int64
>>> df1.loc[1][1]
8
Listing 5-7.Accessing DataFrame Rows
5-3.使用 3D 数据
问题
你需要用pandas
处理三维数据集。
解决办法
pandas
包含一个名为Panel
的新对象,可以有效地处理三维数据集。
它是如何工作的
类似于创建一个DataFrame
,创建一个新的Panel
对象可以用一个DataFrames
的字典来完成。清单 5-8 显示了一个基本的例子。
>>> data_dict = {'item1' : pd.DataFrame(np.random.randn(4, 3)), 'item2' : pd.DataFrame(np.random.randn(4, 2))}
>>> data_panel = pd.Panel(data_dict)
Listing 5-8.Creating a Panel
您可以使用标签访问新Panel
中的每个DataFrames
。例如,您可以用清单 5-9 中的代码得到item2 DataFrame
。
>>> data_panel['item2']
0 1 2
0 -2.126160 -0.677561 NaN
1 -2.110622 -1.535510 NaN
2 -0.387182 -1.412219 NaN
3 -0.370628 0.305436 NaN
Listing 5-9.Accessing DataFrames within a Panel
一旦有了一个DataFrame
,就可以像上一节一样访问单个元素。
5-4.从 CSV 文件导入数据
问题
您需要从 CSV(逗号分隔值)文件中导入离线收集的数据。
解决办法
Pandas 包含一个名为read_csv()
的方法,可以导入和解析 CSV 文件。
它是如何工作的
在pandas
包中有一个顶级方法可以读入 CSV 文件。清单 5-10 显示了最基本的用法。
>>> csv_data = pd.read_csv('data_file.csv')
Listing 5-10.Reading a CSV File
默认情况下,pandas
将从 CSV 文件的第一行读取列名。如果在其他行上有列名,可以使用参数header=X
将pandas
重定向到行X
来提取列名。如果您的文件没有列名,您可以使用参数header=None
,然后用参数names=[col1, col2, …]
提交列名列表。另外,默认情况下,pandas
会将第一列视为每一行的标签。如果行标签在另一列中,可以使用参数index_col=X
。如果您根本没有任何行标签,您可能想要使用参数index_col=False
来强制pandas
不使用任何列。
5-5.保存到 CSV 文件
问题
您希望将数据保存到 CSV 文件中,以便与其他人或应用共享。
解决办法
Series
和DataFrame
对象包含一个名为to_csv()
的方法。
它是如何工作的
如果你需要保存你已经处理过的数据,你可以调用带有文件名的to_csv()
方法,如清单 5-11 所示。
>>> series_data.to_csv('export_file.csv')
Listing 5-11.Saving to a CSV File
有一些参数可用于更改引用所使用的分隔符或字符。默认情况下,pandas
将写出列标题和行标签。如果你只想要数据,你可以使用清单 5-12 中的代码。
>>> data2.to_csv('data_file.csv', header=False, index=False)
Listing 5-12.Saving Data without Headers and Labels
默认情况下,pandas
将覆盖已经存在的输出文件。如果您想附加到一个已经存在的文件,您可以用参数mode='a'
改变输出模式。
5-6.从电子表格导入
问题
您希望从电子表格中导入现有数据。
解决办法
pandas
包括一个从电子表格文件导入单个工作表的方法,以及一个包装类,如果你需要从一个给定的文件中处理多个工作表的话。
它是如何工作的
如果你只需要从一个文件中导入一个工作表,你可以使用清单 5-13 中的代码来完成。
>>> data_frame1 = pd.read_excel('data_file.xsl', sheetname='Sheet1')
Listing 5-13.Importing a Single Spreadsheet Sheet
这将把数据作为单个DataFrame
对象导入。
如果您希望处理多个工作表,为了方便访问,将文件一次性加载到包装类中会更有效。幸运的是,pandas
有这样一个包装类,如清单 5-14 所示。
>>> excel_data = pd.ExcelFile('data_file.xsl')
Listing 5-14.Wrapping a Spreadsheet
in pandas
然后,您可以将这个对象传递给read_excel()
方法,而不是一个文件名。
5-7.保存到电子表格
问题
您想要将您的DataFrame
保存到电子表格文件中。
解决办法
DataFrame
类包含一个名为to_excel()
的方法,它将数据写出到一个文件中。
它是如何工作的
编写输出的最简单方法如清单 5-15 所示。
>>> df.to_excel('output_file.xsl', sheet='Sheet1')
Listing 5-15.Writing Output to a Spreadsheet File
pandas
会根据文件扩展名选择不同的编写引擎。您也可以使用文件名结尾.xslx
保存文件。
5-8.得到头部和尾部
问题
您希望查询数据以了解其结构。
解决办法
有一些函数可以用来查看给定数据集的开头或结尾。一旦你开始使用大型数据集,它们就非常有用。
它是如何工作的
Series
和DataFrame
对象都有名为head()
和tail()
的方法。默认情况下,对于给定的数据集,它们会分别给出前五个条目或后五个条目。如果您想查看更多或更少的数据,您可以包含一个参数来表示您想要查看的条目数量,如清单 5-16 所示。
>>> data_series = pd.Series(np.random.randn(1000))
>>> data_series.head(2)
0 -0.399614
1 1.307006
dtype: float64
>>> data_series.tail(2)
998 0.866424
999 -0.321825
dtype: float64
Listing 5-16.Getting the First and Last Two Data Entries
5-9.汇总数据
问题
您希望获得数据集的统计摘要。
解决办法
pandas
中引入的新数据对象包括一组用于提供数据汇总统计的方法。
它是如何工作的
有几种方法可用于个体统计,如均值或标准差。还有一个名为describe()
的方法提供完整的摘要,如清单 5-17 所示。
>>> data_series.describe()
count 1000.000000
mean -0.029861
std 0.990916
min -3.261506
25% -0.697940
50% -0.048408
75% 0.646266
max 3.595167
dtype: float64
Listing 5-17.Describing Your Data
这些值中的每一个都可以通过单独的方法获得。例如,清单 5-18 展示了如何验证标准差等于方差的平方根。
>>> data_series.std() ** 2
0.9819137628988116
>>> data_series.var()
0.9819137628988115
Listing 5-18.Comparing the Standard Deviation and the Variance
这看起来不错,在浮点数的精度范围内。
5-10.分类数据
问题
您希望对数据进行一些排序作为预处理。
解决办法
Series
和DataFrame
对象包含按索引或按值排序的方法。
它是如何工作的
如果您的数据是以随机顺序输入的,您可能需要在进行实际分析之前做一些预处理。清单 5-19 展示了如何根据行或列标签对DataFrame
进行排序。
>>> df = pd.DataFrame({'one' : [1,2,3], 'two' : [4,5,6], 'three' : [7,8,9]}, index=['b','c','a'])
>>> df
one three two
b 1 7 4
c 2 8 5
a 3 9 6
>>> df.sort_index()
one three two
a 3 9 6
b 1 7 4
c 2 8 5
>>> df.sort_index(axis=1, ascending=False)
two three one
b 4 7 1
c 5 8 2
a 6 9 3
Listing 5-19.Sorting a DataFrame by Index
您可能希望对数据进行排序的另一种方式是根据实际数据值。在这种情况下,您需要决定根据哪一列进行排序。清单 5-20 展示了如何按降序对第二列进行排序。
>>> df.sort_values(by='two', ascending=False)
one three two
a 3 9 6
c 2 8 5
b 1 7 4
Listing 5-20.Sorting a DataFrame by Values
Note
从版本 0.17.0 开始,这些方法返回一个新的排序对象,除非您使用参数inplace=True
。在这个版本之前,排序发生在原始数据对象中。
5-11.按行或按列应用函数
问题
您需要对整个行或列执行函数。
解决办法
DataFrames
和Panels
都有一个叫做apply()
的方法,可以用来跨列或行应用函数。
它是如何工作的
清单 5-21 展示了如何找到每一列和每一行的平均值。
>>> df = pd.DataFrame({'one' : [1,2,3], 'two' : [4,5,6], 'three' : [7,8,9]}, index=['b','c','a'])
>>> df.apply(np.mean)
one 2.0
three 8.0
two 5.0
dtype: float64
>>> df.apply(np.mean, axis=1)
b 4.0
c 5.0
a 6.0
dtype: float64
Listing 5-21.Averaging Rows and Columns
这是使用 lambda 函数的好地方,如果你的函数足够简单,可以放入一个表达式中。清单 5-22 显示了一个简单的 lambda 函数的例子,该函数简单地将数据帧中的值加倍。
>>> df.apply(lambda x: 2*x, axis=1)
one three two
b 2 14 8
c 4 16 10
a 6 18 12
[3 rows x 3 columns]
Listing 5-22.Applying a Lambda Function on a Data Set
5-12.按元素应用函数
问题
您需要将函数应用于数据集中的所有元素或元素的某个子集。
解决办法
新的 pandas 数据对象有两个名为map()
和applymap()
的有用方法,可用于将函数应用到单个元素组。
它是如何工作的
有时候,您需要对数据集的单个元素应用一些函数。清单 5-23 展示了如何对一个数据集的所有元素求平方。
>>> df = pd.DataFrame({'one' : [1,2,3], 'two' : [4,5,6], 'three' : [7,8,9]}, index=['b','c','a'])
>>> df.applymap(lambda x: x*x)
one three two
b 1 49 16
c 4 64 25
a 9 81 36
Listing 5-23.Squaring Data Elements
如果你只想对一个给定的列应用一个函数,清单 5-24 展示了如何将列2
中的值加倍。
>>> df['two'].map(lambda x: 2*x)
b 8
c 10
a 12
Name: two, dtype: int64
Listing 5-24.Doubling a Single Column of Elements
5-13.迭代数据
问题
作为处理工作流的一部分,您需要迭代您的数据集。
解决办法
数据对象是可迭代的对象,在大多数情况下,当你需要循环所有的元素时,可以使用它。
它是如何工作的
如果您想对pandas
提供的一个新数据对象进行基本迭代,您需要意识到它们的行为略有不同。迭代的基本类型有
Series
:迭代每个元素DataFrame
:遍历各列Panel
:迭代项目标签
例如,清单 5-25 展示了如何使用迭代计算每一列的平均值。
>>> df = pd.DataFrame({'one' : [1,2,3], 'two' : [4,5,6], 'three' : [7,8,9]}, index=['b','c','a'])
>>> for col in df:
....: print(df[col].mean())
....:
2.0
8.0
5.0
Listing 5-25.Averaging Each Column of a DataFrame
如果您需要迭代一个DataFrame
的行,清单 5-26 显示了一个例子。
>>> for row_index,row in df.iterrows():
....: print(row_index)
....: print(row)
....:
b
one 1
three 7
two 4
Name: b, dtype: int64
c
one 2
three 8
two 5
Name: c, dtype: int64
a
one 3
three 9
two 6
Name: a, dtype: int64
Listing 5-26.Iterating Over Rows
然而,请注意,迭代可能会相当慢。只要有可能,您可能会希望找到另一种方式来表达您需要执行的处理工作流。
六、函数
任何现代编程语言都需要某种方式将函数组合成可重用的代码块。在 Python 中,可重用代码的最基本单元之一是函数。在这一章中,你将会看到函数是如何在 Python 中工作的,以及它们可能会做一些令人惊讶的事情的地方。
6-1.创建基本函数
问题
您希望创建一个基本函数来处理一个简单的任务,比如求 2 的平方。
解决办法
在 Python 中,可以使用内置的def
关键字来创建一个新函数。
它是如何工作的
清单 6-1 给出了一个例子。
def square_of_two():
ans = 2 * 2
return ans
Listing 6-1.Defining a Basic Function
如您所见,函数代码的主体是由缩进级别定义的。第一行使用def
并创建一个具有给定名称的新函数。在本例中,您创建了一个名为square_of_two()
的函数。现在,您可以像调用任何其他函数一样调用该函数。例如,参见清单 6-2 。
>>> square_of_two()
4
>>> my_ans = square_of_two()
Listing 6-2.Calling a Function
如果您的函数应该向调用语句返回值,那么您需要使用内置的return
关键字。如果你的新函数中有条件或循环,你需要增加这些结构的缩进量,如清单 6-3 所示。
def fact_two():
a = 2
ans = a
while a > 1:
ans = ans * (a-1)
a = a - 1
return ans
Listing 6-3.Factorial of Two Function
这将返回 2 的阶乘,即 2。
6-2.使用命名参数而不是位置参数
问题
您希望将参数传递给函数,可以选择使用名称。这允许参数以任意顺序传递给函数。
解决办法
由于变量名称在 Python 中是非类型化的,所以只需在括号内的参数列表中添加名称。这些可以基于位置使用,也可以通过名称显式使用。
它是如何工作的
添加参数非常简单,如清单 6-4 所示。
def square_num(a):
return a*a
Listing 6-4.Squaring Any Number
然后你可以用两种不同的方式调用这个函数,或者通过位置或者通过标签。清单 6-5 显示了这将会是什么样子。
>>> square_num(2)
4
>>> square_num(a=3)
9
Listing 6-5.Calling Functions with Parameters
如果有多个参数,可以混合使用位置参数和命名参数。唯一需要注意的是,在使用任何命名参数之前,需要包含所有位置参数。清单 6-6 显示了一个简单的例子。
def multiplication(a, b, c):
return a*b*c
>>> multiplication(1, 2, 3)
6
>>> multiplication(2, c=3, b=1)
6
Listing 6-6.Multiplying Many Numbers
如果你尝试类似于multiplication
(1, a=2, c=3)
的东西,你会得到一个错误,因为根据位置规则,你已经给了 a 一个值,然后你试图用命名参数a=2
给它另一个值。同样,如果您试图在一个命名参数之后使用一个位置参数,比如multiplication(a=1,b=2,3)
,您也会得到一个错误。
6-3.在函数中使用默认值
问题
如果没有传递默认值,您希望允许函数使用默认值。
解决办法
定义函数及其输入参数时,如果没有提供任何参数,可以包含一个默认值。
它是如何工作的
在定义函数时,您可以简单地说明需要什么样的默认值,如清单 6-7 所示。
def multiplication(a=1,b=2,c=3):
return a*b*c
Listing 6-7.Defining Default Parameter Values
关于位置参数和命名参数的所有规则仍然适用。清单 6-8 展示了一些例子。
>>> multiplication()
6
>>> multiplication(5)
30
>>> multiplication(1,1)
3
>>> multiplication(c=1)
2
Listing 6-8.Multiplication Examples
既然您有了默认值,那么对于那些没有亲自阅读过代码的人来说,您就有了一些看不见的操作。在这种情况下,切换到使用严格命名的参数有助于澄清代码并帮助将来的代码维护是有意义的。
6-4.实现递归算法
问题
您需要在 Python 程序中实现一个递归算法。
解决办法
Python 支持递归,因此您可以从函数内部调用函数。
它是如何工作的
因为 Python 支持递归,所以可以简单地直接调用函数。典型的例子是计算阶乘,如清单 6-9 所示。
def fact(a):
if a == 1:
return 1
else:
return a * fact(a-1)
>>> fact(5)
120
Listing 6-9.Calculating Factorials Through Recursion
虽然有一些算法实际上只作为递归函数工作,但应该谨慎对待它们。递归函数实际上是嵌套的函数调用。这意味着递归的每一步都需要在下一次调用之前将当前状态推送到堆栈上。根据需要跟踪的内容,您可能会很快用完大量 RAM。此外,每个函数调用都需要更多时间来进行上下文切换。因此,在开始使用递归函数之前,一定要确保这是唯一的解决方案。
6-5.使用 Lambda 函数创建临时匿名函数
问题
您临时需要一个函数(例如,作为另一个函数的参数),并且不需要通过名称访问它。
解决办法
Python 有内置的lambda
语句机制,它提供了创建和使用匿名函数的能力。
它是如何工作的
为了展示如何使用 lambda 函数,我们需要一个需要 lambda 函数的示例。清单 6-10 展示了一个带两个参数的函数和一个函数,并以两个给定的参数作为输入执行给定的函数。
def apply_operator(a, b, f):
return f(a,b)
Listing 6-10.Applying a Function
如果您只想在上面的示例代码中使用一次性函数,可以在调用中直接使用 lambda 函数。清单 6-11 展示了如何应用乘法函数。
>>> apply_operator(2, 3, lambda x, y: x*y)
6
Listing 6-11.Applying a Multiplication Function
lambda 函数的最大限制是它们被限制在一个表达式行中。任何大于这个值的都需要定义为常规函数。
6-6.生成专门的函数
问题
您需要创建一个能够为特殊情况生成专用函数的函数。例如,您可能希望对复数而不是常规浮点数使用不同的平均函数。
解决办法
使用 lambda 函数,您可以生成专门的函数。
它是如何工作的
因为函数只是另一种类型的对象,所以它们可以从函数调用中返回。利用这一事实,您可以创建一个函数,它接受一些输入参数,并踢出由它定义的函数。例如,清单 6-12 基于输入值生成一个缩放函数。
def generate_scaler(a):
return lambda x: a*x
Listing 6-12.Generating Scaling Functions
然后,您可以使用这个生成器创建一个将数字缩放 2 或 3 的函数,如清单 6-13 所示。
>>> doubler = generate_scaler(2)
>>> doubler(3)
6
>>> tripler = generate_scaler(3)
>>> tripler(3)
9
Listing 6-13.Function Generator
Examples
七、类和对象
一旦有了将代码块存储到可重用函数中的方法,想要将多个函数捆绑到更大的可重用代码块(称为对象)中是很自然的事情。除了这些实际的代码,您可能还想以属性的形式存储数据。为了定义这些新对象,您需要创建类,然后将它们实例化为您可以使用的实际对象。在这一章中,你将会看到在你开始定义和使用新对象时出现的一些最常见的任务。
7-1.发现对象的类型(一切都是对象)
问题
Python 中的几乎所有东西都是对象。重要的是弄清楚你拥有什么样的物品。
解决办法
内置函数type()
返回作为参数传入的对象的类。您还可以使用isinstance()
来测试给定的对象是否属于某个特定类的同一类型。
它是如何工作的
清单 7-1 给出了一个例子。
>>> a = 1
>>> type(a)
<class 'int'>
>>> b = "Hello World"
>>> type(b)
<class 'str'>
Listing 7-1.Checking the Type of an Object
这将查询给定的对象并返回输入对象的类型对象。您可以使用它来检查两个对象是否属于同一类。如果您想查看一个对象是否属于某个特定的类,您可以使用isinstance()
,如清单 7-2 所示。
>>> a = 1
>>> isinstance(a, int)
TRUE
Listing 7-2.Is an Object of a Particular Class?
7-2.创建类
问题
您希望创建一个新的类,封装一组方法和属性,以便在其他代码中使用。
解决办法
关键字class
允许你定义一个新的类。
它是如何工作的
与函数一样,定义一个新类就像使用class
关键字,然后拥有一个缩进的代码块一样简单。你可以在清单 7-3 中看到一个非常简单的例子。
class my_simple_class:
a = 3
b = "Hello World"
Listing 7-3.Creating a Simple Class
清单 7-4 展示了一个更复杂的例子,也包括一系列方法。
class my_complex_class:
a = 42
def method1():
print("The value of a is " + str(a))
b = "Hello World"
Listing 7-4.Creating a Complex Class
7-3.添加私有字段
问题
您希望您的类的某些部分是私有的,也就是说,不能从其他代码段访问。
解决办法
在 Python 中,所有的方法和属性都是公开可见的。作为一种解决方案,在属性或方法的名称中添加下划线字符几乎被普遍接受,因为它代表了一个不适合公共使用的元素。
它是如何工作的
定义一个只能在所讨论的类中使用的属性应该至少有一个下划线字符作为前缀。通常的做法是在元素名的开头和结尾添加两个下划线字符,如清单 7-5 所示。
class priv_vars:
__a__ = "This is a private variable
"
Listing 7-5.Creating a Private Variable
有一种特殊形式的私有名称,它促使 Python 使用名称管理系统来重命名元素。如果某个元素可能与其他元素同名,则可以添加至少两个前导下划线字符和最多一个尾随下划线字符。然后 Python 会将类名添加到元素名的前面。例如,如果你有一个像
class my_class1:
__a_ = 1
属性__a_
将被重命名为_my_class__a_
以避免名称冲突。
7-4.子类
问题
您希望在另一个类中提供的功能基础上进行构建。
解决办法
子类化的能力是任何面向对象编程语言的关键。Python 支持从单个基类或多个基类继承。
它是如何工作的
从一个类继承只是在新类的定义中添加一个对相关类的引用。清单 7-6 中给出了一个简单的例子。
class animal:
type = "mammal"
class dog(animal):
breed = "labrador"
>>> my_dog = dog()
>>> my_dog.breed
labrador
>>> my_dog.type
mammal
Listing 7-6.Inheriting from a Class
如您所见,类dog
继承了animal
类型的属性。解析方法和属性的方式是首先检查主类,看它是否存在。如果没有,那么 Python 将检查任何继承的类,看看该属性或方法是否存在。这意味着您可以用自己的版本重写继承的方法或属性。清单 7-7 中animal
类型改为bird
。
class bird(animal):
type = "bird"
>>> my_bird = bird()
>>> my_bird.type
bird
Listing 7-7.Overriding Class Attributes
如果您从多个类继承,您可以简单地将它们作为多个参数添加到您的类定义中,如清单 7-8 所示。
class eagle(animal, bird):
species = "eagle"
>>> my_eagle = eagle()
>>> my_eagle.type
animal
Listing 7-8.
Multiple Inheritance
如您所见,在处理多重继承时,顺序很重要。当 Python 尝试解析方法或属性的定义位置时,它从主类开始,然后从左到右依次遍历继承的类,直到找到所述方法或属性的第一个实例。以上案例中,正确的继承顺序应该是class eagle(bird, animal)
。
7-5.初始化对象
问题
当实例化一个新对象时,需要执行一些初始值或初始处理步骤。
解决办法
您可以使用私有方法__init__
在初始化时运行设置代码。
它是如何工作的
当 Python 实例化一个新对象时,它会查看是否有一个名为__init__
的方法。如果找到了,这个函数会在实例化完成后立即执行。它还可以在实例化时获取参数,这些参数可用于进一步处理设置步骤,如清单 7-9 所示。
class my_complex:
def __init__(self, real_part, imaginary_part):
self.r = real_part
self.i = imaginary_part
>>> complex_num = my_complex(2, 3)
>>> complex_num.r
2
Listing 7-9.
Initialization Functions
参数self
指的是被实例化的对象,允许你在初始化过程中与对象进行交互。
7-6.比较对象
问题
你需要比较两个物体,看它们是否相同。
解决办法
在比较对象时,有两种不同的相等性思想:将一个对象与其自身进行比较,并查看两个不同的对象是否具有相同的属性。
它是如何工作的
第一种类型的等式是测试两个变量名是否实际指向同一个对象。这是 Python 中对象和指向它们的标签分离的副作用。为了测试这种类型的等式,您需要使用操作符is
和is not
,如清单 7-10 所示。
>>> a = "string1"
>>> b = a
>>> a is b
True
Listing 7-10.Comparing Object Identities
下一种类型的比较包括比较两个对象的内容,看它们是否有相同的值。值的概念在 Python 中不是一个通用的概念。对象值的计算由使用运算符==
、!=
、<=
、>=
、<
和>
时执行的运算符代码处理。根据您定义的任何类的详细信息,您可能希望覆盖这些比较运算符的代码。例如,假设您已经创建了一个表示一本书的类,并且您希望使用页数作为给定 book 对象的值。然后,您可以用清单 7-11 中的代码覆盖这些操作符。
class book:
def __init__(self, pages):
self.pages = pages
def __lt__(self, other):
return self.pages < other
def __gt__(self, other):
return self.pages > other
....
Listing 7-11.Overriding Comparison Operators
依此类推,适用于所有的运算符方法。
7-7.创建后更改对象
问题
创建对象后,您需要对其进行更改。
解决办法
在 Python 中,几乎所有对象都是可塑的,可以根据它们的属性和方法进行修改。内置对象,如str
或int
,没有可塑性。从您自己的类定义中创建的对象是可扩展的,可以动态地创建和使用新的属性。
它是如何工作的
例如,您可以使用清单 7-11 中定义的类,通过简单地使用一个名为title
的新属性来添加图书的标题,如清单 7-12 所示。
>>> book1 = book(42)
>>> book1.title = "My Book"
>>> book1.title
My Book
Listing 7-12.Dynamically Created Attributes
您可以通过定义一个什么都不做的类,利用这种延展性来创建非常快速、灵活的数据结构,如清单 7-13 所示。
class my_container:
pass
>>> container1 = my_container()
>>> container1.label = "The first container"
>>> container1.phone = "555-5555"
>>> container1.name = "John Doe"
Listing 7-13.Empty Classes for Data Storage
关键字pass
是一个无操作函数。它实际上占用了表达式应该去的地方的空间,但是告诉 Python 实际上没有任何代码要运行。这使您可以创建一个完全空的对象,以后可以添加到该对象中。
7-8.实现多态行为
问题
您需要包括根据输入内容而变化的行为。
解决办法
Python 通过它是一种鸭式语言的事实来处理多态性。基本上,当一个函数试图使用一个对象作为参数时,它实际上会通过该对象需要实现的一些标准方法从该对象获取值。
它是如何工作的
展示这种技术的最好方式是使用一个例子。清单 7-14 显示了本例中使用的一系列类和函数。
class dog:
def talk(self):
print("bark")
class cat:
def talk(self):
print("meow")
def speak(animal):
animal.talk()
Listing 7-14.Polymorphic Classes and Methods
从这里开始,根据您作为参数传递的动物类型,您将从函数speak()
获得不同的行为。清单 7-15 展示了这种变化行为的一个例子。
>>> my_dog = dog()
>>> my_cat = cat()
>>> speak(my_dog)
bark
>>> speak(my_cat)
meow
Listing 7-15.Polymorphic Behavior
八、元编程
编程的一个关键原则是不要重复自己。每当你不止一次地做同样的任务时,你应该看一看它,看看是否有什么方法可以使它自动化。这是写程序的首要原因。但是这同样适用于编写代码本身的任务。如果您正在重复代码块,您应该后退一步,看看是否有一些更好的方法来达到相同的结果。
处理这个问题的一种技术是元编程。本质上,元编程是影响其他代码的代码。在 Python 中,通常的做法是使用装饰器、元类或描述符。
8-1.使用函数装饰器包装现有代码
问题
您希望通过用其他代码包装一个已经存在的函数来改变它的行为。如果不同的模块使用相同的装饰名,那么这个包装器代码可以换入或换出,允许您以不同的方式修改原始函数。
解决办法
通过在函数定义的顶部添加一个装饰符,使用一个以&符号开始的标签,可以包装一个函数。
它是如何工作的
清单 8-1 展示了一个使用由line_profile
模块提供的装饰器的例子。
from line_profile import *
@profile
def my_adder(x, y):
return x + y
Listing 8-1.Using the Profile Decorator
这段代码用来自line_profile
模块的分析代码包装函数my_adder()
。这个模块不是标准库的一部分,所以您需要将它安装到您的系统上。这与用另一个函数显式包装一个函数是一样的,如清单 8-2 所示。
from line_profiler import *
def my_adder(x, y):
return x + y
my_adder = profile(my_adder)
Listing 8-2.Wrapping a Function
8-2.编写函数装饰器来包装现有代码
问题
你想为一个函数写一个包装器来增加额外的功能。
解决办法
Python 包含了wraps
关键字,该关键字定义了一个可以包装另一个函数并用作装饰器的函数。
它是如何工作的
为了编写自己的装饰器,您需要使用来自functools
模块的wraps
关键字。这个关键字被用作装饰器来帮助定义你自己的新装饰器。清单 8-3 显示了一个打印出被修饰函数的函数名的例子。
from functools import wraps
def function_name(func):
message = func.__qualname__
@wraps(func)
def wrapper((*args, **kwargs)):
print(message)
return func(*args, **kwargs)
return wrapper
Listing 8-3.A Decorator to Print Out Function Names
然后你可以像其他装饰器一样使用它,如清单 8-4 所示。
@function_name
def adder(x,y):
return x+y
Listing 8-4.Using Your Own Decorator
8-3.展开修饰函数
问题
您需要访问已被修饰的函数的功能。
解决办法
你可以通过使用函数的__wrapped__
属性得到原始的解包函数。
它是如何工作的
假设装饰器是使用来自functools
的wraps
函数正确编码的,那么您可以通过使用__wrapped__
属性获得原始函数,如清单 8-5 所示。
>>> adder(2,3)
adder
5
>>> adder.__wrapper__(2,3)
5
Listing 8-5.Getting the Unwrapped Function
8-4.使用元类改变类的结构
问题
您需要向类中添加额外的功能,类似于函数的装饰器。这可以通过使用元类改变一个类是哪个对象的实例来实现。
解决办法
元类的使用方式类似于子类。
它是如何工作的
当使用元类时,将它包含在类定义中,如清单 8-6 所示。
class my_counter(metaclass=Singleton):
pass
Listing 8-6.Using a Metaclass
默认情况下,类是类型class
的实例。清单 8-6 中的代码使新类成为Singleton
类的实例,而不是类型class
。您还可以在类定义中设置元类,如清单 8-7 所示。
class my_counter():
__metaclass__ = Singleton
pass
Listing 8-7.Setting the __metaclass__ Attribute
在清单 8-6 和 8-7 中,您的新类被创建为Singleton
类的一个实例。这是在 Python 中使用 singleton 设计模式的一种方法。
8-5.编写元类
问题
您需要通过编写自己的元类来改变类的实例化方式。
解决办法
通过使用元类,您可以重新定义一个类实际上是如何实例化的,例如,允许您创建只能实例化一次的类(singleton 设计模式),或者被缓存的类。这些示例用于日志类或流数据解析器。
它是如何工作的
您可以通过构建一个覆盖实例化过程中使用的一个或多个函数的类来创建元类。例如,您可以覆盖__call__
函数来创建一个不能实例化的类,如清单 8-8 所示。
class CannotInit(type):
def __call__(self, *args, **kwargs):
raise TypeError("Cannot instantiate")
Listing 8-8.A Metaclass That Stops Instantiation
现在,当您试图将它用作元类并直接实例化新类时,将会引发一个异常。
如果您需要更复杂的行为,例如在单例中,您可以覆盖多个函数,如清单 8-9 所示。
class MySingleton(type):
def __init__(self, *args, **kwargs):
self.__instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
return self.__instance
Listing 8-9.Creating a Singleton Metaclass
这段代码捕获了使用这个元类的任何类的实例化和调用,因此一次只能存在一个实例。
8-6.使用签名改变函数接受的参数
问题
您希望能够在运行时控制函数的签名。这允许您动态地更改函数接受的参数。例如,可以强制函数在一种情况下只使用关键字参数,然后在另一种情况下允许使用位置参数或关键字参数。
解决办法
inspect
模块包括创建和使用函数签名所需的工具。
它是如何工作的
清单 8-10 中的例子展示了如何创建一个新的签名。
>>> from inspect import Signature, Parameter
>>> params = [Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
... Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
... Parameter('z', Parameter.KEYWORD_ONLY, default=None)]
>>> my_sig = Signature(params)
>>> print(my_sig)
(x, y=42, *, z=None)
Listing 8-10.Creating a Signature
在这段代码中,您使用Parameter
类创建一个函数参数列表。每种类型的参数都有关键字。需要注意的一点是,如果在一个普通的函数定义中有一个只包含关键字的参数列表,那么可以使用星号来标记哪些参数是只包含关键字的。当您打印出新创建的签名时,就会显示出来。
要使用这个签名,您可以使用bind
方法获取位置和关键字参数的一般列表,并将它们绑定到您创建的签名中的参数。清单 8-11 中给出了一个例子。
def my_func(*args, **kwargs):
bound_values = my_sig.bind(*args, **kwargs)
for name, value in bound_values.arguments.items():
print(name, value)
Listing 8-11.Using a Signature
这样,您可以让同一个函数使用不同的签名绑定参数,具体取决于您需要如何处理它们。
九、网络和互联网
互联网彻底改变了我们使用电脑的方式。以前,我们关注的是我们能用办公桌上的硬件做什么;现在我们可以考虑在分布在全球的机器上可以做什么工作。在本章中,您将学习通过网络(如互联网)与其他机器通信的一些核心技术。您将从查看最底层的方法之一开始:使用套接字。秘籍的其余部分将着眼于更高层次的技术,这些技术隐藏了围绕互联网通信的许多复杂性。
9-1.打开套接字连接
问题
您希望打开一个原始网络套接字连接来处理非结构化数据通信。
解决办法
Python 标准库包括一个socket
类,它公开了网络通信的底层接口。
它是如何工作的
socket
类提供了一种在非常低的级别访问网络硬件的方法。因此,它必须支持许多不同的网络协议。对于这些方法,您将关注最常见的情况,即希望通过以太网连接创建 TCP/IP 套接字。socket
模块包含了socket
类和其他几个你可以使用的实用函数和类。清单 9-1 展示了如何创建一个新的套接字并连接到一个远程机器。
import socket
host = '192.168.0.1'
port = 5050
my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_sock.connect((host, port))
Listing 9-1.Opening a Socket to a Remote Machine
在这段代码中,机器地址以包含 IP 地址的字符串形式给出,端口以数字形式给出。这个实例化方法创建了一个socket
对象,它既可以用来建立到远程机器的传出连接,也可以用来监听来自这些远程机器的传入连接请求。如果您只对建立一个到远程机器的传出连接感兴趣,您可以使用create_connection()
方法,如清单 9-2 所示。
import socket
host = '192.168.0.1'
port = 5050
my_sock = socket.create_connection
((host, port))
Listing 9-2.Making an Outgoing Socket Connection
这个新创建的socket
对象现在可以用来向远程机器发送数据和从远程机器接收数据。当您完成一个给定的套接字连接时,不要忘记关闭它,以便操作系统可以干净地关闭该连接并在它之后进行清理。清单 9-3 展示了这样做的样板代码。
my_sock.close()
Listing 9-3.Closing a Socket Connection
9-2.通过套接字读/写
问题
您希望通过开放的套接字连接与远程机器通信。
解决办法
socket
类包含许多通过套接字连接发送和接收数据的不同方法。
它是如何工作的
一旦套接字被打开,您就可以使用来自socket
对象的方法来发送和接收数据。发送数据最基本的方法是使用send()
方法,如清单 9-4 所示。
msg = b'Hello World'
mesglen = len(msg)
totalsent = 0
while totalsent < msglen:
sent = my_sock.send(msg[totalsent:])
totalsent = totalsent + sent
Listing 9-4.Sending Data Over a Socket
这里有几点需要注意。首先,套接字通过网络发送字节,因此您的消息需要是一个字节串。第二件要注意的事情是,send()
方法并不保证在任何特定的调用中会发送多少数据。它所做的只是返回在任何特定调用中成功发送的字节数,因此需要一个while
循环来继续发送,直到您确定所有内容都已传输完毕。如果您正在发送简单的数据块,您可以使用sendall()
方法,如清单 9-5 所示,它将处理循环,直到所有的数据都被发送完。
my_sock.sendall(b'Hello World')
Listing 9-5.Using sendall() with a Socket
接收数据的方法与发送数据的方法非常相似。一个主要的区别是,您需要告诉方法一次读入多少字节。例如,清单 9-6 展示了如何读入上面发送的数据,并确保得到了所有数据。
data_in = my_sock.recv(1024)
Listing 9-6.Reading Data from a Socket
这是可行的,因为您确实知道正在发送的消息长度小于 1,024 字节。如果消息较长,或者是可变的,就必须一遍又一遍地循环,直到收集到所有独立的块,就像发送数据时必须循环一样。与sendall()
方法等效的接收方法是recv_into()
方法。它允许您将数据接收到一个预先构造的缓冲区中,当所有的数据都已被接收或缓冲区已被填满时停止。清单 9-7 中给出了一个例子,展示了如何将多达 1024 个字节读入一个缓冲区。
buffer = bytearray(b' ' * 1024)
my_sock.recv_into(buffer)
Listing 9-7.Receiving Data Directly into a Buffer
9-3.用 POP 阅读电子邮件
问题
您想从 POP 电子邮件服务器上阅读电子邮件。
解决办法
Python 标准库包含一个名为poplib
的模块,它封装了与 POP 服务器的通信。
它是如何工作的
与 POP 服务器通信涉及几个步骤。最基本的初始代码包括打开到 POP 服务器的连接和认证,如清单 9-8 所示。
import getpass, poplib
pop_serv = poplib.POP3('192.168.0.1')
pop_serv.user(getpass.getuser())
pop_serv.pass_(getpass.getpass())
Listing 9-8.Connecting to a POP Server and Authenticating
如您所见,您还导入了模块getpass
。该模块帮助您的代码安全地向最终用户询问密码。getuser()
方法还向操作系统查询最终用户的用户名。如果它与 POP 服务器的用户名相同,那就太好了。否则,您需要将它硬编码到您的脚本中,或者您必须明确地向最终用户询问他们的 POP 用户名。如果您的 POP 服务器正在监听非标准端口,您可以将其作为另一个参数。如果您使用的 POP 服务器更加安全,您需要使用POP3_SSL
类来代替。现在,您可以与 POP 服务器进行交互..清单 9-9 展示了如何获取邮箱的当前状态。
msg_count, box_size = pop_serv.stat()
Listing 9-9.Getting the Status of a POP Mailbox
您可以使用清单 9-10 中的代码获得当前邮箱消息的列表。
msg_list = pop_serv.list()
Listing 9-10.Listing the Messages in a POP Mailbox
当您想查看单个电子邮件时,可以使用方法retr()
,如清单 9-11 所示。
message = pop_serv.retr(1)
Listing 9-11.Retrieving Individual E-Mails from a POP Server
此方法使用消息索引。(示例中的索引 1)来决定检索哪个电子邮件。它还将选定的电子邮件标记为已在 POP 服务器上阅读。您还可以使用dele()
方法清理您的邮箱,其中您将电子邮件索引作为参数。与任何与系统资源交互的代码一样,不要忘记用类似清单 9-12 的代码彻底关闭任何打开的连接。
pop_serv.quit()
Listing 9-12.Closing a POP Connection
9-4.用 IMAP 阅读电子邮件
问题
你需要从 IMAP 邮件服务器上阅读邮件。
解决办法
Python 标准库包括一个名为imaplib
的模块,它简化了与 IMAP 电子邮件服务器的通信。
它是如何工作的
imaplib
模块包含一个主类,它管理与 IMAP 服务器的通信。清单 9-13 展示了如何初始化和认证一个 IMAP 连接。
import imaplib, getpass
my_imap = imaplib.IMAP4('myimap.com')
my_imap.login(getpass.getuser(), getpass.getpass())
Listing 9-13.Creating an IMAP Connection
您可以使用getpass
模块从最终用户那里安全地获取密码。如果您的 IMAP 服务器使用 SSL,您需要使用IMAP4_SSL
类。IMAP 提供了更多的组织结构来组织您的电子邮件。其中包括拥有多个可用邮箱的能力。因此,在处理个人电子邮件之前,您需要选择一个邮箱。要获得电子邮件列表,您需要实际进行搜索。清单 9-14 展示了如何获取默认邮箱中所有邮件的列表。
my_imap.select()
typ, data = my_imap.search(None, 'ALL')
Listing 9-14.Getting a List of E-Mails from IMAP
然后可以在data
变量中循环返回的电子邮件索引。对于这些索引中的每一个,您可以调用fetch()
方法。如果您愿意,您可以有选择地只获取电子邮件的一部分。清单 9-15 展示了如何从 IMAP 服务器获取整个电子邮件。
email_msg = my_imap.fetch(email_id, '(RFC822)')
Listing 9-15.Fetching E-Mails from an IMAP Server
然后,您可以抽出电子邮件中您感兴趣的部分。IMAP 有一套非常完整的命令,可以用来处理邮箱和个人电子邮件。例如,清单 9-16 向您展示了如何删除一封电子邮件。
my_imap.store(email_id, '+FLAGS', '\\Deleted')
my_imap.expunge()
Listing 9-16.Deleting E-Mails from an IMAP Server
当您使用完 IMAP 服务器后,不要忘记清理所有东西,如清单 9-17 所示。
my_imap.close()
my_imap.logout()
Listing 9-17.Shutting Down an IMAP Connection
9-5.发送电子邮件
问题
你需要发一封电子邮件。
解决办法
Python 标准库包括一个名为smtplib
的模块,它可以处理与 SMTP 服务器的通信。
它是如何工作的
电子邮件是使用 SMTP 协议通过互联网发送的。smtplib
包括一个基类来处理与SMTP
类的连接,如清单 9-18 所示。
import smtplib, getpass
my_smtp = smtplib.SMTP('my.smtp.com')
my_smtp.login(getpass.getuser(), getpass.getpass())
Listing 9-18.Connecting to an SMTP Server
只有当您的 SMTP 服务器需要身份验证时,才需要最后一行。如果您的 SMTP 服务器使用 SSL,您需要使用SMTP_SSL
类来代替。一旦您的连接打开,您现在就可以发送电子邮件,如清单 9-19 所示。
from_addr = 'me@email.com'
to_addr = 'you@email.com'
msg = 'From: me@email.com\r\nTo: you@email.com\r\n\r\nHello World'
my_smtp.sendmail(from_addr, to_addr, msg)
Listing 9-19.Sending an E-Mail Message
一旦你发送了你的电子邮件,你可以用对象的方法来清理。
9-6.阅读网页
问题
你需要得到一个网页的内容。
解决办法
Python 标准库包括一个名为urllib
的类模块,它处理几种不同协议上的通信。要与 web 服务器对话,您需要使用子模块urllib.request
。
它是如何工作的
几乎所有连接到 web 服务器的复杂性都被封装在模块urllib.request
中的代码中。对于一个基本的连接,您可以使用方法urlopen()
,如清单 9-20 所示。
import urllib.request
my_web = urllib.request.urlopen('http://www.python.org')
Listing 9-20.Connecting to a Web Server
然后,您可以从该 URL 读取数据。与大多数网络通信一样,数据是作为一系列字节读取的。清单 9-21 展示了如何从你所连接的 URL 中读取并打印前 100 个字节。
print(my_web.read(100))
Listing 9-21.Reading Data from a URL
9-7.张贴到网页
问题
您需要通过 GET 或 POST 与 web 表单进行通信。
解决办法
Python 标准库中的urllib.request
模块支持使用 GET 或 POST 方法将表单数据发送到 web 服务器。
它是如何工作的
urllib.request
模块包括一个名为Request
的类,它可以处理与 web 服务器更复杂的交互。清单 9-22 展示了如何创建一个新的连接。
import urllib.request
mydata = b'some form data'
my_req = urllib.request.Request('http://form.host.com', data=mydata, method='POST')
Listing 9-22.Connecting to a Web Form
然后您可以在urlopen()
方法中使用这个新的Request
对象,如清单 9-23 所示。
my_form = urllib.request.urlopen(my_req)
print(my_form.status)
print(my_form.reason)
Listing 9-23.Opening a Request Object
9-8.充当服务器
问题
您希望创建一个侦听传入连接的网络应用。
解决办法
Python 标准库中的socket
类支持监听传入的连接。
它是如何工作的
清单 9-24 展示了如何创建一个socket
对象来监听给定的端口号。
import socket
host = ''
port = 4242
my_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_server.bind((host, port))
my_server.listen(1)
Listing 9-24.Listening on a Network Port
您应该知道,根据您的计算机上使用的设置,您可能会收到来自防火墙的警告。创建套接字后,您需要显式地接受可以读取的传入连接,如清单 9-25 所示。
conn, addr = my_server.accept()
print('Connected from host ', addr)
data = conn.recv(1024)
Listing 9-25.Accepting Incoming Connections
就像任何套接字连接一样,不要忘记使用close()
方法干净地关闭网络连接。
十、模块和包
使用 Python 的最大优势之一是可用模块的大环境。几乎每一种可以想象的用途都有一个模块。事实上,这甚至是 XKCD 网络漫画的主题。在本章中,您将了解如何通过安装和使用可用的模块来利用已经完成的所有工作。您还将了解如何将您自己的工作打包成可以与其他人共享的模块。
10-1.导入模块
问题
您希望将给定模块中的功能导入到当前的 Python 程序中。
解决办法
Python 包含了import
关键字来处理整个模块的导入,或者只导入可用功能的选定部分。
它是如何工作的
清单 10-1 展示了一个导入整个模块的基本例子。
>>> import math
Listing 10-1.Basic Importing of a Module
这段代码将标准模块math
导入到当前 Python 解释器的名称空间中。您可以使用点符号来访问这个模块中包含的任何功能,如清单 10-2 所示。
>>> math.sin(45)
0.8509035245341184
Listing 10-2.Dot Notation for Modules
为了减少键入的数量,您可以在程序中使用别名。清单 10-3 展示了如何用as
关键字为math
标准模块做这件事。
>>> import math as m
>>> m.cos(45)
0.5253219888177297
Listing 10-3.Using Aliases
如果您只需要某些功能,您可以用关键字from
导入特定的项目,如清单 10-4 所示。
>>> from math import sin, pi
>>> sin(pi/2)
1.0
Listing 10-4.Importing Parts of a Module
如您所见,这将math
模块的导入部分放入 Python 程序的名称空间中。然后,您可以在不使用前面显示的点符号的情况下使用它们。您可以用清单 10-5 中的语句添加给定模块中的所有内容。
>>> from math import *
Listing 10-5.Importing All of a Module
请注意,每次导入都会增加名称冲突的几率。您最终可能会得到一个给定函数的多个实例。上次导入的版本是执行给定函数名时使用的版本。你需要意识到由此可能引发的复杂情况。
10-2.从源代码安装模块
问题
您需要将一个模块从源代码安装到一个标准库目录中。
解决办法
当从源代码安装时,许多模块被编写为使用 Python 中包含的一个名为distutils
的系统。
它是如何工作的
Distutils
是 Python 中包含的一个非常基本的系统。对于更简单的模块,处理安装过程已经足够了。该模块包括一个名为setup.p
y 的特殊文件,用于处理细节。当您使用distutils
安装一个包时,首先要将源文件解压到一个临时目录中。然后,您需要运行清单 10-6 中给出的命令。
python setup.py install
Listing 10-6.Installation with Distutils
这将运行模块所需的所有步骤,以便将它安装在 Python 库目录之一中。
10-3.从 Pypi 安装模块
问题
您想要安装 Pypi 存储库中的一个可用模块。
解决办法
工具 pip 提供了直接从 Pypi 存储库中轻松安装模块和包的能力。
它是如何工作的
在早期版本的 Python 中,pip 工具不可用。它从版本开始提供
-
- Python 2 >= 2.7.9
-
- Python 3 >= 3.4
对于早期版本,您首先需要安装它。如果您使用的是 Linux,大多数发行版都包含一个 pip 包。对于其他操作系统,或者如果您想要安装最新版本,您可以从 Pypi 网站( https://pip.pypa.io/en/latest/installing/
)下载所需的软件包,并按照附带的说明进行操作。然后,您可以使用清单 10-7 中的命令安装感兴趣的 Python 模块。
pip install numpy
Listing 10-7.Installing a Module with pip
使用 pip 真正强大的部分是它将处理依赖性检查。这样,您就可以专注于安装您需要的部件。如果您试图在一个您没有权限的系统上安装模块,您总是可以使用清单 10-8 中所示的命令将其安装在您的主目录中。
pip install --user numpy
Listing 10-8.Installing a Module in Your Home Directory
10-4.使用 pip 升级模块
问题
您需要更新系统上已经安装的软件包。
解决办法
pip 工具包括一个升级选项,该选项将检查 Pypi 存储库以查看是否有更新的版本。
它是如何工作的
要检查新版本,使用清单 10-9 中给出的命令。
pip install --upgrade numpy
Listing 10-9.Upgrading Packages
这种形式的更新选项对所有依赖项进行积极的更新。相反,您可以使用清单 10-10 中的命令进行“必要时升级”更新。
pip install --upgrade --no-deps numpy
pip install numpy
Listing 10-10.Doing a Selective Upgrade
如您所见,这实际上是一个两步过程,应该只更新那些需要更新的依赖项。
十一、数字和数值
Python 越来越多的应用领域之一是在科学界。一个问题,也一直是一个问题,就是 Python 在做数值计算的时候效率不是很高。幸运的是,Python 的设计旨在使扩展其功能变得相对容易。有助于科学计算的核心模块是Numpy
模块。Numpy 将处理数字计算的最低效部分外包给用 c 编写的外部库,它使用的标准开源库与其他专门编写的应用中使用的相同,用于处理大量的数字计算。
Numpy 功能的核心是由一个叫做 array 的新对象提供的。数组是包含一种数据类型元素的多维对象。这意味着 Numpy 模块中的函数可以自由地假设可以对数据做什么,而不必在访问数据时检查每个元素。
11-1.创建数组
问题
您希望创建数组用于其他 Numpy 函数。
解决办法
创建数组最简单的方法是使用提供的creation
函数获取列表中的现有数据,并将其转换成一个新的数组对象。也可以使用 empty 函数创建一个新的空数组对象。
它是如何工作的
最简单的形式的array
函数简单地接受一个值列表并返回一个新的数组对象,如清单 11-1 所示。
>>> import numpy as np
>>> list1 = [1, 2, 3.0, 4]
>>> array1 = np.array(list1)
>>> array1
array([1., 2., 3., 4.])
Listing 11-1.Basic Array Creation
这将返回一个一维数组,其中每个元素都是一个实数。array
函数的默认行为是选择保存原始列表中每个元素的最小数据类型。您可以专门选择想要在代码中使用的数据类型,比如清单 11-2 。
>>> complex1 = np.array(list1, dtype=complex)
>>> complex1
array([1.+0.j, 2.+0.j, 3.+0.j, 4.+0.j])
Listing 11-2.Creating an Array of Complex Numbers
如果您有一个需要处理的数据矩阵,您可以简单地提交一个列表列表,其中每个列表都是您的矩阵的一行,如清单 11-3 所示。
>>> matrix1 = np.array([[1, 2], [3, 4]])
>>> matrix1
array([[1, 2],
[3, 4]])
Listing 11-3.Creating a Matrix
如果您还没有准备好数据,但是想要一个存储数据的地方,有一个函数可以创建一个固定大小和特定数据类型的空数组。例如,清单 11-4 展示了如何创建一个空的二维整数数组。
>>> empty1 = np.empty([2, 2], dtype=int)
>>> empty1
array([[-1073741821, -1067949133],
[ 496041986, 19249760]])
Listing 11-4.Creating an Empty Array of Integers
这个函数的问题是,它可能无法以任何方式初始化这些值,这取决于它所运行的操作系统。你最后得到的只是那些内存位置中存在的数据。虽然这稍微快了一点,但这确实意味着您需要意识到新数组中的初始值是垃圾数据。如果需要从一些初始值开始,可以从 0 或 1 开始,如清单 11-5 所示。
>>> zero1 = np.zeros((2, 3), dtype=float)
>>> zero1
array([[0., 0., 0.],
[0., 0., 0.]])
>>> ones1 = np.ones((3, 2), dtype=int)
>>> ones1
array([[1, 1],
[1, 1],
[1, 1]])
Listing 11-5.Creating Arrays of Zeroes and Ones
请注意,对于新创建的数组的维度,这两个函数采用一系列值,而不是一个列表。
11-2.复制数组
问题
您需要复制一个数组以供进一步处理。
解决办法
在程序的不同部分共享数据有三种方式:无拷贝访问、浅层拷贝和深层拷贝。
它是如何工作的
通过一次使用多个变量,可以使程序的不同部分可以访问数组。在清单 11-6 中,您可以看到如何将同一个数组赋给两个不同的变量。
>>> a = np.ones((6,), dtype=int)
>>> a
array([1, 1, 1, 1, 1, 1])
>>> b = a
Listing 11-6.Using No-Copy Sharing
和 Python 的其他部分一样,这两个变量指向内存中的同一个实际对象。您可以使用其中任何一个来影响实际对象。
第二种类型的访问共享是通过浅层拷贝,其中不拷贝数据本身,只拷贝关于数据的信息。这是可能的,因为数组对象由两部分组成。第一个是存储在数组中的数据,而第二个包含关于数组的元数据,例如数组的形状。清单 11-7 展示了如何通过创建一个视图来创建一个浅层副本。
>>> view1 = ones1.view()
>>> # Do these variables point to the same object?
>>> view1 is ones1
False
>>> view1.base is ones1
True
Listing 11-7.
Shallow Copies
您可以通过使用新视图的base
属性来访问原始对象。您可以通过视图更改元数据,如清单 11-8 所示。
>>> view1.shape = 2,3
>>> ones1
array([[1, 1],
[1, 1],
[1, 1]])
>>> view1
array([[1, 1, 1],
[1, 1, 1]])
Listing 11-8.Changing the Shape of a View
这将改变存储数据的矩阵的形状(列数和行数)。第三种复制形式是深度复制,即复制数组的所有部分。这由copy
方法处理,如清单 11-9 所示。
>>> copy1 = a.copy()
>>> a is copy1
False
>>> a is copy1.base
False
Listing 11-9.
Deep Copy
of an Array
11-3.访问数组数据
问题
您需要访问数组的单个元素或子部分。
解决办法
您可以使用多维列表索引来访问单个元素,并且可以使用切片来访问子部分。
它是如何工作的
对于一维数组,可以使用与列表相同的索引来访问单个元素。清单 11-10 显示了一个简单的例子。
>>> a[1] = 2
>>> a
array([1, 2, 1, 1, 1])
Listing 11-10.Changing the Value of an Array Element
切片也以同样的方式工作,如清单 11-11 所示。
>>> a[1:3]
array([2, 1])
Listing 11-11.Getting a Slice of an Array
需要注意的一点是,slice 实际上返回的是原始数组的一个浅层副本,因此不会复制原始数据。
当处理多维数组时,您只需通过为每个额外的维度添加一个额外的值来扩展索引。例如,清单 11-12 展示了如何从矩阵中获取单个元素。
>>> ones1[1, 1] = 2
>>> ones1
array([[1, 1],
[1, 2],
[1, 1]])
Listing 11-12.Accessing One Element from a Matrix
如果您对获取单个行感兴趣,您可以使用清单 11-13 中的例子。
>>> ones1[1, : ]
array([1, 2])
Listing 11-13.Selecting a Row from a Matrix
11-4.操作矩阵
问题
你需要操纵一个给定的矩阵。这包括反演、转置和计算范数。
解决办法
包含一整套线性代数工具来处理矩阵操作。
它是如何工作的
如果你从一个简单的 2 乘 2 矩阵开始,你可以用清单 11-14 中的代码转置它。
>>> a = np.array([[1.0, 2.0], [3.0, 4.0]])
>>> np.linalg.inv(a)
array([[-2., 1.],
[1.5, -0.5]])
Listing 11-14.Inverting a Matrix
linalg
子模块也提供了一个计算定额的函数,如清单 11-15 所示。
>>> np.linalg.norm(a)
5.4772255750516612
Listing 11-15.Finding a Norm
如果你想寻找一个矩阵的轨迹,这实际上是一个数组对象的方法,如清单 11-16
>>> a.trace()
5.0
Listing 11-16.Finding the Trace of a Matrix
矩阵的转置也是数组的一种方法,如清单 11-17 所示。
>>> a.transpose()
array([[1., 3.],
[2., 4.]])
Listing 11-17.Finding the Transpose of a Matrix
11-5.计算快速傅立叶变换
问题
你需要计算一个快速傅立叶变换来观察一些数据集合的频谱。
解决办法
Numpy
提供了一套不同类型的 FFT(快速傅立叶变换)函数。
它是如何工作的
离散 FFT 可用于一维、二维或 n 维数据。然而,每一种情况的数学方法都非常不同。所以Numpy
为每种情况提供了独立的函数,如清单 11-18 所示。
# a is a 1-dimensional array
np.fft.fft(a)
# b is a 2-dimensional array
np.fft.fft2(b)
# c is a 3-dimensional array
np.fft.fftn(c)
Listing 11-18.Discrete FFTs
如你所见,所有的 FFT 功能实际上都被安排在一个名为fft
的Numpy
子模块中。如果您使用的数据集大于所选 FFT 函数的适用范围,则使用最后 x 个轴。例如,如果您在一维 FFT 中使用数组c
,它将使用最后一个轴作为计算的输入。如果你愿意,你可以用axis
参数指定一个不同的轴,如清单 11-19 所示。
np.fft.fft(c, axis=1)
Listing 11-19.FFT Over Other Axes
11-6.将文件数据加载到数组中
问题
您希望将数据从文件直接加载到数组中。
解决办法
Numpy
可以读写纯文本文件,以及自己特殊的二进制格式。
它是如何工作的
要从纯文本文件中读取数据,可以使用函数loadtxt()
,如清单 11-20 所示。
>>> txt1 = np.loadtxt('mydata.txt')
Listing 11-20.Reading in a Text File
此函数假设您的数据以列和行的形式排列,其中每行就是一行。定义列是通过用其他字符分隔各个值来完成的。默认情况下,这是通过空白来完成的。科学数据的常用格式是逗号分隔值(CSV)。如果是这种情况,您可以用清单 11-21 中给出的代码加载您的数据。
>>> txt2 = np.loadtxt('mydata.txt', delimiter=',')
Listing 11-21.Loading a CSV File
如果你有以Numpy
的特殊二进制格式保存的数据,你可以使用一个简单的load
命令将其加载回内存,如清单 11-22 所示。
>>> data = np.load('mydata.npy')
Listing 11-22.Loading Binary Data
11-7.保存数组
问题
您希望将数组中的数据保存到磁盘。
解决办法
与加载数据一样,保存数据时有几个选项。您可以将其保存为Numpy
的二进制格式,也可以保存为某种原始文本格式。
它是如何工作的
要使用Numpy
的二进制格式保存数据,您可以简单地使用save
函数,如清单 11-23 所示。
>>> np.save('mydata.npy', data)
Listing 11-23.Saving Data Using Numpy’s Binary Format
如果你在上面的调用中给它的文件名没有一个.npy
文件扩展名,一个将被添加到它里面。相反,如果您想将数据保存到一个纯文本文件中,以便其他程序可以使用,您可以使用savetxt
函数调用,如清单 11-24 所示。
>>> np.savetxt('mydata.csv', data, delimiter=',')
Listing 11-24.Saving a CSV File
在这种情况下,您显式地将分隔符设置为逗号,得到一个 CSV 文件。如果不设置分隔符,默认为单个空格字符。
11-8.生成随机数
问题
你需要生成高质量的随机数。
解决办法
Numpy
提供一个 Mersenne Twister 伪随机数发生器,提供非常优质的随机数。它可以提供基于几种分布的随机数,如二项式分布、卡方分布、伽玛分布和指数分布。
它是如何工作的
如果您需要使用特定分布的随机数,您可以使用RandomState
提供的方法来生成它们。清单 11-25 展示了如何从几何分布中生成一个随机值。
>>> rand1 = np.random.geometric(p=0.5)
Listing 11-25.Generating Random Numbers from a Geometric Distribution
大多数发生器都包含控制每个发行版细节的参数。它们通常还包含一个size
参数,您可以用它来请求一组随机值,而不仅仅是一个。
如果您想要一个可重复的随机数序列(例如,如果您正在测试代码),您可以用清单 11-26 中的代码显式地设置一个种子。
>>> np.random.seed(42)
Listing 11-26.Setting a Seed for Random Number Generation
当RandomState
被创建时,这个种子也被初始化。如果你不提交一个,那么RandomState
要么尝试从操作系统随机数生成器(例如,Linux 上的/dev/urandom
)读取一个值,要么根据时钟设置种子。
在大多数情况下,您可以获得与清单 11-27 中的代码一起使用的随机数类型。
>>> rand2 = np.random.random()
Listing 11-27.Generating Random Numbers
11-9.计算基本统计数据
问题
您需要对存储在数组中的数据进行基本的统计。
解决办法
Numpy
提供了一系列统计函数,可以对不同维度的数组进行操作。你可以做你可能需要的所有标准的简单统计分析。
它是如何工作的
给定一组存储在一维数组中的数据,您可以用清单 11-28 中的代码找到平均值、中值、方差和标准差。
>>> a = np.array([1, 2, 3, 4, 5])
>>> np.mean(a)
3.0
>>> np.median(a)
3.0
>>> np.var(a)
2.0
>>> np.std(a)
1.4142135623730951
Listing 11-28.Basic Statistics
如果您有多维数据,您可以选择沿着哪个轴计算这些统计数据。
11-10.计算直方图
问题
您有一系列的数据,您需要将这些数据分组并计算直方图。
解决办法
Numpy
包含一些相关的函数来处理直方图,包括一维直方图和多维直方图。
它是如何工作的
假设您已经将数据序列存储在变量b
中,您可以使用清单 11-29 中的代码生成一个直方图。
>>> b = np.array([1,2,1,2,3,1,2,3,3,2,1])
>>> np.histogram(b)
(array([4, 0, 0, 0, 0, 4, 0, 0, 0, 3], dtype=int64),
array([ 1\. , 1.2, 1.4, 1.6, 1.8, 2\. , 2.2, 2.4, 2.6, 2.8, 3\. ]))
Listing 11-29.Generating a Simple Histogram
默认情况下,Numpy
会尝试将您的数据分组到 10 个箱中。第一个数组告诉您每个容器中有多少个值,第二个数组告诉您每个容器的边界。您可以通过添加第二个参数来设置箱子的数量,如清单 11-30 所示。
>>> np.histogram(b, 3)
(array([4, 4, 3], dtype=int64),
array([ 1\. , 1.66666667, 2.33333333, 3\. ]))
Listing 11-30.Histograms with a Set Bin Count
十二、并发
几十年来,计算机的速度越来越快,但我们开始遇到一些物理学的限制。这意味着为了完成更多的工作,我们需要并行使用多个过程。Python 中有几种技术可以支持代码的并发执行。
第一种技术是使用线程来分解工作。这种方法的主要问题是,它受到由 GIL(全局解释器锁)引起的瓶颈的影响。执行 I/O 或使用特定模块(如 numpy)的线程可以绕过这个瓶颈。如果你需要做更多的计算工作,你可以使用进程来代替。在本章中,你将看到 Python 中的几个可用选项。
12-1.创建线程
问题
你想创建一个线程来完成一些任务。
解决办法
Python 标准库包含一个名为threading
的模块,该模块包含一个Thread
类。
它是如何工作的
主类Thread
支持并行运行多个函数。清单 12-1 展示了如何创建和运行一个基本线程。
import threading
def print_sum():
print('The sum of 1 and 2 is 3')
my_thread = threading.Thread(target=print_sum)
my_thread.start()
Listing 12-1.Creating a Thread
您应该注意到,您创建的线程在您调用start()
方法之前不会开始执行目标函数。如果这个函数是一个运行时间更长的函数,您可以通过使用is_alive()
方法来检查它是否还在运行。它将返回一个布尔值,告诉你它是否还在运行。如果没有给定线程的结果就无法继续,可以调用join()
方法强制等待,直到线程全部完成。
12-2.使用锁
问题
您需要控制线程对特定资源的访问。
解决办法
threading
模块包括一个Lock
类来控制线程访问。
它是如何工作的
当线程需要安全地访问全局资源时,使用锁。清单 12-2 展示了如何创建和使用一个锁对象。
import threading
sum = 0
my_lock = threading.Lock()
def adder():
global sum, my_lock
my_lock.acquire()
sum = sum + 1
my_lock.release()
my_thread = threading.thread(target=adder)
my_thread.start()
Listing 12-2.Creating a Lock Object
默认情况下,如果锁已经被另一个线程获取,那么锁对象的acquire()
方法就会阻塞。相反,如果你想在等待锁的时候做些别的事情,你可以使用acquire()
方法中的参数blocking=False
。它将立即返回,给出一个布尔值,表明获取尝试是否成功。
12-3.设置障碍
问题
您需要通过设置一个公共停止点来同步线程活动。
解决办法
threading
模块包括一个可用于设置公共停止点的障碍对象。
它是如何工作的
在许多语言中,使用屏障涉及简单的函数调用,而在 Python 中,屏障是用对象来管理的。清单 12-3 展示了如何为五个线程创建一个屏障。
import threading
b = threading.Barrier(5, timeout=10)
Listing 12-3.Creating a Barrier Object
正如您所看到的,您必须明确地告诉 barrier 对象有多少线程将使用它。您还可以设置一个超时值,该值是允许线程等待屏障得到满足的最长时间。为了实际使用 barrier 对象,每个线程都需要调用wait()
方法。
12-4.创建流程
问题
您需要为多重处理创建多个进程。
解决办法
Python 标准库包括一个名为multiprocessing
的模块,该模块包含一个Process
类。
它是如何工作的
如果您的代码受到 GIL 的影响,一种解决方法是使用类Process
在主 Python 进程之外生成其他任务。该接口与线程的接口非常相似。例如,清单 12-4 展示了如何创建一个新流程并开始运行。
import multiprocessing
def adder(a, b):
return a+b
proc1 = multiprocessing.Process(target=adder, args=(2,2))
proc1.start()
proc1.join()
Listing 12-4.Creating a New Process
正如您所看到的,您新创建的流程对象是用start()
方法执行的,您可以用join()
方法强制代码的主要部分等待结果。
12-5.进程间的通信
问题
您需要将信息从一个流程对象发送到另一个流程对象。
解决办法
multiprocessing
模块有两个可用于进程间通信的类:pipe
和queue
类。
它是如何工作的
因为流程对象在 Python 解释器的主要部分之外执行,所以与它们或它们之间的通信需要更多的工作。最基本的通信形式是管道。清单 12-5 展示了如何创建一个新的管道对象。
import multiprocessing
def func(pipe_end):
pipe_end.send(['hello', 'world'])
pipe_end.close()
parent_end, child_end = multiprocessing.Pipe()
proc1 = multiprocessing.Process(target=func, args=(child_end,))
proc1.start()
print(parent_end.recv())
proc1.join()
Listing 12-5.Creating a Pipe
如您所见,管道是一个简单的通信通道,有两端,进程可以从中读取和写入。管道是全双工的,所以消息可以从两端发送。然而,管道的主要问题是末端一次只能被一个进程使用。如果两个进程试图同时从同一端读取或写入,数据可能会损坏。
另一种不同的技术是使用队列对象与。队列是一个 FIFO(先进先出)对象。它可以接受来自多个进程的数据,并且多个进程可以从队列中取出数据。清单 12-6 展示了如何创建和使用队列对象。
import multiprocessing
def func(queue1):
queue1.put(['hello', 'world'])
my_queue = multiprocessing.Queue()
proc1 = multiprocessing.Process(target=func, args=(my_queue,))
proc1.start()
print(my_queue.get())
proc1.join()
Listing 12-6.Creating a Queue
12-6.创造一批工人
问题
您需要启动并运行一个进程池。
解决办法
Python 标准库模块multiprocessing
包含一个Pool
类来管理任务队列。
它是如何工作的
当您有一系列需要处理的任务时,您可以创建一个进程池来完成这些任务。清单 12-7 展示了如何创建一个由四个工作进程组成的池,并让它们处理一堆任务。
import multiprocessing
def adder(a):
return a+a
pool1 = multiprocessing.Pool(processes=4)
Listing 12-7.Creating a Pool of Processes
这个新创建的池对象有几种不同的方法来在进程之间划分任务。这些方法有阻塞版本和非阻塞版本。例如,清单 12-8 展示了如何使用map
方法。
# This method blocks untill all processes return
pool1.map(adder, range(10))
# This method returns a result object
results = pool1.map_async(adder, range(10))
results.wait()
Listing 12-8.Mapping a Function to a Pool of Processes
清单 12-8 中的最后一行用于阻塞和等待,直到返回所有结果。当您准备使用外包任务的所有结果时,可以使用它。
12-7.创建子流程
问题
您需要生成一个子流程来处理外部任务。
解决办法
Python 标准库包含一个名为subprocess
的模块,可以生成外部进程。
它是如何工作的
subprocess
模块旨在取代运行外部流程的旧的os.system
和os.spawn
方法。该模块包含一个名为run()
的方法,这是使用子流程的常用方法。例如,清单 12-9 展示了如何在 Linux 机器或 Mac OS 机器上获得当前目录下的文件列表。
import subprocess
results = subprocess.run(['ls', '-l'], stdout=subprocess.PIPE)
print(results.stdout)
Listing 12-9.Spawning a Subprocess
返回的 results 对象包含许多关于外部进程如何运行的信息,包括退出代码。
12-8.安排事件
问题
您需要为以后的某个时间安排任务。
解决办法
Python 标准库包含一个名为sched
的模块,其中有几个对象和方法,在不同时间调度工作时非常有用。
它是如何工作的
为了安排未来的任务,您可以创建一个Scheduler
对象来获取事件队列并管理它们。您可以使用enter()
方法将事件添加到队列中,如清单 12-10 所示。
import sched, time
def print_time():
print(time.time())
my_sched = sched.scheduler()
my_sched.enter(10, 1, print_time)
Listing 12-10.Creating a Scheduler
当事件触发时,enter()
方法接受一个延迟、一个优先级和一个要执行的函数。如果您想在特定时间触发事件,可以使用enterabs()
方法。一旦您有了一个更大的事件列表,您就可以使用调度器的queue
属性来检查当前队列。如果您到达了程序的末尾,您可以使用run()
方法强制代码停止并等待,直到所有事件都结束。
十三、工具
所有编程语言都使用外部工具,以使程序员的开发和执行更容易。Python 在这方面也没有什么不同。在这一章中,你将会看到一些为 Python 编程语言编写代码时最常用的外部工具。这些包括设置环境的工具、更好的解释器、完整的编码环境,甚至是外壳替换。
13-1.创建虚拟环境
问题
您希望为特定的 Python 程序创建和使用虚拟环境。
解决办法
创建和管理虚拟环境的常用方法是通过virtualenv
工具。
它是如何工作的
Python 的优势之一,它对第三方模块非常健康的选择,也是它的问题之一。你很快就会得到一个大规模的模块库,这些模块只在某些时候使用。最小化这个问题的一个方法是为每个项目建立一个单独的环境,这样你就可以在这个环境中只安装你所需要的模块。第一步,安装virtualenv
。清单 13-1 展示了如何用 pip 来做这件事。
pip install virtualenv
Listing 13-1.Installing virtualenv
安装后,您可以创建新的虚拟环境。清单 13-2 展示了如何为一个新项目创建一个新的空白环境。
virtualenv project1
Listing 13-2.Creating a New Virtual Environment
这个命令创建一个名为project1
的新目录,并安装运行 Python 程序所需的一切,以及安装和管理模块。为了使用这个新环境,您将目录更改为project1
子目录,并获取 shell 脚本./bin/activate
。许多最常用的 Linux shells 都有不同的版本,还有 Windows cmd
shell 和 Windows powershell
。这个脚本设置了几个环境变量来使用包含的解释器和模块库。
当您使用完有问题的环境后,您可以运行脚本deactivate
来撤销环境更改。如果您决定不再需要某个特定的虚拟环境,只需删除整个相关目录及其所有内容。完整文档可在 https://virtualenv.pypa.io/en/stable/
获得。
13-2.使用 Ipython Shell
问题
您希望使用更好的解释器外壳,而不是 Python 解释器使用的默认外壳。
解决办法
IPython 解释器外壳在默认的 Python 解释器外壳上提供了许多增强。
它是如何工作的
您可以使用清单 13-3 中所示的命令安装最新版本的 IPython。
pip install ipython
Listing 13-3.Installing IPython
要使用这个解释器,您只需要在命令提示符下执行命令ipython
。它与许多 Linux shells 非常相似,因为它提供了制表符补全、命令历史和更复杂的命令编辑等功能。
IPython 解释器中更强大的功能之一是称为 magics 的命令集。这些特殊的命令以一个%
符号开始,后面跟着一些关键字。例如,清单 13-4 展示了如何为一个给定的操作计时。
%timeit x = range(1000000)
Listing 13-4.Using the timeit Magic Function
有些魔术可以运行外部脚本,加载或保存命令,甚至影响 IPython 解释器的颜色。清单 13-5 展示了如何将之前的一系列命令保存到一个文件中。
# Saving a series of commands to a file
%save myfile.py 2-5 10
Listing 13-5.Saving to a File with Ipython Magics
这样,您可以从一次实验中保存有用的行。load
魔法函数将文件的内容读入 IPython 前端,就好像您刚刚输入了它们一样。清单 13-6 展示了如果你加载一个只有一条print
语句的文件会发生什么。
In [7]: %load myfile.py
In [8]: print("Hello world")
Listing 13-6.Loading a File with IPython Magic
您还可以创建自己的神奇函数,进一步扩展 IPython 的功能。主要文档可在 http://ipython.readthedocs.io/en/stable/index.html
找到。
13-3.使用 Jupyter 环境
问题
您希望使用更完整的环境,以便于交互式开发。
解决办法
你可以使用 Jupyter,它是 IPython 的 web 接口的一个分支。它提供了一个类似于 Maple、Mathematica 或 Matlab 等商业应用的接口。
它是如何工作的
第一步是在您的系统上安装 Jupyter。清单 13-7 展示了如何使用 pip 来完成这项工作。
pip install jupyter
Listing 13-7.Installing Jupyter
在命令提示符下,执行命令jupyter notebook
将启动 web 服务器和 Python 引擎,然后启动连接到新启动的服务器的 web 浏览器。Jupyter 正迅速成为用 Python 进行科学研究的事实平台。除了继承自 IPython 的增强功能,Jupyter 还包括绘制内嵌 matplotlib 图形等功能。作为 Jupyter 的一部分,实际上还有一套非常完整的其他工具,例如笔记本查看器和笔记本评分工具,供您在课堂环境中使用 Jupyter 时使用。主文档位于 http://jupyter.readthedocs.io/en/latest/index.html
。
13-4.使用 xonsh 作为替换外壳
问题
Linux 或 Mac OS 上的命令 shell 是非常个人化的选择。它是您用来与机器上的一切进行交互的环境。您可能希望使用 Python 作为在计算机上工作的命令 shell。
解决办法
您可以使用名为xonsh
的新项目作为替换命令 shell。
它是如何工作的
可以用 pip 安装xonsh
,如清单 13-8 所示。
pip install xonsh
Listing 13-8.Installing xonsh
一旦安装完毕,xonsh
就可以像其他命令 shell 一样使用。xonsh
至少部分兼容 bash。这是指运行其他程序、拥有和使用命令历史记录以及处理后台作业的能力。除了这种常见的功能,您还拥有 Python 引擎的所有功能。这意味着您可以使用 Python 模块在命令 shell 中添加功能。清单 13-9 展示了如何从命令行直接获得随机数。
jbernard@DESKTOP-QPKN2QC ∼ <branch-timeout> $ import numpy as np
jbernard@DESKTOP-QPKN2QC ∼ <branch-timeout> $ np.random.random()
0.48053753953641054
Listing 13-9.Using Python Modules within xonsh
主文档站点位于 http://xon.sh/index.html
。
十四、测试和调试
这本书主要是给你一些如何编写 Python 代码的技巧和诀窍。很少的篇幅花在使这段代码尽可能好上。在这一章中,你将会看到分析代码性能的技术。您还将看到在您编写的代码中出现错误时调试程序的方法。
14-1.为一段代码计时
问题
你想对一段代码计时,看看它运行了多长时间。
解决办法
Python 标准库包括一个名为timeit
的包,它可以多次运行代码并获得平均运行时间。
它是如何工作的
如果您有 Python 语句,您可以从命令行通过timeit
包运行它们,如清单 14-1 所示。
python -m timeit 'print(42)'
10000000 loops, best of 3: 0.035 usec per loop
Listing 14-1.Using the timeit Command
清单 14-2 展示了如何在 Python 解释器中完成类似的任务。
>>> import timeit
>>> timeit.timeit('42*42', number=1000)
0.0001980264466965309
Listing 14-2.Timing Python Code with timeit
如您所见,您需要明确地告诉timeit
运行 Python 语句的次数。
14-2.分析代码
问题
您希望分析您的代码,看看性能瓶颈在哪里。
解决办法
Python 标准库包括两个包,可以用来分析您的代码:profile
和cProfile
。
它是如何工作的
profile
和cProfile
都为分析工具提供了一个公共接口来查看代码的性能。这两个包的主要区别在于它们在分析代码时各自的性能。包cProfile
是用 C 写的,所以它对你自己代码的运行时间影响很小。然而,为了获得这样的速度,无论您在哪个系统上分析代码,都需要对它进行编译。package profile
是用纯 Python 写的,所以运行起来会比cProfile
慢一些。然而,优点是profile
将在您的代码运行的任何地方运行,并且很容易扩展profile
的功能以添加额外的特性。
有几种方法可以将分析包用于您自己的代码。如果您的代码已经捆绑在一组函数中,您可以简单地在分析器下运行它,如清单 14-3 所示。
>>> def my_func():
… return 42
>>> import profile
>>> profile.run('my_func')
4 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 :0(exec)
1 0.000 0.000 0.000 0.000 :0(setprofile)
1 0.000 0.000 0.000 0.000 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 profile:0(my_func)
0 0.000 0.000 profile:0(profiler)
Listing 14-3.Running the Profiler
如您所见,您可以获得相当多的关于代码中每个函数调用所花费时间的信息。通过分析器运行您自己的代码还有其他几种方法。如果您想要分析的程序已经打包成一个脚本文件,您可以选择从命令行通过分析器运行它,如清单 14-4 所示。
python -m profile -o myscript.out myscript.py
Listing 14-4.Running the Profiler from the Command Line
该命令将把二进制版本的分析结果转储到文件myscript.out
中。如果您需要在远程机器上运行概要分析步骤,但想在以后查看结果,这是很方便的。使用pstats
包可以看到结果。清单 14-5 展示了如何从这个二进制文件中获得基本的统计数据。
>>> import pstats
>>> p = pstats.Stats('myscript.out')
>>> p.print_stats()
Sun Sep 11 20:39:14 2016 myscript.out
9 function calls in 0.000 seconds
Random listing order was used
ncalls tottime percall cumtime percall filename:lineno(function)
2 0.000 0.000 0.000 0.000 C:\Users\berna_000\Anaconda3_4\lib\encodings\cp850.py:18(encode)
1 0.000 0.000 0.000 0.000 :0(print)
2 0.000 0.000 0.000 0.000 :0(charmap_encode)
1 0.000 0.000 0.000 0.000 :0(setprofile)
1 0.000 0.000 0.000 0.000 myscript.py:1(<module>)
1 0.000 0.000 0.000 0.000 profile:0(<code object <module> at 0x000001F1B82040C0, file "myscript.py", line 1>)
0 0.000 0.000 profile:0(profiler)
1 0.000 0.000 0.000 0.000 :0(exec)
<pstats.Stats object at 0x000001F1B8208550>
Listing 14-5.Reading a Profiling Run
在pstats
中有很多选项可以帮助你挖掘结果输出文件。
14-3.跟踪子程序
问题
您需要跟踪您的代码使用了哪些子例程,以查看您的程序还使用了哪些其他函数。
解决办法
Python 标准库包括一个名为trace
的包,它可以为您提供覆盖列表、调用者/被调用者关系以及所有被调用函数的列表。
它是如何工作的
可以从命令行使用trace
模块,如果您已经将代码捆绑在脚本文件中,这将非常有用。清单 14-6 展示了你如何做来追踪你的脚本。
python -m trace --trace myscript.py
--- modulename: myscript, funcname: <module>
myscript.py(1): print('42')
--- modulename: cp850, funcname: encode
cp850.py(19): return codecs.charmap_encode(input,self.errors,encoding_map)[0]
42 --- modulename: cp850, funcname: encode
cp850.py(19): return codecs.charmap_encode(input,self.errors,encoding_map)[0]
--- modulename: trace, funcname: _unsettrace
trace.py(77): sys.settrace(None)
Listing 14-6.Tracing a Program
您可以使用以下跟踪选项来收集不同种类的数据:
| `--count` | 计算每条语句执行的次数 | | `--trace` | 执行时显示行 | | `--listfuncs` | 执行时显示功能 | | `--trackcalls` | 显示调用关系 |您也可以在 Python 代码中使用trace
。它包括两个主要类别:Trace
和CoverageResults
。清单 14-7 展示了如何通过一个命令进行跟踪。
>>> import trace
>>> tracer = trace.Trace()
>>> tracer.run('print("Hello World")')
Hello World
--- modulename: trace, funcname: _unsettrace
trace.py(80): sys.settrace(None)
Listing 14-7.Tracing a Command in Python Code
14-4.跟踪内存分配
问题
您需要跟踪程序中的内存分配,以了解内存的使用情况。
解决办法
Python 标准库包括一个名为tracemalloc
的模块,它可以跟踪内存分配和内存使用统计。
它是如何工作的
要使用tracemalloc
,您需要启动它,以便它可以随着时间的推移收集内存信息。清单 14-8 展示了如何获得内存使用中的前 10 名违规者。
import tracemalloc
tracemalloc.start()
# Run you code here
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for curr_stat in top_stats:
print(curr_stat)
Listing 14-8.Getting Memory Statistics
您还可以通过拍摄多个快照来查看内存使用如何随时间变化。有益的是,快照对象有一个名为compare_to()
的方法,允许您查看它们之间的区别,如清单 14-9 所示。
import tracemalloc
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
# run your code
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
Listing 14-9.Comparing Two Memory Snapshots
然后您可以查看top_stats
对象,看看内存使用是如何随时间变化的。
14-5.执行单元测试
问题
您希望对代码运行单元测试来验证程序行为。
解决办法
Python 标准库包括一个名为unittest
的模块,可用于为您的代码构建测试用例。
它是如何工作的
为了创建单元测试,你需要子类化TestCase
类并添加测试用例来验证你的代码。每个测试用例必须以 test 作为前缀来命名。然后使用assertEqual()
、assertTrue()
、assertFalse()
和assertRaises()
来验证条件。清单 14-10 中显示了一个简单的例子。
import unittest
class MyTestCase(unittest.TestCase):
def test_the_answer(self):
assertEqual(self.curr_val, 42)
if __name__ == '__main__':
unittest.main()
Listing 14-10.A Simple Test Case
这只是对在 Python 中使用测试用例的简短介绍。有整本书致力于组织和设计测试驱动代码。
14-6.调试代码
问题
您需要调试代码中出现的问题。
解决办法
Python 标准库包括一个名为pdb
的包,它为代码的操作提供了一个调试接口。
它是如何工作的
如果你想交互式地使用它,你可以在你的 Python 解释器中导入pdb
模块,并用它来运行你的代码,如清单 14-11 所示。
>>> import pdb
>>> pdb.run('my_func()')
Listing 14-11.Running Code Under the Debugger
这将进入调试器的交互模式。这通过提示(pdb)
突出显示。接口类似于其他基于文本的调试器,比如gdb
。如果你想在调试器中运行整个脚本,你可以从命令行执行,如清单 14-12 所示。
python -m pdb myscript.py
Listing 14-12.Debugging a Script File
如果您的脚本异常退出,这将使您进入调试器的事后分析会话。如果您大致知道问题可能出在哪里,您可以添加一行代码来中断调试器,如清单 14-13 所示。
import pdb; pdb.set_trace()
Listing 14-13.Dropping into the Debugger
然后,您可以逐句通过代码来定位代码中问题的根源。如果您处于交互式会话中,可以通过调试器手动运行代码。清单 14-14 展示了一个单步执行函数的例子。
>>> import pdb
>>> def myfunc():
.... print("Hello World")
>>> pdb.run('myfunc()')
> <string>(1)<module>()->None
(Pdb) step
--Call--
> <ipython-input-11-b0e3e2c712c8>(1)myfunc()
-> def myfunc():
(Pdb) step
> <ipython-input-11-b0e3e2c712c8>(2)myfunc()
-> print("Hello World")
(Pdb) step
Hello World
--Return--
> <ipython-input-11-b0e3e2c712c8>(2)myfunc()->None
-> print("Hello World")
(Pdb) step
--Return--
> <string>(1)<module>()->None
(Pdb) step
> /usr/lib/python3.4/bdb.py(435)run()
-> self.quitting = True
(Pdb) step
Listing 14-14.Stepping Through a Function with pdb
十五、C 和其他扩展
Python 的一大优点是它不是每项工作的最佳工具,更重要的是,它知道它不是每项工作的最佳工具。由于这种自我意识,Python 从一开始就被设计成可以用 c 语言编写的代码进行扩展。这种能力是由一个名为 Cython 的模块提供的,该模块可从 http://cython.org
获得。在这一章中,你将看到一些不同的方法,你可以把 Cython 包含在你自己的 Python 程序中,以提高它的性能或者增加额外的功能。
15-1.编译 Python 代码
问题
你想把你的 Python 代码编译成 C 以获得加速。
解决办法
Cython 包提供了一种混合编译 C 代码和 Python 的机制。
它是如何工作的
初始设置适用于本章中的以下所有示例。你的系统上需要有一个 C 编译器。如果您运行的是 Linux 或 Mac OS,那么您可以使用 gcc 作为所需的编译器。要在 Mac OS 上获得编译器,您需要安装 XCode 包。至于 Windows,涉及的步骤更多。Cython 文档中有一个完整的附录,专门用于指导如何设置 Windows。同样,您需要安装 Cython。您可以从源代码安装它,也可以使用 pip 安装它,如清单 15-1 所示。
pip install --user cython
Listing 15-1.Installing Cython with pip
一旦所有的东西都安装好了,你就需要编写将要编译成 c 的 Python 代码。这些代码保存在一个以.pyx
结尾的文件中,而不是以.py
结尾。然后,这个 Cython 源文件以几种不同的方式之一进行编译。对于较大的项目,处理编译的最灵活和健壮的方法是编写一个setup.py
文件并使用distutils
。在这么短的篇幅里介绍这个方法有点太复杂了。令人高兴的是,有一些其他更简单的方法可以让您立即在代码中使用 Cython。
清单 15-2 展示了一个例子。只有一个功能的文件。
def print_msg():
print("Hello World")
Listing 15-2.HelloWorld.pyx File
Cython 包含一个名为pyximport
的模块,它将编译。pyx
当您尝试导入文件时,它们会在后台运行。清单 15-3 展示了如何在交互式 Python 会话中使用它。
>>> import pyximport
>>> pyximport.install()
>>> import HelloWorld
>>> print_msg()
Hello World
Listing 15-3.Using pyximport
这适用于整个源文件。如果您希望编译的代码段更短,您可以让 Cython 直接在 Python 源代码的中间进行内联编译。清单 15-4 给出了一个内联编译代码的例子。
>>> import cython
>>> def my_adder(a, b):
... ret = cython.inline("return a+b")
...
Listing 15-4.Using Inlined Cython Code
为了提高效率,内联代码的编译版本被缓存。
15-2.使用静态类型
问题
你想通过给对象一个类型来加速对它们的访问。
解决办法
您可以安装 Cython 模块以及支持的 C 编译器,来定义新类型,这些新类型的访问和处理速度比 Python 对象快得多。
它是如何工作的
为了使用静态类型,Cython 引入了一个名为cdef
的新关键字。当使用这种方法时,您可以获得比用 Cython 编译 Python 代码更快的速度。清单 15-5 展示了一个集成问题的例子。
def f(x):
return x**2-42
def integrate_f(a, b, N):
s = 0
dx = (b-a)/N
for I in range(N):
s += f(a+i*dx)
return s*dx
Listing 15-5.Pure Python Integration Problem
在 Cython 下编译它将提供一定程度的加速,但是仍然会发生类型检查。这在循环中尤其昂贵,因为变量被多次访问。清单 15-6 展示了相同的例子,除了使用了cdef
关键字。
def f(double x):
return x**2-42
def integrate_f(double a, double b, int N):
cdef int i
cdef double s, dx
s = 0
dx = (b-a)/N
for I in range(N):
s += f(a+i*dx)
return s*dx
Listing 15-6.Integration Problem Using Static Typing
这段代码去掉了所有那些代价高昂的类型检查,在试图优化代码时会是一个很大的帮助。为了编译这些文件,您可以使用 Cython 命令行工具来生成一个 C 源文件,该文件可以被编译成一个共享对象,并导入到 Python 中。假设上面的例子保存在一个名为mycode.pyx
的文件中,清单 15-7 展示了如何使用 GCC。
cython myfile.pyx
gcc -shared -o myfile.so myfile.c `python3-config --includes`
Listing 15-7.Compiling Cython Code Manually
然后,您可以从 Python 中导入这个新编译的共享对象。
15-3.从 C 中调用 Python
问题
您希望能够从 C 程序中调用 Python 代码。
解决办法
标准库包括名为Python.h
的头文件,这使得 Python 可以从 c 中调用。
它是如何工作的
当您想从 C 中调用 Python 代码时,有两个主要的函数可用:Py_Initialize()
和Py_Finalize()
。第一个函数启动 Python 解释器,第二个函数再次关闭它。在两次函数调用之间,您可以运行您的 Python 代码。清单 15-8 展示了一个可以执行一串 Python 代码的例子。
#include "Python.h"
void run_pycode(const char* code) {
Py_Initialize();
PyRun_SimpleString(code);
Py_Finalize();
}
Listing 15-8.Running Python Code from C
这对于较短的代码来说很好,但是如果你有一个完整的脚本,你可以从你的 C 程序中运行它,如清单 15-9 所示。
#include "Python.h"
Int main() {
Py_Initialize();
FILE* file = fopen("./my_script.py", "r");
PyRun_SimpleFile(file, "./my_script.py");
Py_Finalize();
}
Listing 15-9.Running a Python Script from C
15-4.从 Python 调用 C
问题
您希望从 Python 程序中调用外部 C 代码。
解决办法
标准的 Python API 包括帮助连接 Python 和 c 的代码,Cython 包使这种通信更加容易。
它是如何工作的
关键字cdef extern from
告诉 Cython 导入 C 函数的位置。清单 15-10 显示了一个.pyx
文件的例子。
cdef extern from "hello_world.c":
void print_msg()
Listing 15-10.Importing External C Code
清单 15-11 显示了相关的 C 源代码文件。
static void print_msg() {
printf("Hello World");
}
Listing 15-11.Imported C Code
这是一个比 Python 中包含的标准 API 更简单的导入 C 代码的接口。
十六、Arduino 和 RPi 秘籍
对于业余发明家来说,Raspberry PI 和 Arduino 是设计和构建自己技术的巨大资源。Raspberry Pi 已经成为事实上的单板计算机(或 SBC ), Python 已经成为 Pi 上使用的事实上的语言。在其他情况下,您可能实际上需要一个微控制器来提供与计算机的接口,或者作为一个更有限的控制单元。在这些情况下,Arduino 的所有变体都成为了标准。虽然在这一章中你将只探索 Arduino 和 Raspberry Pi,但是还有其他几个选项可供你选择。
16-1.向 Arduino 发送数据
问题
您想要向 Arduino 发送数据或指令。
解决办法
您可以使用 Python 模块pyserial
通过串行连接与 Arduino 通信。
它是如何工作的
可以用pip
安装pyserial
,如清单 16-1 所示。
pip install --user pyserial
Listing 16-1.Installing pyserial
您需要在 Arduino 上预装一个程序,该程序可以接受指令或数据。完成后,使用串行电缆将 Arduino 连接到运行 Python 的计算机。然后你可以让你的 Python 代码向 Arduino 发送信息,比如清单 16-2 中的代码。
>>> import serial
>>> ser = serial.Serial('/dev/tty.usbserial', 9600)
>>> ser.write(b'5')
Listing 16-2.Sending Data to an Arduino
在第二行中,您需要将设备条目/dev/tty.usbserial
替换为适合您的系统的位置。在write()
方法中,需要前面的b
将 Unicode 字符串转换成简单的字节字符串。
16-2.从 Arduino 读取数据
问题
你想从 Arduino 读取数据。
解决办法
同样,您可以使用pyserial
模块从 Arduino 的串行连接中读取数据。
它是如何工作的
假设预先加载在 Arduino 上的代码期望通过串行连接发送数据,您可以使用 Python 模块pyserial
来读取这些数据。清单 16-3 给出了一个例子。
>>> import serial
>>> ser = serial.Serial('/dev/tty.usbserial', 9600)
>>> data = ser.readline()
Listing 16-3.Reading Data from an Arduino
readline()
方法希望在发送的数据末尾有一个新行,所以确保 Arduino 代码在每次输出数据时都发送这个新行。
16-3.写入 Raspberry Pi 的 GPIO 总线
问题
你想在 Raspberry Pi 的 GPIO 总线上写输出。
解决办法
RPi.GPIO
模块提供了 Python 代码和物理 GPIO 总线之间的接口。
它是如何工作的
模块RPi.GPIO
包含在 Raspbian 的包库中。清单 16-4 展示了如何在你的树莓 Pi 上安装它。
sudo apt-get install python-dev python-rpi.gpio
Listing 16-4.Installing the RPi.GPIO Module
为了使用RPi.GPIO
模块,需要几个步骤来设置 GPIO 总线和处理通信。清单 16-5 显示了一个将 GPIO 引脚设置为高电平的例子。
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
GPIO.setup(1, GPIO.OUT, initial=GPIO.LOW)
GPIO.output(1, GPIO.HIGH)
Listing 16-5.Setting a GPIO Pin to High
setmode()
方法设置要使用的管脚编号方案。GPIO.BOARD
使用电路板上的引脚编号。setup()
方法将一个特定的管脚设置为输入或输出,并可选地设置管脚的初始值。然后,您可以使用output()
方法将输出发送到有问题的管脚。如您所见,GPIO 总线输出二进制数据,要么为低电平,要么为高电平。
16-4.从 Raspberry Pi 的 GPIO 总线读取
问题
你想从 Raspberry Pi 的 GPIO 总线读取输入。
解决办法
您可以使用RPi.GPIO
Python 模块从 GPIO 总线读取数据。
它是如何工作的
清单 16-6 提供了如何从一个 GPIO 引脚读入数据的基本示例。
import RPi.GPIO as GPIO
GPIO.setup(1, GPIO.IN)
if GPIO.input(1):
print('Input was HIGH')
else:
print('Input was LOW')
Listing 16-6.Reading Data from the GPIO Bus
如您所见,输入以二进制高电平或低电平接收。