Как получить текущую функцию в переменную? - PullRequest
29 голосов
/ 20 декабря 2010

Как я могу получить переменную, которая содержит текущую выполняемую функцию в Python?Я не хочу имя функции.Я знаю, что могу использовать inspect.stack для получения текущего имени функции.Я хочу фактический вызываемый объект.Можно ли это сделать, не используя inspect.stack для получения имени функции и затем eval, вводя имя для получения вызываемого объекта?

Редактировать: У меня есть причина для этого,но это даже отдаленно не хороший.Я использую plac для анализа аргументов командной строки.Вы используете его, выполнив plac.call(main), который генерирует объект ArgumentParser из сигнатуры функции "main".Внутри «main», если есть проблема с аргументами, я хочу выйти с сообщением об ошибке, которое включает текст справки из объекта ArgumentParser, что означает, что мне нужно получить прямой доступ к этому объекту, вызвав plac.parser_from(main).print_help().Было бы неплохо иметь возможность вместо этого сказать: plac.parser_from(get_current_function()).print_help(), так что я не полагаюсь на функцию с именем "main".Прямо сейчас моя реализация get_current_function будет выглядеть так:

import inspect    
def get_current_function():
    return eval(inspect.stack()[1][3])

Но эта реализация опирается на функцию, имеющую имя, которое, я полагаю, не слишком обременительно.Я никогда не собираюсь делать plac.call(lambda ...).

В долгосрочной перспективе, возможно, было бы более полезно попросить автора плаката реализовать метод print_help для печати текста справки функции, которая была наиболеенедавно вызывали с помощью plac или чего-то подобного.

Ответы [ 10 ]

26 голосов
/ 22 декабря 2010

Кадр стека сообщает нам, в каком объекте кода мы находимся. Если мы сможем найти объект функции, который ссылается на этот объект кода в его атрибуте __code__, мы нашли функцию.

К счастью, мы можем спросить сборщика мусора, какие объекты содержат ссылку на наш объект кода, и просеять их, вместо того, чтобы обходить каждый активный объект в мире Python. Обычно существует всего несколько ссылок на объект кода.

Теперь функции могут совместно использовать объекты кода, и в случае, если вы возвращаете функцию из функции, то есть замыкание. Когда существует более одной функции, использующей данный объект кода, мы не можем сказать, какая это функция, поэтому мы возвращаем None.

import inspect, gc

def giveupthefunc():
    frame = inspect.currentframe(1)
    code  = frame.f_code
    globs = frame.f_globals
    functype = type(lambda: 0)
    funcs = []
    for func in gc.get_referrers(code):
        if type(func) is functype:
            if getattr(func, "__code__", None) is code:
                if getattr(func, "__globals__", None) is globs:
                    funcs.append(func)
                    if len(funcs) > 1:
                        return None
    return funcs[0] if funcs else None

Некоторые тестовые случаи:

def foo():
    return giveupthefunc()

zed = lambda: giveupthefunc()

bar, foo = foo, None

print bar()
print zed()

Я не уверен насчет характеристик производительности, но я думаю, что это будет хорошо для вашего варианта использования.

14 голосов
/ 20 декабря 2010

Это то, что вы просили, так близко, как я могу подойти.Протестировано в Python версий 2.4, 2.6, 3.0.

#!/usr/bin/python
def getfunc():
    from inspect import currentframe, getframeinfo
    caller = currentframe().f_back
    func_name = getframeinfo(caller)[2]
    caller = caller.f_back
    from pprint import pprint
    func = caller.f_locals.get(
            func_name, caller.f_globals.get(
                func_name
        )
    )

    return func

def main():
    def inner1():
        def inner2():
            print("Current function is %s" % getfunc())
        print("Current function is %s" % getfunc())
        inner2()
    print("Current function is %s" % getfunc())
    inner1()


#entry point: parse arguments and call main()
if __name__ == "__main__":
    main()

Вывод:

Current function is <function main at 0x2aec09fe2ed8>
Current function is <function inner1 at 0x2aec09fe2f50>
Current function is <function inner2 at 0x2aec0a0635f0>
13 голосов
/ 20 декабря 2010

Недавно я потратил много времени, пытаясь сделать что-то подобное, и в итоге ушел от этого. Там много угловых случаев.

Если вам нужен самый низкий уровень стека вызовов, вы можете просто указать имя, которое используется в операторе def. Это будет связано с функцией, которую вы хотите через лексическое закрытие.

