Python: декоратор статических переменных

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

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

@static(x=0)
def f():
    x += 1
    print x

f() #prints 1
f() #prints 2

Мне все равно, будет ли реализация static длинной или хакерской, пока она работает, как указано выше.

Я создал эту версию, но она допускает только синтаксис <function>.<varname>, который довольно быстро становится громоздким с более длинными именами функций и переменных.

def static(**assignments):
    def decorate(func):
        for var, val in assignments.items():
            setattr(func, var, val)
        return func
    return decorate

Я думал о разных вещах, но не мог приступить к работе:

  1. Преобразование f (оформленной функции) в вызываемый класс и каким-то образом прозрачное сохранение статических переменных в self.
  2. Изменение глобальных переменных f() внутри декоратора и вставка операторов «global x» в код для f.
  3. Преобразование f в генератор, в котором мы связываем переменные вручную, а затем напрямую выполняем код f.

person bukzor    schedule 23.10.2009    source источник


Ответы (8)


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

class Static(object):
    def __init__(self, **kwargs):
        self.kwargs = kwargs

    def __call__(self, f):
        def wrapped_f():
            try:
                new_kwargs = {}
                for key in self.kwargs:
                    i = getattr(f, key)
                    new_kwargs[key] = i
                self.kwargs = new_kwargs
            except:
                pass
            for key, value in f(**self.kwargs).items():
                setattr(f, key, value)
        return wrapped_f

@Static(x=0, y=5, z='...')
def f(x, y, z):
    x += 1
    y += 5
    print x, y, z
    return locals()

Результат будет:

>>> f()
1 10 ...
>>> f()
2 15 ...
>>> f()
3 20 ...

РЕДАКТИРОВАТЬ:

Я нашел кое-что на http://code.activestate.com/recipes/410698/ и решил попробовать добавить его к этому. Теперь работает без возврата.

РЕДАКТИРОВАТЬ снова: изменено на, чтобы сделать его на несколько секунд быстрее. Редактировать 3; изменено на функцию вместо класса

def static(**kwargs):
    def wrap_f(function):
        def probeFunc(frame, event, arg):
            if event == 'call':
                frame.f_locals.update(kwargs)
                frame.f_globals.update(kwargs)
            elif event == 'return':
                for key in kwargs:
                    kwargs[key] = frame.f_locals[key]
                sys.settrace(None)
            return probeFunc
        def traced():
            sys.settrace(probeFunc)
            function()
        return traced
    return wrap_f

проверено:

@static(x=1)
def f():
    x += 1

global_x = 1
def test_non_static():
    global global_x
    global_x += 1


print 'Timeit static function: %s' % timeit.timeit(f)
print 'Timeit global variable: %s' % timeit.timeit(test_non_static)

выход:

Timeit static function: 5.10412869535
Timeit global variable: 0.242917510783

Использование settrace значительно замедляет его.

person chris    schedule 24.10.2009
comment
Спасибо. Это довольно близко, но я хотел бы удалить требование возврата locals(), если это возможно. Кто-нибудь может улучшить этот? - person bukzor; 24.10.2009
comment
Вау, это чертовски близко! Не могли бы вы отредактировать ответ, чтобы удалить аргументы, как вы предложили? Тогда это точно ответит на мой вопрос. Мы можем работать над тем, чтобы сделать его быстрым и красивым после того, как он заработает. - person bukzor; 25.10.2009

К тому времени, когда ваш декоратор получит объект функции f, он уже будет скомпилирован — в частности, он был скомпилирован с учетом того, что x является локальным (поскольку он назначается с присваиванием +=), обычная оптимизация (в 2.* вы можете отменить оптимизацию , по ошеломляющей цене производительности, начиная f с exec ''; в 2.* оптимизацию не победить). По сути, чтобы использовать желаемый синтаксис, вы должны перекомпилировать f (путем восстановления его исходных кодов, если вы знаете, что они будут доступны во время выполнения, или, что гораздо сложнее, с помощью взлома байт-кода) с каким-либо измененным исходным кодом — как только вы Если мы решили пойти по этому пути, самый простой способ, вероятно, состоит в том, чтобы изменить x на f.x во всем теле f.

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

