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

возможно ли получить доступ к атрибутам объекта функции python из области действия функции?

например. давай

def f():
    return SOMETHING

f._x = "foo"
f()           # -> "foo"

Теперь, что ЧТО-ТО должно быть, если мы хотим, чтобы содержимое атрибута _x возвращало "foo"? если это возможно (просто)

спасибо

UPDATE:

Мне бы тоже хотелось получить следующую работу:

g = f
del f
g()          # -> "foo"

ОБНОВЛЕНИЕ 2:

Заявление о том, что это невозможно (если это так) и почему, является более удовлетворительным, чем предоставление способа, как подделать его, например. с объектом, отличным от функции

Ответы [ 15 ]

47 голосов
/ 09 июля 2010

Решение

Сделать один из аргументов функции по умолчанию ссылкой на саму функцию.

def f(self):
    return self.x
f.func_defaults = (f,)

Пример использования:

>>> f.x = 17
>>> b = f
>>> del f
>>> b()
17

Объяснение

Первоначальный плакат хотел найти решение, которое не требует поиска по общему имени. Простое решение

def f():
    return f.x

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

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

def f(self=f):
    return self.x

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

декоратор

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

def self_reference(f):
    f.func_defaults = f.func_defaults[:-1] + (f,)
    return f

@self_reference
def foo(verb, adverb='swiftly', self=None):
    return '%s %s %s' % (self.subject, verb, adverb)

Пример:

>>> foo.subject = 'Fred'
>>> bar = foo
>>> del foo
>>> bar('runs')
'Fred runs swiftly'
24 голосов
/ 24 июня 2010

Вы можете просто использовать класс, чтобы сделать это

>>> class F(object):
...     def __call__(self, *args, **kw):
...         return self._x
... 
>>> f=F()
>>> f._x = "foo"
>>> f()
'foo'
>>> g=f
>>> del f
>>> g()
'foo'
15 голосов
/ 08 июля 2010

Хорошо, давайте посмотрим на то, что функция:

>>> def foo():
...     return x
... 
>>> foo.x = 777
>>> foo.x
777
>>> foo()
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "<interactive input>", line 2, in foo
NameError: global name 'x' is not defined
>>> dir(foo)
['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__get__', 
'__getattribute__', '__hash__', '__init__', '__module__', '__name__', '__new__', 
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 
'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 
'func_globals', 'func_name', 'x']
>>> getattr(foo, 'x')
777

Ага!Таким образом, атрибут был добавлен к объекту функции, но он не увидит его, потому что вместо этого ищет глобальный x.

Мы можем попытаться получить кадр выполнения функции и попытаться посмотреть, что там есть.(по сути то, что предложил Энтони Конг, но без inspect модуля):

>>> def foo():
...     import sys
...     return sys._getframe()
... 
>>> fr = foo()
>>> dir(fr)
['__class__', '__delattr__', '__doc__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 'f_back', 'f_builtins', 'f_code', 'f_exc_traceback', 'f_exc_type', 'f_exc_value', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_restricted', 'f_trace']
>>> fr.f_locals
{'sys': <module 'sys' (built-in)>}
>>> fr.f_code
<code object foo at 01753020, file "<interactive input>", line 1>
>>> fr.f_code.co_code
'd\x01\x00d\x00\x00k\x00\x00}\x00\x00|\x00\x00i\x01\x00\x83\x00\x00S'
>>> fr.f_code.co_name
'foo'

Ага!Поэтому, может быть, мы можем получить имя функции из имени блока кода, а затем посмотреть в обход атрибута?Конечно же:

>>> getattr(fr.f_globals[fr.f_code.co_name], 'x')
777
>>> fr.f_globals[fr.f_code.co_name].x
777
>>> def foo():
...     import sys
...     frm = sys._getframe()
...     return frm.f_globals[frm.f_code.co_name].x
... 
>>> foo.x=777
>>> foo()
777

Отлично!Но выдержит ли переименование и удаление оригинальной функции?

>>> g = foo
>>> g.func_name
'foo'
>>> g.func_code.co_name
'foo'

Ах, очень сомнительно.Объект функции и его объект кода все еще настаивают на том, что они называются foo.Конечно же, вот где он ломается:

>>> g.x
777
>>> g.x=888
>>> foo.x
888
>>> g()
888
>>> del foo
>>> g()
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "<interactive input>", line 4, in foo
KeyError: 'foo'

Черт!Так что в общем случае это невозможно сделать с помощью самоанализа через фреймы выполнения.Похоже, проблемы заключаются в том, что существует разница между объектом функции и объектом кода - объекты кода - это то, что выполняется, и является всего лишь одним атрибутом func_code объекта-функции и кактакой не имеет доступа к атрибуту func_dict, где наш атрибут x:

