ラベル functools の投稿を表示しています。 すべての投稿を表示
ラベル functools の投稿を表示しています。 すべての投稿を表示

2019年8月3日

演算子の話⑤ functools.total_ordering

前回、__lt__() や __eq__() が定義されていても <= や >= はフォールバックしてくれない、という話をしました。
少し気の利かない仕様のような気がします。
これをフォローしてくれるのが functools の total_ordering です。

total_ordering はデコレータで、__lt__()、__le__()、__gt__()、__ge__() の4つの不等式系関数のうち最低1つと __eq__() を持っているクラスを修飾し、全6つの不等号・等号演算子に対応するようになります。

@total_ordering  # デコレータ
class Stored(object):
    def __init__(self, value):
        self.value = value

    def __lt__(self, rhs):
        return self.value < rhs.value

    def __eq__(self, rhs):
        return self.value == rhs.value

# 全6つの不等号・等号に対応
>>> Stored(1) < Stored(2)
True

>>> Stored(1) <= Stored(2)
True

>>> Stored(1) > Stored(2)
False

>>> Stored(1) >= Stored(2)
False

>>> Stored(1) == Stored(2)
False

>>> Stored(1) != Stored(2)
True
__le__() や __ge__() が定義されていなくても、<= や >= が呼ばれています。

と、一見便利そうな total_ordering ですが、実際にはあまり使う機会がありません。
第一の理由は、わざわざ total_ordering で修飾するのが煩わしい、ためです。__lt__() を定義した時点で < とそのフォールバックの > に対応でき、さらに __eq__() を定義することで == とそのフォールバックの != に対応できます。つまり、total_ordering の前提条件の時点で6つの不等号・等号のうち、4つはカバーできることになります。なので、total_ordering を使うよりも、あと1つ __le__() あたりを定義してした方が手っ取り早い、というわけです。
もう一つの理由は、total_ordering を使うと動作が遅くなる、ためです。不等式は数学的クラスでよく使われますが、数学的クラスでは動作速度が重要となります。そのような使用目的では、total_ordering を使うよりも、愚直に全6つの不等式・等式系関数を定義した方が、動作は速くなります。

2016年2月8日

functools.reduce()

高階関数の1つ、functools.reduce() を紹介します。

reduce() の第一引数は引数を2つ取る関数、第二引数はコンテナ、となります。
以下は具体的な例です。

>>> from functools import reduce
>>> from operator import mul

>>> c = [1, 2, 3, 4, 5]
>>> reduce(mul, c)
120
上記 reduce() の中で実際に行われている処理は、以下のようになります。
コンテナの各要素を関数でまとめ上げる、といった感じですね。
mul(mul(mul(mul(1, 2), 3), 4), 5)

reduce() を使わない一般的なループ文は以下のようになります。
>>> m = 1
>>> for i in c:
>>>     m *= i

>>> m
120
同じ処理の実装に3行必要となっていますので、reduce() の方がスッキリとした実装であると言えます。

書き方以外にも、reduce() は c[0] から計算を始めているのに対し、一般的なループ文の例では1行目で m の初期化を行っている点も相違点です。
reduce() の第二引数のサイズが0の時、および1の時には、それぞれ以下のように動作します。特にサイズが0の場合は例外発生するので要注意です。

# サイズが0ならエラー
>>> reduce(int.mul, [])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: reduce() of empty sequence with no initial value

# サイズが1なら[0]
>>> reduce(int.mul, [10])
10

以下は reduce() のもう少し実践的な使用例です。文字列の XORチェックサムを求めています。
>>> from functools import reduce
>>> from operator import xor

>>> s = "ABC"
>>> reduce(xor, (ord(i) for i in s))
64




以下余談です。

Python 2 では高階関数として map(), filter(), reduce() の3つがビルトイン関数として用意されていました。
しかし、Python 3 では reduce() のみが functools へと移されました。ビルトインから別モジュールへの移動ですから、事実上の降格です。

私にはこの決定が良い物とは思えません。
と言うのも、map() および filter() は内包表記を使うことで、同等の処理を実現できます。仮に map() と filter() が降格になるならば、それは「Python は内包表記を推している」という風に受け取られると思います。
しかし reduce() にはこれと言った代替手段は無く、reduce() を使わないとなると普通にループ文を書くしかありません。代替手段の無い reduce() こそ、ビルトインに残る方が良いと思うのですが、、、。
降格の一番の理由は、Guido氏(Python の開発者)が reduce() を「直感的でない」と否定的に見ていることのようです。

2015年12月6日

functools.partial()

引数の部分適用を実現する functools.partial() を紹介します。
部分適用とは、一部引数の値を固定にし、別の関数として扱うことです。


リトルエンディアンで2バイト、4バイト、2バイトの整数が格納されているバイト列を考えます。

>>> import binascii
>>> b = binascii.unhexlify('0100020000000300')

このバイト列から2バイト、4バイト、2バイトの順で3つの整数を取り出すソースコードは以下のようになります。
>>> i1 = int.from_bytes(b[0:2], 'little')
>>> i2 = int.from_bytes(b[2:6], 'little')
>>> i3 = int.from_bytes(b[6:8], 'little')
>>> i1, i2, i3
(1, 2, 3)

上のソースコードでは、int.from_bytes() を呼ぶ度に 引数'little' を指定しているのが、あまり格好良い感じではないですね。省略できればなぁ~、と思えてきます。
ここで、functools.partial() を使って引数の部分適用をすると、ソースコードが一気に見やすくなります。
以下は functools.partial() の具体的な使用例です。
>>> from functools import partial
>>> to_int = partial(int.from_bytes, byteorder='little')

>>> i1 = to_int(b[0:2])
>>> i2 = to_int(b[2:6])
>>> i3 = to_int(b[6:8])
>>> i1, i2, i3
(1, 2, 3)
バイト列から整数への変換部分がスッキリしましたね。


上例は functools.partial() の便利さを示すものですが、functools.partial() のより大きな恩恵として「部分適用した関数を変数に代入可能」が挙げられます。

つまり、、、

>>> def other_func(to_int):
...     print(to_int(b[0:2]))
...     print(to_int(b[2:6]))
...     print(to_int(b[6:8]))
...

# partialの戻り値は変数に代入可能
>>> other_func(partial(int.from_bytes, byteorder='little'))
1
2
3

# partial無しでは変数に代入できない
>>> other_func(int.from_bytes(byteorder='little'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Required argument 'bytes' (pos 1) not found
上記のように、functools.partial() ならば部分適用した関数を変数(ここでは引数)へ代入可能です。
functools.partial() 無しで実現するには、別途関数を定義する、lambdaを使う、等の方法がありますが、簡潔さと可読性の両観点から、部分適用には functools.partial() を使うのが最適であると言えます。