Например:

def recursive(*args, **kwargs):
    me = recursive

me теперь будет ссылаться на рассматриваемую функцию независимо от области, из которой вызывается функция, если она не переопределена в области, в которой происходит определение. Есть ли какая-то причина, почему это не сработает?

Чтобы получить функцию, выполняющуюся выше в стеке вызовов, я не мог придумать ничего, что можно было бы надежно сделать.

10 голосов
/ 22 декабря 2010

Вот еще одна возможность: декоратор, который неявно передает ссылку на вызываемую функцию в качестве первого аргумента (аналогично self в методах связанных экземпляров). Вы должны украсить каждую функцию, для которой вы хотите получить такую ​​ссылку, но, как говорится, «явное лучше, чем неявное».

Конечно, он имеет все недостатки декораторов: другой вызов функции немного снижает производительность, а подпись упакованной функции больше не видна.

import functools

def gottahavethatfunc(func):

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(func, *args, **kwargs)

    return wrapper

Тестовый пример показывает, что декорированная функция по-прежнему получает ссылку на себя, даже если вы измените имя, к которому привязана функция. Это потому, что вы изменяете только привязку функции-оболочки. Это также иллюстрирует его использование с лямбдой.

@gottahavethatfunc
def quux(me):
    return me

zoom = gottahavethatfunc(lambda me: me)

baz, quux = quux, None

print baz()
print zoom()

При использовании этого декоратора с методом экземпляра или класса метод должен принимать ссылку на функцию в качестве первого аргумента и традиционную self в качестве второго.

class Demo(object):

    @gottahavethatfunc
    def method(me, self):
        return me

print Demo().method()

Декоратор использует закрытие для хранения ссылки на упакованную функцию в оболочке. Непосредственно создание замыкания может быть более чистым и не потребует дополнительных затрат на вызов функции:

def my_func():
    def my_func():
        return my_func
    return my_func
my_func = my_func()

Внутри внутренней функции имя my_func всегда относится к этой функции; его значение не зависит от глобального имени, которое может быть изменено. Затем мы просто «поднимаем» эту функцию в глобальное пространство имен, заменяя ссылку на внешнюю функцию. Работает и в классе:

class K(object):
    def my_method():
        def my_method(self):
             return my_method
        return my_method
    my_method = my_method()
6 голосов
/ 03 ноября 2011

Я просто определяю в начале каждой функции «ключевое слово», которое является просто ссылкой на фактическое имя функции.Я просто делаю это для любой функции, если она нужна или нет:

def test():
    this=test
    if not hasattr(this,'cnt'):
        this.cnt=0
    else:
        this.cnt+=1
    print this.cnt
4 голосов
/ 20 декабря 2010

Стек вызовов не сохраняет ссылку на саму функцию - хотя бегущий кадр как ссылка на объект кода, который является кодом, связанным с данной функцией.

(Функции - это объекты с кодом и некоторой информацией об их среде, такие как замыкания, имя, глобальный словарь, строка документа, параметры по умолчанию и т. Д.).

Поэтому, если вы запускаете обычную функцию, вам лучше использовать ее собственное имя в глобальном словаре для вызова самого себя, как было указано.

Если вы используете какой-то динамический или лямбда-код, в котором вы не можете использовать имя функции, единственное решение состоит в том, чтобы перестроить другой объект-функцию, который повторно использует текущий запущенный объект кода, и вместо этого вызвать эту новую функцию.

Вы потеряете пару вещей, таких как аргументы по умолчанию, и может быть трудно заставить его работать с замыканиями (хотя это можно сделать).

Я написал сообщение в блоге о том, как именно - вызывать анонимные функции изнутри себя - надеюсь, что код там может вам помочь:

http://metapython.blogspot.com/2010/11/recursive-lambda-functions.html

С другой стороны: избегайте использования o inspect.stack - он слишком медленный, так как он перестраивает много информации при каждом вызове. вместо этого предпочтительнее использовать inspect.currentframe для работы с кадрами кода.

Это может показаться сложным, но сам код очень короткий - я вставлю его ниже. Пост выше содержит больше информации о том, как это работает.

from inspect import currentframe
from types import FunctionType

lambda_cache = {}