>>> g
<function foo at 0x0173AE30>
>>> type(g)
<type 'function'>
>>> g.func_code
<code object foo at 017532F0, file "<interactive input>", line 1>
>>> type(g.func_code)
<type 'code'>
>>> g.func_dict
{'x': 888}

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

8 голосов
/ 08 июля 2010

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

def factory():
    def inner():
        print inner.x
    return inner


>>> foo=factory()
>>> foo.x=11
>>> foo()
11
>>> bar = foo
>>> del foo
>>> bar()
11
3 голосов
/ 13 июля 2010

Вот декоратор, который вводит current_fun в глобалы функций перед выполнением функции. Это довольно взломать, но также и весьма эффективно.

from functools import wraps


def introspective(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        exists = 'current_fun' in f.func_globals
        old = f.func_globals.get('current_fun',None)
        f.func_globals['current_fun'] = wrapper
        try:
            return f(*args, **kwargs)
        finally:
            if exists:
                f.func_globals['current_fun'] = old
            else:
                del f.func_globals['current_fun']
    return wrapper

@introspective
def f():
    print 'func_dict is ',current_fun.func_dict
    print '__dict__ is ',current_fun.__dict__
    print 'x is ',current_fun.x

Вот пример использования

In [41]: f.x = 'x'

In [42]: f()
func_dict is  {'x': 'x'}
__dict__ is  {'x': 'x'}
x is  x

In [43]: g = f

In [44]: del f

In [45]: g()
func_dict is  {'x': 'x'}
__dict__ is  {'x': 'x'}
x is  x
3 голосов
/ 24 июня 2010

Я сомневаюсь, что это лучший способ сделать это, но вы можете получить доступ к атрибутам, используя имя метода в методе:

>>> def foo():
...   print foo.x
... 
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
AttributeError: 'function' object has no attribute 'x'
>>> foo.x = 5
>>> foo()
5
2 голосов
/ 24 июня 2010

Ответ довольно прост. Просто используйте имя факта, которое ищется во время выполнения, а не во время компиляции:

def f():
    return f._x

f._x = "foo"
f()           # -> "foo"
1 голос
/ 14 июля 2010

Здесь используется немного хакерский подход, но, возможно, он является наиболее правильным, поскольку он работает и с вызовом g().Он работает, потому что полагается на то, что проверка байт-кода выполняется модулем dis в качестве ярлыка.

Это выглядит более хакерским, чем на самом деле отчасти, потому что вызов dis.disassemble() выводит на стандартный выводпоэтому я перенаправляю это в StringIO.Я использую disassemble() для его функции выделения последней инструкции (добавьте туда строку print text, чтобы увидеть, как она выглядит), и это облегчает получение предыдущего LOAD_NAME и переменной, которую оно использовало.

Можно было бы использовать чистую библиотеку проверки байт-кода, чтобы сделать это без использования модуля dis, но это доказывает, что это возможно.Это может быть не самый надежный подход, но опять же, возможно, он будет работать в большинстве случаев.Я не потратил достаточно времени на изучение внутренних элементов или байт-кода Python, чтобы узнать, предшествуют ли большинству CALL_FUNCTION байт-кодам инструкции, которые выберет трюк регулярного выражения.

import inspect
import dis
import re
import sys
import StringIO

def f():
    caller = inspect.stack()[1][0]
    sys.stdout = StringIO.StringIO()
    dis.disassemble(caller.f_code, caller.f_lasti)
    text = sys.stdout.getvalue()
    sys.stdout = sys.__stdout__
    match = re.search(r'LOAD_NAME.*\((.*?)\)\s+-->', text)
    name = match.group(1)
    try:
        func = caller.f_locals[name]
    except KeyError:
        func = caller.f_globals[name]
    return func._x

f._x = 'foo'
print 'call f():', f()
g = f
del f
print 'call g():', g()

Это создает следующий вывод:

call f(): foo
call g(): foo
1 голос
/ 25 июня 2010

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

def f2():
    import inspect
    frame = inspect.currentframe()
    fname = frame.f_code.co_name
    fobj = frame.f_globals[fname]
    print fobj._x


f2._x = 2
f2() 
0 голосов
/ 07 декабря 2016

Мне очень нравится это.

from functools import update_wrapper

def dictAsGlobals(f):
    nf = type(f)(f.__code__, f.__dict__, f.__name__, f.__defaults__, f.__closure__)
    try: nf.__kwdefaults__ = f.__kwdefaults__
    except AttributeError: pass
    nf.__dict__ = f.__dict__
    nf.__builtins__ = f.__globals__["__builtins__"]
    return update_wrapper(nf, f)

@dictAsGlobals
def f():
    global timesCalled
    timesCalled += 1
    print(len.__doc__.split("\n")[0])
    return factor0 * factor1

vars(f).update(timesCalled = 0, factor0 = 3, factor1 = 2)

print(f())
print(f())
print(f.timesCalled)
...