(この記事は私の blog の http://umezawa.dyndns.info/wordpress/?p=7317 の転載です)
また Python+MySQL ネタ。
前置き
カーソルを使ったら閉じないといけません。処理後にその流れでそのまま cursor.close()
を呼ぶように書くと例外が飛んだり長い処理の途中でうっかり return
したりしたときに呼び漏れが発生するので、try
~finally
のような構造を使いたいところです。(Java でもたぶんそう(20年前の知識)。C++ だとデストラクタが使えるので違う書き方になる)
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM table")
rows = cursor.fetchall()
...
finally:
cursor.close()
カーソルと with
文
ところで、Python には with
文というのがあって、 with
文の中のブロックを(抜け方にかかわらず)抜けたときに一定の終了処理を自動で実行することができます。PyMySQL と mysqlclient ではカーソルに対してこれを使うことができます。
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM table")
rows = cursor.fetchall()
...
# with から抜けるときに cursor.close() が自動的に呼ばれる
残念なことに MySQL Connector/Python だとこの書式は使えません。with
文は with
の後に来るオブジェクトの __enter__
を呼んで with
を抜けるときに __exit__
を呼ぶという仕組みなのですが、カーソルにはこれらのマジックメソッドが実装されていないからです。
Traceback (most recent call last):
File "./test_cursor_with.py", line 29, in <module>
with conn.cursor() as cur:
AttributeError: __enter__
ただし、 with
を抜けるときに close()
を呼んでくれるようになる closing
というラッパーメソッドが Python 標準の contextlib
モジュールの中にあるので、自前でラッパーを書いたりする必要はありません。
from contextlib import closing
with closing(conn.cursor()) as cursor:
cursor.execute("SELECT * FROM table")
rows = cursor.fetchall()
...
コネクションの場合
さて、カーソルを with
で使えるならコネクションでも使ってみようかと思うわけですが、何も考えずに with
に渡すとおかしな挙動になります。(commit
や rollback
についてはとりあえず考えないことにします)
with pymysql.connect(**mysql_kwargs) as conn:
print(conn.__class__.__module__ + "." + conn.__class__.__name__)
pymysql.cursors.Cursor
コネクションが来ることを期待していたのですが、何故かカーソルになって渡ってきます。 mysqlclient でも同じ。なんでやねん。
コネクション型である pymysql.connections.Connection
の定義を見てもそもそも __enter__
とかは定義されていないのでエラーになるのが期待されると思うのですが、なんでこんな挙動になるのか分かりません。うーん。
使用環境
- Ubuntu 18.04 LTS
- Python 3.6.8
- PyMySQL 0.8.0
- MySQL Connector/Python 2.1.6
- mysqlclient 1.3.10