け日記

最近はPythonでいろいろやってます

PythonでRedisを参照・更新する

仕事でPythonアプリケーションからアクセスするRedisの導入を検討した際に、redis-pyでRedisを参照・更新する方法について調べましたので、備忘録にしておきます。

redis-pyのドキュメントはこちらです。
http://redis-py.readthedocs.io/

DockerでRedisコンテナを起動

実験用の環境をDockerで立ち上げ、localhost:6379で公開します。

$ docker run --name redis -d -p 6379:6379 redis redis-server --appendonly yes
$ docker ps
CONTAINER ID        IMAGE                          COMMAND                  CREATED              STATUS              PORTS                    NAMES
0481cc795f8f        redis                          "docker-entrypoint.s…"   About a minute ago   Up About a minute   0.0.0.0:6379->6379/tcp   redis

インストール

いつもどおりpipでインストールします。

$ pip install redis
$ pip list | grep redis
redis              2.10.6

接続

StrictRedisクラスを生成することでRedisとのコネクションを張ります。
なお、Redisクラスというのもあり、これでも接続できますが、後方互換性維持のために残されているインタフェースなので、新しく作るアプリケーションではStrictRedisを使ったほうが良さそうです。

import redis

# ホスト、ポート、DB番号を指定
conn = redis.StrictRedis(host='localhost', port=6379, db=0)

ConnectionPoolクラスでコネクションプールも作れます。
こちら↓の方の投稿では、接続まで倍くらい早いようです。

PythonでRedisを扱う(redis-pyの基本) - [Dd]enzow(ill)? with DB and Python

redis_pool = redis.ConnectionPool(host='localhost', port=6379, db=0, max_connections=4)
conn = redis.StrictRedis(connection_pool=redis_pool)

文字列値のset/get

まずはkey-valueのvalueが単一の文字列の場合を見ていきます。

基本は、StrictRedisクラスのset(key, value)で値の更新、get(key)で値の参照となります。

# 値のset
conn.set('key01', 'value01')  # 成功するとTrueが返る

# 値のget
value = conn.get('key01')  # バイナリ値で取得

print(type(value))
# <class 'bytes'>

print(str(value, encoding='utf-8'))  # str()で変換
# 'value01'

存在しないキーでgetするとNoneが返されます。

print(conn.get('keyXX'))
# None

また、存在しているキーでsetすると上書きされます。

conn.set('key01', 'new value')
conn.get('key01')
# b'new value'

なお、キーの削除はdelete()

conn.delete('key01')  # 返り値は削除したキー数
conn.delete('key02', 'key03')  # 複数同時削除も可能

有効期限

キーに有効期限を設定することもできます。
引数exに秒数を指定すると、その秒数経過後にgetしても値は取り出せなくなります。ミリ秒で指定する場合は引数pxを使います。

conn.set('key02', 'value02', ex=10)

print(conn.get('key02'))
# b'value02'

# 10秒後に再度get
print(conn.get('key02'))
# None

set後に有効期限を更新する場合、expire(key, seconds)で、有効期限を秒数で設定することができます。また、expireat(key, when)ならdatetime型の値を渡すことで"いつ無効にするのか"といったこともできます。

import datetime

# "key03"を10秒後に削除
conn.expire('key03', 10)

# "key04"を2018/6/3 17時に削除
conn.set('key04', 'value04')
conn.set('key04', datetime.datetime(2018, 6, 3, 17))

リスト

valueに使えるのは単一の値だけではありません。複数の値を関連付けられるvalueの型として、リスト、ハッシュ、セットというのが提供されています。

リスト型は、順序を持つ連結リストです。左端(先頭)または右端(末尾)から値を挿入・削除できます。

  • lpush / rpush : で値を追加
  • lrange : インデックスで値を参照 (参照された値は削除されない)
  • lpop / rpop : 単一の値を参照 (参照された値は削除される)
# 末尾へ"A", "B", "C"の順で追加
conn.rpush('key01', 'A')  # 1
conn.rpush('key01', 'B')  # 2
conn.rpush('key01', 'C')  # 3

# 先頭へ"Z"を追加
conn.lpush('key01', 'Z')  # 4

