質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.64%

関数をどの名前で呼び出したか知る方法

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 8
  • VIEW 3,783
退会済みユーザー

退会済みユーザー

 前提・実現したいこと

pythonで関数を呼び出した時、どの名前で呼び出したか知る方法はありませんか?

たとえば関数numを定義し、zero, one, two, threeという別名を付けます。
このときに、この関数がone()で呼ばれたのかtwo()で呼ばれたのか区別する方法はないでしょうか。

 該当のソースコード

from inspect import stack

def num():
    s = stack()
    f, c = s[0].function, s[1].code_context
    print(f, c)  # num, ['print(one() + two() + three())\n']
    func_names = ['zero', 'one', 'two', 'three']
    return func_names.index(f) if f in func_names else -1

zero = one = two = three = num
print(one() + two() + three()) # 現状では-3になってしまうのを6になるようにしたい

 試したこと

inspect.stackから関数呼び出し時の情報を取得できないかと考えたのですが、.functon は常に元々の関数名('num')が格納されていました。.code_contextからは呼び出し時の情報は拾ってこれるようですが、サンプルコードのように同じ行で複数回呼び出しをしている場合に区別する方法がありません。


hayataka2049さんからの確認についてですが、
今回は技術的な興味からなので、関数のみで対応する方法がないか考えています。
検索してみたところ、classを使う場合は以下のような方法を発見することができました。

class My_Number:
    def __init__(self, n):
        self.n = n  # この数字で誰が呼ばれたのか区別する

    def __call__(self, *args, **kwargs):
        return self.n

zero, one, two, three = [My_Number(i) for i in range(4)]
print(one() + two() + three())
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • hayataka2049

    2018/11/19 21:33 編集

    純粋に技術的興味からの質問と理解して良いですか? 念の為に確認させてください
    (もし何らかの実用を意図しているなら他の方法を紹介します)

    キャンセル

回答 2

checkベストアンサー

+7

func2 = func1

のようなコードの意味を考えてみますとグローバル環境であれローカル環境であれ

func1という識別子に結び付いた値」を取り出し、それをfunc2という名前に結び付いた「値」に設定する

という意味になります。これはどんな型の値であっても同じでありもちろん「関数」であっても(Pythonの関数は第一級関数、つまり関数自体もある種の型の値であることに注意してください)同じです。

さてzero()というのはどう評価されるかといえば

  • (1) zeroというプライマリ式の評価
    結果は関数のインスタンスで、当然ながらnumに結び付いた関数インスタンスと同一になります。

  • (2) (1)で求めた関数インスタンスを呼び出す
    その関数がPythonのコードで書かれたものであれば(C++で実装されたネイティブな関数でなければ)、関数インスタンスの__code__プロパティーなどからcodeオブジェクトを取り出し、それを実行スタックに記録した上でcodeオブジェクトをバイトコードインタープリタで実行する。

(1)がミソです。関数呼び出しの文法は最初のころ「関数名 ( 引数式の並び )」であると捉えるのが典型的だと思いますが、実際はそうではなく「プライマリ式 ( 引数式の並び )」です。つまり「関数名」などという言語仕様はなくて「単なるプライマリ式」を評価した結果がcallableだったのでそれを呼び出せただけです。Pythonインタープリタは「ひたすら値を計算するのが仕事」です。よってその値が何に由来したものだったかといったデバッグにしか使い道がなさそうな情報は最小限しか記録しません。おそらくPythonの設計者は「どの識別子に結び付いていた値だったか」はなくても、スタックトレース(バックトレース)で充分と考えたのだと思います。

ただし、「関数を起動した際のプログラムがどのようなものだったかを調べる」方法はなくはないです。

 強引な方法

zero()のようにして起動された関数は呼び出し元のスタックフレームの中にあるcodeインスタンスのバイトコードが以下のようになっているでしょうから、

   ...
   10 LOAD_GLOBAL   X (zero)
   12 CALL_FUNCTION 0

バイトコードの中のCALL_FUNCTIONの直前の式がLOAD_GLOBAL Xであることを確認した上で識別子一覧からX番目を取り出せばその識別子がzeroであったことがわかります。かなりさぼってますがそういう実装を書いてみますと・・・

import inspect

