python3での、class定義時のクラス変数と、関数定義時のデフォルト引数についての誤解


python2/3で初学者がよくハマるものに、class変数&関数の仮引数のインスタンス問題がある。

実例

典型的には次の通りのコードになるだろう。

import datetime
import time

class class1(object):
    _datetime = datetime.datetime.now()


def func1(_datetime=datetime.datetime.now()):
    return _datetime


c1 = class1()
print(c1._datetime)
time.sleep(1)
c2 = class1()
print(c2._datetime)
time.sleep(1)

f1 = func1()
print(f1)
time.sleep(1)
f2 = func1()
print(f1)

このスクリプトを実行すると、ほぼ同じ時刻のdatetimeが並ぶ。
途中にsleep(1)を入れてるにも関わらずだ。

2021-09-16 HH:07:04.506008
2021-09-16 HH:07:04.506008
2021-09-16 HH:07:04.506029
2021-09-16 HH:07:04.506029

HHは伏せ字 :)

別にdatetimeがバグってるわけではない

次の通りのコードを実行すれば、おおむね1秒違いの結果が得られるはずだ。

import datetime
import time


class class1(object):
    _datetime = None

    def __init__(self):
        self._datetime = datetime.datetime.now()


def func1():
    return datetime.datetime.now()


c1 = class1()
print(c1._datetime)
time.sleep(1)
c2 = class1()
print(c2._datetime)
time.sleep(1)

f1 = func1()
print(f1)
time.sleep(1)
f2 = func1()
print(f1)

では、class定義と、関数の仮引数処理がバグっているのか?

class定義と関数定義の実態

ここからが本題だが。pythonの場合、一般的なプログラミング言語とは、class定義および関数定義の意味合いがかなり異なる。

import datetime
#import time


class class1(object):
    _datetime = datetime.datetime.now()


def func1(_datetime=datetime.datetime.now()):
    return _datetime


print(class1)
print(class1.__class__)
print(func1)
print(func1.__class__)

これを実行すると次のような結果がえられるはずだ。

<class '__main__.class1'>
<class 'type'>
<function func1 at 0x7f16241ef040>
<class 'function'>

この結果の示すところは

  • class定義とは、type型のクラスオブジェクト(この場合はclass1)の作成であり
  • 関数定義とは、functionクラスのfunc1インスタンスの作成である

ということだ。

最初のサンプルで、datetimeの値に2系統あったが、これはそれぞれclass1/func1の定義タイミングに一致しているためである。

pythonでは、全てのオブジェクトは変数とほぼ同義の扱いができる。
その証拠に、いずれも削除(または上書きができる)

import datetime
#import time


class class1(object):
    _datetime = datetime.datetime.now()


def func1(_datetime=datetime.datetime.now()):
    return _datetime


del class1
del func1
class1()

これを実行すれば、"class1"が見つからないため、NameErrorが発生するはずだ。

class定義とは、type型のクラスオブジェクトの作成である

このことはドキュメントにも明記されている。

9. クラス — Python 3.9.4 ドキュメント

クラスはデータと機能を組み合わせる方法を提供します。 新規にクラスを作成することで、新しいオブジェクトの 型 を作成し、その型を持つ新しい インスタンス が作れます。 クラスのそれぞれのインスタンスは自身の状態を保持する属性を持てます。 クラスのインスタンスは、その状態を変更するための (そのクラスが定義する) メソッドも持てます。

問題の現象を深堀してみよう。
id()関数により、オブジェクトの固有IDを確認できる。本件のように、同じインスタンスかどうか確認するのにはちょうど良い(別の方法もあるが)

import datetime
#import time


class class1(object):
    _datetime = datetime.datetime.now()


print(class1.__dict__)
# {'__module__': '__main__', '_datetime': datetime.datetime(2021, 9, 16, 23, 20, 1, 437108), '__dict__': <attribute '__dict__' of 'class1' objects>, '__weakref__': <attribute '__weakref__' of 'class1' objects>, '__doc__': None}
print(id(class1._datetime))
# 139647377446928
a = class1()
print(a._datetime)
# 2021-09-16 23:20:01.437108
print(id(a._datetime)) # class1._datetimeと同じはず
# 139647377446928
a._datetime = datetime.datetime.now() # 中身を上書きする
print(id(a._datetime))
# 139817154633568

このスクリプトの表示結果の通り、class1.__datetime__はすでにインスタンス化されており、a.__datetime__はその同じオブジェクト参照であることがわかる。

もちろん、a.__datetime__は上書きすることができて、そうすれば今度こそ別インスタンスとなる。

応用例

標準ライブラリではこの挙動が積極的に使われてる。
詳細は次に例示するが、class定義の簡素かつ柔軟な機構により成り立っている。
故にpythonでは、classをDSL代わりに使うこともよくみうけられる。

例えばenum.Enum

enum --- 列挙型のサポート — Python 3.9.4 ドキュメント

>>> class Color(NoValue):
...     RED = auto()
...     BLUE = auto()
...     GREEN = auto()
...
>>> Color.GREEN
<Color.GREEN>

auto()は、よくある自動採番機能である。一旦、数値でない値に置き換えておき、後でまとめてナンバリングしてくれるわけだ。

関数定義とは、functionクラスのインスタンスの作成である

import datetime
#import time
import dis


def func1(_datetime=datetime.datetime.now()):
    return _datetime


print(dis.dis(func1))
print(dir(func1))
print(func1.__defaults__)
a = func1()
print(a)
print(id(func1.__defaults__[0]))
print(id(a))

デフォルト引数の内容は、func1.__defaults__で確認できる。

  7           0 LOAD_FAST                0 (_datetime)
              2 RETURN_VALUE
None
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
(datetime.datetime(2021, 9, 16, 23, 33, 32, 898944),)
2021-09-16 23:33:32.898944
139968948846528
139968948846528

バイトコードの上では引数のデフォルト値の入れ替え等の処理は見当たらない。どうやら言語実装の内部で行われているようだ。

この件についてのオフィシャル記述はあまり多くはない。
Function オブジェクト — Python 3.9.4 ドキュメント

この文中で語られてる PyFunction_Get* の部分が、dir(function)のマジックメソッドにそれぞれ一致することに注目してほしい。