Как функция может стабильно ссылаться сама на себя?

[Код в оригинальной версии был сильно перепутан. Даже после того, как я исправил код, в посте осталось несколько очень запутанных опечаток. Я считаю, что я, наконец, исправил их все тоже. Примите искренние извинения.]

Два приведенных ниже вызова alias дают разные результаты, потому что объект, связанный с переменной my_own_id, изменяется между двумя вызовами:

>>> def my_own_id():
...     me = my_own_id
...     return id(me)
... 
>>> alias = my_own_id
>>> alias()
4301701560
>>> my_own_id = None
>>> alias()
4296513024

Что я могу присвоить me в определении my_own_id, чтобы его вывод оставался инвариантным по отношению к последующим переопределениям переменной my_own_id? (IOW, чтобы внутренняя переменная me всегда ссылалась на один и тот же объект функции?)

(Я могу получить текущий кадр (с inspect.currentframe()), но он содержит только ссылку на текущий объект кода, а не на текущую функцию.)

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


person kjo    schedule 10.08.2012    source источник
comment
Где определяется spam?   -  person Martijn Pieters    schedule 10.08.2012
comment
Пример несколько сбивает с толку — являются ли ham и spam ссылками на my_own_id()?   -  person voithos    schedule 10.08.2012
comment
Хорошая находка :-) [вы должны отредактировать вопрос, переименовав my_own_id в спам] Я понятия не имею, почему это происходит. Если вы снова назначите, он останется стабильным: foo = ham; ham(); del ham; foo() (что, я полагаю, является ответом на ваши вопросы, хотя и без какого-либо понимания :-)   -  person thebjorn    schedule 10.08.2012
comment
Это интересно: >>> id(my_own_id) 45375408 >>> alias = my_own_id >>> id(alias) 45375408 >>> my_own_id = None >>> id(my_own_id) 505354444 >>> id(alias) 45375408 >>> alias() 505354444   -  person jamylak    schedule 10.08.2012
comment
@MartijnPieters: мой код был перепутан; Я исправил это сейчас. Извините за путаницу.   -  person kjo    schedule 10.08.2012
comment
@voithos: см. мой комментарий MartijnPieters. Приносим извинения и вам, и всем остальным за сильно искаженный исходный пост.   -  person kjo    schedule 10.08.2012
comment
Вопрос очень интересный. Как получить объект функции из самой функции?   -  person Stefano Borini    schedule 11.08.2012
comment
Обратите внимание, что возвращаемое значение из последнего вызова alias() — это идентификатор None, т. е. каков идентификатор объекта, к которому привязано имя «my_own_id»?   -  person Russell Borogove    schedule 11.08.2012


Ответы (7)


Кажется, что обращение к my_own_id будет искать 'my_own_id' в словаре глобального пространства имен, поэтому это имя всегда будет использоваться в определении функции. Поскольку это имя может быть присвоено различным значениям, извлекаемое значение также может измениться. Если вы сделаете me аргументом по умолчанию, вы можете назначить его самой функции в определении функции, чтобы сохранить ссылку на реальную функцию.

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