# 先頭から2つを参照
conn.lrange('key01', 0, 1)  # [b'Z', b'A']
# 先頭から4つを参照
conn.lrange('key01', 0, 3) # [b'Z', b'A', b'B', b'C']

# 先頭から1つを取り出す (取り出された"Z"は削除されます)
conn.lpop('key01')  # b'Z'
conn.lrange('key01', 0, 2)  # [b'A', b'B', b'C']

# 末尾から1つを取り出す (取り出された"C"は削除されます)
conn.rpop('key01')  # b'C'
conn.lrange('key01', 0, 1)  # [b'A', b'B']

ハッシュ

値が(Pythonで言うところの)ディクショナリ型となっている、ハッシュ型という型もあります。

幅(width)と高さ(height)を持つ長方形を格納する場合を考えてみましょう。

  • hset : ハッシュ型の追加
  • hget : ハッシュ型の値の取得
  • hgetall : キーに紐づく全ての値をdict型で
  • hmset : ハッシュ型の値をdict型で一括追加
# ハッシュ型の追加
conn.hset('rect1', 'width', '10.0')
conn.hset('rect1', 'height', '15.0')

# ハッシュ型の参照
conn.hget('rect1', 'width')  # b'10.0'
# キーに紐づくすべての値をdict型で返す
conn.hgetall('rect1')  # {b'width': b'10.0', b'height': b'15.0'}

# dict型で追加
conn.hmset('rect2', {'width':'7.5', 'height':'12.5'})
conn.hgetall('rect2')  # {b'width': b'7.5', b'height': b'12.5'}

セット

集合内に重複した値を持たないのがセット型です。Pythonのset型の値と対応します。

  • sadd : セット型の値の追加
  • smembers : セット型の値の参照で、set型で返される
  • sintersunionなどの集合演算が使える
# セット型の追加
conn.sadd('key01', 'A')

# セット型の参照
conn.smembers('key01')  # {b'A'}

conn.sadd('key01', 'B')
conn.sadd('key01', 'C')
conn.sadd('key01', 'A')  # 重複キーでsaddした場合、返り値は0

# 重複する"A"は1つになる
conn.smembers('key01')  # {b'A', b'C', b'B'}

conn.sadd('key02', 'B', 'D', 'B', 'A')
conn.smembers('key02')  # {b'D', b'A', b'B'}

# 積集合
conn.sinter('key01', 'key02')  # {b'A', b'B'}

# 和集合
conn.sunion('key01', 'key02')  # {b'D', b'A', b'C', b'B'}

ソート済みセット

上述のセットは順序を持ちませんが、ソート済みセットはある浮動小数(スコアと呼ばれる)によってソートされたセットです。

例えば、4人の生徒の数学のテストの点数をソート済みセットで表現してみます。

  • zadd : ソート済みセットの追加
  • zrange : ソート済みセットの取得
    • 引数withscoresでスコア付きのタプルリストが返る
    • 引数descをTrueにすると降順ソート(デフォルトは昇順)
# ソート済みセットの追加
conn.zadd('math', 75.0, 'Tanaka')

# ソート済みセットの取得
conn.zrange('math', 0, 1)  # [b'Tanaka']
# スコア付き
conn.zrange('math', 0, 0, withscores=True)  # [(b'Tanaka', 75.0)]

conn.zadd('math', 95.0, 'Suzuki', 60.0, 'Sato', 75.0, 'Komeda')
conn.zcard('math')  # 4

# 昇順
conn.zrange('math', 0, 3, withscores=True)  # [(b'Sato', 60.0), (b'Komeda', 75.0), (b'Tanaka', 75.0), (b'Suzuki', 95.0)]

# 降順
conn.zrange('math', 0, 3, desc=True, withscores=True)  # [(b'Suzuki', 95.0), (b'Tanaka', 75.0), (b'Komeda', 75.0), (b'Sato', 60.0)]

# 降順でトップ3つ
conn.zrange('math', 0, 2, desc=True, withscores=True)  # [(b'Suzuki', 95.0), (b'Tanaka', 75.0), (b'Komeda', 75.0)]

おわりに

redis-pyを使って、PythonからRedisをCRUDする方法についてまとめました。
ここで紹介した以外にも、ビット配列型やインクリメンタルに参照するスキャンなどもredis-pyで扱えますので、ドキュメントを参考にしてみてください。