LoginSignup
7
10

More than 3 years have passed since last update.

【Python中級者への道】__call__でクラスインスタンスを関数のように呼び出す

Posted at

まとめへのリンク

はじめに

Pythonの勉強をするために、acopyという群知能ライブラリを写経していました。

acopyでは、多くのPythonの面白い文法・イディオムが使われており、その中でも便利だなぁというのをまとめています。

今回は、クラスのインスタンスを関数のように呼び出す__call__属性についてみてみます。

__call__属性メソッド

__call__属性をクラスに定義すると、
インスタンスを()演算子で関数のように呼び出すことができます。

class Person:

    def __init__(self, age, name):
        self.age = age
        self.name = name

    def __repr__(self):
        return f'age = {self.age}, name = {self.name}'

    def __call__(self, *args, **kwargs):
        return self.name


'''
age = 20, name = ron
ron
'''
p = Person(20, 'ron')
print(p)
print(p())

以上の例では、Personクラスに__call__属性が定義されています。
これによって、関数呼び出しのようにインスタンスが呼び出されると、pの名前を返します。

このように、関数のように呼び出せるようにするのが、__call__です。

活用例

それでは、この()を使うとどう嬉しいことがあるのでしょうか?

単純なインターフェース

あまりするべきで無い気がしますが、単純なインターフェースとして
使用できるのかなと思います。

class Add:

    def __init__(self, x):
        self.x = x

    def __call__(self, y):
        return self.x + y


class Sub:

    def __init__(self, x):
        self.x = x

    def __call__(self, y):
        return self.x - y


'''
20
10
50
'''
arr = [Add(10), Sub(20), Add(40)]
for ice in arr:
    print(ice(10))

なんか違う気がしますね。

関数に状態をもたせる

主にこれの意味があるのかなと思います。
C++では、structに()を持たす関数オブジェクトというものが存在します。
それについて触れた自分の記事です。(俺のunordered_mapがこんなにpair型をキーにできないわけがない)

関数には状態を持たせづらいですが、クラスには状態を簡単に持たすことができます。
すると、()演算子の使用回数や、重い処理のDPのような計算結果の取得などが可能になります。

class Count:
    cls_count = 0

    def __init__(self):
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        Count.cls_count += 1
        return self.count, Count.cls_count


c1 = Count()
c2 = Count()

'''
(1, 1)
(1, 2)
(2, 3)
(3, 4)
(2, 5)
'''
print(c1())
print(c2())
print(c1())
print(c1())
print(c2())

以上のコードでは、Countクラスを作り、それを関数呼び出ししています。
内部でcls_count, countという変数・内部状態を保存しています。

以上は適当な例ですが、()という関数呼び出しに内部状態を保存することができます。

また、以下はエラトステネスの篩を関数呼び出ししています。
一回計算したあとは、内部キャッシュした値を返しています。

class Era:

    def __init__(self, N):
        self.N = N
        self.primes = []
        self.ready = False

    def __call__(self, x):
        if self.ready:
            return self.primes[x]
        else:
            self.primes = [True] * (self.N + 1)
            self.primes[0] = self.primes[1] = False

            i = 2
            while i * i <= self.N:
                if self.primes[i]:
                    for j in range(i * 2, N + 1, i):
                        self.primes[j] = False
                i += 1
            return self.primes[x]


N = 100
era = Era(N)

for i in range(0, N + 1):
    print(i, era(i))

このように、外部からは()で関数のように呼び出したいですが、内部状態をもたせたいときは、__call__を使用すると良さそうです。

参考文献

7
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
10