В любом случае, я отказываюсь от попыток исказить язык слишком далеко от его очевидных дизайнерских намерений: даже если бы я придумал какой-нибудь хакерский, хрупкий кладж, его, несомненно, было бы невозможно поддерживать. В этом случае намерения Python очень ясны: bar-имена, которые повторно связываются внутри функций, являются локальными для этой функции, если только они явно не обозначены как глобальные — и точка. Таким образом, ваша попытка заставить barenames (которые перепривязываются внутри функции) означать что-то совершенно иное, чем «местные жители», — это именно такая борьба.

Редактировать: если вы готовы отказаться от настойчивого использования barnames для своей «статики», то внезапно вы больше не боретесь с Python, а скорее «в соответствии с зерном» языка (несмотря на сбой дизайна globalnonlocal], но это отдельная тирада ;-). Так, например:

class _StaticStuff(object):
  _static_stack = []
  def push(self, d):
    self._static_stack.append(d)
  def pop(self):
    self._static_stack.pop()
  def __getattr__(self, n):
    return self._static_stack[-1][n]
  def __setattr__(self, n, v):
    self._static_stack[-1][n] = v
import __builtin__
__builtin__.static = _StaticStuff()

def with_static(**variables):
  def dowrap(f):
    def wrapper(*a, **k):
      static.push(variables)
      try: return f(*a, **k)
      finally: static.pop()
    return wrapper
  return dowrap

@with_static(x=0)
def f():
    static.x += 1
    print static.x

f()
f()

Это работает так, как вы хотите, печатая 1, а затем 2. (Я использую __builtin__, чтобы упростить использование with_static для оформления функций, живущих в любом модуле, конечно). У вас может быть несколько разных реализаций, но ключевым моментом любой хорошей реализации является то, что "статические переменные" будут полными именами, а не пустыми именами. - явно указывать, что они не являются локальными переменными, играть с зерном языка и так далее. (Аналогичные встроенные контейнеры и основанные на них полные имена должны были использоваться в дизайне Python вместо ошибок дизайна global и nonlocal для обозначения других типов переменных, которые не являются локальными. и, следовательно, не следует использовать barenames... ну, вы можете реализовать специальный контейнер globvar на тех же строках, что и вышеперечисленные static, даже не нуждаясь в декорировании, хотя я не уверен, что это полностью осуществимо для nonlocal футляр [возможно, с каким-то украшением и малейшим количеством черной магии...;=)]).

Изменить: комментарий указывает, что приведенный код не работает, когда вы декорируете только функцию, возвращающую замыкание (вместо декорирования самого замыкания). Правильно: конечно, вы должны декорировать конкретную функцию, которая использует static (а может быть только одна, по определению переменных function-static!), а не случайную функцию, которая на самом деле не использует static, а скорее просто оказывается в какой-то лексической связи с тем, что делает. Например:

def f():
  @with_static(x=0)
  def g():
    static.x += 1
    print static.x
  return g

x = f()
x()
x()

это работает, а перемещение декоратора на f вместо g не работает (и не может быть).

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

