はじめに
- MayaPython上の話です
環境
- Autodesk Maya 2020.4 (Python 2.7.11)
- Autodesk Maya 2023.3 (Python 3.9.7)
サンプルプロジェクト
経緯
super(class, self)を使ったスクリプトでreload()を行うと、
TypeError: super(type, obj): obj must be an instance or subtype of type
というようなエラーが出てしまう。
結論
クラスのidが破綻しないようにreload()する順番に気をつける。
以下詳しく解説します。
なにが起きているのか
まずreload()によってクラスにどんな変化が起こるかを理解する必要があります。
下図のディレクトリ構成を例にして説明します。
super_test/
├__init__.py
├darkdragon.py
├dragon.py
└main.py
start.py(scriptEditorから起動する用)
まずdragon.pyにDragonクラスを定義したとします。
(実装内容自体に特に意味はありません)
class Dragon(object):
def __init__(self, hp, atk):
self.__hp = hp
self.__atk = atk
def attack(self):
print(u'攻撃!')
attack()を呼び出してみます。
from .dragon import Dragon
def main():
dragon = Dragon(130, 80)
dragon.attack()
from super_test import main
main.main()
攻撃!
ここまでは問題ありませんね。
ではDragonクラスをreloadしてみましょう。
from importlib import reload
from super_test import dragon, main
reload(dragon)
main.main()
攻撃!
問題ありませんね。
ここで1つreload()における重要な要素を確認します。
それはidです。
id
main.pyにDragonクラスのidをプリントする一文を追加します。
from .dragon import Dragon
def main():
+ print(id(Dragon), 'in main function')
dragon = Dragon(130, 80)
dragon.attack()
実行してみます。
2993528039936 in main function
攻撃!
13桁の数字が出力されました。
これがidです。
もう一度実行してみましょう。
2993528028608 in main function
攻撃!
2993528039936
2993528028608
数字が微妙に変わりましたね。
idというのはクラスの情報が格納されているメモリ番号です。
リロードするたびに違う番号が割り当てられます。
つまり全く同じクラスだったとしても別物として扱われるのです。
ためしにこんな実験をしてみましょう。
from importlib import reload
from super_test import dragon
print(id(dragon.Dragon), 'first import')
reload(dragon)
print(id(dragon.Dragon), 'second import')
実行すると
2993528055984 first import
2993528056928 second import
やはりidが変わっています。
super(class, self)を使う
さてここからが本題です。
まずはDragonクラスを継承してDarkDragonクラスをつくります。
from .dragon import Dragon
class DarkDragon(Dragon):
def __init__(self, hp, atk):
super(DarkDragon, self).__init__(hp, atk)
インスタンス化するオブジェクトもDragonからDarkDragonに変更します。
from .darkdragon import DarkDragon
def main():
dark_dragon = DarkDragon(130, 80)
dark_dragon.attack()
from importlib import reload
from super_test import darkdragon, main
reload(darkdragon)
main.main()
実行すると
攻撃!
無事実行できました。
それではここでmain.pyもreloadしてみましょう
from importlib import reload
from super_test import darkdragon, main
reload(main)
reload(darkdragon)
main.main()
実行すると
# エラー: TypeError: file C:/Users/UserName/Document/maya/scripts/super_test/darkdragon.py line 5: super(type, obj): obj must be an instance or subtype of type
エラーが出てしまいました。
これが本題。
冒頭でお話した問題です。
エラーを読む
super(type, obj): obj must be an instance or subtype of type
super(type, obj)のobjは「typeのinstance」または「typeのsubtype」でなければならない
だそうです。
今回エラーが出ているのは下記の一文ですから、
super(DarkDragon, self).__init__(hp, atk)
この変数名に合わせるなら、
super(DarkDragon, self)のselfは「DarkDragonのinstance」または「DarkDragonのsubtype」でなければならない
となります。
DarkDragonを更に継承したりはしていないので、今回該当するのは前者の「DarkDragonのinstance」です。
でもおかしいですね。selfはちゃんとDarkDragonのinstanceのはずです...
本当にそうでしょうか?
先ほどreload()するとクラスのidが変わってしまうということを確認しました。
printデバッグをしてidがどうなっているか確認してみましょう。
idを確認する
hoge = Hoge()
print(hoge.__class__)
インスタンス.__class__でそのインスタンスのクラスを取得することができます。
これを活用してDarkDragonのイニシャライザにprintを仕込んでみます。
from .dragon import Dragon
class DarkDragon(Dragon):
def __init__(self, hp, atk):
print(id(self.__class__), 'class of self')
print(id(DarkDragon), 'in DarkDragon class')
super(DarkDragon, self).__init__(hp, atk)
実行してみます
2321255725456 class of self
2321255697136 in DarkDragon class
予想通りではありますが残念なことにidが異なるようです。
先に答えを言ってしまうと、
reload()の順番を逆にすることでidは同じになります。
darkdragon -> main の順にreload()するということですね。
from importlib import reload
from super_test import darkdragon, main
+reload(darkdragon)
reload(main)
-reload(darkdragon)
main.main()
実行すると
2321255716016 class of self
2321255716016 in DarkDragon class
攻撃!
なにがどのタイミングでreloadされているのか
この問題を理解するにはreload()の挙動を理解する必要があります。
importlib.realod()はモジュールを引数にして、そのモジュールをリロードします。
しかしそのモジュールがインポートしているモジュールはリロードされません。
つまり再帰的にリロードするわけではないのです。
from importlib import reload
from super_test import darkdragon, main
reload(main) # mainはリロードされる。mainがimportしているモジュール(darkdragonなど)はリロードされない
reload(darkdragon)
main.main()
つまりreload(main)時点では古いdarkdragonが読み込まれることになります。
そして次にreload(darkdragon)することでdarkdragonモジュールはリロードされます。
Pythonのモジュールインポートは自身のモジュールに取り込むという仕様になっており、
モジュール同士が参照で繋がっているわけではありません。
darkdragonモジュールをリロードして新しくしても、
main内のdarkdragonモジュールは古いままです。
ですのでこれを解決する場合、
先にdarkdragonをリロードしてしまい、後でmainをリロードします。
こうすればmainのリロード時にmain内のdarkdragonは最新のものが取り込まれます。
from importlib import reload
from super_test import darkdragon, main
reload(darkdragon) # darkdragonがリロードされる
reload(main) # mainはリロードされる。mainがimportしているモジュール(darkdragonなど)はリロードされないが、現時点でのdarkdragonはリロード済みなので、結果的に新しいdarkdragonをインポートすることになる
main.main()
おわりに
いかがでしたか?
Pythonのモジュールインポートまわりは色々と複雑なので、
興味がある方はぜひ調べてください!
またこのような問題を根本的に解決するために、
再起reloadを自作するのも面白いかもしれませんね。
おまけ
importlib.reload()はをPython2では使えません。
Python2では組み込みのreload()を使う必要があります。
下記のようなutilを作ると便利です。
import sys
def custom_reload(module):
if get_python_major_version() == 2:
reload(module) # Python2系
else:
import importlib # Python3系
importlib.reload(module)
def get_python_major_version():
ver = sys.version
major_ver = ver.split('.')[0]
return int(major_ver)
そもそもとして
Python3系のみのサポートでよい場合は、引数が無いsuper()を使いましょう。
それが一番楽です。
おまけ2
引数が無いsuper()はクラス内のメソッドからしか使うことができません。
なのでリスト内包表記などのスコープが別になってしまう記法では引数が無いsuper()は使えません。
今回の問題と同じErrorが出ます。
しかし逆に言えば引数ありのsuper()はクラス外のスコープからも呼べるということですね。
参考
https://docs.python.org/ja/3.5/library/functions.html?highlight=type#super
https://qiita.com/risuoku/items/a6bda20e0004660a65de
https://qiita.com/Mt_Snow/items/6af7f94295dc572598a1