Python: декоратор статических переменных - PullRequest
3 голосов
/ 23 октября 2009

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

Я понимаю, что существуют разные способы создания статических переменных в 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.

Ответы [ 8 ]

8 голосов
/ 23 октября 2009

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

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

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

Редактировать : Если вы готовы отказаться от настойчивости в использовании голых имен для своей "статики", то вдруг вы больше не сражаетесь с 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, чтобы указать другие виды переменных, которые не ' t локальные и, следовательно, не должны использовать голые имена ... ну, вы можете реализовать специальный контейнер globvar в тех же строках, что и выше static, даже без необходимости декорирования, хотя я не уверен это вполне выполнимо для случая nonlocal [возможно с некоторым украшением и малейшим количеством черной магии ...; =)]).

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

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

x = f()
x()
x()

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

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

7 голосов
/ 24 октября 2009

Вот декоратор, который, кажется, работает. Обратите внимание, что это требует возврата 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 ...

EDIT:

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

EDIT снова: изменено на несколько секунд быстрее. Редактировать 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 значительно замедляет его.

6 голосов
/ 20 сентября 2011

Вот действительно простое решение, которое работает так же, как обычные статические переменные 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
3 голосов
/ 25 октября 2009

Вы могли бы сделать что-то подобное (но я не тестировал это подробно; использовал 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

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

1 голос
/ 23 октября 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
0 голосов
/ 10 апреля 2013

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

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 с dict(the_original, **the_updates). Наконец, я сохранил несколько строк, получив доступ к конструктору функции через type(func), а не к типу импорта --- и объекты класса являются фабричными методами, поэтому используйте их!

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

0 голосов
/ 20 сентября 2011

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

# 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))

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

0 голосов
/ 23 октября 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
...