可以将 "with" 块创建为只包含一次的 运行 代码吗?

Can a "with" block be created to run code it contains only once?

不管 method/functions 被调用多少次,只执行一段代码一次的最短且最好的方法是什么?

代码在方法中。 举个例子:

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

当然,这太簿记了……而且可扩展性不强。

我希望它更像 with

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

function/method 周围的代码必须在每次调用时执行...只有 once 仅在第一次执行。

我正在使用 2.7。

once 可能涉及实现,但用法必须简单 w/o 更多逻辑和簿记。

我主要在调试时需要这个,其中某些方法被多次调用,但我想打印一次或两次调试信息...

'warnings' 有不完全相同但相似的功能……不知道他们是怎么做到的。

所以我不确定这个问题的动机是否只是想要一种语法上令人愉悦的优雅方法......或者是否对在跳过的函数中使用一次性代码的性能有任何担忧每次调用它。我只是假设这是一个性能问题(这有点没有实际意义 - 请参阅底部的注释)。

如果一次性行为可以在代码的其他地方完成并从函数中删除,那是一个可以考虑的选项。但如果不能...

例如,优化函数 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 的函数检查变量然后 returns 与替换自身的函数之间不会有太大的性能差异。

您可以在下面看到,处理一次性行为的第二个最有效的方法就是在需要它的函数中对其进行编码,并检查一个变量以查看它是否已被调用(下面的最后一个示例)

>>> # 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 -------]]]
...

总结...

  • 另一个函数中的一次性行为。

    • 一次函数检查一个 var 看它是否完成然后 returns。

      • 一次性完成后每次调用 foo() 浪费 7 条指令。
    • 一次性函数在任务完成后被空操作取代。

      • 一次性完成后每次调用 foo() 浪费 5 条指令。
  • foo() 本身检查一个变量,然后跳过一次性块。

    • 2 条检查 var 的指令,如果任务已经完成则跳转。

如果期望获得任何显着的性能提升,尝试以上述方式优化代码可能不值得。脚本代码已经很慢了,绕过一次性行为的 2-7 条指令并不重要。如果某个函数已被确定为减慢一切,那么它承载的算法需要重新设计,或者可以用本机代码替换,或者两者兼而有之。

你考虑过decorator吗?类似于:

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

我认为这是管理上述全局状态的更好方法。

您可以使用函数 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

我认为您找不到任何 'decent' 实现 with 语句(上下文管理器)。 只需检查以下问题:Skipping execution of -with- block

上下文管理器在进入代码块之前和离开代码块之后执行代码,但它们始终执行代码块,除非进行了一些非常繁重的黑客攻击。

但是,您可能会通过 if 语句和内省或关键参数得到与您正在寻找的东西类似的东西。请注意内省 非常非常慢 所以不要在性能关键代码中使用它。对于调试,如果循环不经常执行可能没问题。

你实现了一个神奇的功能,它可以确定它在源代码中的哪个位置被调用并进行簿记,它 returns 对于每个调用者的位置第一次为 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

与 martineau 的解决方案比较:

  • 我建议的解决方案会慢得多。这是否是一个问题取决于上下文,但请进一步查看我的替代解决方案。
  • 即使在条件代码中,您也可以对每个函数进行多次检查,它们将被单独评估
  • 您不必修饰函数即可使用 first_time()
  • first_time() 可以在模块级别使用,所以基本上可以在代码的任何地方使用。
  • 您可以通过 first_state.clear()
  • 进行重置
  • 通过将线程 ID 添加到键元组中,您可以允许每个线程调用一次

更快更丑陋的替代方案 对于下面的紧密循环,解决方案大约快 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

此替代实现允许为性能关键代码显式传递 unique 键。所以这些显式参数避免了自省的使用,反而让调用代码更难看。对于性能不关键的代码,您不传递密钥,它的行为与我最初建议的解决方案相同。

示例:

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)

类似于 gelonida,但使用 dict 以便您可以自定义在它之前调用标记函数的次数 returns 一个假值。

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