2013-11-20 2 views
1

Я часто делаю интерактивную работу на Python, которая включает в себя некоторые дорогостоящие операции, которые я не хочу часто повторять. Обычно я запускаю любой файл Python, над которым я часто работаю.Запомните функцию, чтобы она не была сброшена при повторном запуске файла в Python

Если я пишу:

import functools32 

@functools32.lru_cache() 
def square(x): 
    print "Squaring", x 
    return x*x 

Я получаю такое поведение:

>>> square(10) 
Squaring 10 
100 
>>> square(10) 
100 
>>> runfile(...) 
>>> square(10) 
Squaring 10 
100 

То есть, повторный запуск файла очищает кэш. Это работает:

try: 
    safe_square 
except NameError: 
    @functools32.lru_cache() 
    def safe_square(x): 
     print "Squaring", x 
     return x*x 

но когда функция долго он чувствует себя странно иметь свое определение внутри try блока. Я могу сделать это вместо того, чтобы:

def _square(x): 
    print "Squaring", x 
    return x*x 

try: 
    safe_square_2 
except NameError: 
    safe_square_2 = functools32.lru_cache()(_square) 

, но он чувствует себя довольно надуманный (например, в призыве декоратор без знака «@»)

Есть простой способ справиться с этим, что-то вроде:

@non_resetting_lru_cache() 
def square(x): 
    print "Squaring", x 
    return x*x 

?

+0

Ваши изменения на самом деле не работают, если только вы не перезапускаете сценарий в одном сеансе интерпретатора Python, например, 'execfile'-ing. – abarnert

+0

См. Ниже; Я удалил слово «persistent», чтобы быть более четким. Повторное выполнение сценария в одном сеансе интерпретатора Python - это то, что я снимаю здесь. – kuzzooroo

+0

Это очень странный способ запуска скриптов, но я понимаю, почему вы хотите это сделать. Позвольте мне написать еще один ответ с некоторыми комментариями. – abarnert

ответ

6

Запись сценария, который будет выполняться несколько раз в том же сеансе, - это странная вещь.

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

Однако вы сделали вещи более уродливыми, чем необходимо.

Во-первых, вы можете просто сделать это:

@functools32.lru_cache() 
def _square(x): 
    print "Squaring", x 
    return x*x 

try: 
    safe_square_2 
except NameError: 
    safe_square_2 = _square 

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


Там это потенциальная проблема здесь с рекурсивными функциями. Это уже связано с тем, как вы работаете, и кеш никак не добавляет к нему, но вы можете только заметить из-за кеша, поэтому я объясню его и покажу, как его исправить. Рассмотрим эту функцию:

@lru_cache() 
def _fact(n): 
    if n < 2: 
     return 1 
    return _fact(n-1) * n 

При повторном EXEC сценарий, даже если у вас есть ссылка на старый _fact, это будет в конечном итоге вызова нового _fact, потому что это доступ к _fact как глобальное имя. Это не имеет никакого отношения к @lru_cache; удалите это, и старая функция будет еще в конечном итоге вызывает новый _fact.

Но если вы используете переименование трюк выше, вы можете просто позвонить переименованный вариант:

@lru_cache() 
def _fact(n): 
    if n < 2: 
     return 1 
    return fact(n-1) * n 

Теперь старый _fact будет вызывать fact, который до сих пор старый _fact. Опять же, это работает одинаково с или без декодера кеша.


Помимо этого первоначального трюка, вы можете разложить весь этот узор в простой декоратор. Я объясню шаг за шагом ниже или посмотрю this blog post.


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

Вы не можете передавать имена как объекты в Python. И вы не хотите использовать отвратительный фрейм-хак, чтобы справиться с этим. Поэтому вам придется передавать имена вокруг как строки. икэ это:

globals().setdefault('fact', _fact) 

globals функция просто возвращает глобальный словарь текущего осциллографа. Который является dict, что означает, что он имеет метод setdefault, что означает, что он установит глобальное имя fact на значение _fact, если оно еще не имеет значения, но ничего не сделает, если это произойдет. Это именно то, что вы хотели. (Вы также можете использовать setattr на текущем модуле, но я думаю, что этот способ подчеркивает, что сценарий должен быть (неоднократно) выполнен в чужой области, не используется в качестве модуля.)

Итак, вот что завернуто в функции:

def new_bind(name, value): 
    globals().setdefault(name, value) 

... который вы можете превратить это в декоратора почти тривиальным:

