Может ли метод Python проверить, был ли он вызван изнутри самого себя?

Допустим, у меня есть функция Python f и fhelp. fhelp предназначен для рекурсивного вызова самого себя. f не следует вызывать рекурсивно. Есть ли способ для f определить, был ли он вызван рекурсивно?


person Joseph Turian    schedule 26.10.2011    source источник
comment
Ну, разве ты не можешь просто не звонить f из f ?   -  person madjar    schedule 26.10.2011
comment
Философия Python заключается в том, что все мы разумные взрослые, и поэтому читаем и уважаем документацию. Просто добавьте в свою документацию комментарий о том, что f не следует вызывать рекурсивно. Кстати. принимает ли f какие-либо пользовательские функции в качестве входных данных? Если нет, вы, как автор этой функции, должны быть в состоянии убедиться, что она не вызывает себя рекурсивно.   -  person Björn Pollex    schedule 26.10.2011
comment
@madjar: рекурсивные вызовы могут быть косвенными: f вызывает предоставленную пользователем функцию k, которая, в свою очередь, снова вызывает f.   -  person Björn Pollex    schedule 26.10.2011
comment
@Björn Pollex попал в самую точку. Существует много уровней косвенности, и я хочу убедиться, что какая-то подфункция не вызывает f.   -  person Joseph Turian    schedule 30.10.2011


Ответы (2)


Для этого используйте модуль traceback:

>>> import traceback
>>> def f(depth=0):
...     print depth, traceback.print_stack()
...     if depth < 2:
...         f(depth + 1)
...
>>> f()
0  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
 None
1  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in f
  File "<stdin>", line 2, in f
 None
2  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in f
  File "<stdin>", line 4, in f
  File "<stdin>", line 2, in f
 None

Таким образом, если какая-либо запись в стеке указывает на то, что код был вызван из f, вызов был (не)прямо рекурсивным. Метод traceback.extract_stack дает вам легкий доступ к этим данным. Оператор if len(l[2] ... в приведенном ниже примере просто подсчитывает количество точных совпадений имени функции. Чтобы сделать его еще красивее (спасибо agf за идею), вы можете превратить его в декоратор:

>>> def norecurse(f):
...     def func(*args, **kwargs):
...         if len([l[2] for l in traceback.extract_stack() if l[2] == f.func_name]) > 0:
...             raise Exception, 'Recursed'
...         return f(*args, **kwargs)
...     return func
...
>>> @norecurse
... def foo(depth=0):
...     print depth
...     foo(depth + 1)
...
>>> foo()
0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in func
  File "<stdin>", line 4, in foo
  File "<stdin>", line 5, in func
Exception: Recursed
person jro    schedule 26.10.2011

Вы можете использовать флаг, установленный декоратором:

def norecurse(func):
    func.called = False
    def f(*args, **kwargs):
        if func.called:
            print "Recursion!"
            # func.called = False # if you are going to continue execution
            raise Exception
        func.called = True
        result = func(*args, **kwargs)
        func.called = False
        return result
    return f

Тогда вы можете сделать

@norecurse
def f(some, arg, s):
    do_stuff()

и если f будет вызван снова во время его работы, called будет True и вызовет исключение.

person agf    schedule 26.10.2011
comment
У этого есть некоторая нежелательная память: скажем, ваш f определяется так: def f(func): print func(). Если вы вызовете его с помощью f(f), вы получите предупреждение о рекурсии. Если вы затем вызовете его с помощью f(lambda: 'foo'), вы также получите предупреждение, хотя оно не рекурсивно... - person jro; 26.10.2011
comment
Это не будет надежно работать в приложении, где функция вызывается из нескольких потоков, хотя я предполагаю, что она может быть адаптирована для работы и там с помощью threading.local в качестве хранилища для логического значения. - person Lauritz V. Thaulow; 26.10.2011
comment
@jro Я предположил, что это для целей отладки, и OP не хочет продолжать выполнение, если обнаружена нежелательная рекурсия. Если он действительно хочет продолжить, достаточно просто добавить func.called = False перед raiseing. - person agf; 26.10.2011
comment
@lazyr Почему я мог предположить, что код должен быть потокобезопасным, если это не упоминалось в вопросе? - person agf; 26.10.2011
comment
Я не имел в виду, что вы должны были. Комментарий должен был предостеречь и дать совет тем, кто может использовать это решение в таком случае. Решение traceback является потокобезопасным, а это — нет, но его можно сделать таким. - person Lauritz V. Thaulow; 26.10.2011
comment
Вероятно, вам следует использовать try: result = func(*args, **kwargs) finally: func.called = False на тот случай, если обернутая функция выдает исключение. - person Duncan; 26.10.2011