nikkie-ftnextの日記

イベントレポートや読書メモを発信

Pythonでsortedのkey引数にlambdaを渡すときは、代わりにoperatorモジュールのitemgetterやattrgetterを使ってみませんか?

はじめに

「頑張れ!」ってきっと 愛してるって言葉♪ nikkieです。

今回はPython標準ライブラリの中から推しライブラリの1つを取り上げます。
その名もoperator!

この中からitemgetterattrgetterをご紹介!

目次

operatorのitemgetter・attrgetterのユースケース:sortedのkey引数

「ソート HOW TO」1で取り上げられています2
https://docs.python.org/ja/3/howto/sorting.html#operator-module-functions

ageの昇順でソートする2例です。
Python 3.11.6で動作確認しています。

タプルバージョン

>>> from operator import itemgetter
>>> student_tuples = [
...     ('john', 'A', 15),
...     ('jane', 'B', 12),
...     ('dave', 'B', 10),
... ]
>>> sorted(student_tuples, key=itemgetter(2))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

クラスのインスタンスバージョン

>>> from operator import attrgetter
>>> class Student:
...     def __init__(self, name, grade, age):
...         self.name = name
...         self.grade = grade
...         self.age = age
...     def __repr__(self):
...         return repr((self.name, self.grade, self.age))
...
>>> student_objects = [
...     Student('john', 'A', 15),
...     Student('jane', 'B', 12),
...     Student('dave', 'B', 10),
... ]
>>> sorted(student_objects, key=attrgetter("age"))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

itemgetterやattrgetterは関数のようなものを返しているんです!

>>> itemgetter(2)(('john', 'A', 15))
15
>>> attrgetter("age")(Student('john', 'A', 15))
15

もちろん、key引数にlambda student: student[2]lambda student: student.ageも渡せます。
ラムダ式https://docs.python.org/ja/3/tutorial/controlflow.html#lambda-expressions

ですが、私はitemgetterやattrgetterを使うのが好みです。

itemgetter・attrgetterのメリット その1:読みやすい

例えばこちらの記事、スッキリ書けると紹介しています。

これは私も同感です。
lambdaで書くより短いですよね

さらに、上の例ではタプルの場合、なぜインデックス2にアクセスするのかという点を

age_getter = itemgetter(2)

のような形で名前を付けて表せるのがいいなと思っています。

余談:lambda(無名関数)に名前を付けるのはPEP 8に反する

「itemgetterやattrgetterを使わなくても、以下のように名前を付ければいいのでは」というアイデアも浮かびますよね。

age_getter = lambda student: student[2]

Pythonのコーディング規約を記したPEP 8では、これはWrongとして指摘されています。
無名関数を変数に代入してはいけません!(関数定義をしましょう)

Always use a def statement instead of an assignment statement that binds a lambda expression directly to an identifier

意訳 lambda式を直接識別子に束縛する代入文の代わりに、常にdef文を使いましょう

# Wrong:
f = lambda x: 2*x
# Correct:
def f(x): return 2*x

拾い読みです。

The use of the assignment statement eliminates the sole benefit a lambda expression can offer over an explicit def statement (i.e. that it can be embedded inside a larger expression)

意訳 代入文の使用(Wrongの例)は、明示的なdef文を上回るlambda式の唯一の恩恵を排除してしまう
(恩恵とは、すなわち、lambda式はより大きな式の内側に組み込める)

itemgetter・attrgetterのメリット その2:速い(らしい)

今回のアウトプットをきっかけに知った情報です。
上で挙げた「ソート HOW TO」に以下の一節があります。
https://docs.python.org/ja/3/howto/sorting.html#operator-module-functions

operator モジュールには itemgetter(), attrgetter() そして methodcaller() 関数があります。

これらの関数を利用すると、上の例はもっと簡単で高速になります:

簡単になるというのは「読みやすい」で触れましたが、高速になる!?
これは理由がピンとこなかったので気になりました。

速い理由はおそらく__slots__によるのでは

(ここは理解を突き詰めきれずに書いているので、気になる点がありましたらご指摘いただけると嬉しいです)

itemgetterやattrgetterの実装を覗いたところ、これらはクラスとして実装されていました。

__call__を実装しているので、返り値のインスタンスは関数のように呼び出せます3

age_getter = itemgetter(2)
age_getter(('john', 'A', 15))  # __call__ が呼ばれる

高速に関係するかもと思ったのが、クラスの__slots__

# https://github.com/python/cpython/blob/v3.11.6/Lib/operator.py#L271-L277
class itemgetter:
    __slots__ = ('_items', '_call')
# https://github.com/python/cpython/blob/v3.11.6/Lib/operator.py#L232-L240
class attrgetter:
    __slots__ = ('_attrs', '_call')

用語集より
https://docs.python.org/ja/3/glossary.html#term-__slots__

クラス内での宣言で、インスタンス属性の領域をあらかじめ定義しておき、インスタンス辞書を排除することで、メモリを節約します。これはよく使われるテクニックですが、正しく扱うには少しトリッキーなので、稀なケース、例えばメモリが死活問題となるアプリケーションでインスタンスが大量に存在する、といったときを除き、使わないのがベストです。

ふむふむ、メモリが節約できるらしい

エキスパートPythonプログラミング 改訂3版4 4.6.35

この機能は、属性が少ないクラスにおいて、すべてのインスタンス__dict__を作らないことで、メモリ消費を節約することを目的としています。(Kindle版 p.254)

データモデルのドキュメントを見に行くと
https://docs.python.org/ja/3/reference/datamodel.html#slots

__dict__ を使うのに比べて、節約できるメモリ空間はかなり大きいです。 属性探索のスピードもかなり向上できます。

だから高速になるってことなのかなー?

Python Distilled』 7.27(ダメ押し)

インスタンスを多数生成する場合は、__slots__を使うことでメモリ使用量を大幅に削減し、実行時間を少し改善できます。

終わりに

Pythonの推しモジュールの1つ operatorからitemgetterやattrgetterの紹介でした。
sortedのkey引数に渡す関数(のようなオブジェクト)にitemgetterやattrgetterを使うと、2つのメリットがあるのでオススメです。

  • 読みやすさ
    • lambdaより短く書ける
    • 名前を付けられる
  • 高速
    • 実装を見たところ__slots__(によるのではないか)

高速というのを今回知ったので、sortedのkey引数に限らず、lambda(無名関数)を渡したいと思ったときに「代わりにoperatorのitemgetterやattrgetter(やmethodcaller)でできないかな」と攻めて考えていこうと思います


  1. Python実践レシピ』の9.1.5でも雰囲気がつかめると思います
  2. 過去記事のこちらでも取り上げました。温故知新!「ソート HOW TO」で知ったDecorate-Sort-Undecorate(key引数がある今、これを使う必要はないわ) - nikkie-ftnextの日記
  3. PEP 8に則る形でdefで関数定義して、インスタンス_call属性に設定しています(self._call = func
  4. 最新は改訂4版です
    https://www.tumblr.com/asciidwango/723883250102173696/%E3%82%A8%E3%82%AD%E3%82%B9%E3%83%91%E3%83%BC%E3%83%88python%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E6%94%B9%E8%A8%824%E7%89%88
  5. 引用箇所のあとに「クラスの属性などを凍結できる」と続きます。気になる方は書籍でお確かめください