person Alex Martelli    schedule 23.10.2009
comment
Я согласен, но я надеюсь, что этот kludge/hack не будет нужен бесконечно. Моя цель состояла в том, чтобы продемонстрировать, что это возможно или невозможно. Я хотел бы когда-нибудь использовать этот вопрос либо в качестве начальной реализации, либо в качестве контрпримера для PEP. Я не уверен, что согласен с вашей оценкой намерений дизайна, поскольку нелокальные переменные находятся в python3. nonlocal очень похож, но, к сожалению, не очень удобен в качестве статических переменных в стиле C. (python.org/dev/peps/pep-3104) Может быть, кто-то сможет справиться с этой задачей, используя python3.0? ...Я не могу вставлять абзацы. - person bukzor; 24.10.2009
comment
@buzkor, я довольно хорошо знаю намерения дизайна, поскольку я был одной из сторон, обсуждавших их по мере их формирования (хотя окончательное решение остается за Гвидо). nonlocal — это просто еще один вариант global (мне тоже не нравится, я думаю, что они противоречат большей части довольно последовательного дизайна Python, и использование специального ключевого слова для создания квалифицированных имен было бы намного лучше — но ясно Я проиграл эти дебаты с точки зрения окончательных решений Гвидо). Я могу отредактировать свой ответ, чтобы показать, как это будет работать. - person Alex Martelli; 24.10.2009
comment
@buzkor, отредактировал мой ответ, чтобы показать, как все становится гладким и надежным, как только вы отказываетесь от тяги к barenames, означающим вещи, отличные от локальных переменных, - действительно необоснованное требование, независимо от дизайнерских сбоев глобальных и нелокальных. Пространства имен — это отличная идея, давайте сделаем больше таких: использование квалифицированных имен означает, что я делаю еще одно пространство имен, а не пытаюсь втиснуть его в существующее пространство имен (то, которое используется для локальных переменных, т. е. barenames). ) вещи, которые не принадлежат туда. Никаких недостатков, все преимущества, все Pythonic, гладко и мило. - person Alex Martelli; 24.10.2009
comment
@Alex Martelli: это решение не работает, если f возвращает закрытие. - person Anand Chitipothu; 28.10.2009
comment
@Anand: отлично работает с закрытием или чем-то еще, конечно, если декоратор обертывает любую функцию, которая на самом деле использует статику! - person Alex Martelli; 28.10.2009
comment
@AlexMartelli Я только что заметил, что вы сравниваете 2.* с 2.*, что не имеет смысла. Я полагаю, вы имеете в виду 3. * во втором случае? - person bukzor; 10.04.2013

Вот действительно простое решение, которое работает так же, как обычные статические переменные Python.

def static(**kwargs):
  def wrap(f):
    for key, value in kwargs.items():
      setattr(f, key, value)
    return f
  return wrap

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

@static(a=0)
def foo(x):
  foo.a += 1
  return x+foo.a

foo(1)  # => 2
foo(2)  # => 4
foo(14) # => 17

Это более точно соответствует обычному способу выполнения статических переменных python.

def foo(x):
  foo.a += 1
  return x+foo.a
foo.a = 10
person DRayX    schedule 20.09.2011
comment
Думаю, это больше недействительно, так как вопрос был изменен, чтобы включить это решение, но указывает, что это становится громоздким с длинными именами функций. - person DRayX; 16.02.2012

Вы можете сделать что-то подобное (но я не тестировал это широко, использовал CPython 2.6):

import types

def static(**dict):
    def doWrap(func):
        scope = func.func_globals
        scope.update(dict)
        return types.FunctionType(func.func_code, scope)
    return doWrap

# if foo() prints 43, then it's wrong
x = 42

@static(x = 0)
def foo():
   global x
   x += 1
   print(x)

foo() # => 1
foo() # => 2

Это требует объявления этих переменных как глобальных и затеняет глобальные переменные верхнего уровня, но в остальном должно работать. Хотя насчет производительности не уверен.

person Cat Plus Plus    schedule 24.10.2009

Как насчет этого, без декораторов?

class State(dict):
    """Object interface to dict."""
    def __getattr__(self, name):
        try:
            return self[name]
        except KeyError:
            raise AttributeError, name

def f(d=State(x=0)):
    d.x += 1
    return d.x

И вот он в действии:

>>> f()
1
>>> f()
2
>>> f()
3
person Community    schedule 23.10.2009
comment
Неинформированный пользователь может попытаться использовать этот аргумент и нарушить работу функции. В этом случае мы создаем аргумент, который никогда не предназначался для использования в качестве аргумента. Я понимаю, что это довольно стандартная практика в Python, но я не думаю, что так должно быть. Это истинная цель моего вопроса. - person bukzor; 24.10.2009

Вот кое-что, что может быть намного яснее. Это не связано с декораторами или взломом.

class F( object ):
    def __init__( self ):
        self.x= 0
    def __call__( self ):
        self.x += 1
        print self.x

f= F()

Теперь у вас есть функция f со статической переменной.