def new_bind(name): 
    def wrap(func): 
     globals().setdefault(name, func) 
     return func 
    return wrap 

, который можно использовать, как это:

@new_bind('foo') 
def _foo(): 
    print(1) 

Но подождите, есть еще! func, что new_bind получает будет __name__, не так ли? Если вы будете придерживаться именования, как и о том, что «частное» имя должно быть «общественное» имя с _ приставкой, мы можем сделать это:

def new_bind(func): 
    assert func.__name__[0] == '_' 
    globals().setdefault(func.__name__[1:], func) 
    return func 

И вы можете увидеть, где это происходит:

@new_bind 
@lru_cache() 
def _square(x): 
    print "Squaring", x 
    return x*x 

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


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


И, конечно, если вы хотите, чтобы объединить эти два декораторов в единое целое, это тривиально, чтобы сделать это, и назвать его non_resetting_lru_cache.

Однако я бы сохранил их отдельно. Я думаю, что более очевидно, что они делают. И если вы когда-нибудь захотите обернуть еще один декоратор вокруг @lru_cache, вы, вероятно, все еще захотите, чтобы @new_bind был самым внешним декоратором, не так ли?


Что делать, если вы хотите поместить new_bind в модуль, который можно импортировать? Тогда это не сработает, потому что оно будет ссылаться на глобалы этого модуля, а не на тот, который вы сейчас пишете.

Вы можете исправить это, явно передав свой globals dict или ваш объект модуля или имя вашего модуля в качестве аргумента, например @new_bind(__name__), чтобы он мог найти ваши глобальные переменные вместо своего. Но это уродливо и повторяемо.

Вы также можете исправить это с помощью уродливого фрейма. По крайней мере, в CPython, sys._getframe() может быть использован для получения кадра вызывающего абонента, и frame objects есть ссылка на их глобал пространство имен, так:

def new_bind(func): 
    assert func.__name__[0] == '_' 
    g = sys._getframe(1).f_globals 
    g.setdefault(func.__name__[1:], func) 
    return func 

Обратите внимание на большом ящике в документации, которая говорит вам это «деталь реализации ", который может применяться только к CPython и предназначен только для внутренних и специализированных целей. Возьмите это всерьез. Всякий раз, когда у кого-то есть классная идея для stdlib или встроенных функций, которые могут быть реализованы в чистом Python, но только с использованием _getframe, он обычно обрабатывается почти так же, как идея, которая вообще не может быть реализована в чистом Python. Но если вы знаете, что делаете, и хотите использовать это, и вам будут нужны только текущие версии CPython, это сработает.

+0

Извиняется за то, что вы написали что-то в два раза дольше, чем «постоянный» ответ, хотя конечный результат - 6 строк довольно тривиального кода (в отличие от 239 строк, которые были достаточно сложными, и я чувствовал необходимость их git). Но я думаю, стоило пройти все шаги, чтобы добраться туда. Я мог бы вытащить весь средний материал и превратить его в сообщение в блоге где-нибудь и просто ссылку на него и подытожить его здесь, если кто-то подумает, что это слишком много для ответа. – abarnert

+1

Умный! Я бы не подумал о создании нового кеша, но не использовал его. Чтобы запустить мой Python (2.7), мне пришлось изменить «rebind \ n» на «rebind() \ n». – kuzzooroo

+0

@kuzzooroo: Извините, да. Ранняя функция «rebind» - это функция, возвращающая декоратор; более поздний может быть просто декоратором, поэтому вам это не понадобится, но не без меня редактирования кода. Я это сделаю; спасибо, что указали это. – abarnert

5

В stdlib нет persistent_lru_cache. Но вы можете построить один довольно легко.

The functools source связан непосредственно с the docs, так как это один из тех модулей, который так же полезен, как и пример кода, так как он предназначен для непосредственного использования.

Как вы можете видеть, кеш всего лишь dict. Если вы замените, что, скажем, в shelf, он будет настойчив автоматически:

def persistent_lru_cache(filename, maxsize=128, typed=False): 
    """new docstring explaining what dbpath does""" 
    # same code as before up to here 
    def decorating_function(user_function): 
     cache = shelve.open(filename) 
     # same code as before from here on. 

Конечно, это работает только, если ваши аргументы являются строками. И это может быть немного медленным.

Таким образом, вы можете вместо этого сохранить его как в памяти dict, а просто писать код, соленья его в файл atexit, и восстанавливает его из файла, если он присутствует при запуске:

