Можно ли создать блок with для запуска кода, который он содержит только один раз? - PullRequest
0 голосов
/ 02 марта 2020

Какой самый короткий и лучший способ выполнить фрагмент кода только один раз, независимо от того, сколько раз вызывается метод / функции?

Код находится внутри метода. Просто для примера:

once = 0
def fun():
  if once == 0 : 
    print 234
    once += 1

Конечно, это слишком много бухгалтерии ... и не очень расширяемое.

Я бы хотел, чтобы это было больше похоже на with.

 def fun():
   ......
   once : ... code ..
   ......

код функции / метода должен выполняться при каждом вызове ... только once выполняется только в первый раз.

Я использую 2.7.

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

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

' warnings ' не совсем точно, но похоже , функциональность как-то ... не знаю, как они это делают.

Ответы [ 5 ]

1 голос
/ 04 марта 2020

Не думаю, что вы сможете найти какую-либо «приличную» реализацию с помощью оператора with (менеджер контекста). Просто проверьте следующий вопрос: Пропуск выполнения -with- block

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

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

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

На мой взгляд, имя first_time() может быть более понятным, чем once(), но это всего лишь деталь.

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

def f(a):
    if first_time():
        print("first time f was called (a=%s)" % a)
    return 2 * a

def g(a):
    if first_time():
        print("first time g was called (a=%s)" % a)
    return 3 * a

for v in (1, 2):
    rslt = f(v)
    print("F(%d) = %d" % (v, rslt))

g(3)
g(4)

Для python2 это будет:

import inspect
first_state = set()
def first_time():
    curframe = inspect.currentframe()
    calframe = inspect.getouterframes(curframe, 2)
    calfi = calframe[1]
    key = calfi[1], calfi[2]
    # print("K", key)  # uncomment for debugging
    if key in first_state:
        return False
    first_state.add(key)
    return True

Для python3 Это будет:

import inspect
first_state = set()
def first_time():
    curframe = inspect.currentframe()
    calframe = inspect.getouterframes(curframe, 2)
    calfi = calframe[1]
    key = calfi.filename, calfi.lineno
    3 print("K", key)  # uncomment for debugging
    if key in first_state:
        return False
    first_state.add(key)
    return True

Сравнение с решением Мартино:

  • Мое предлагаемое решение будет намного медленнее. Это зависит от контекста, является ли это проблемой, но посмотрите на мое альтернативное решение ниже.
  • Вы можете иметь несколько проверок для каждой функции даже в условном коде, они будут оцениваться индивидуально
  • you не нужно украшать функцию для использования first_time ()
  • first_time () можно использовать на уровне модуля, поэтому в основном в любом месте вашего кода.
  • вы сможете выполнить сброс, выполнив first_state.clear ()
  • добавив идентификатор потока к кортежу ключа, который можно разрешить вызывать один раз для потока

Более быстрая и уродливая альтернатива Для жесткой Приведенное ниже решение l oop примерно в 20.000 (двадцать тысяч) раз быстрее

Я только что измерил небольшой пример с python 3.6

import inspect
first_state = set()
def first_time(*key):
    if not key:
        curframe = inspect.currentframe()
        calframe = inspect.getouterframes(curframe, 2)
        calfi = calframe[1]
        key = calfi[1], calfi[2]
    # print("K", key)  # uncomment for debugging
    if key in first_state:
        return False
    first_state.add(key)
    return True

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

Пример:

import time
t0 = time.time()
c = 0
for v in range(1000):
    if first_time(__file__, "critical_1"):
        print("first time within performance critical loop")
    c += v
t - time.time() - t0
print("Required time: %f" % t)

Реализация print_once:

Если это только для печати отладочных сообщений, то, возможно, реализуйте непосредственно print_once()

import inspect
print_once_state = set()
def print_once(msg, *args, key=None):
    if not key:
        curframe = inspect.currentframe()
        calframe = inspect.getouterframes(curframe, 2)
        calfi = calframe[1]
        key = calfi[1], calfi[2]
    if key not in print_once_state:
        if msg is None:
            print("entered %s first time" % (key,))
        else:
            print(msg, *args)
    print_once_state.add(key)

и пример:

import time
t0 = time.time()
c = 0
for v in range(1000):
    print_once("in loop: c = ", c, key=(__file__, "critical_1"))
    c += v
t - time.time() - t0
print("Required time: %f" % t)

1 голос
/ 02 марта 2020

Рассматривали ли вы декоратор ? Что-то вроде:

import functools

class once:
    """
    Function decorator to allow only one single execution of 
    a function; however, always return the result of that one
    and only execution.
    """
    def __init__(self, wrapped):
        self.wrapped = wrapped
        functools.update_wrapper(self, wrapped)

    def __call__(self, *args, **kwargs):
        if not hasattr(self, "retval"):
            self.retval = self.wrapped(*args, **kwargs)
        return self.retval

Затем вы можете украсить свои функции следующим образом:

