Python 高手编程系列四百零二:处理错误与速率限制

在处理这些问题时,你可能会遇到的最后一个问题是外部服务提供商施加的速率限制。以
使用 Google Maps API 为例,在撰写本书时,免费和未经身份验证的请求的官方费率限制为每
秒 10 个请求和每天 2,500 个请求。当使用多线程时,很容易耗尽这样的限制。更严重的问题
是,因为我们没有覆盖任何故障的场景,而处理多线程 Python 代码中的异常比平常更复杂。
当客户端超过 Google 的速率时,api.geocode()函数将抛出异常,这是个好消息。
但是这个异常是单独引发的,不会导致整个程序崩溃。工作线程当然会立即退出,但是主线
程将等待 work_queue 上存储的所有任务完成(使用 work_queue.join()调用)。这意味
着我们的工作线程应该优雅地处理可能的异常,并确保队列中的所有项目都被处理。如果不
做进一步的改进,我们可能会遇到一些情况,一些工作线程崩溃,程序永远不会退出。
让我们对我们的代码进行一些小的改动,以便为可能出现的任何问题做好准备。在工
作线程中的异常情况下,我们可以在 results_queue 队列中放置一个错误实例,并将当
前任务标记为完成,与没有错误时一样。这样,我们确保主线程在 work_queue.join()
中等待时不会无限期地锁定。主线程然后可以检查结果并重新提出在结果队列中发现的任
何异常。下面是可以以更安全的方式处理异常的 worker()和 main()函数的改进版本:
def worker(work_queue, results_queue):
while True:
try:
item = work_queue.get(block=False)
except Empty:
break
else:
try:
result = fetch_place(item)
except Exception as err:
results_queue.put(err)
else:
results_queue.put(result)
finally:
work_queue.task_done()
def main():
work_queue = Queue()
results_queue = Queue()
for place in PLACES:
work_queue.put(place)
threads = [
Thread(target=worker, args=(work_queue, results_queue))
for _ in range(THREAD_POOL_SIZE)
]
for thread in threads:
thread.start()
work_queue.join()
while threads:
threads.pop().join()
while not results_queue.empty():
result = results_queue.get()
if isinstance(result, Exception):
raise result
present_result(result)
当我们准备好处理异常时,是时候打破我们的代码并超过速率限制。我们可以通过修改
一些初始条件轻松地做到这一点。我们可以增加地理编码的位数和线程池的大小如下所示:
PLACES = (
‘Reykjavik’, ‘Vien’, ‘Zadar’, ‘Venice’,
‘Wrocław’, ‘Bolognia’, ‘Berlin’, ‘Słubice’,
‘New York’, ‘Dehli’,
) * 10
THREAD_POOL_SIZE = 10
如果你的执行环境足够快,你应该很快就会得到类似的错误如下:
$ python3 threadpool_with_errors.py
New York, NY, USA, 40.71, -74.01
Berlin, Germany, 52.52, 13.40
Wrocław, Poland, 51.11, 17.04
Zadar, Croatia, 44.12, 15.23
Vienna, Austria, 48.21, 16.37
Bologna, Italy, 44.49, 11.34
Reykjavík, Iceland, 64.13, -21.82
Venice, Italy, 45.44, 12.32
Dehli, Gujarat, India, 21.57, 73.22
Slubice, Poland, 52.35, 14.56
Vienna, Austria, 48.21, 16.37
Zadar, Croatia, 44.12, 15.23
Venice, Italy, 45.44, 12.32
Reykjavík, Iceland, 64.13, -21.82
Traceback (most recent call last):
File “threadpool_with_errors.py”, line 83, in
main()
File “threadpool_with_errors.py”, line 76, in main
raise result
File “threadpool_with_errors.py”, line 43, in worker
result = fetch_place(item)
File “threadpool_with_errors.py”, line 23, in fetch_place
return api.geocode(place)[0]
File “…\site-packages\gmaps\geocoding.py”, line 37, in geocode
return self._make_request(self.GEOCODE_URL, parameters, “results”)
File “…\site-packages\gmaps\client.py”, line 89, in _make_request
)(response)
gmaps.errors.RateLimitExceeded: {‘status’: ‘OVER_QUERY_LIMIT’, ‘results’: [],
‘error_message’: ‘You have exceeded your rate-limit for this API.’, ‘url’:
‘https://maps.googleapis.com/maps/api/geocode/json?address=Wroc%C5%82aw&sens
or=false’}
前面的异常当然不是错误代码的结果。对于这个免费的服务,这个程序有点过快。它
产生了太多的并发请求,为了正常工作,我们需要一种方法来限制它们的速率。
对工作速度的限制通常被称为节流。PyPI 上有几个包,可以限制任何类型的工作的速
率,并且易于使用。但是我们不会在这里使用任何外部代码。节流是一个很好的用于介绍
一些线程的锁定原语的机会,所以我们将尝试从头开始构建一个解决方案。
我们将使用的算法有时被称为令牌桶(token bucket),并且非常简单。
• 存在具有预定量的令牌的桶。
• 每个令牌响应单个权限以处理一项工作。
• 每次工作者要求一个或多个令牌(权限)时:
○ 我们测量从上次我们重新装满桶所花费的时间;
○ 如果时间差允许它,我们用对这个时间差响应的令牌量重新填充桶;
○ 如果存储的令牌的数量大于或等于请求的数量,我们减少存储的令牌的数量并
返回那个值;
○ 如果存储的令牌的数量小于请求的数量,我们返回零。
两个重要的事情是总是用零令牌来初始化令牌桶,并且从不允许它用根据我们的标准
量化时间以令牌表示的更多的令牌来填充令牌桶。如果我们不遵守这些预防措施,我们可
以释放超过速率限制的令牌。因为在我们的情况下,速率限制以每秒的请求数表示,我们
不需要处理任意时间。我们假设我们测量的基础是一秒钟,因此我们永远不会存储更多的
令牌比允许的那个时间量的请求数。下面是允许使用令牌桶算法进行调节的类的示例实现:
from threading import Lock
class Throttle:
def init(self, rate):
self._consume_lock = Lock()
self.rate = rate
self.tokens = 0
self.last = 0
def consume(self, amount=1):
with self._consume_lock:
now = time.time()

时间测量在第一令牌请求上初始化以避免初始突发

if self.last == 0:
self.last = now
elapsed = now - self.last

请确保传递时间的量足够大以添加新的令牌

if int(elapsed * self.rate):
self.tokens += int(elapsed * self.rate)
self.last = now

不要过度填满桶

self.tokens = (
self.rate
if self.tokens > self.rate
else self.tokens
)

如果可用最终分派令牌

if self.tokens >= amount:
self.tokens -= amount
else:
amount = 0
return amount
这个类的用法很简单。假设我们在主线程中只创建了一个 Throttle 实例(例如
Throttle(10)),并将其作为位置参数传递给每个工作线程。在不同线程中使用相同的数
据结构是安全的,因为我们使用来自 threading 模块的 Lock 类的实例防止其内部状态
的操作。我们现在可以更新 worker()函数实现,等待每个项目,直到 throttle 释放一
个新的令牌如下所示:
def worker(work_queue, results_queue, throttle):
while True:
try:
item = work_queue.get(block=False)
except Empty:
break
else:
while not throttle.consume():
pass
try:
result = fetch_place(item)
except Exception as err:
results_queue.put(err)
else:
results_queue.put(result)
finally:
work_queue.task_done()

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值