Python 整洁编程(三)
原文:Clean Python
六、生成器和迭代器
迭代器和生成器是 Python 中有用的工具。它们可以使处理不同的数据问题变得更容易,并且帮助您编写更干净、性能更好的代码。
Python 有一个库来利用这两个特性。你将在这一章中学习它们,并且你将探索不同的问题,这些问题可以被生成器和迭代器轻松地处理,而不需要太多的努力。
利用迭代器和生成器
在这一节中,您将探索迭代器和生成器的不同特性,并了解这两个特性可以在代码中的什么地方得到更好的应用。这两个特性主要用于解决不同的数据问题。
理解迭代器
一个迭代器是一个作用于数据流的对象。迭代器对象有一个名为__next__的方法,当你使用for循环、列表理解或任何遍历所有数据点的方法从对象或其他数据结构中获取数据时,在后台会调用__next__方法。
清单 6-1 展示了如何创建一个类并使它成为一个迭代器。
class MutiplyByTwo:
def __init__(self, number):
self.number = number
self.count = 0
def __next__(self):
self.counter += 1
return self.number * self.counter
mul = Mutiple(500)
print(next(mul))
print(next(mul))
print(next(mul))
>>> 500
>>> 1000
>>> 1500
Listing 6-1Iterator Class
让我们看看 Python 中迭代器的实际工作方式。在前面的代码中,有一个名为MultiplyByTwo的类,该类有一个名为__next__的方法,每当调用该方法时都会返回一个新的迭代器。迭代器需要通过使用__next__中的计数器变量来记录 in 在序列中的位置。然而,如果您试图在一个for循环中使用这个类,您会发现它会抛出一个错误,如下所示:
for num in MultiplyByTwo(500):
print(num)
>>> MultiplyByTwo object is not iterable.
有趣的是,MultiplyByTwo是迭代器,不是可迭代的。所以,for循环在这里不起作用。那么,什么是可迭代的?让我们看看 iterables 和迭代器有什么不同。
iterable 对象有一个名为__iter__的方法,它返回一个迭代器。当在任何对象上调用__iter__时,它返回迭代器,迭代器可以用来迭代对象以获取数据。在 Python 中,字符串、列表、文件和字典都是可迭代的例子。
当您尝试对它们执行一个for循环时,效果很好,因为循环返回一个迭代器。
既然你已经理解了可迭代和迭代器,让我们修改类MultiplyByTwo成为可迭代的。见清单 6-2 。
class MultiplyByTwo:
def __init__(self, num):
self.num = num
self.counter = 0
def __iter__(self):
return self
def __next__(self):
self.counter += 1
return self.number * self.counter
for num in MutliplyByTwo(500):
print(num)
Listing 6-2Iterator Class with the for Loop
这个迭代器永远运行,这在某些情况下可能是有用的,但是如果您想要有限数量的迭代器呢?清单 6-3 展示了如何实现这一点。
class MultiplyByTwo:
def __init__(self, num, limit):
self.num = num
self.limit = limit
self.counter = 0
def __iter__(self):
return self
def __next__(self):
self.counter += 1
value = self.number * self.counter
if value > self.limit:
raise StopIteration
else:
return value
for num in MutliplyByTwo(500, 5000):
print(num)
Listing 6-3Iterator Class with StopIteration
当你举起StopIteration;,你的MultiplyByTwo对象得到它已经用完限制的信号,引发一个由 Python 自动处理的异常,并退出循环。
什么是发电机?
生成器对于读取大量数据或大量文件非常有用。发生器可以暂停和恢复。生成器返回可以像列表一样迭代的对象。然而,与列表不同,它们很懒,一次生成一个项目。与其他数据结构相比,在处理大型数据集时,生成器的内存效率要高得多。
让我们试着创建一个类似于上一个例子中迭代器的乘法函数。参见清单 6-4 。
def multiple_generator(num, limit):
counter = 1
value = number * counter
while value <= limit:
yield value
counter += 1
value = number * counter
for num in multiple_generator(500, 5000):
print(num)
Listing 6-4Generator Example
你会注意到这比迭代器例子要短得多,因为你不需要定义__next__和__iter__。您也不需要跟踪内部状态或引发异常。
你可能已经注意到的新东西是yield关键字。yield类似于return,但是它不是终止函数,而是简单地暂停执行,直到请求另一个值。与迭代器相比,生成器更具可读性和性能。
何时使用迭代器
当您处理大量文件或数据流形式的数字时,迭代器非常有用。迭代器为您提供了一次处理一部分数据的灵活性,而不是将所有数据加载到内存中。
假设您有一个包含一系列数字的 CSV 文件,您需要从这个 CSV 文件中计算数字的总和。您可以将 CSV 文件中的数据序列存储在一个列表中,然后计算总和,或者使用迭代器方法,逐行读取 CSV 文件,然后计算每行的总和。
让我们来看看这两种方式,这样您就可以理解其中的区别,如清单 6-5 所示。
import csv
data = []
sum_data = 0
with open("numbers.csv", "r") as f:
data.extend(list(csv.reader(f)))
for row in data[1:]:
sum_data += sum(map(int, row))
print(sum_data)
Listing 6-5Read a CSV File Using a List
请注意,您在这里将数据保存在一个列表中,然后计算列表中数字的总和。这可能会增加内存开销,并可能导致内存泄漏,因为您是以 CSV 文件和列表的形式复制内存中的数据,如果您正在读取一个大文件,这可能会很危险。在这里,迭代器可以通过只从 CSV 文件中获取一行来节省您的时间,因此您不必一次将所有数据都转储到内存中。参见清单 6-6 。
import csv
sum_data = 0
with open('numbers.csv', 'r') as f:
reader = csv.reader(f)
for row in list(reader)[1:]:
sum_data += sum(map(int, row))
print(sum_data)
Listing 6-6Read a CSV File Using an Iterator
这段代码计算一行的总和,并通过让迭代器从 CSV 文件中给你一组新的数据,把它加到下一行。
迭代器的另一个用例是从数据库中读取数据。让我们考虑这样一个场景,电子商务公司通过在线商店销售产品,用户通过在线支付购买这些产品。用户的付款存储在一个名为Payment的表中,24 小时后,一个自动化系统查询Payment表,并计算过去 24 小时的总利润。
有两种方法可以解决这个问题。第一个选项是查询Payment表,获得一个金额列表,然后计算这些金额的总和。在平常的日子里,这可能行得通,但请考虑某个特定的日子,比如黑色星期五或某个公司有数百万笔交易的假日。一次在内存中加载数百万条记录可能会使系统崩溃。第二个选项是查询表,但按行或按行数(如 100 或 1,000)获取数据,然后计算总事务。在 Django 中,你可以做一些如清单 6-7 所示的事情。
def get_total_payment():
payments = Payment.objects.all()
sum_amount = 0
if payments.exists():
for payment in payments.iterator():
sum_amount += payment
return sum_amount
Listing 6-7Read Payment Information from a Database Using an Iterator
这段代码通过一次一行地从数据库中获取数据来计算总量,而不是一次加载所有数据。
使用 itertools
Python 有一个名为itertools的模块,它收集了有用的方法。我不能在这里涵盖所有的方法,但我会谈谈其中的一些。
组合()
itertools.combinations(iterable, r)
这个工具给出了长度为r的iterable的组合元组,在前一行中是 2。
from itertools import combinations
print(list(combinations('12345',2)))
[('1', '2'), ('1', '3'), ('1', '4'), ('1', '5'),
('2', '3'), ('2', '4'), ('2', '5'),
('3', '4'), ('3', '5'),
('4', '5')
]
置换()
itertools.permutations(iterable, r)
这将返回所有长度为r的排列;如果r是None,那么r的默认长度就是一个 iterable 的长度。
from itertools import permutations
print(permutations(['1','2','3']))
print(list(permutations(['1','2','3'])))
[('1', '2', '3'), ('1', '3', '2'),
('2', '1', '3'), ('2', '3', '1'),
('3', '1', '2'), ('3', '2', '1')
]
产品()
itertools.product(iterable, r)
此工具计算输入 iterable 的笛卡尔积。这类似于嵌套循环。
例如,product(x, y)将如下所示:
((x,y) for x in A for y in B)
from itertools import product
print(list(product([1,2,3],repeat = 2)))
[(1, 1), (1, 2), (1, 3),
(2, 1), (2, 2), (2, 3),
(3, 1), (3, 2), (3, 3)
]
计数()
itertools.count(start=0, step=1)
count()是一个迭代器,返回从数字start开始的等距数字。
举个例子,你告诉count()用步骤 4 返回一个数字迭代器。
import itertools
for num in itertools.count(1, 4):
print(item)
if item > 24:
break
>>> 1, 5, 9, 13, 17, 21
groupby(群件)
itertools.groupby(iterable, key=None)
itertools.groupby tool helps you to group items.
举个简单的例子,假设您需要按如下方式对角色进行分组:
numbers = 555441222
result = []
for num, length in groupby(numbers):
result.append((len(list(length)), int(num)))
print(*result)
>>> (3, 5)(2,4)(1,1)(3,2)
itertools里还有其他有用的方法真的很有用。建议你去 https://docs.python.org/3.7/library/itertools.html 了解更多信息。
为什么发电机很有用
像迭代器一样,生成器也节省内存。因为迭代器能够进行惰性进化,所以可以通过只获取操作所需的数据来节省内存。因此,在从数据库中读取大文件时,可以使用生成器来节省内存和 CPU 周期。
所以,假设你想以懒惰的方式读取文件;您可以使用yield关键字,它会为您创建一个生成器函数。参见清单 6-8 。
def read_in_chunks(file_handler, chunk_size=1024):
"""Lazy function (generator) to read a file piece by piece.
Default chunk size: 1k."""
while True:
data = file_handler.read(chunk_size)
if not data:
break
yield data
f = open('large_number_of_data.dat')
for piece in read_in_chunks(f):
print(piece)
Listing 6-8Read in a Chunk Using a Generator
在这里,您正在读取一个大文件,而不是将while文件加载到内存中。
列表理解与迭代器
列表理解和迭代器是生成数字的两种不同方式,它们在生成数字时如何在内存中保存数据或执行操作方面有很大的不同。
# This is iterators expression to generate numbers up to 200.
(x*2 for x in xrange(200))
# List comprehension expression to generate numbers up to 200
[x*2 for x in xrange(200)]
这里的主要区别是列表理解在完成后会将所有 200 个数字保存在内存中。然而,迭代器创建一个动态生成数字的 iterable 对象,所以在迭代器的情况下速度很快。此外,迭代器让您可以灵活地传递对象,动态生成一个数字。
利用 yield 关键字
在深入研究yield之前,我先说说如何在 Python 中使用yield关键字。
当你在你的一个函数内部定义yield时,调用该函数会给你一个生成器对象;然而,那并不运行你的功能。一旦你得到一个生成器对象,每次你从生成器中提取一个对象(通过使用for循环或者通过使用next(),Python 将执行这个函数,直到到达yield关键字。一旦 Python 到达了yield关键字,它就交付对象并暂停,直到你提取它。一旦提取了对象,Python 就会在yield之后继续运行代码,直到到达另一个yield(可能是同一个yield关键字,也可能是不同的yield)。一旦发生器耗尽,它将退出并出现一个StopIteration异常,这个异常由for循环自动处理。
换句话说,yield是一个像return一样使用的关键字,除了函数返回一个生成器。参见清单 6-9 。
def generate_numbers(limit):
for item in xrange(limit):
yield item*item
print(f"Inside the yield: {item}")
numbers = generate_numbers() # create a generator
print(numbers) # numbers is an object!
<generator object generate_numbers at 0xb7555c34>
for item in numbers:
print(item)
0
1
4
Listing 6-9Generate a Number Using a Generator
这里您使用关键字yield创建了一个生成器函数。注意,当您调用函数generate_numbers()时,您会得到numbers对象,这是一个生成器对象。然后,您可以使用它来动态生成数字。
当你第一次在一个for循环中调用生成器对象时,它从generator_numbers的开始运行函数,直到遇到yield关键字,然后它停止并返回循环的第一个值。一旦它第二次调用,就从下一行开始,也就是print(f"Inside the yield: {item}")。它继续这样做,直到达到一个极限。
从...屈服
从 Python 3 开始就使用了yield from关键字。yield from的主要用例是从其他生成器获取一个值,如清单 6-10 所示。
def flat_list(iter_values):
"""flatten a multi list or something."""
for item in iter_values:
if hasattr(item, '__iter__'):
yield from flat_list(item)
else:
yield item
print(list(flat_list([1, [2], [3, [4]]])))
>>> [1, 2, 3, 4]
Listing 6-10Generate a Number Using the yield from Keyword
您没有迭代flat_list,而是使用了yield from,这不仅缩短了代码行,还使您的代码更加整洁。
与数据结构相比,产量更快
如果您正在处理大量数据并且需要速度,那么显然您应该使用生成器来生成数据,而不是依赖于像列表或元组这样的数据结构。
这里有一个简单的例子:
data = range(1000)
def using_yield():
def wrapper():
for d in data:
yield d
return list(wrapper())
def using_list():
result = []
for d in data:
result.append(d)
return result
如果您运行这两个代码示例,您会注意到使用yield肯定比使用列表快。
摘要
生成器和迭代器真的很有用,尤其是在处理大量数据或大文件的时候。您需要格外小心内存和 CPU 消耗,因为过度消耗会导致内存泄漏等问题。Python 为您提供了像itertools和yield这样的工具来帮助您避免所有这些问题。当你处理大文件、使用数据库或调用多个 API 时,要格外勤奋;您也许可以使用这些工具来使您的代码更加整洁和高效。
七、利用新的 Python 特性
最新 Python 3 版本中引入的新特性使得用 Python 编写程序变得有趣多了。Python 已经有了很多很棒的特性,Python 3 使它成为一种特性更加丰富的语言。Python 3 附带了一些特性,比如对异步编程、类型、更好的性能、迭代器改进等的原生支持。
在这一章中,你将会学到一些新的特性,这些特性可以使你的代码比以前的 Python 版本更好,性能更高。您将了解使用任何或所有这些特性是如何有用的,以及您应该考虑在代码中的什么地方使用它们。
注意
你可以在 https://docs.python.org/3/whatsnew/3.7.html 的官方文档中探索 Python 的新特性。在撰写本书时,Python 3 仍处于开发阶段,因此可能会有一些这里没有提到的改进。换句话说,请留意 Python 官方文档,了解最新的特性。
异步编程
如果你曾经用另一种语言如 JavaScript 做过异步编程(或简称为 async programming ),你可能知道这不是一个简单的话题。在 Python 3.4 之前,有一种方法可以使用第三方库进行异步编程,但与 NodeJS 这样对异步编程非常友好的语言相比,总感觉有点笨拙。
Python 在这个问题上很灵活,因为您可以编写同步和异步代码。与同步编程相比,使用异步编程可以使您的代码更加高效,因为它可以更有效地利用资源。然而,知道什么时候应该使用异步编程,什么时候不应该使用非常重要。
在进一步讨论之前,让我们讨论一下异步和同步编程。在同步世界中,事情一次发生一件。你调用一个函数或操作,你的程序控制在它继续做下一件事之前等待它完成。当一个函数完成其操作时,该函数返回结果。当操作被函数执行时,你的系统除了等待它完成之外不做任何其他事情。
在异步世界中,多件事情可以同时发生。当您开始一个动作或调用一个函数时,您的程序会继续运行,您可以执行其他动作或调用其他函数,而不只是等待异步函数完成。一旦异步函数完成了工作,程序控件就可以访问结果。
作为一个例子,我们假设您必须通过调用不同公司的股票 API 来获取不同公司的股票数据。在同步代码中,您将调用第一个 stock API 并等待得到回复,然后您将进行另一个调用并等待它完成。这是运行程序的简单方法;然而,程序花费太多时间等待响应。在异步代码中,调用第一个 stock API,然后是第二个,第三个,一直到从其中一个 API 得到结果。您收集结果并继续调用其他库存 API,而不是等待结果。
在本节中,您将探索 Python 中的异步编程,以便理解如何使用它。这是 Python 异步编程的三个主要构件:
-
事件循环的主要任务是管理不同的任务,并将它们分配执行。事件循环注册每个任务,并负责这些任务之间的流控制。
-
协程是调度事件循环运行的函数。一个
await将控制流释放回事件循环。 -
Futures 代表一个任务的结果,这个任务可能已经执行,也可能没有执行。这个结果可能是个例外。
Python 中的异步引入
为了在 Python 编程中实现异步范式,Python 引入了两个主要组件。
-
asyncio :这是允许 API 运行和管理协程的 Python 包。
-
async/await : Python 引入了两个新的关键字来处理异步代码。它们帮助您定义协程。
基本上,Python 现在有能力以两种不同的方式运行,异步或同步。根据您选择的方式,您在设计代码时应该有不同的想法,因为代码的功能和行为是不同的。这些样式也有彼此不同的库。换句话说,异步和同步编码的风格和语法互不相同。
为了说明这一点,如果你正在进行 HTTP 调用,你不能使用阻塞的requests库;因此,您可能想考虑使用aiohttp来进行 HTTP 调用。类似地,如果你使用 Mongo 驱动,你不能依赖像mongo-python这样的同步驱动。你必须使用类似motor的异步驱动程序来访问 MongoDB。
在同步世界中,在 Python 中没有实现并发性或并行性的简单方法。但是,可以选择使用 Python 的线程模型并行运行代码;然而,在异步世界中(不要把这和并行混淆),事情已经变得更好了。现在一切都在一个事件循环中运行,这让您可以同时运行几个协程。这些协同程序同步运行,直到它们到达await然后暂停,将控制权交给事件循环。另一个协程将有机会执行一个动作,或者会发生一些其他事情。
同样需要注意的是,不能在同一个函数中混合使用异步和同步代码。例如,您不能将await与同步功能一起使用。
在深入异步编程之前,有几件事情您应该知道,尤其是在 Python 世界中。
-
在同步编程中,当您想要暂停执行或让程序不做任何事情时,通常使用 Python
time.sleep(10)函数。然而,在异步世界中,这不会像你期望的那样工作。你应该用await asyncio.sleep(10);这不会将控制返回给事件循环,并且会阻碍整个过程。其他什么也不会发生,这可能是一件好事,因为当代码从一个await调用转移到另一个调用时,这使得竞争情况更难发生。 -
如果你在异步函数中使用阻塞代码,Python 不会抱怨你使用它;然而,事情会痛苦地慢下来。此外,Python 有调试模式,这将警告您那些由于常见错误而阻塞太久的东西。
-
当您在同一代码库中编写异步和同步代码时,您可能需要考虑使用重复的代码。在大多数情况下,为异步和同步代码使用同一个库或助手是不可能的。
-
编写异步代码时,您应该假设与同步代码的完全控制相比,执行时的控制流可能会丢失。尤其是当您的代码中运行多个协同程序时,会发生多种情况。
-
可以想象,在异步世界中,调试变得越来越困难。目前还没有好的调试工具或技术。
-
在 Python 中测试异步代码不是很方便。缺少测试异步代码的好库。你可能会看到一些库正在努力实现这一点,但是它们还没有像 JavaScript 等其他编程语言那样成熟。
-
在同步代码中使用 Python 的
async关键字,比如在同步函数中使用await,会导致语法错误。
改变异步设计代码的思维模式也很重要。如果你的代码库中既有异步代码又有同步代码,那么你必须以不同的方式来看待它们。async def里面的任何东西都是异步代码,其他的都是同步代码。
在两种情况下,您应该考虑使用异步代码。
-
从异步代码中调用异步代码,您可以使用所有的 Python 关键字,如
await和async,以充分利用 Python 异步编码。 -
在 Python 3.7 中,只需调用
asyncio中的run()函数,就可以从同步代码中调用异步代码。
总的来说,写异步代码不像用 Python 写同步代码那么容易。Python 异步模型基于事件、回调、传输、协议和未来等概念。好消息是asyncio库正在发展,每个版本都在改进。Python asyncio将会一直存在!
注意
在编写任何异步代码之前,确保您对以异步方式编写代码有正确的想法,尤其是当您有同步编程背景时。很多时候你会觉得自己搞不懂异步编程。使用少量的异步代码,并以最小的影响将其引入到您的代码库中,是开始使用它的好方法。对异步代码进行良好的测试将确保代码库中的更改不会破坏现有的功能。Python 的异步世界正朝着更好的方向快速发展。因此,请关注 Python 的新版本,了解异步编程中的所有新特性。
它是如何工作的
我已经谈了一些asyncio特性的背景,所以现在让我们看看asyncio在现实世界中是如何工作的。Python 引入了asyncio包来编写异步代码。该包提供了两把钥匙,async和await。让我们深入一个简单的异步示例,看看 Python 异步实际上是如何工作的。见清单 7-1 。
import asyncio
async def hello(first_print, second_print):
print(first_print)
await asyncio.sleep(1)
print(second_print)
asyncio.run(hello("Welcome", "Good-bye"))
Welcome
Good-bye
Listing 7-1Async, Simple Hello Example
清单 7-1 显示了一些简单的asyncio代码;它先打印Welcome,一秒钟后打印Good - bye。让我们看看这是如何工作的。首先asyncio.run()调用异步函数hello,并传入两个参数:Welcome和Good - bye。调用hello函数时,首先打印first_print,然后等待一秒钟打印second_print。这种行为可能看起来像同步代码;然而,深入细节可能会让您感到惊讶,并且有助于您理解异步代码实际上是如何工作的。让我们先了解一下这里使用的一些术语。
协程函数
在 Python 中,任何被定义为async def的函数都可以被称为协程。这里,async def hello(first_print, second_print)可以称为协程函数。
协同程序对象
调用协程函数返回的对象称为协程对象。在后面的例子中,您会更清楚地看到协程函数和现实世界中的协程对象之间的区别。
asyncio.run()
该功能是asyncio模块的一部分。这是任何异步代码的主入口点,应该只调用一次。它做了几件事。
-
它负责运行传递的协程,在前面的例子中,协程运行的是
async def hello协程函数。 -
它还管理
asyncio事件循环。这基本上创建了一个新的事件循环,并在结束时关闭它。
等待
await是将功能控制传递回事件循环并暂停协程执行的关键字。在前面的例子中,当 Python 遇到await关键字时,它暂停hello协程执行一秒钟,并将控制传递回事件循环,一秒钟后恢复。
在进入细节之前,让我们看一个更简单的例子,看看会发生什么。通常会暂停协程函数的执行,直到它在等待什么。当协程的结果返回时,执行重新开始。await有一些规则。
-
它只能在
async def函数内部使用。 -
如果在普通函数中定义它,它会引发一个异常。
-
要调用协程函数,必须等待结果返回。
-
当你使用类似于
await func(的东西时,要求func()是一个可调整的对象,这意味着它应该是另一个协程函数或者是定义了返回迭代器的__await__()方法的对象。
现在让我们来看一个更有用的例子,如清单 7-2 所示,其中您将尝试并发运行一些事情,并利用异步特性。
import asyncio
import time
async def say_something(delay, words):
print(f"Before: {words}")
await asyncio.sleep(delay)
print(f"After: {words}")
async def main():
print(f"start: {time.strftime('%X')}")
await say_something(1, "First task started.")
await say_something(1, "Second task started.")
print(f"Finished: {time.strftime('%X')}")
asyncio.run(main())
Listing 7-2asyncio Running Two Tasks
结果如下:
start: 11:30:11
Before: First task started.
After: First task started.
Before: Second task started.
After: Second task started.
Finished: 11:30:13
这里,通过调用协程函数say_something两次并等待两个版本都完成,您运行了同一个协程两次。正如您将在结果中注意到的那样,say_something协程首先运行,等待一秒钟,然后完成协程。然后被main()协程再次调用,执行另一个任务,也就是一秒钟后打印第二个任务。这不是你使用async时想要的;看起来同步代码仍然在运行。异步代码背后的主要思想是你可以同时运行两次say_something。
让我们转换这段代码并并发运行它,如清单 7-3 所示。与前面的清单相比,您可能会注意到代码中的一些重大变化。
import asyncio
import time
async def say_something(delay, words):
print(f"Before: {words}")
await asyncio.sleep(delay)
print(f"After: {words}")
async def main():
print(f"Starting Tasks: {time.strftime('%X')}")
task1 = asyncio.create_task(say_something(1, "First task started"))
task2 = asyncio.create_task(say_something(2, "Second task started"))
await task1
await task2
print(f"Finished Tasks: {time.strftime('%X')}")
asyncio.run(main())
Listing 7-3asyncio Running Code Concurrently
结果如下:
Starting Tasks: 11:43:56
Before: First task started
Before: Second task started
After: First task started
After: Second task started
Finished Tasks: 11:43:58
正如您在结果中所看到的,这个函数正在用不同的参数同时运行相同的协程,这就是您想要同时运行的东西。
让我们分析一下这个例子中发生了什么:
-
say_something协程从参数的第一个任务开始,称为task1。 -
当遇到关键字
await时,它会暂停执行一秒钟。 -
一旦
task1遇到await,它就挂起正在运行的协程,并将控制权返回给事件循环。 -
另一个名为
task2的任务是通过用参数将协程的函数say_something包装在create_task中创建的。 -
当第二个任务
task2开始运行时,它会遇到类似于async def say_something协程中的task1的await关键字。 -
然后,它让
task2暂停两秒钟,并将控制返回给事件循环。 -
现在事件循环恢复第一个任务(
task1),因为asyncio.sleep已经完成(休眠一秒)。 -
当任务
task1完成工作时,第二个任务task2恢复该任务并完成它。
您可能已经注意到的第一件事是asyncio.create_task(),它使函数作为一个asyncio任务并发地运行协程。
任务
每当使用类似于asyncio.create_task()的方法调用任何协程函数时,该协程都会被自动调度为很快运行。
任务帮助你同时运行协程函数,Python 在 Python asyncio世界中把这些运行的协程称为任务。让我们看一个使用asyncio库创建任务的简单例子;见清单 7-4 。
import asyncio
async def value(val):
return val
async def main():
# Creating a task to run concurrently
# You can create as many task as possible here
task = asyncio.create_task(value(89))
# This will simply wait for task to finish
await task
asyncio.run(main())
Listing 7-4Simple Task Creation Example
另一种创建任务并等待所有任务完成的方法是使用asyncio.gather函数。asyncio.gather能够将所有协程函数作为任务运行,并在返回事件循环之前等待它们的结果。
让我们看一个简单的例子;参见清单 7-5 。
import asyncio
import time
async def greetings():
print("Welcome")
await asyncio.sleep(1)
print("Good By")
async def main():
await asyncio.gather(greetings(), greetings())
def say_greet():
start = time.perf_counter()
asyncio.run(main())
elapsed = time.perf_counter() - start
print(f"Total time elapsed: {elapsed}")
asyncio.run(say_greet())
Listing 7-5Using asyncio.gather to Run Tasks Concurrently
当您运行这段代码时,您会看到类似这样的内容:
Welcome
Welcome
Good By
Good By
Total time elapsed: 1.006283138
让我们尝试使用asyncio.gather来理解前面的代码是如何运行的。当您运行这段代码时,您会注意到Welcome在控制台上出现了两次,然后Good By运行了两次。在打印两个Welcome和两个Good By消息之间有轻微的延迟。
当你从say_greet()调用异步main()函数时,那么与greetings()函数对话就是事件循环的工作,执行greetings()可以被称为任务。
在前面的代码中,您有两个正在运行的任务可以执行greetings()函数。
有一个话题我没讲过,就是await关键词。这是 Python 中asyncio编程的重要关键词之一。任何可以和await一起使用的对象都可以被称为可应用的对象。理解可适应对象也很重要,因为这将让你更好地了解asyncio库是如何操作的,以及如何在 Python 中的不同任务之间切换。
合适的对象
如前所述,任何与await一起使用的对象都被称为可应用对象。大多数的asyncioAPI 接受一个合适的对象。
异步代码中的可适应对象有以下类型。
协同程序
在前一节中,我已经提到了协程的概念。在这里,您将进一步探索这一点,并了解它是如何成为一种可盈利的类型。
所有的协程函数都是可调度的,所以它们可以被其他协程等待。您也可以将协程定义为子例程,但是它可以在不破坏异步环境中的状态的情况下退出。参见清单 7-6 。
import asyncio
async def mult(first, second):
print(f"Calculating multiply of {first} and {second}")
await asyncio.sleep(1)
num_mul = first * second
print(f"Multiply of {num_mul}")
return num_mul
async def sum(first, second):
print(f"Calculating sum of {first} and {second}")
await asyncio.sleep(1)
num_sum = first + second
print(f"Sum is {num_sum}")
return num_sum
async def main(first, second):
await sum(first, second)
await mult(first, second)
asyncio.run(main(7, 8))
Listing 7-6Coroutine Awaiting from Another Coroutine
结果如下:
Calculating sum of 7 and 8
Sum is 15
Calculating multiply of 7 and 8
Multiply of 56
正如您将在示例中注意到的,您多次调用协程,并使用带有await关键字的协程。
任务
当使用asyncio的asyncio.create_task()方法将协程包装在一个任务中时,协程被调度运行。大多数情况下,如果你使用异步代码,你就要处理create_task方法来并发运行你的协程。参见清单 7-7 。
import asyncio
async def mul(first, second):
print(f"Calculating multiply of {first} and {second}")
await asyncio.sleep(1)
num_mul = first * second
print(f"Multiply of {num_mul}")
return num_mul
async def sum(first, second):
print(f"Calculating sum of {first} and {second}")
await asyncio.sleep(1)
num_sum = first + second
print(f"Sum is {num_sum}")
return num_sum
async def main(first, second):
sum_task = asyncio.create_task(sum(first, second))
mul_task = asyncio.create_task(sum(first, second))
await sum_task
await mul_task
asyncio.run(main(7, 8))
Listing 7-7create_task Helping to Schedule a Coroutine to Run
结果如下:
Calculating sum of 7 and 8
Calculating sum of 7 and 8
Sum is 15
Sum is 15
正如您在这个例子中看到的,通过利用asyncio方法asyncio.create_task创建任务,您正在同时运行两个不同的协程。
一旦创建了一个任务,就可以使用await关键字同时运行新创建的任务。一旦这两项任务都完成了,就将结果发送到一个事件循环。
期货
Futures 是一个可应用的对象,表示异步操作的未来结果。协程需要等待,直到Future对象返回响应或完成操作。大多数情况下,您不会在代码中显式使用Future对象。然而,Future对象已经被asyncio暗中照顾了。
当一个未来的实例被创建时,这意味着它还没有完成,但将在未来的某个时候完成。
Future有类似于done()和cancel()的方法。不过,您通常不需要编写这样的代码,但是理解Future对象是必不可少的。
Future对象实现__await__()方法,Future对象的工作是保存某个状态和结果。
Future具有以下状态:
-
PENDING:这表示一个Future正在等待完成。 -
CANCELLED:如上所述,可以使用 cancel 方法取消一个Future对象。 -
FINISHED:有两种方法可以完成一个Future对象:作为Future.set_result()或者作为一个带有Future.set_exception()的异常。
清单 7-8 展示了一个Future对象的例子。
from asyncio import Future
future = Future()
future.done()
Listing 7-8Future Object
结果如下:
False
这可能是一个学习更多关于asyncio.gather的好时机,因为你现在可能更好地理解了在asyncio世界中一个可行的方法是如何工作的。
注意
这里我只介绍了gather方法;然而,我建议您也看看其他的asyncio方法,看看它们的语法是什么样子的。通常,您会了解这些函数需要哪种类型的输入以及原因。
其语法如下所示:
asyncio.gather(*aws, loop=None, return_exceptions=False)
可以是一个协程,也可以是调度给一个任务的一系列协程。当所有任务完成后,asyncio.gather方法将它们聚集起来并返回结果。它按照这些可应用对象的顺序运行任务。
默认情况下,return_exceptions的值是False,这意味着如果任何一个任务返回异常,当前正在运行的其他任务将不会暂停,并将继续运行。
如果return_exception的值为True,则认为是成功的结果,并在结果列表中汇总。
超时设定
除了引发异常之外,在等待任务完成时,您还可以进行某种超时。
asyncio有一个名为asyncio.wait_for(aws, timeout, *)的方法,可以用来设置任务运行的超时。如果超时发生,它会取消任务并引发异常asyncio.TimeoutError。超时值可以是None或float或int;如果超时是None,它将阻塞,直到Future对象完成。
清单 7-9 展示了一个异步超时的例子。
import asyncio
async def long_time_taking_method():
await asyncio.sleep(4000)
print("Completed the work")
async def main():
try:
await asyncio.wait_for(long_time_taking_method(), timeout=2)
except asyncio.TimeoutError:
print("Timeout occurred")
asyncio.run(main())
Listing 7-9Async Timeout
>> Timeout occurred
在清单 7-9 中,方法long_time_taking_method需要大约 4000 秒;但是,您已经为Future对象设置了两秒钟的超时,所以如果结果不可用,它会在两秒钟后转到asyncio.TimeoutError。
注意
本节讨论的方法是asyncio代码中最常见的方法;然而,还有一些其他的库和方法出现在asyncio库中,它们不太常见或者用于更高级的场景。如果你有兴趣了解更多关于asyncio的知识,可以看看 Python 官方文档。
异步发电机
异步发生器使得在async函数中使用yield成为可能。因此,任何包含yield的async函数都可以被称为异步生成器。拥有异步发电机的想法是复制同步yield的功能。唯一的区别是你可以把那个函数叫做async。
与同步发电机相比,异步发电机无疑提高了发电机的性能。根据 Python 文档,异步生成器比同步生成器快 2.3 倍。参见清单 7-10 。
import asyncio
async def generator(limit):
for item in range(limit):
yield item
await asyncio.sleep(1)
async def main():
async for item in generator(10):
print(item)
asyncio.run(main())
Listing 7-10
Async Generators
这将在一秒钟的差异内打印项目 1 至 9。这个例子展示了如何在异步协程中的代码中使用异步生成器。
异步理解
Python 异步功能提供了实现异步理解的工具,类似于同步代码理解list、dict、tuple和set的方式。换句话说,异步理解类似于在异步代码中使用理解。
让我们看看清单 7-11 中的例子,它展示了如何利用异步理解。
import asyncio
async def gen_power_two(limit):
item = 0
while item < limit:
yield 2 ** item
item += 1
await asyncio.sleep(1)
async def main(limit):
gen = [item async for item in gen_power_two(limit)]
return gen
print(asyncio.run(main(5)))
Listing 7-11Async Comprehension
这将打印一个从 2 到 16 的数字列表;但是,您必须等待五秒钟才能看到结果,因为它将完成所有任务,然后返回结果。
异步迭代器
你已经看到了一些迭代器的例子,比如asyncio.gather,它是迭代器的一种形式。
在清单 7-12 中,您可以使用asyncio.as_completed()查看一个迭代器,它在任务完成时获取任务。
import asyncio
async def is_odd(data):
odd_even = []
for item in data:
odd_even.append((item, "Even") if item % 2 == 0 else (item, "Odd"))
await asyncio.sleep(1)
return odd_even
async def is_prime(data):
primes = []
for item in data:
if item <= 1:
primes.append((item, "Not Prime"))
if item <= 3:
primes.append((item, "Prime"))
if item % 2 == 0 or item % 3 == 0:
primes.append((item, "Not Prime"))
factor = 5
while factor * factor <= item:
if item % factor == 0 or item % (factor + 2) == 0:
primes.append((item, "Not Prime"))
factor += 6
await asyncio.sleep(1)
return primes
async def main(data):
odd_task = asyncio.create_task(is_odd(data))
prime_task = asyncio.create_task(is_prime(data))
for res in asyncio.as_completed((odd_task, prime_task)):
compl = await res
print(f"completed with data: {res} => {compl}")
asyncio.run(main([3, 5, 10, 23, 90]))
Listing 7-12async Iterator Using as_completed
结果如下:
completed with data: <coroutine object as_completed.._wait_for_one at 0x10373dcc8>
=> [(3, 'Odd'), (5, 'Odd'), (10, 'Even'), (23, 'Odd'), (90, 'Even')]
completed with data: <coroutine object as_completed.._wait_for_one at 0x10373dd48>
=> [(3, 'Prime'), (3, 'Not Prime'), (10, 'Not Prime'), (90, 'Not Prime'), (90, 'Not Prime')]
正如您在清单 7-12 的结果中所看到的,两个任务同时运行,并根据传递给两个协程的列表获得质数和奇数/偶数状态。
当使用asyncio.gather函数时,你可以通过使用asyncio.gather而不是asyncio.as_completed来创建类似的任务,如清单 7-13 所示。
import asyncio
async def is_odd(data):
odd_even = []
for item in data:
odd_even.append((item, "Even") if item % 2 == 0 else (item, "Odd"))
await asyncio.sleep(1)
return odd_even
async def is_prime(data):
primes = []
for item in data:
if item <= 1:
primes.append((item, "Not Prime"))
if item <= 3:
primes.append((item, "Prime"))
if item % 2 == 0 or item % 3 == 0:
primes.append((item, "Not Prime"))
factor = 5
while factor * factor <= item:
if item % factor == 0 or item % (factor + 2) == 0:
primes.append((item, "Not Prime"))
factor += 6
await asyncio.sleep(1)
return primes
async def main(data):
odd_task = asyncio.create_task(is_odd(data))
prime_task = asyncio.create_task(is_prime(data))
compl = await asyncio.gather(odd_task, prime_task)
print(f"completed with data: {compl}")
return compl
Listing 7-13Using asyncio.gather for Iterating on a Task
结果如下:
asyncio.run(main([3, 5, 10, 23, 90]))
completed with data:
[[(3, 'Odd'), (5, 'Odd'), (10, 'Even'), (23, 'Odd'), (90, 'Even')], [(3, 'Prime'), (3, 'Not Prime'), (10, 'Not Prime'), (90, 'Not Prime'), (90, 'Not Prime')]]
您可能会注意到,您不需要编写循环,因为asyncio.gather已经为您做了;它收集所有结果数据并将其发送回调用者。
异步代码要考虑的第三方库
除了asyncio,还有几个第三方库可以实现同样的目标。大多数第三方库试图克服你在asyncio中看到的一些问题。
然而,考虑到 Python asyncio库中的持续改进,我建议在你的项目中使用asyncio,除非你需要asyncio完全没有的东西。
让我们看看一些可用于异步代码的第三方库。
酷吏欧
Curio 是一个第三方库,允许您使用 Python 协程执行并发 I/O。它基于一个任务模型,提供线程和进程之间交互的高级处理。清单 7-14 显示了一个使用 Curio 库编写异步代码的简单例子。
import curio
async def generate(limit):
step = 0
while step <= limit:
await curio.sleep(1)
step += 1
if __name__ == "__main__":
curio.run(generate, 10)
Listing 7-14Curio Example
这将以异步方式生成 1 到 10 个数字。Curio 通过调用run()启动内核,并使用async def等方法定义任务。
任务应该在 Curio 内核中运行,它有责任运行到没有任务可运行为止。
使用 Curio 时要记住的是,它运行一个异步函数作为任务,每个任务都需要在 Curio 内核中运行。
让我们再看一个 Curio 库的例子,它实际上运行多个任务。参见清单 7-15 。
import curio
async def generate(limit):
step = 0
while step <= limit:
await curio.sleep(1)
step += 1
async def say_hello():
print("Hello")
await curio.sleep(1000)
async def main():
hello_task = await curio.spawn(say_hello)
await curio.sleep(3)
gen_task = await curio.spawn(generate, 5)
await gen_task.join()
print("Welcome")
await hello_task.join()
print("Good by")
if __name__ == '__main__':
curio.run(main)
Listing 7-15Curio Multiple Tasks
您可能已经猜到了,这显示了创建和加入任务的过程。这里有两个主要概念需要把握。
spawn方法将协程作为参数,并启动新的hello_task任务。
join方法在返回内核之前等待任务完成。
我希望这有助于您了解 Curio 如何在 Python 中实现并发性。你可以查看古董官方文档了解更多细节。
三人组
Trio 是一个像 Curio 一样的现代开源库。它有望让用 Python 编写异步代码变得更容易。Trio 中值得注意的一些功能如下:
-
它具有良好的可扩展性机制。
-
它可以同时运行 10,000 个任务。
-
Trio 是用 Python 编写的,对于那些想深入了解事物如何工作的开发人员来说,这可能很有用。
-
快速入门更容易,因为 Trio 文档非常棒。如果你想寻找一个特定的功能,它都有很好的记录。
让我们快速地看一下 Trio 的一个简单例子,感受一下 Trio 异步代码。参见清单 7-16 。
import trio
async def greeting():
await trio.sleep(1)
return "Welcome to Trio!"
trio.run(greeting)
>> Welcome to Trio!
Listing 7-16Trio, Simple Async Code
正如您所看到的,理解代码是怎么回事真的很容易。Trio 使用run()方法运行async函数,该方法启动greeting异步函数执行,然后暂停执行一秒钟,最后返回结果。
让我们看一个更有用的例子,你可以用 Trio 运行多个任务。
让我们将清单 7-13 asyncio版本的is_odd和is_prime异步函数转换成 Trio,这样你就能更好地理解 Trio 的用法。见清单 7-17 。
import trio
async def is_odd(data):
odd_even = []
for item in data:
odd_even.append((item, "Even") if item % 2 == 0 else (item, "Odd"))
await trio.sleep(1)
return odd_even
async def is_prime(data):
primes = []
for item in data:
if item <= 1:
primes.append((item, "Not Prime"))
if item <= 3:
primes.append((item, "Prime"))
if item % 2 == 0 or item % 3 == 0:
primes.append((item, "Not Prime"))
factor = 5
while factor * factor <= item:
if item % factor == 0 or item % (factor + 2) == 0:
primes.append((item, "Not Prime"))
factor += 6
await trio.sleep(1)
return primes
async def main(data):
print("Calculation has started!")
async with trio.open_nursery() as nursery:
nursery.start_soon(is_odd, data)
nursery.start_soon(is_prime, data)
trio.run(main, [3, 5, 10, 23, 90])
Listing 7-17Trio Running Multiple Tasks
正如您可能已经注意到的,您在is_prime和is_odd异步函数中没有做太多改变,因为它们在这里的工作方式与asyncio相似。
这里的主要区别是 in main()函数。您没有调用asyncio.as_completed,而是使用了trio.open_nursery方法,该方法获得了nursery对象。nursery使用函数nursery.start_soon开始运行异步协同程序。
一旦nursery.start_soon包装了异步函数is_prime和is_odd,这两个任务就开始在后台运行。
async with语句的最后一个块强制main()函数停止并等待所有协程完成;然后从nursery退出。
一旦运行了清单 7-17 中的示例,您可能会注意到它的运行方式类似于asyncio示例,其中is_prime和is_odd函数同时运行。
注意
在撰写本书时,Curio 和 Trio 是两个著名的异步代码库。对asyncio有很好的理解,有助于你快速跳转到任何第三方库。我建议在选择任何第三方库之前对asyncio有一个很好的了解,因为大多数库的底层都使用了一些 Python 异步特性。
用 Python 输入
Python 是一种动态语言,所以在用 Python 编写代码时,通常不需要担心定义类型。如果您使用的是 Java 或。NET 中,即使在编译代码之前,您也必须了解类型;否则,这些语言将抛出错误。
数据类型有助于调试和读取大型代码库。然而,像 Python 和 Ruby 这样的语言给了你灵活性和自由,让你不用担心数据类型,而是专注于业务逻辑。
类型化是动态语言世界中的一个话题,有些开发人员喜欢类型,有些不喜欢使用类型。
Python 有以typing模块形式提供的类型,所以我建议在您的项目中尝试一下,看看它们对您是否有意义。
我发现它们在编写代码时非常有用,尤其是在调试和记录代码时。
Python 中的类型
从 Python 3 开始,可以在代码中使用类型。然而,类型在 Python 中是可选的。当您运行代码时,它不会检查类型。
即使你定义了错误的类型,Python 也不会抱怨。如果你想确保你写的是正确的类型,你可以考虑使用像mypy这样的工具,如果你没有正确的类型,它会报错。
现在 Python 允许您通过简单地添加: <data_types>来在代码中添加类型。参见清单 7-18 。
def is_key_present(data: dict, key: str) -> bool:
if key in data:
return True
else:
return False
Listing 7-18Adding Types in Python
这里你通过传递一个字典和一个键来寻找字典中的一个键。该函数还将参数的类型定义为data: dict和key: str,并将类型返回为-> bool。这是你在 Python 中编写类型所需要做的。
Python 理解这种语法,并假设您已经编写了正确的类型,而无需验证它们。然而,作为一名开发人员,它会让您了解传递给函数的类型。
您可以使用 Python 中本地可用的所有数据类型,而无需使用任何其他模块或库。Python 支持list、dict、int、str、set、tuple等类型。,不需要任何其他模块。但是,在某些情况下,您可能需要更高级的类型,这将在下一节中看到。
打字模块
对于高级用途,Python 引入了一个名为typing的模块,它为您提供了更多类型来添加到您的代码库中。开始可能要花一些努力来习惯语法和类型,但是一旦你理解了这个模块,你可能会觉得它使你的代码更整洁,可读性更好。
有很多内容要谈,所以让我们直接开始吧。typing模块提供了基本类型,例如Any、Union、Tuple、Callable、TypeVar、Generic等等。让我们简单地谈谈其中的一些类型,以便对它们有所了解。
联盟
如果您事先不知道什么类型将被传递给一个函数,但是该函数期望从有限的类型集中获得一个类型,那么您可以使用Union。这里有一个例子:
from typing import Union
def find_user(user_id: Union[str, int]) -> None:
isinstance(user_id, int):
user_id = str(user_id)
find_user_by_id(user_id)
...
这里,user_id可以是str或int,所以你可以使用Union来确保你的函数期望user_id作为str或int。
任何的
这是一种特殊的类型;其他类型都和Any一致。它有所有的值和所有的方法。如果不知道这个函数在运行时接受哪种类型,可以考虑使用这种类型。
from typing import Any
def stream_data(sanitize: bool, data: Any) -> None:
if sanitize:
...
send_to_pipeline_for_processing(data)
元组
正如您可能从它的名字中猜到的那样,这是一种元组类型。唯一的区别是您可以定义元组包含的类型。
from typing import Tuple
def check_fraud_users(users_id: Tuple[int]) -> None:
for user_id in users_id:
try:
check_fraud_by_id(user_id)
exception FraudException as error:
...
类型变量和泛型
如果你想定义你自己的类型或者重命名特定的类型,你可以利用typing中的TypeVar来实现。这有助于提高代码的可读性,并为自定义类定义类型。
这是一个更高级的typing概念。大多数时候,你可能不需要它,因为你会发现typing模块给了你足够多的类型。
from typing import TypeVar, Generics
Employee = TypeVar("Employee")
Salary = TypeVar
def get_employee_payment(emp: Generics[Employee]) -> :
...
可选择的
当您怀疑类型None也将作为值而不是定义的类型传递时,可以使用Optional。所以,不要写成Union[str, None],你可以简单地写成Optional[str]。
from typing import Optional
def get_user_info_by_id(user_id: Optional[int]) -> Optional[dict]:
if user_id:
get_data = query_to_db_with_user_id(user_id)
return get_data
else:
return None
这是对 Python 中的typing模块的介绍。在typing模块中有许多其他类型可用,您可能想在您现有的代码库中使用。可以参考 Python 官方文档了解更多。
https://docs.python.org/3/library/typing.html
数据类型会降低代码速度吗?
一般来说,使用typing模块或类型不会影响代码的性能。然而,typing模块提供了一个名为typing.get_type_hints的方法来返回对象的类型提示,第三方工具可以使用它来检查对象的类型。Python 在运行时不进行类型检查,所以这根本不会影响你的代码。
根据 Python PEP 484 1 :
虽然提议的类型模块将包含一些用于运行时类型检查的构建块,特别是 get_type_hints()函数,但必须开发第三方包来实现特定的运行时类型检查功能,例如使用 decorators 或元类。使用类型提示进行性能优化是留给读者的练习。
打字如何帮助编写更好的代码
类型化可以帮助您进行静态代码分析,以便在将代码投入生产之前捕捉类型错误,并防止出现一些明显的错误。
有像mypy这样的工具,你可以将它们作为软件生命周期的一部分添加到你的工具箱中。mypy可以通过部分或全部运行您的代码库来检查类型是否正确。mypy还可以帮助你检测错误,比如当函数返回值时检查None类型。
键入有助于使您的代码更整洁。与使用注释记录代码不同,在 docstring 中指定类型,您可以使用类型而不会有任何性能损失。
如果您使用的是 PyCharm 或 VSCode 之类的 IDE,typing模块也可以帮助您完成代码。众所周知,早期的错误捕捉和干净的代码对于任何大型项目的长期持续都是非常重要的。
打字陷阱
在使用 Python 的typing模块时,您应该注意一些陷阱。
-
**没有很好的记录。**类型注释没有被很好地记录。在编写定制类或高级数据结构时,可能很难弄清楚如何编写正确的类型。当您开始使用
typing模块时,这可能会很困难。 -
**类型不严格。**因为类型提示不严格,你不能保证一个变量是它的注释所声称的类型。在这种情况下,您并没有提高代码的质量。因此,编写正确的类型是每个开发人员的责任。
mypy可能是检查类型的一个解决方案。 -
**不支持第三方库。**当你在使用第三方库时,你可能会发现自己很焦虑,因为在很多情况下你不知道特定第三方工具的正确类型,比如数据结构或类。在这些情况下,你可能最终会使用任何一个。
mypy也不支持所有那些第三方库给你查。
注意
typing模块无疑是朝着正确方向迈出的一大步,但是typing模块可能还需要很多改进。不过,正确使用typing一定会帮你发现一些细微的 bug 和类型错误。将类型与类似mypy的工具一起使用肯定会有助于使您的代码更加整洁。
super()方法
super()方法语法现在更容易使用,可读性更好。您可以使用super()方法进行继承,方法如下:
class PaidStudent(Student):
def __int__(self):
super().__init__(self)
类型提示
正如我提到的,Python 有一个名为typing的新模块,它在代码中给你类型提示。
import typing
def subscribed_users(limit_of_users: int) -> Dict[str, int]:
...
使用 pathlib 实现更好的路径处理
pathlib是 Python 中的一个新模块,可以帮助你读取文件、连接路径、显示目录树和其他功能。
使用pathlib,一个文件路径可以由一个合适的Path对象来表示,然后你可以在那个Path对象上执行不同的操作。它具有查找最后修改的文件、创建唯一的文件名、显示目录树、计算文件数量、移动和删除文件、获取文件的特定组成部分以及创建路径的功能。
让我们看一个例子,其中的resolve()方法找到了文件的完整路径,如下所示:
import pathlib
path = pathlib.Path("error.txt")
path.resolve()
>>> PosixPath("/home/python/error.txt")
path.resolve().parent == pathlib.Path.cwd()
>>> False
print()现在是一个函数
print()现在是一个函数。在以前的版本中,它是一个语句。
-
老 :
print "Sum of two numbers is", 2 + 2 -
新 :
print("Sum of two number is", (2+2))
拳头
Python 引入了一种新的改进的写字符串的方法,叫做 f-string 。这使得代码比以前的版本(如% format和format方法)更具可读性。
user_id = "skpl"
amount = 50
f"{user_id} has paid amount: ${amount}"
>>> skpl has paid amount: $50
使用 f-string 的另一个原因是它比以前的版本更快。
根据人教版 498 2 :
F 字符串提供了一种在字符串中嵌入表达式的方法,使用了最少的语法。应该注意,f 字符串实际上是一个在运行时计算的表达式,而不是一个常数值。在 Python 源代码中,f-string 是一个文字字符串,前缀为 f,包含在大括号 内的表达式。表达式被替换为它们的值。
仅关键字参数
Python 现在允许使用*作为函数参数来定义只有关键字的参数。
def create_report(user, *, file_type, location):
...
create_report("skpl", file_type="txt", location="/user/skpl")
现在调用create_report的时候,要在*后面提供一个关键字参数。您可以强制其他开发人员使用位置参数来调用函数。
保持字典的顺序
现在字典保留了插入的顺序。以前,你必须使用OrderDict来做到这一点,但现在默认字典可以做到这一点。
population_raking = {}
population_raking["China"] = 1
population_raking["India"] = 2
population_raking["USA"] = 3
print(f"{population_raking}")
{'China': 1, 'India': 2, 'USA': 3}
可重复拆包
现在 Python 为您提供了迭代解包的灵活性。这是一个很酷的特性,可以迭代地解包变量。
*a, = [1] # a = [1]
(a, b), *c = 'PC', 5, 6 # a = "P", b = "C", c = [5, 6]
*a, = range(10)
查看官方 Python 文档,了解 Python 中更多的新特性。
摘要
本章重点介绍了新的主要特性,如asyncio和typing,以及次要特性,如 pathlib 和 order dictionary。然而,Python 版本 3 中还有许多其他令人兴奋的新特性。
查看所有改进的 Python 文档总是一个好的做法。Python 有很棒的文档,非常容易浏览,可以帮助你理解任何库、关键字或模块。我希望这一章给了你足够的动力,让你在现有的代码库或新项目中尝试这些特性。
Footnotes 1https://www.python.org/dev/peps/pep-0484/
2
https://www.python.org/dev/peps/pep-0498/
八、调试和测试 Python 代码
如果您正在编写代码,尤其是生产代码,那么代码具有良好的日志记录特性和测试用例非常重要。两者都确保您可以跟踪错误并修复出现的任何问题。Python 有一套丰富的内置库,用于调试和测试 Python 代码,我将在本章中介绍。
注意
与任何编程语言一样,Python 有很多工具可以在代码中添加日志和测试。在专业环境中,对这些工具有一个很好的理解是很重要的,因为在生产环境中运行软件可以为你赚钱。由于生产代码中的错误或缺陷而造成的损失对公司或产品来说是灾难性的。因此,在将代码投入生产之前,您需要进行日志记录和测试。拥有某种度量和性能跟踪工具也是有帮助的,这样你就可以了解当你的软件在现实世界中被数百万用户使用时会是什么样子。
排除故障
作为开发人员,调试是最重要的技能之一。大多数开发人员没有投入足够的精力去学习调试;他们通常只是在需要的时候尝试不同的事情。调试不应该是事后想起的过程;这是一种在对代码中的实际问题得出任何结论之前排除不同假设的技术。在本节中,您将探索调试 Python 代码的技术和工具。
调试工具
在本节中,我将介绍一下pdb、ipdb和pudb。
物理数据库
pdb是调试 Python 代码最有用的命令行工具之一。pdb提供堆栈信息和参数信息,并在pdb调试器中跳转代码命令。要在 Python 代码中设置调试器,您可以编写如下代码:
import pdb
pdb.set_trace()
一旦控制权到达启用了pdb调试器的那一行,您就可以使用pdb命令行选项来调试您的代码。pdb给你以下命令:
-
h:帮助命令 -
w:打印堆栈跟踪 -
d:向下移动当前帧数 -
u:向上移动当前帧数 -
s:执行当前行 -
n:继续执行直到下一行 -
unt [line number]:继续执行,直到一个行号 -
r:继续执行,直到当前功能返回
在pdb中还有其他命令行选项。你可以在 https://docs.python.org/3/library/pdb.html 查看全部。
ipdb
与pdb类似,ipdb是一个调试器命令行工具。它给了你和pdb一样的能力,额外的优势是你可以在 IPython 上使用ipdb。您可以添加ipdb调试器,如下所示:
import ipdb
ipdb.set_trace()
一旦安装完毕,您可以在ipdb中检查所有可用的命令。大多数情况下,这些与pdb相似,如下所示:
ipdb> ?
Documented commands (type help <topic>):
========================================
EOF bt cont enable jump pdef psource run unt
a c continue exit l pdoc q s until
alias cl d h list pfile quit step up
args clear debug help n pinfo r tbreak w
b commands disable ignore next pinfo2 restart u whatis
break condition down j p pp return unalias where
Miscellaneous help topics:
==========================
exec pdb
Undocumented commands:
======================
retval rv
你可以在 https://pypi.org/project/ipdb/ 找到更多关于ipdb的信息。
ipdb具有与pdb相同的命令行选项,如下所示:
-
h:帮助命令 -
w:打印堆栈跟踪 -
d:向下移动当前帧数 -
u:向上移动当前帧数 -
s:执行当前行 -
n:继续执行直到下一行 -
unt [line number]:继续执行,直到一个行号 -
r:继续执行,直到当前功能返回
pudn
pudb是一个功能丰富的调试工具,比pdb和ipdb功能更多。它是一个基于控制台的可视化调试器。您可以在编写代码时调试代码,而不是像使用pdb或ipdb那样跳到命令行。它看起来更像一个 GUI 调试器,但运行在控制台上,这使得它比 GUI 调试器更轻量级。
您可以通过添加以下代码行在代码中添加调试器:
import pudb
pudb.set_trace()
它有很好的文档。你可以在 https://documen.tician.de/pudb/starting.html 找到更多关于pudb的信息和它的所有特性。
在pudb调试界面中,可以使用以下按键:
-
n:执行下一条命令 -
s:单步执行一个函数 -
c:继续执行 -
b:在当前行设置断点 -
e:显示抛出异常的回溯 -
q:打开一个对话框,退出或重启正在运行的程序 -
o:显示原始控制台/标准输出画面 -
m:在不同的文件中打开一个模块 -
L:转到一行 -
!:转到屏幕底部的 Python 命令行子窗口 -
?:显示包含快捷命令完整列表的帮助对话框 -
<SHIFT+V>:将上下文切换到屏幕右侧的变量子窗口 -
<SHIFT+B>:将上下文切换到屏幕右侧的断点子窗口 -
<CTRL+X>:在代码行和 Python 命令行之间切换上下文
例如,一旦你进入pudb显示,按下b将在继续执行c快捷键后停止执行的那一行设置一个断点。一个有用的选项是设置一个可变条件,在该条件下应用断点。一旦条件满足,控制将在该点停止。
您还可以通过创建类似于~/.config/pudb/pudb.cfg的文件来配置pudb,如下所示:
[pudb]
breakpoints_weight = 0.5
current_stack_frame = top
custom_stringifier =
custom_theme =
display = auto
line_numbers = True
prompt_on_quit = True
seen_welcome = e027
shell = internal
sidebar_width = 0.75
stack_weight = 0.5
stringifier = str
theme = classic
variables_weight = 1.5
wrap_variables = True
断点
breakpoint是 Python 3.7 中引入的新关键字。它给你调试代码的能力。breakpoint类似于讨论的其他命令行工具。您可以编写如下代码:
x = 10
breakpoint()
y = 20
breakpoint也可以使用PYTHONBREAKPOINT环境变量进行配置,为调试器提供一个由breakpoint()函数调用的方法。这很有帮助,因为您可以轻松地更改调试器模块,而无需更改任何代码。举个例子,如果想禁用调试,可以使用PYTHONBREAKPOINT=0。
在生产代码中使用日志模块而不是打印
如前所述,日志是任何软件产品的重要组成部分,Python 有一个名为logging的库。日志记录还有助于您理解代码的流程。如果您有日志记录,它会通过提供堆栈跟踪让您知道问题出在哪里。您可以简单地通过如下方式导入库来使用logging库:
import logging
logging.getLogger(__name__).addHandler(logging.NullHandler())
logging库有五个标准级别,表示事件的严重程度。见表 8-1 。
表 8-1
记录标准级别
|水平
|
数值
|
| --- | --- |
| CRITICAL | Fifty |
| ERROR | Forty |
| WARNING | Thirty |
| INFO | Twenty |
| DEBUG | Ten |
| NOTSET | Zero |
因此,您可以编写类似于清单 8-1 的代码。
import logging
from logging.config import dictConfig
logging_config = dict(
version=1,
formatters={
'f': {'format':
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s'}
},
handlers={
'h': {'class': 'logging.StreamHandler',
'formatter': 'f',
'level': logging.DEBUG}
},
root={
'handlers': ['h'],
'level': logging.DEBUG,
},
)
dictConfig(logging_config)
logger = logging.getLogger()
logger.debug("This is debug logging")
Listing 8-1Logging Configuration
假设您想要捕获日志的整个堆栈跟踪;你可以做一些类似于清单 8-2 的事情。
import logging
a = 90
b = 0
try:
c = a / b
except Exception as e:
logging.error("Exception ", exc_info=True)
Listing 8-2Stack Trace Logging
日志记录中的类和函数
logging模块有许多类和函数,可用于定义您自己的日志记录类,并为您的特定需求和项目配置日志记录。
日志模块中定义的最常用的类如下:
-
Logger:这是logging模块的一部分,由应用直接调用以获取logger对象。它有许多方法,如下所示:-
setLevel:设置日志记录的级别。当记录器被创建时,它被设置为NOSET。 -
isEnableFor:该方法检查由logging.disable(level)设置的日志级别。 -
debug:在这个记录器上记录级别为DEBUG的消息。 -
info:在这个记录器上记录带有INFO的消息。 -
warning:在这个记录器上记录带有WARNING的消息。 -
error:在这个记录器上记录级别为ERROR的消息。 -
critical:在这个记录器上记录一个级别为CRITICAL的消息。 -
log:在该记录器上记录整数级别的消息。 -
exception:在这个记录器上记录一个级别为ERROR的消息。 -
addHandler:将指定的处理程序添加到该记录器中。
-
-
Handler:Handler是其他有用的处理程序类的基类,比如StreamHandler、FileHandler、SMTPHandler、HTTPHandler等等。这些子类将日志输出发送到相应的目的地,如sys.stdout或磁盘文件。-
createLock:这将初始化线程锁,该线程锁可用于串行化对底层 I/O 功能的访问。 -
setLevel:将处理器设置为一个级别。 -
flush:这确保记录输出已被刷新。 -
close:Handler的子类确保它从被覆盖的close()方法中被调用。 -
format:对输出日志进行格式化。 -
emit:实际上是记录指定的日志信息。
-
-
Formatter:这是通过指定一个列出输出应该包含的属性的字符串格式来指定输出格式的地方。-
format:格式化字符串。 -
formatTime:格式化时间。它与time.strftime()一起用于格式化记录的创建时间。默认值为'%Y-%m-%d %H:%M:%S, uuu',其中uuu以毫秒为单位。 -
formatException:格式化具体的异常信息。 -
formatStack:格式化字符串上的堆栈信息。
-
您还可以为正在运行的应用配置日志记录,如清单 8-3 所示。
[loggers]
keys=root,sampleLogger
[handlers]
keys=consoleHandler
[formatters]
keys=sampleFormatter
[logger_root]
level=DEBUG
handlers=consoleHandler
[logger_sampleLogger]
level=DEBUG
handlers=consoleHandler
qualname=sampleLogger
propagate=0
[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=sampleFormatter
args=(sys.stdout,)
[formatter_sampleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
Listing 8-3Logging Configuration File
现在您可以使用这个配置文件,如清单 8-4 所示。
import logging
import logging.config
logging.config.fileConfig(fname='logging.conf', disable_existing_loggers=False)
# Get the logger specified in the file
logger = logging.getLogger(__name__)
logger.debug('Debug logging message')
Listing 8-4Use Logging Configuration
这与清单 8-5 中所示的 YAML 文件的配置相同。
version: 1
formatters:
simple:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: simple
stream: ext://sys.stdout
loggers:
sampleLogger:
level: DEBUG
handlers: [console]
propagate: no
root:
level: DEBUG
handlers: [console]
Listing 8-5Logging Configuration in YAML
您可以阅读这个文件,如清单 8-6 所示。
import logging
import logging.config
import yaml
with open('logging.yaml', 'r') as f:
config = yaml.safe_load(f.read())
logging.config.dictConfig(config)
logger = logging.getLogger(__name__)
logger.debug('Debug logging message')
Listing 8-6Use Logging Configuration YAML File
你可以在 https://docs.python.org/3/library/logging.html 找到更多关于伐木的信息。
使用度量库来识别瓶颈
我见过很多开发人员不理解度量在产品代码中的价值。度量从代码中收集不同的数据点,例如代码特定部分的错误数量或第三方 API 的响应时间。还可以定义指标来捕获特定的数据点,比如当前登录到 web 应用的用户数量。通常按请求、每秒、每分钟或定期收集指标,以随时间监控系统。
有很多第三方应用用于收集生产代码的度量,比如 New Relic、Datadog 等等。您可以收集不同种类的指标。您可以将它们归类为性能指标或资源指标。性能指标可能如下:
-
吞吐量:这是单位时间内系统所做的工作量。
-
误差:这是单位时间内误差结果的数量或误差率。
-
性能:表示完成一个单位工作所需的时间。
除了这些点之外,还有几个数据点可以用来捕捉应用的性能。除了性能指标之外,还有资源指标等指标,您可以使用这些指标来获取资源指标,如下所示:
-
利用率:这是资源忙碌的时间百分比。
-
可用性:这是资源响应请求的时间。
在使用指标之前,请考虑要使用哪种数据点来跟踪您的应用。使用度量标准肯定会让你对你的应用更有信心,你可以测量你的应用性能。
IPython 是如何有用的
IPython 是 Python 的 REPL 工具。IPython 帮助您在命令行运行代码,并且无需太多配置就可以测试它。IPython 是一个真正聪明成熟的 REPL;它有很多功能,比如制表符补全和神奇的功能,比如%timeit、%run等等。您还可以获得历史记录,并在 IPython 中调试您的代码。有一些调试工具明确地工作在 IPython 上,比如ipdb。
IPython 的主要特性如下:
-
全面的对象内省
-
输入历史,它在会话中是持久的
-
使用自动生成的引用在会话期间缓存输出结果
-
可扩展的制表符补全,默认支持 Python 变量和关键字、文件名和函数关键字的补全
-
“神奇”命令的可扩展系统,用于控制环境和执行许多与 IPython 或操作系统相关的任务
-
一个丰富的配置系统,可以在不同的设置之间轻松切换(比每次改变
$PYTHONSTARTUP环境变量更简单) -
会话记录和重新加载
-
用于特殊用途情况的可扩展语法处理
-
通过用户可扩展的别名系统访问系统外壳
-
容易嵌入到其他 Python 程序和 GUI 中
-
对
pdb调试器和 Python 分析器的集成访问
命令行界面继承了前面列出的功能,并增加了以下内容:
-
真正的多行编辑得益于
prompt_toolkit -
键入时语法高亮显示
-
与命令行编辑器集成,实现更好的工作流程
当与兼容的前端一起使用时,内核允许以下功能:
-
可以创建 HTML、图像、LaTEX、声音和视频的丰富显示的对象
-
使用
ipywidgets包的交互式小部件
您可以按如下方式安装 IPython:
pip install ipython
IPython 的入门真的很容易;您只需键入命令ipython,您将进入ipython命令 shell,如下所示:
`Python 3.7.0
键入“版权”、“信用”或“许可证”了解更多信息
IPython 6 . 4 . 0——一种增强的交互式 Python。键入“?”寻求帮助。
[1]中的*:*
现在你可以像这样开始使用ipython命令:
In [1]: print("hello ipython")
你可以在 https://ipython.readthedocs.io/en/stable/interactive/index.html 找到更多关于 IPython 的信息。
测试
对于任何软件应用,拥有测试代码和拥有应用代码一样重要。测试确保您没有部署错误的代码。Python 有很多有用的库,使得编写不同类型的测试变得更加容易。
为什么测试很重要
测试和你的实际代码一样重要。测试确保运输代码按预期工作。您应该在开始编写应用代码的第一行时就开始编写测试代码。测试不应该是事后的想法,也不应该只是为了测试而测试。测试应该确保每段代码都产生预期的行为。
有几个原因让您应该考虑在软件开发生命周期中尽早编写测试。
-
为了确保你正在构建正确的东西,一旦你开始写代码,在你的软件生命周期中进行测试是很重要的。如果没有测试来检查预期的行为,就很难确保您处于正确的道路上。
-
您希望尽早发现任何重大变化。当您对代码的一部分进行更改时,很有可能会破坏代码的其他部分。您希望在早期而不是在投入生产后检测到代码中断。
-
测试在记录你的代码中也扮演了一个角色。测试是记录代码的一种非常有用的方法,不需要为代码的每一部分专门编写文档。
-
进行测试的另一个好处是让新开发人员加入进来。当一个新的开发人员加入团队时,他们可以通过运行和阅读测试来开始熟悉代码,这可以让您了解代码的流程。
如果您想确保您的代码如您所期望的那样工作,并且您的用户在使用软件时有一段美好的时光,那么您应该在您的产品代码中使用测试。
pytestvs . unittest
Python 有很多令人惊奇的测试库。Pytest 和 UnitTest 是两个最著名的库。在这一节中,您将看到这两个库之间的主要区别,这样您就可以决定使用哪个库来测试您的代码。
两者都是大众图书馆;然而,它们之间的多重差异使人们选择一个而不是另一个。在决定选择哪一个之前,让我们看看你想要考虑的一些主要特性。
Pytest 是第三方库,UnitTest 是 Python 中的内置库。要使用 Pytest,必须安装它,但这没什么大不了的。
pip install pytest
UnitTest 需要继承TestCase,需要有一个类来编写和运行测试。Pytest 在这方面更加灵活,因为您可以通过函数或类来编写测试。清单 8-7 显示了 UnitTest,而清单 8-8 显示了 Pytest。
import pytest
def test_simple():
assert 2 == 2
def test_tuple():
assert (1, 3, 4) == (1, 3, 4)
Listing 8-8Pytest Example 1
from unittest import TestCase
class SimpleTest(TestCase):
def test_simple(self):
self.assertTrue(True)
def test_tuple(self):
self.assertEqual((1, 3, 4), (1, 3, 4))
def test_str(self):
self.assertEqual('This is unit test', 'this is')
Listing 8-7UnitTest Example 1
您可能已经注意到,UnitTest 使用了TestCase实例方法;但是,Pytest 有一个内置的断言。Pytest 断言在不了解不同断言方法的情况下更容易阅读。然而,UnitTest 断言更具可配置性,并且有更多的方法可以断言。
在 https://docs.python.org/3/library/unittest.html#assert-methods 可以看到 UnitTest 的所有断言方法,在 https://docs.pytest.org/en/latest/reference.html 可以看到 Pytest 的所有断言方法。
清单 8-9 显示 UnitTest,清单 8-10 显示 Pytest。
import pytest
def not_equal():
assert 2 != 2
def assert_false():
x = 0
assert x is 0
def assert_in():
assert 5 in [1, 3, 8, 5]
Listing 8-10Pytest Example 2
from unittest import TestCase
class SimpleTest(TestCase):
def not_equal(self):
self.assertNotEqual(2, 3) # 2 != 3
def assert_false(self):
x = 0
self.assertFalse(x) # bool(x) is false
def assert_in(self):
self.assertIn(5, [1, 3, 8, 5]) # 5 in [1, 3, 8, 5]
Listing 8-9UnitTest Example 2
您可能会注意到,Pytest 比 UnitTest 更容易断言。与 UnitTest 相比,Pytest 的可读性更强。
Pytest 用代码片段突出错误,而 UnitTest 没有那个特性;它显示了一行错误,没有突出显示。这在未来的版本中可能会改变,但是目前 Pytest 有更好的错误报告。清单 8-11 显示了 Pytest 控制台输出,而清单 8-12 显示了 UnitTest 控制台输出。
Traceback (most recent call last):
File "~<stdin>~", line 11, in simple.py
ZeroDivisionError: integer division or modulo by zero
Listing 8-12UnitTest Console Output
>>> py.test simple.py
============================= test session starts =============
platform darwin -- Python 3.7.0 -- py-1.4.20 -- pytest-2.5.2
plugins: cache, cov, pep8, xdist
collected 2 items
simple.py .F
=================================== FAILURES =================
___________________________________ test_simple_______________
def test_simple():
print("This test should fail")
> assert False
E assert False
simple.py:7: AssertionError
------------------------------- Captured stdout ---------------
This test should fail
====================== 1 failed, 1 passed in 0.04 seconds ====
Listing 8-11Pytest Console Output
Pytest 有类似于fixture的设置方法,您可以为模块、会话和函数进行配置。UnitTest 有方法setUp和tearDown。清单 8-13 显示了 Pytest 夹具,而清单 8-14 显示了 UnitTest 夹具。
from unittest import TestCase
class SetupBaseTestCase(TestCase):
def setUp(self):
self.sess = CallClassBeforeStartingTest()
def test_simple():
self.sess.call_function()
def tearDown(self):
self.sess.close()
Listing 8-14UnitTest Tests Using Setup and Teardown
import pytest
@pytest.fixture
def get_instance():
s = CallClassBeforeStartingTest()
s.call_function()
return s
@pytest.fixture(scope='session')
def test_data():
return {"test_data": "This is test data which will be use in different test methods"}
def test_simple(test_data, get_instance):
assert test_instance.call_another_function(test_data) is not None
Listing 8-13Pytest Fixture
您会注意到,Pytest 和 UnitTest 处理测试设置的方式不同。这是 Pytest 和 UnitTest 之间的一些主要区别。然而,两者都是功能丰富的工具。
我通常更喜欢使用 Pytest,因为它易于使用和阅读。但是,如果您习惯使用 UnitTest,请不要觉得必须使用 Pytest。用你觉得舒服的任何东西。选择测试工具是次要的;主要目标应该是对你的代码进行良好的测试!
性能测试
属性测试是测试函数的一种方法,在这种情况下,你需要提供大量的输入。你可以在 https://hypothesis.works/articles/what-is-property-based-testing/ 了解更多信息。
Python 有一个名为hypothesis的库,非常适合编写属性测试。hypothesis很好用,如果熟悉 Pytest,就更容易了。
您可以按如下方式安装hypothesis:
pip install hypothesis
您可以看到一个使用hypothesis进行属性测试的例子,如清单 8-15 所示。
from hypothesis import given
from hypothesis.strategies import text
@given(text())
def test_decode_inverts_encode(s):
assert decode(encode(s)) == s
Listing 8-15Property Testing
在这里,hypothesis提供了各种文本来测试函数test_decode_inverts_encode,而不是您提供那组数据来解码文本。
如何创建测试报告
有很多工具可以生成测试报告。事实上,Pytest 和 UnitTest 都会这样做。测试报告有助于理解测试结果,也有助于跟踪测试覆盖率的进展。然而,这里我严格地说的是测试报告的生成。
当您运行一个测试时,报告生成可以为您提供运行一个测试的完整概述,以及通过/失败的结果。您可以使用以下工具之一来完成此操作:
pip install pytest-html
pytest -v tests.py --html=pytest_report.html --self-contained-html
一个叫做nose的工具有内置的报告生成工具。如果您正在使用nose,您可以通过运行如下命令来生成测试:
nosetests -with-coverage --cover-html
有了 UnitTest,你可以使用TextTestRunner,如清单 8-16 所示。
class TestBasic(unittest.TestCase):
def setUp(self):
# set up in here
class TestA(TestBasic):
def first_test(self):
self.assertEqual(10,10)
def second_test(self):
self.assertEqual(10,5)
Listing 8-16UnitTest with TextTestRunner Part 1
让我们假设您有前面的测试要运行。UnitTest 为您提供了一个名为TextTestRunner的方法来生成测试报告,如清单 8-17 所示。
import test
test_suite = unittest.TestLoader().loadTestFromModule(test)
test_results = unittest.TextTestRunner(verbosity=2).run(test_suite)
Listing 8-17UnitTest with TextTestRunner Part 2
如果您运行这段代码,它将为TestBasic类生成报告。
除了这里讨论的工具之外,还有大量的 Python 第三方库,它们在生成报告的方式方面提供了很大的灵活性,它们是非常强大的工具。
自动化单元测试
自动化单元测试意味着单元测试无需您启动即可运行。能够在与主代码或主要代码合并的同时运行单元测试意味着您可以确保新的更改不会破坏任何现有的特性或功能。
正如我已经讨论过的,对任何代码库进行单元测试都是非常重要的,并且您会希望使用某种 CI/CD 流来运行它们。这还假设您正在使用某种版本控制(如 Git)或第三方工具(如 GitHub 或 GitLab)来存储代码。
运行测试的理想流程如下:
-
使用版本控制提交更改。
-
将更改推送到某种版本控制中。
-
使用像 Travis 这样的第三方工具从版本控制中触发单元测试,Travis 会自动运行测试并将结果发送到版本控制。
-
在测试通过之前,版本控制不应该允许合并到主版本。
让您的代码准备好投入生产
在投入生产之前,有一些事情是很重要的,以确保交付的代码是高质量的,并按预期工作。在将变更或新代码部署到产品中之前,每个团队或公司都有不同的步骤。我不会讨论任何一个部署到生产中的理想过程。然而,您可以在当前的部署管道中引入一些东西,以使您的 Python 代码更好,并且在生产中不容易出错。
在 Python 中运行单元和集成测试
正如已经提到的,进行单元测试是很重要的。除了单元测试之外,进行集成测试也有很大的帮助,尤其是如果你在代码库中有很多可移动的部分。
众所周知,单元测试有助于检查特定的代码单元,并确保该代码单元正常工作。对于集成测试,重要的是测试代码的一部分是否与代码的另一部分一起工作而没有任何错误。集成测试帮助您检查代码是否作为一个整体工作。
使用林挺使代码一致
代码分析器分析你的源代码中潜在的错误。Linters 解决了代码中的以下问题:
-
句法误差
-
像使用未定义变量这样的结构性问题
-
违反代码风格指南
代码林挺给你的信息可以很容易地浏览。这对于代码非常有用,尤其是对于一个大项目,当有大量移动代码,并且所有从事代码工作的开发人员都可以就特定的代码风格达成一致时。
有很多 Python 林挺代码。您应该使用哪种类型取决于您或您的开发团队。
使用林挺有很多好处。
-
通过对照编码标准检查代码,它可以帮助您写出更好的代码。
-
它可以防止您犯明显的错误,比如语法错误、打字错误、格式错误、样式不正确等等。
-
它节省了开发人员的时间。
-
它帮助所有开发人员就特定的代码标准达成一致。
-
它非常易于使用和配置。
-
很容易设置。
让我们看看 Python 中一些流行的林挺工具。如果您正在使用现代的 IDE 工具,如 VSCode、Sublime 或 PyCharm,您会发现这些工具已经提供了某种类型的林挺。
薄片 8
flake8是最流行的林挺工具之一。它是pep8、pyflakes和循环复杂性的包装器。它的误报率很低。
您可以使用以下命令轻松设置它:
pip install flake8
派林特
对林挺来说是另一个不错的选择。与flake8相比,它需要更多的设置,并给出更多的误报,但是如果你需要对你的代码进行更严格的林挺检查,pylint可能是你的合适工具。
使用代码覆盖率来检查测试
代码覆盖是一个过程,在这个过程中,您检查许多为代码编写的测试(或者更准确地说,被不同测试触及的代码)。代码覆盖率确保您有足够的测试来确定代码的质量。代码覆盖率应该是你的软件开发生命周期的一部分;它不断提高你的代码的质量标准。
Python 有一个名为 Coverage.py 的工具,这是一个检查测试覆盖率的第三方工具。您可以按如下方式安装它:
pip install coverage
在安装 Coverage.py 时,一个名为coverage的 Python 脚本被放在您的 Python 脚本目录中。Coverage.py 有许多决定所执行操作的命令。
-
run:运行 Python 程序,收集执行数据 -
report:报告覆盖结果 -
html:生成带有覆盖结果的带注释的 HTML 列表 -
xml:生成一个包含覆盖率结果的 XML 报告 -
annotate:用覆盖率结果注释源文件 -
erase:删除之前收集的覆盖数据 -
combine:合并多个数据文件 -
debug:获取诊断信息
您可以按如下方式运行覆盖率报告:
coverage run -m packagename.modulename arg1 arg2
还有其他工具可以直接与版本控制系统集成,比如 GitHub。这些工具对于更大的团队来说可能更方便,因为检查可以在新代码提交审查后立即运行。将代码覆盖作为软件生命周期的一部分,可以确保您不会在产品代码上冒任何风险。
将 virtualenv 用于您的项目
virtualenv是应该成为每个开发人员工具链一部分的工具之一。您可以使用它来创建独立的 Python 环境。当您安装virtualenv并为您的项目创建一个环境时,virtualenv会创建一个包含您的项目需要运行的所有可执行文件的文件夹。
您可以按如下方式安装 virtualenv:
pip install virtualenv
我建议在这里寻找更多关于 virtualenv 的信息:
https://docs.python-guide.org/dev/virtualenvs/
摘要
对于任何生产代码,拥有帮助您调试和更好地监控代码的工具是非常重要的。正如您在本章中所了解到的,Python 有很多工具可以让您在将代码部署到生产环境之前更好地准备代码。这些工具不仅帮助您在数百万用户使用您的应用时保持理智,还帮助您维护您的代码以供长期使用。确保您的应用利用了这些工具,因为从长远来看,投资这些工具肯定会有回报。在生产中部署应用时,拥有正确的过程与构建新功能一样重要,因为这将确保您的应用是高质量的。