hack のためのネタ帳, etc,,,

状況

再帰関数書くのに、def で定義した関数名書くのが面倒臭い。
何が面倒臭いって、
import inspect
def factorial_a(n):
    if not isinstance(n, int): raise ValueError(f"{inspect.currentframe().f_code.co_name}() only accepts integral values")
    if n < 0: raise ValueError(f"{inspect.currentframe().f_code.co_name}() not defined for negative values")
    return 1 if n < 1 else factorial_a(n-1) * n;
みたいな関数名を factorial_b にした場合に、再帰の部分も書き換えないといけない。

JavaScript の arguments.callee みたいに、名前指定せず実行中の関数呼ぶ機能が欲しい。

解法

python recurse itself without name」や「python arguments.callee」でググったら以下のページを見つけた。

なんか、decorator の @wrapper 構文使うと関数 wrap 出来るらしくて、第一パラメータに self 入れといてそこに arguments.callee 相当の値(つまり、当該の関数自身)を渡すような細工を出来るっぽい。
その際、functools.wraps を使うのが定番な模様。

とりあえず、以下の関数定義してデコレータとして使うと、第1引数が callee 扱いになる。
# @reference https://stackoverflow.com/a/35951133
from functools import wraps
def recfun(f):
  @wraps(f)
  def _f(*a, **k): return f(_f, *a, **k)
  return _f
import inspect
@recfun
def factorial(self, n):
    '''Calculate n!.'''
    if not isinstance(n, int): raise ValueError(f"{inspect.currentframe().f_code.co_name}() only accepts integral values")
    if n < 0: raise ValueError(f"{inspect.currentframe().f_code.co_name}() not defined for negative values")
    return 1 if n < 1 else self(n-1) * n;
とりあえず、これが正解っぽい。

2022-06-23: 追記

これ、よく考えたら inspect も不要だった。

@recfun
def factorial(self, n):
    '''Calculate n!.'''
    if not isinstance(n, int): raise ValueError(f"{self.__name__}() only accepts integral values")
    if n < 0: raise ValueError(f"{self.__name__}() not defined for negative values")
    return 1 if n < 1 else self(n-1) * n;

おぉ、凄い書き易いぞ!
ただ、これ wrap されるので、呼び出し時に微妙にオーバーヘッドかかるのが嫌ってのはあるかもしれないけど

/2022-06-23: 追記



inspect.currentframe().f_code.co_name を eval() して
callee = eval(inspect.currentframe().f_code.co_name)
みたいにする方法もあるみたいだが、再帰の度に評価されるの辛い。

デフォルト引数に
def factorial(n, _callee = factorial):
    '''Calculate n!.'''
    if not isinstance(n, int): raise ValueError(f"{_callee.__name__}() only accepts integral values")
    if n < 0: raise ValueError(f"{_callee.__name__}() not defined for negative values")
    return 1 if n < 1 else _callee(n-1) * n;
ってするのは、2か所書き換え必要なのでちょっと面倒だけど、書き換える箇所は近くなるので悪くないのでは?
と思ったが、def factorial() で関数する時点ではまだ factorial が定義されてないので参照できなかった。

じゃ、関数内で最初に代入すればと思ったが
def factorial(n):
    '''Calculate n!.'''
    callee = factorial
    if not isinstance(n, int): raise ValueError(f"{callee.__name__}() only accepts integral values")
    if n < 0: raise ValueError(f"{callee.__name__}() not defined for negative values")
    return 1 if n < 1 else callee(n-1) * n;
みたいになるので、docstring が長いと、書き換え必要な個所が遠くなってしまうので嬉しくない感じ。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Wiki内検索

フリーエリア

管理人/副管理人のみ編集できます