Python 2.7.17 (default, Oct 20 2019, 14:46:50) 
[GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> @once
... def fun():
...     print 234
... 
>>> fun()
234
>>> fun()
>>> 

Я бы посмотрел на это как на лучший способ управления глобальным состоянием, упомянутым выше. .

1 голос
/ 02 марта 2020

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

import functools

def add_once(f):
    d = {'once': 1}
    sentinel = object()

    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        g = f.__globals__

        oldvalue = g.get('once', sentinel)
        g['once'] = d['once']

        try:
            res = f(*args, **kwargs)
        finally:
            if oldvalue is sentinel:
                del g['once']
            else:
                g['once'] = oldvalue

        d['once'] = 0
        return res
    return wrapped

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

@add_once
def func():
    print('Starting')
    if once:
        print('EXECUTED ONE TIME ONLY')
    print('Ending')

func()
func()

Вывод:

Starting
EXECUTED ONE TIME ONLY
Ending
Starting
Ending
1 голос
/ 02 марта 2020

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

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

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

>>> def foo():
...     global foo
...     print("doing some lengthy initialization only needed once.")
...     print("onto other things...")
...     foo = _foo
...     
>>> def _foo():
...     print("onto other things...")
...   

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

Переопределение одноразовой функции - это стратегия, которую можно выполнить внутри самой одноразовой функции, например:

>>> def slow_initialization():
...     global slow_initialization
...     print("taking forever to update database - once...")
...     slow_initialization = _disable_slow_initialization
...     
>>> def _disable_slow_initialization():
...     pass
...     
>>> def foo():
...     slow_initialization()
...     print("now doing other stuff...")
...     
>>> foo()
taking forever to update database - once...
now doing other stuff...
>>> 
>>> foo()
now doing other stuff...
>>> 

Первый пример, очевидно, оптимален с точки зрения инструкций.

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

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

>>> # foo() calling a one-time init function that checks a var then 
>>> # returns. The init function incurs 4 instructions after the one 
>>> # time behavior has been done (not including the jump to it from foo().
>>> def init_strategy_1():
...     if _initialized:
...         return
...     print("taking forever to update database - once...")
>>>
>>> dis.dis(init_strategy_1)
  2           0 LOAD_GLOBAL              0 (_initialized)
              2 POP_JUMP_IF_FALSE        8

  3           4 LOAD_CONST               0 (None)
              6 RETURN_VALUE
      [[[------ truncated -------]]]
>>>
>>> # If the one-time function were replaced by a no-op function, the
>>> # cost is just two instructions to jump back to foo()
>>> def init_strategy_2():
...     pass
>>>
>>> dis.dis(init_strategy_2)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
>>> 
>>>
>>> # Placing the one-time code in another function incurs a few 
>>> # instructions to call the function from within foo().
>>> def foo():
...     init_strategy()
...     print("doing other things...")
... 
>>> dis.dis(foo)

  2           0 LOAD_GLOBAL              0 (init_strategy)
              2 CALL_FUNCTION            0
              4 POP_TOP
>>>
>>>
>>> # Instructionwise, the most efficient way to implement one-time
>>> # behavior is to check a variable within foo() and skip the block.
>>> def foo():
...     if not _initialized:
...         print("performing initialization tasks...")
...     print("Now doing other things...")
...
>>> dis.dis(foo)
  2           0 LOAD_GLOBAL              0 (_initialized)
              2 POP_JUMP_IF_TRUE        12
       [[[------ truncated -------]]]
...

суммирование ...

  • Одноразовое поведение в другой функции.

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

      • 7 инструкций, потраченных впустую за вызов foo () после одноразового выполнения.
    • Одноразовая функция заменяется на no-op после выполнения задачи.

      • 5 разовых инструкций за вызов foo () после одноразового выполнения.
  • foo () сама проверяет переменную, а затем пропускает одноразовый блок.

    • 2 инструкции, чтобы проверить переменную и затем перейти, если задача уже выполнена.

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

0 голосов
/ 04 марта 2020

Аналогично gelonida, но с использованием dict, так что вы можете настроить количество раз, которое вызывается дозорной функцией, прежде чем она возвращает ложное значение.

import traceback
import threading

_once_upon_lock = threading.Lock()
_once_upon_dict = {}

def once_upon_a_time(num_tries=1):
    """Use knowledge of where this function is called in source
    code to count how many times it has been called. When 
    num_tries is exceeded, returns 0 so a simple `if` can be
    used to control program flow.
    """
    # lock protects multithreading
    parent = traceback.extract_stack(limit=2)[0]
    with _once_upon_lock:
        # get filename + line number of the caller. This makes
        # every usage of this function in source code unique.
        key = "{}:{}".format(parent[0], parent[1])
        # see how many times this call has been made, defaulting 
        # to zero
        new_val = _once_upon_dict.setdefault(key, 0) + 1
        # times up? return a falsy value, else the number 
        # of times we've made this call.
        if new_val > num_tries:
            return 0
        else:
            _once_upon_dict[key] = new_val
            return new_val


def bar():
    print "bar", once_upon_a_time() # note: calls on different lines
    print "bar", once_upon_a_time() #       are independent

def foo():
    bar()

def main():
    for i in range(3):
        foo()
    for i in range(3):
        if once_upon_a_time(2):
            print("main")
    print _once_upon_dict.keys()

main()
...