pythonで、何としてでも内包メソッドを呼ばせない方法は？

## privateメソッドの実装は可能？

python の記述ルールでは、クラスメソッドの、メソッド名の先頭に二重のアンダースコアを記述すれば容易には呼べない仕組みですが、アンダースコアによる命名のルール（'_クラス名' + 'メソッド名'）を知っていれば呼び出すことができてしまいます。

そこで、需要はないと思われますが、何が何でもローカルからしか、呼ばれないようにするコードを書いてみました。

~~~python:ex_private.py
#!/usr/bin/python
# coding: utf-8

"""
private 呼び出しをチェックする方法
"""

import traceback as tb
import re

# インデントレベル
##（この辺は直接関係ない）
indent = 0

# メソッドの呼び出しをトレースするデコレータ
#（この辺は直接関係ない）
def printmethod(func):
    def wrapper(*args, **kwargs):
        global indent
        _wsp = ' ' * indent * 2
        print(f'{_wsp}{func.__name__}: begin')
        indent += 1
        try:
            func(*args, **kwargs)
        except BaseException as ex:
            _wsp2 = ' ' * indent * 2
            print(f'{_wsp2}{func.__name__}: Exception: {ex}')
            raise
        finally:
            indent -= 1
            print(f'{_wsp}{func.__name__}: end')
    return wrapper

# インデントに合わせてプリント
##（この辺は直接関係ない）
def xprint(msg):
    _wsp = ' ' * indent * 2
    print(f'{_wsp}{msg}')


# プライベートCALLかどうかをチェックする
# ------------------------------------
def private_check(skip=1):
    # 引数チェック
    if not isinstance(skip, int):
        raise TypeError('Argument skip must be type int.')
    elif skip < 0:
        raise ValueError('Argument skip must be greater then -1.')
    # 内包クラス情報
    class classinfo:
        __slots__ = ['name', 'lineno', 'cname']
        def __init__(self):
            self.name = ''
            self.lineno = 0
            self.cname = ''
        # クラス名.メンバ名は呼び出し可能？
        @property
        def ok(self):
            _pass = False
            try:
                callable(eval(f'{self.cname}.{self.name}'))
                _pass = True
            except:
                pass
            return _pass
        # オブジェクトの文字列表記
        def __str__(self):
            _sout = f'{self.cname}'
            _sout += f'{"." if self.cname else ""}'
            _sout += f'{self.name}'
            return _sout
    # コールスタックを取得
    _stk = tb.extract_stack()
    # ソース記述から所属クラスを前方スキャン
    def _back_class_scan(lines, start):
        for i in range(start, -1, -1):
            _m = re.match(r'(class|def)\s+([^(:]+)[(:]', lines[i])
            if _m:
                return _m.group(2)
        return None
    # コールスタックのスキャン
    _stack = []
    for i in range(len(_stk) - (2 + skip), -1, -1):
        _map = classinfo()
        _map.name = _stk[i].name
        _map.lineno = _stk[i].lineno
        # コールスタックにはクラス名がないのでソースから探す
        _lines = []
        with open(_stk[i].filename, 'rt') as fp:
            _lines = fp.readlines()
        if _map.lineno > 0 and _map.lineno <= len(_lines):
            _class = _back_class_scan(_lines, _map.lineno - 1)
            _map.cname = _class if _class is not None else ''
            # モジュールのときはクラス名なし
            if _map.name == '<module>':
                _map.cname = ''
        # デコレータ @printmethod, xprint, private_check 自身は除く
        if _map.cname not in ['printmethod', 'xprint', 'private_check']:
            _stack.append(_map)
    # 内容をダンプしてみる
    for i in range(len(_stack)):
        print(f'callstack > "{_stack[i]}"')
    # 呼び出し元の１つ上（skip=1）が自己クラス以外のとき
    if len(_stack) > 0 and not _stack[0].ok:
        # 汝、あなたは、プライベートメソッドを直接呼んではならない！
        raise RuntimeError('You cannot call a private method directly.')
    return _stack

# テストコード

class TestClass:
 
    def __init__(self):
        pass

    @printmethod
    def _private_A(self, a, b, c):
        private_check()      # private呼び出しチェック
        xprint(f'I am _private_A.')

    @printmethod
    def _private_B(self, a, b):
        private_check()      # private呼び出しチェック
        xprint(f'I am _private_B.')

    @printmethod
    def public_A(self):
        xprint(f'I am public_A.')
        self._private_A(1,2,3)

    @printmethod
    def public_B(self):
        xprint(f'I am public_B.')
        self._private_B(1,2)

if __name__ == '__main__':

    x = TestClass()

    x.public_A()
    print('')

    x.public_B()
    print('')

    try:
        x._private_B(5,5)
    except:
        print("!!!!ERROR!!!!")
    print('')

    try:
        x._private_A(5,5,5)
    except:
        print("!!!!ERROR!!!!")
    print('')
~~~

### 実行結果

~~~shell:
$ ex_private.py 
public_A: begin
  I am public_A.
  _private_A: begin
    call-stack> "TestClass.public_A"
    call-stack> "<module>"
    I am _private_A.
  _private_A: end
public_A: end

public_B: begin
  I am public_B.
  _private_B: begin
    call-stack> "TestClass.public_B"
    call-stack> "<module>"
    I am _private_B.
  _private_B: end
public_B: end

_private_B: begin
  call-stack> "<module>"
  _private_B: Exception: You cannot call a private method directly.
_private_B: end
!!!!ERROR!!!!

_private_A: begin
  call-stack> "<module>"
  _private_A: Exception: You cannot call a private method directly.
_private_A: end
!!!!ERROR!!!!

$ 
~~~

## 終わりに

クラス名を探すのに、ソースァイル以外から探せればオーバーヘッドも少なく良いかもしれない。更に、デコレータ化すればもっと実用的かもしれない。