def num_test():
    try:
        fr = inspect.currentframe().f_back
        code = fr.f_code
        bi = fr.f_lasti
        assert code.co_code[bi-2] == 0x74  # LOAD_GLOBAL
        ci = code.co_code[bi-1]
        name = code.co_names[ci]
        print(name)
    except:
        print('予想外です!')

zero = one = num_test
zero()  # ==> zero
one()   # ==> one

def main():
    local_one = one
    local_one()  # ==> 予想外です!

main()


(CPython 3.7で確認)

上のようなコードはインタープリタの内部仕様に依存しすぎているので、デバッグ目的でのみ用いるのが無難ではないでしょうか?

 現実的な方法

現実的にはzeroやoneなどの値を異なる関数インスタンスにするのがもっとも素直な方法だと思います。

zero, one, two = ((lambda x: lambda: x)(v) for v in range(3))

# もちょっと分解して書くなら

def num_function(value):  # 関数を返す関数
    def dummy():
        return value
    return dummy

zero, one, two = (num_function(v) for v in range(3))

# もっとたくさんの識別子の定義が必要なら

names = ['zero', 'one', 'two', ..., 'one_hundred']
for i, name in enumerate(names):
    globals()[name] = num_function(i)  # これはひどい...

この実装はもはや質問者さんの意図とは離れたものですが、先に書いた怪しげな実装に比べ、単純明快です。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/11/20 12:31

    回答ありがとうございます。やはり別インスタンスにしないと無理そうですね。

    キャンセル

  • 2018/11/20 13:13

    その方がよさそうです。sys.argv[0]を見て動作を変えるスクリプトなんてのもよくありそうなので本件のアイデアは面白いと思うのですが、いかんせんPythonの関数呼び出しのレベルでそれをやるのは少々厳しそうですね。

    キャンセル

+2

解決策ではなく、できませんでした報告です。

とりあえず改行すればできるんじゃないかと思ったのですが、

import inspect

def f():
    s = inspect.stack()
    print(s[1].code_context)
    return -1

one = two = three = f
print(one(),
      two(),
      three())
print(one() + 
      two() + 
      three())

""" =>
['print(one(),\n']
['      two(),\n']
['      three())\n']
-1 -1 -1
['      two() + \n']
['      two() + \n']
['      three())\n']
-3
"""

上はまあまあですが、下は期待通り動きません。しばらく考えた結果、演算子だとインタプリタスタックが期待通り積み上がらないのだろうと推測しました(深くは追求していません)。

となると、かなり難しいんじゃないかなぁという気がします。


期待通りではないかもしれませんが、

zero = one = two = three = num
magic() # ここに一行入れて良いならなんでもし放題ではある
print(one() + two() + three())

方法は色々思いつきます(globals()を書き換えるのは前提として、情報を持たせられるクラスのインスタンス(当然__call__も定義)にすげ替えるとか)。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/11/20 10:13 編集

    code_contextはスタックフレームにあるbyte code位置(f_lasti)をcodeインスタンスが保持している行番号情報(co_lnotab)から検索して行番号を特定し生成していると思いますが、co_lnotabを見ると
    (exp1 +
    exp2 +
    ...
    expN)
    みたいなコードはexp1~expN-1のbyte codeが全部exp2の行にあると記録されてました。
    実際のソースコードの行になるべく一致するようになっていた方がスタックトレースを見たとき混乱しなくて済むと思うのですが、行番号情報はあくまでデバッグ情報でしかないためbyte code compilerの実装がおざなり(?)になっているだけのような気もします。ただpythonのコードって複数行にわたることがほぼないのでむしろ「この程度の精度で充分でありこれ以上綿密な実装はオーバースペック」という判断なのかも知れません。
    ただ、デバッグの際「実際の実行ラインがスタックトレースに出ているより後の可能性がある」という知識が役立つことが(いつか)くるかもです・・・

    キャンセル

  • 2018/11/20 12:19

    回答ありがとうございます。inspect.stack()からのアプローチでは無理そうなことが分かっただけでも大収穫です。

    キャンセル

  • 2018/11/20 20:08

    >KSwordOfHasteさん
    私も気づいたときはひどい実装だと思いましたが、演算子でまでデバッグ情報を拾うオーバーヘッドや、メンテナンスの手間を考えると妥当かもしれないと考え直しました。

    >kichirb3さん
    そう言っていただけると嬉しいです。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.64%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る