フリーランチ食べたい

No Free Lunch in ML and Life. Pythonや機械学習のことを書きます。

pathlibで見るPythonの演算子オーバーロード活用

pathlibって便利ですよね

最近pathlibの便利さが様々なところで語られています。

pathlibの様々な機能は上記の記事やドキュメントを読んでいただければわかるので、今日はその1つに、Pythonのオーバーロードを説明するのに良い機能があるので紹介したいと思います。

pathlibはこんな風にパスを書けます。

from pathlib import Path

etc_dir = Path('/etc')
init_dir = 'init.d'
print(etc_dir/init_dir/'reboot')
# => /etc/init.d/reboot

最初に見ると、ちょっとギョッとするのではないでしょうか?
pathlibでは上記のように、1 / 2 #=> 0.5 のように 変数 / 変数 という書き方でパスを書くことができます。慣れてくるとLinuxのパスを普通に書いているような気持ちで書けるので気持ち良いです。本来は除算の演算子である / でどうしてこんな風に書けるのでしょうか?答えはソースを読むとわかります。

https://github.com/python/cpython/blob/3.6/Lib/pathlib.py#L898

    def __truediv__(self, key):
        return self._make_child((key,))

この __truediv__ メソッドが答えです。

Pythonは演算子をオーバーロードできる

公式ドキュメントの 「3. Data model」を読むとPythonの演算子をオーバーロードできることがわかります。

3. Data model — Python 3.7.3 documentation

The following methods can be defined to emulate numeric objects. Methods corresponding to operations that are not supported by the particular kind of number implemented (e.g., bitwise operations for non-integral numbers) should be left undefined.

object.__add__(self, other)  
object.__sub__(self, other)  
object.__mul__(self, other)  
object.__matmul__(self, other)  
object.__truediv__(self, other)
...

これで +-/ といった演算子を再定義できます。それではこの機能を使って擬似的にpathlibを実装してみると次のようになります。

class myPath():
    def __init__(self, path):
        self._path = path
    
    def __truediv__(self, add_path):
        self._path = self._path + '/' + add_path
        return self

    def __str__(self):
        return self._path

etc_dir = myPath('/etc')
init_dir = 'init.d'
print(etc_dir / init_dir / 'reboot')
# => /etc/init.d/reboot

無事、pathlibと同様の挙動が実装できました。 pathlibは演算子オーバーロードをとても上手く使った実装だと思います

他にも演算子オーバーロードはこんなところでも

pathlib以外で自分が良く使う演算子オーバーロードはnumpyの @ です。これを使うと機械学習の実装では欠かすことのできないドット積(内積)の計算を行うことができます。元々は dot メソッドを呼び出さなければいけなかったのですが、途中で演算子オーバーロードとして実装されたので簡単に書けるようになりました。

import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

print(a * b)
# [[ 1  4  9]
#  [16 25 36]
#  [49 64 81]]

# 前は dot メソッドを呼び出す必要があった
print(a.dot(b))
# [[ 30  36  42]
#  [ 66  81  96]
#  [102 126 150]]

# 今は@演算子でdot積が計算できる
print(a @ b)
# [[ 30  36  42]
#  [ 66  81  96]
#  [102 126 150]]

演算子オーバーロード、とても便利ですね。

最後に

  • この記事を書いていている最中に知ったのですが、C++ でも最近、同様の演算子オーバーロードが導入されたようです。 std::filesystem::path - cppreference.com
  • 最初に発明した人は誰なのでしょうか?頭良いな〜と思いました。
  • 演算子オーバーロードの詳しい説明や活用方法は「Fluent Python」に丁寧に書いてあるのでさらに知りたい方はオススメです
    Fluent Python ―Pythonicな思考とコーディング手法

    Fluent Python ―Pythonicな思考とコーディング手法