【備忘録】Python3系でアンダーバー2つ付きのクラスメソッドをオーバーライドしようとしてコケた話

Python3系を使っているプロジェクトでハマった時のお話。

こんなクラスがあったとします

import threading


class ThreadRunner(object):
    def __init__(self):
        self.__thread = None
        self.__event = threading.Event()

    def start(self):
        self.__event.clear()
        self.__thread = threading.Thread(self.__run)

    def stop(self):
        self.__event.set()
        self.__thread.join()
        self.__thread = None

    def is_running(self):
        return not self.__event.is_set()

    def __run(self):
        raise NotImplementedError("class is abstract.")

threadingのThread()とEvent()をまとめたごく簡単なユーティリティです。
子クラスには、__run()をオーバーライドしてもらって、
よしなに使えるようにと処理部分は未実装にしてありました。

実際に継承して動かしてみました。

import time
from .ThreadRunner import ThreadRunner


class TimeWaiter(ThreadRunner):
    def __init__(self):
        super().__init__()

    def __run(self):
        while True:
            # なんかの処理
            time.sleep(1)

↑こんな感じで継承して、

from TimeWaiter import TimeWaiter

waiter = TimeWaiter()
waiter.start()

いざ。

Exception in thread Thread-1:
Traceback (most recent call last):
  File "C:\Program Files\Python36\lib\threading.py", line 916, in _bootstrap_inner
    self.run()
  File "C:\Program Files\Python36\lib\threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "C:\test\ThreadRunner.py", line 23, in __run
    raise NotImplementedError("class is abstract.")
NotImplementedError: class is abstract.

エラー吐きました。
オーバーライドして無効化されているはずなのに、
親メソッドのNotImplementedError()を投げる処理が呼ばれいます。

先頭にアンダーバーが2つある関数・変数はちょっと特殊らしい

片っ端から情報をあさっていくと、以下の記事にたどり着きました。

【備忘録】Pythonにおけるアンダースコア"_"の役割について
http://qiita.com/ikki8412/items/ab482690170a3eac8c76

Pythonの変数やメソッドの命名について(アンダーバー)
https://teratail.com/questions/41277

1つ目の記事のコメント欄と、2つ目の記事の回答に、自分がコケていた内容に対する回答が載っていました。
関数名や変数名の先頭にアンダーバーを2つ付けると、
内部で自動的に名前が変わり(マングリングというらしいです)、別物になるとのこと。

class ThreadRunner(object):
    def __init__(self):
        self.__thread = None
        self.__event = threading.Event()

    def start(self):
        self.__event.clear()
        self.__thread = threading.Thread(self.__run)

    def stop(self):
        self.__event.set()
        self.__thread.join()
        self.__thread = None

    def is_running(self):
        return not self.__event.is_set()

    def __run(self):
        raise NotImplementedError("class is abstract.")

先程も例に挙げたThreadRunnerクラスですが、これは以下と同義ということに。

class TimeWaiter(object):
    def __init__(self):
        self.__thread = None
        self.__event = threading.Event()

    def start(self):
        self.__event.clear()
        self.__thread = threading.Thread(self.ThreadRunner__run)

    def stop(self):
        self.__event.set()
        self.__thread.join()
        self.__thread = None

    def is_running(self):
        return not self.__event.is_set()

    def ThreadRunner__run(self):
        raise NotImplementedError("class is abstract.")

    def TimeWaiter__run(self):
        while True:
            # なんかの処理
            time.sleep(1)

※あくまでイメージです

子クラスでいくら継承しても、親クラス側で名前が変わってしまい
オーバーライドしたことになってないのでした。

この場合、アンダーバーを1つにすると良いようでした。
アンダーバーを1つにすることによりマングリングが効かなくなり、
内部でオーバーライドとして扱ってるくれるようになります。

 
Pythonに慣れ親しんだ人からすれば当たり前なんだろうけど…
自分は触り始めて間もないので、結構あっさりハマって時間を溶かしました…。

以上、備忘録まで。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.