示例:构建数据库迭代器

SCAN 命令虽然可以以迭代的形式访问数据库,但它使用起来并不是特别方便,比如说:

  • SCAN 命令每次迭代都会返回一个游标,而用户需要手动地将这个游标用作下次迭代时的输入参数,如果用户不小心丢失或者弄错了这个游标的话,那么就可能会给迭代带来错误或者麻烦。

  • SCAN 命令每次都会返回一个包含两个元素的结果,其中第一个元素为游标,而第二个元素才是当前被迭代的键,如果迭代器能够直接返回被迭代的键,那么它使用起来就会更加方便。

为了解决以上这两个问题,我们可以在 SCAN 命令的基础上进行一些修改,实现出代码清单 11-1 所示的迭代器:这个迭代器不仅会自动记录每次迭代的游标以防丢失,它还可以直接返回被迭代的数据库键以供用户使用。


代码清单 11-1 数据库迭代器:/database/db_iterator.py

  1. class DbIterator:
  2.  
  3. def __init__(self, client, match=None, count=None):
  4. """
  5. 创建一个新的迭代器。
  6. 可选的 match 参数用于指定迭代的匹配模式,
  7. 而可选的 count 参数则用于指定我们期待每次迭代能够返回的键数量。
  8. """
  9. self.client = client
  10. self.match = match
  11. self.count = count
  12. # 当前迭代游标
  13. self.current_cursor = 0
  14. # 记录迭代是否已经完成的状态变量
  15. self.iteration_is_over = False
  16.  
  17. def next(self):
  18. """
  19. 以列表形式返回当前被迭代到的数据库键,
  20. 返回 None 则表示本次迭代已经完成。
  21. """
  22. if self.iteration_is_over:
  23. return None
  24. # 获取下次迭代的游标以及当前被迭代的数据库键
  25. next_cursor, keys = self.client.scan(self.current_cursor, self.match, self.count)
  26. # 如果下次迭代的游标为 0 ,那么表示迭代已完成
  27. if next_cursor == 0:
  28. self.iteration_is_over = True
  29. # 更新游标
  30. self.current_cursor = next_cursor
  31. # 返回当前被迭代的数据库键
  32. return keys

作为例子,以下代码展示了如何使用这个迭代器去迭代一个数据库:

  1. >>> from redis import Redis
  2. >>> from db_iterator import DbIterator
  3. >>> client = Redis(decode_responses=True)
  4. >>> for i in range(50): # 向数据库插入 50 个键
  5. ... key = "key{0}".format(i)
  6. ... value = i
  7. ... client.set(key, value)
  8. ...
  9. True
  10. True
  11. ...
  12. True
  13. >>> iterator = DbIterator(client)
  14. >>> iterator.next() # 开始迭代
  15. ['key46', 'key1', 'key27', 'key39', 'key15', 'key0', 'key43', 'key12', 'key49', 'key41', 'key10']
  16. >>> iterator.next()
  17. ['key23', 'key7', 'key9', 'key20', 'key18', 'key3', 'key5', 'key34', 'key32', 'key40']
  18. >>> iterator.next()
  19. ['key4', 'key33', 'key30', 'key45', 'key38', 'key31', 'key6', 'key16', 'key25', 'key14', 'key13']
  20. >>> iterator.next()
  21. ['key29', 'key2', 'key42', 'key11', 'key48', 'key28', 'key8', 'key44', 'key21', 'key26']
  22. >>> iterator.next()
  23. ['key22', 'key47', 'key36', 'key17', 'key19', 'key24', 'key35', 'key37']
  24. >>> iterator.next() # 迭代结束
  25. >>>

注解

redis-py 提供的迭代器

实际上,redis-py 客户端也为 SCAN 命令实现了一个迭代器 ——用户只需要调用 redis-py 的 scan_iter() 方法,就会得到一个 Python 迭代器,然后就可以通过这个迭代器对数据库中的键进行迭代:

  1. scan_iter(self, match=None, count=None) unbound redis.client.Redis method
  2. Make an iterator using the SCAN command so that the client doesn't
  3. need to remember the cursor position.
  4.  
  5. ``match`` allows for filtering the keys by pattern
  6.  
  7. ``count`` allows for hint the minimum number of returns

redis-py 提供的迭代器跟 DbIterator 一样,都可以让用户免去手动输入游标的麻烦,但它们之间也有不少区别:

  • redis-py 的迭代器每次迭代只返回一个元素。

  • 因为 redis-py 的迭代器是通过 Python 的迭代器特性实现的,所以用户可以直接以 for key in redis.scan_iter() 的形式进行迭代。(DbIterator 实际上也可以实现这样的特性,但是由于 Python 迭代器的相关知识并不在本书的介绍范围之内,所以我们这个自制的迭代器才没有配备这一特性。)

  • redis-py 的迭代器也拥有 next() 方法,但这个方法每次被调用时只会返回单个元素,并且它在所有元素都被迭代完毕时将抛出一个 StopIteration 异常。

以下是一个 redis-py 迭代器的使用示例:

  1. >>> from redis import Redis
  2. >>> client = Redis(decode_responses=True)
  3. >>> client.mset({"k1":"v1", "k2":"v2", "k3":"v3"})
  4. True
  5. >>> for key in client.scan_iter():
  6. ... print(key)
  7. ...
  8. k1
  9. k3
  10. k2

因为 redis-py 为 scan_iter() 提供了直接支持,它比需要额外引入的 DbIterator 更为方便一些,所以本书之后展示的所有迭代程序都将使用 scan_iter() 而不是 DbIterator 。不过由于这两个迭代器的底层实现是相仿的,所以使用哪个其实差别都并不大。