>>> from functools import wraps
>>> def save_id(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(func, *args, **kwargs)
        return wrapper


>>> @save_id
def my_own_id(me): # me is passed implicitly by save_id
    return id(me)

>>> alias = my_own_id
>>> alias()
40775152
>>> my_own_id = 'foo'
>>> alias()
40775152
person jamylak    schedule 10.08.2012
comment
Вы действительно пробовали это? Я получаю NameError в python 2.6.1; в момент анализа аргументов имя my_own_id еще не существует. - person Russell Borogove; 11.08.2012
comment
@RussellBorogove только что попробовал это на 2.6.5 и 2.7, и, похоже, это работает. ideone.com/EtXTY и ideone.com/bbBSC - person jamylak; 11.08.2012
comment
Ах, я пропустил строку my_own_id = None. Вы получаете идентификатор None, а не идентификатор функции. (Я говорю о вашей первой строфе здесь, а не о версии декоратора.) - person Russell Borogove; 11.08.2012
comment
@RussellBorogove О, я просто избавлюсь от этого решения. Думаю, я мог получить идентификатор object()... - person jamylak; 11.08.2012

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

Более простой и удобный способ — написать для этого декоратор, который предоставит nonlocalvariable, содержащую ссылку на саму функцию.

from functools import wraps

def know_thyself(func):
   @wraps(func):
   def new_func(*args, **kwargs):
        my_own_id = func
        return func(*args, **kwargs)
   return new_func

И может использоваться как:

>>> @know_thyself
... def my_own_id():
...     me = my_own_id
...     return id(me)
... 

Существует еще один возможный подход, далеко не такой чистый, с использованием интроспекции фрейма и перестроением новой функции с повторным использованием того же объектного кода. Я использовал это в этом посте о самореферентном лямбда-выражении в Python: http://metapython.blogspot.com.br/2010/11/recursive-lambda-functions.html

person jsbueno    schedule 10.08.2012
comment
Вероятно, вы захотите изменить имена, поскольку в том виде, в каком они написаны сейчас, это не совсем работает: вы определяете переменную own_id, но затем не используете ее. - person Danica; 11.08.2012
comment
Э-э, я тоже не думаю, что это работает... нелокальная область работает только в охватывающей области, а не через вызывающие объекты. поскольку определение my_own_id не находится внутри ни know_thyself, ни new_func; ни одно из имен не видно (за исключением некоторого кадра, проверяющего махинации) - person SingleNegationElimination; 11.08.2012

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

>>> def be_known():
...     global my_own_id
...     def _my_own_id():
...         return id(_my_own_id)
...     my_own_id = _my_own_id
... 
>>> be_known()
>>> my_own_id()
140685505972568
>>> alias, my_own_id = my_own_id, None
>>> alias()
140685505972568

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

person SingleNegationElimination    schedule 10.08.2012

Подход декоратора, вероятно, лучший. Вот еще для развлечения:

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

    def fn(fnid=None):
        print "My id:", fnid
    fn.func_defaults = (id(fn),)
    
  2. Здесь есть несколько способов получить текущую функцию: код Python получить текущую функцию в переменную?; большинство из них связано с поиском currentframe.f_code в различных местах. Они работают без каких-либо изменений исходной функции.

person Luke    schedule 10.08.2012

Это из-за масштаба

>>> def foo():
...     x = foo
...     print x
... 
>>> foo()
<function foo at 0x10836e938>
>>> alias = foo
>>> alias()
<function foo at 0x10836e938>
>>> foo = None
>>> alias()
None
>>> foo = []
>>> alias()
[]
>>> del foo
>>> alias()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
NameError: global name 'foo' is not defined
>>> 
person Stefano Borini    schedule 10.08.2012
comment
Я думаю, что вопрос не в том, почему, а в том, как обойти. - person Luke; 11.08.2012
comment
@Люк: да, я знаю. Мне тоже любопытно. Я оставляю ответ как есть для дальнейшего использования. - person Stefano Borini; 11.08.2012

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

>>> def my_own_id(me=[None]):
    if not me[0]:
        me[0] = my_own_id
    return id(me[0])

>>> alias = my_own_id
>>> alias()
40330928
>>> my_own_id = None
>>> alias()
40330928

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

person Mark Ransom    schedule 10.08.2012
comment
Это, конечно, работает только в том случае, если функция вызывается до переназначения имени. - person Danica; 11.08.2012
comment
@Dougal, вы можете поставить первый вызов сразу после определения, просто чтобы убедиться. - person Mark Ransom; 11.08.2012
comment
Ну да, но если это дорогостоящий вызов функции, вам не обязательно этого делать. Просто указываю, что это не надежно. - person Danica; 11.08.2012

person    schedule
comment
Интересно, но это не сработает, как вы увидите, заменив строку me = my_own_id на me = _this_fn() в тестовом примере, который я опубликовал. Из любопытства, зачем возиться с del code и del frame (а вместе с ними и с необходимостью структуры try...finally) в _this_fn? - person kjo; 18.02.2013