def myself (*args, **kw):
    caller_frame = currentframe(1)
    code = caller_frame.f_code
    if not code in lambda_cache:
        lambda_cache[code] = FunctionType(code, caller_frame.f_globals)
    return lambda_cache[code](*args, **kw)

if __name__ == "__main__":
    print "Factorial of 5", (lambda n: n * myself(n - 1) if n > 1 else 1)(5)

Если вам действительно нужна сама исходная функция, вышеприведенную функцию «я» можно сделать для поиска в некоторых областях (например, в глобальном словаре вызывающей функции) объекта функции, объект кода которого будет совпадать с объектом, извлеченным из фрейма. вместо создания новой функции.

3 голосов
/ 18 сентября 2014

sys._getframe (0) .f_code возвращает именно то, что вам нужно: кодовый объект выполняется.Имея объект кода, вы можете получить имя с помощью codeobject .co_name

1 голос
/ 08 мая 2016

Вот вариант (Python 3.5.1) ответа get_referrers (), который пытается различить замыкания, использующие один и тот же код:

import functools
import gc
import inspect

def get_func():
    frame = inspect.currentframe().f_back
    code = frame.f_code
    return [
        referer
        for referer in gc.get_referrers(code)
        if getattr(referer, "__code__", None) is code and
        set(inspect.getclosurevars(referer).nonlocals.items()) <=
        set(frame.f_locals.items())][0]

def f1(x):
    def f2(y):
        print(get_func())
        return x + y
    return f2

f_var1 = f1(1)
f_var1(3)
# <function f1.<locals>.f2 at 0x0000017235CB2C80>
# 4

f_var2 = f1(2)
f_var2(3)
# <function f1.<locals>.f2 at 0x0000017235CB2BF8>
# 5

def f3():
    print(get_func())    

f3()
# <function f3 at 0x0000017235CB2B70>

def wrapper(func):
    functools.wraps(func)
    def wrapped(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapped

@wrapper
def f4():
    print(get_func())    

f4()
# <function f4 at 0x0000017235CB2A60>

f5 = lambda: get_func()    

print(f5())
# <function <lambda> at 0x0000017235CB2950>
1 голос
/ 21 декабря 2010

ОК, после прочтения вопроса и комментариев снова, я думаю, что это достойный тестовый пример:

def foo(n):
  """ print numbers from 0 to n """
  if n: foo(n-1)
  print n

g = foo    # assign name 'g' to function object
foo = None # clobber name 'foo' which refers to function object
g(10)      # dies with TypeError because function object tries to call NoneType

Я попытался решить его с помощью декоратора, чтобы временно закрыть глобальное пространство имен и переназначить объект функции наоригинальное название функции:

def selfbind(f):
   """ Ensures that f's original function name is always defined as f when f is executed """
   oname = f.__name__
   def g(*args, **kwargs):

      # Clobber global namespace
      had_key = None
      if globals().has_key(oname):
         had_key = True
         key = globals()[oname]
      globals()[oname] = g

      # Run function in modified environment
      result = f(*args, **kwargs)

      # Restore global namespace
      if had_key: 
         globals()[oname] = key
      else:
         del globals()[oname]

      return result

   return g

@selfbind
def foo(n):
   if n: foo(n-1)
   print n

g = foo   # assign name 'g' to function object
foo = 2   # calling 'foo' now fails since foo is an int
g(10)     # print from 0..10, even though foo is now an int
print foo # prints 2 (the new value of Foo)

Я уверен, что я не продумал все варианты использования.Самая большая проблема, которую я вижу, - это объект функции, намеренно изменяющий то, на что указывает его собственное имя (операция, которая будет перезаписана декоратором), но это должно быть в порядке, если рекурсивная функция не переопределяет свое собственное имя в серединеповторяющихся.

Все еще не уверен, что мне когда-нибудь понадобится это сделать, но размышлять было интересно.

0 голосов
/ 08 мая 2016

Исправление моего предыдущего ответа, потому что проверка subdict уже работает с "<=", вызванным для dict_items, и дополнительные вызовы set () приводят к проблемам, если есть dict-значения, которые сами диктуют: </p>

import gc
import inspect


def get_func():
    frame = inspect.currentframe().f_back
    code = frame.f_code
    return [
        referer
        for referer in gc.get_referrers(code)
        if getattr(referer, "__code__", None) is code and
        inspect.getclosurevars(referer).nonlocals.items() <=
        frame.f_locals.items()][0]
...