def decorating_function(user_function): 
     # ... 

     try: 
      with open(filename, 'rb') as f: 
       cache = pickle.load(f) 
      except: 
       cache = {} 
     def cache_save(): 
      with lock: 
       with open(filename, 'wb') as f: 
        pickle.dump(cache, f) 
     atexit.register(cache_save) 

     # … 
     wrapper.cache_save = cache_save 
     wrapper.cache_filename = filename 

Или, если вы хотите, чтобы он записывал все N новых значений (чтобы вы не потеряли весь кеш, скажем, _exit или segfault или кто-то вытащил шнур), добавьте это во вторую и третью версии wrapper, справа после misses += 1:

  if misses % N == 0: 
       cache_save() 

См here для рабочей версии все до этого момента (с использованием save_every в качестве аргумента «N», и недобросовестный 1, который вы, вероятно, не хотят в реальной жизни).

Если вы хотите быть действительно умным, возможно, скопируйте кеш и сохраните его в фоновом потоке.

Вы можете продлить cache_info включить что-то вроде количества записей в кэш, количество промахов с момента последней кэш записи, количество записей в кэше при запуске, ...

И есть, вероятно, другие способы улучшения это.

От быстрого теста с save_every=1, это делает кэш на обоих get_pep и fib (от functools Docs) настойчивым, без измеримого замедления до get_pep и очень небольшого спада до fib в первый раз (обратите внимание, что fib(100) имеет 100097 хитов против 101 промахов ...), и, конечно, большое ускорение до get_pep (но не fib) при повторном запуске. Итак, что вы ожидаете.

+0

Я изменил «@persistent ...» на «@non_resetting ...», чтобы быть более четким. Когда кеш остается включенным, если я покину Python и перезагружаюсь, это не ключ; ключ заключается в том, что, когда я повторно запускаю свой файл в том же окне консоли, я не теряю все свое состояние. Я считаю, что полка будет работать, если бы мои ключи были целыми, с побочным эффектом, который я получил бы _real_ persistence. – kuzzooroo

0

Я не могу сказать, что я просто не буду использовать «уродливый фрейм-код» @ abarnert, но вот версия, которая требует, чтобы вы передавали глобальные символы звонящего модуля. Я думаю, что стоит опубликовать, учитывая, что decorator functions with arguments are tricky and meaningfully different from those without arguments.

def create_if_not_exists_2(my_globals): 
    def wrap(func): 
     if "_" != func.__name__[0]: 
      raise Exception("Function names used in cine must begin with'_'") 
     my_globals.setdefault(func.__name__[1:], func) 
     def wrapped(*args): 
      func(*args) 
     return wrapped 
    return wrap 

Что вы можете использовать в другом модуле, как это:

from functools32 import lru_cache 
from cine import create_if_not_exists_2 

@create_if_not_exists_2(globals()) 
@lru_cache() 
def _square(x): 
    print "Squaring", x 
    return x*x 

assert "_square" in globals() 
assert "square" in globals() 
0

я приобрел достаточно знакомы с декораторов во время этого процесса, что мне было комфортно, принимая качели на решение проблемы другим способом :

from functools32 import lru_cache 

try: 
    my_cine 
except NameError: 
    class my_cine(object): 
     _reg_funcs = {} 

     @classmethod 
     def func_key (cls, f): 
      try: 
       name = f.func_name 
      except AttributeError: 
       name = f.__name__ 
      return (f.__module__, name) 

     def __init__(self, f): 
      k = self.func_key(f) 
      self._f = self._reg_funcs.setdefault(k, f) 

     def __call__(self, *args, **kwargs): 
      return self._f(*args, **kwargs) 


if __name__ == "__main__": 
    @my_cine 
    @lru_cache() 
    def fact_my_cine(n): 
     print "In fact_my_cine for", n 
     if n < 2: 
      return 1 
     return fact_my_cine(n-1) * n 

    x = fact_my_cine(10) 
    print "The answer is", x 

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

  1. Вы должны знать, какие атрибуты нужно искать для ассоциирования имени с функцией. Мой первый удар в нем смотрел только на имя func_name, которое не удалось, когда передано объект lru_cache.
  2. Сброс функции болезнен: del my_cine._reg_funcs[('__main__', 'fact_my_cine')], и качели, которые я взял при добавлении __delitem__, не увенчались успехом.
Смежные вопросы