はじめに
「頑張れ!」ってきっと 愛してるって言葉♪ nikkieです。
今回はPython標準ライブラリの中から推しライブラリの1つを取り上げます。
その名もoperator!
この中からitemgetterとattrgetterをご紹介!
目次
- はじめに
- 目次
- operatorのitemgetter・attrgetterのユースケース:sortedのkey引数
- itemgetter・attrgetterのメリット その1:読みやすい
- itemgetter・attrgetterのメリット その2:速い(らしい)
- 終わりに
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の実装を覗いたところ、これらはクラスとして実装されていました。
- https://github.com/python/cpython/blob/v3.11.6/Lib/operator.py#L271
- https://github.com/python/cpython/blob/v3.11.6/Lib/operator.py#L232
__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)でできないかな」と攻めて考えていこうと思います
- 『Python実践レシピ』の9.1.5でも雰囲気がつかめると思います↩
- 過去記事のこちらでも取り上げました。温故知新!「ソート HOW TO」で知ったDecorate-Sort-Undecorate(key引数がある今、これを使う必要はないわ) - nikkie-ftnextの日記↩
-
PEP 8に則る形でdefで関数定義して、インスタンスの
_call
属性に設定しています(self._call = func
)↩ - 最新は改訂4版です ↩
- 引用箇所のあとに「クラスの属性などを凍結できる」と続きます。気になる方は書籍でお確かめください↩