privateメソッドの実装は可能?
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]}"')
# 呼び出し元の1つ上(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('')
実行結果
$ 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!!!!
$
終わりに
クラス名を探すのに、ソースァイル以外から探せればオーバーヘッドも少なく良いかもしれない。更に、デコレータ化すればもっと実用的かもしれない。