f() #prints 1
f() #prints 2
person S.Lott    schedule 23.10.2009
comment
Это работает, не используя barenames — проблема полностью связана с желанием использовать barnames, которые изменяются внутри функции, как нечто иное, чем локальные имена этой функции (если только функция явно не определяет их как глобальные); это не проблема с полными именами (такими как self.x или что-то еще, что не пустое имя). - person Alex Martelli; 23.10.2009
comment
@Alex Martelli: Хотя верно то, что в вопросе заданы barenames, я полагаю, что barenames — это ошибка и не стоит усилий по написанию сложного декоратора. Локальные Bar-имена так же плохи, как и глобальные — они визуально лишены области действия. - person S.Lott; 23.10.2009
comment
Главное было сделать функцию красивой — такой же простой, как и дизайн. Решение f.x, найденное ранее, кажется лучше. - person bukzor; 24.10.2009

Когда вам нужно сохранить состояние между вызовами функции, почти всегда лучше использовать генератор/сопрограмму или объект. Поскольку вы хотите использовать «голые» имена переменных, вам понадобится версия сопрограммы.

# the coroutine holds the state and yields rather than returns values
def totalgen(x=0, y=0, z=0):
    while True:
       a, b, c = (yield x, y, z)
       x += a
       y += b
       z += c

# the function provides the expected interface to callers
def runningtotal(a, b, c, totalgen=totalgen()):
    try:
        return totalgen.send((a, b, c))    # conveniently gives TypeError 1st time
    except TypeError:
        totalgen.next()                    # initialize, discard results
        return totalgen.send((a, b, c))

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

person kindall    schedule 20.09.2011
comment
Интересно, есть ли способ воплотить эту идею в декораторе. - person bukzor; 21.09.2011
comment
Вероятно, не обошлось без большого количества взломов байт-кода, и даже тогда было бы сложно, если бы в функции было более одного оператора return. Однако, если у меня будет немного времени, похоже, это будет забавный проект. - person kindall; 21.09.2011
comment
... если подумать, если бы я собирался заняться хакерством с байт-кодом, я бы, вероятно, не пошел по этому пути к реализации статики; есть более простые способы. - person kindall; 27.09.2011

Небольшая подстройка другого ответа:

def static(**kwargs):
    def decorator(func):
        return type(func)(func.func_code, dict(func.func_globals, **kwargs))
    return decorator

message = "goodbye, world!"
@static(message="hello, world!")
def hello(): print message

hello()

Мне показалось странным переопределять встроенное имя именем аргумента функции, поэтому я изменил **dict на более каноническое **kwargs. Я также сохранил несколько строк, и IMO сделал код чище, создав новый словарь с dict(the_original, **the_updates). Наконец, я сэкономил несколько строк, обратившись к конструктору функции через type(func), а не через импорт --- типы и объекты класса являются фабричными методами, так что используйте их!

Я также удалил объявление global. Это работает до тех пор, пока вы не перепривязываете переменную, то есть удаление global фактически делает указатель (но не объект) доступным только для чтения. Если вы используете его таким образом, возможно, let будет лучшим именем, чем static для введенной таким образом привязки.

person Jonas Kölker    schedule 10.04.2013
comment
Я считаю, что это дает неожиданные результаты в случае изменяемых глобальных переменных. Если функция печатает глобальный x, и x меняет значение между определением и вызовом, функция все равно будет печатать старое значение x, я полагаю, поскольку вы берете копию глобальных переменных. - person bukzor; 10.04.2013
comment
При попытке использовать этот декоратор в сценарии из ОП я получаю x += 1 UnboundLocalError: local variable 'x' referenced before assignment. - person bukzor; 10.04.2013
comment
хорошее замечание о глобалах! Как я уже сказал, удаление объявления global работает до тех пор, пока переменная доступна только для чтения, что нарушает ваш пример. Однако это может сработать, если у x есть метод iadd. - person Jonas Kölker; 11.04.2013
comment
Мой пример на самом деле является вопросом, поэтому странно говорить, что он нарушает ваш ответ. - person bukzor; 12.04.2013