【问题标题】:Identifying equivalent varargs function calls for memoization识别等效的可变参数函数调用以进行记忆
【发布时间】:2013-01-15 07:43:02
【问题描述】:

我正在使用this decorator 的变体进行记忆:

# note that this decorator ignores **kwargs
def memoize(obj):
    cache = obj.cache = {}

    @functools.wraps(obj)
    def memoizer(*args, **kwargs):
        if args not in cache:
            cache[args] = obj(*args, **kwargs)
        return cache[args]
    return memoizer

我想知道,有没有一种基于argskwargs 的合理记忆方法,特别是在两个函数调用指定的参数在位置上和通过关键字分配不同但具有完全相同的参数的情况下?

【问题讨论】:

    标签: python decorator memoization


    【解决方案1】:

    此解决方案使用检查模块来提取位置参数和关键字参数的参数名称。然后在名称:值对的有序元组上执行记忆查找。它可以容忍作为位置参数和关键字参数传递的参数。如果有多余的位置参数,它们会按照它们出现在单独元组中的顺序存储。

    This uses Michele Simionato's decorator package 以确保保留函数签名。因为它检查正在记忆的函数的argspec,所以如果与不保留argspec 的装饰器实现组合,它将失败。

    from decorator import decorator as robust_decorator
    
    def argument_signature(function,*args,**kwargs):
        '''
        Convert the function arguments and values to a unique set. 
        Throws ValueError if the provided arguments cannot match argspec.
        '''
        named_store = {} # map from parameter names to values 
        named,vargname,kwargname,defaults = inspect.getargspec(function)
        available = zip(named,args)
        nargs     = len(available)
        ndefault  = len(defaults) if not defaults is None else 0
        nnamed    = len(named)
        # All positional arguments must be filled
        nmandatory = nnamed - ndefault
        if nargs<nmandatory: raise ValueError('Not enough positional arguments')
        # Assign available positional arguments to names    
        for k,v in available:
            if k in named_store: raise ValueError('Duplicate argument',k)
            named_store[k] = v
        # If not all arguments are provided, check **kwargs and defaults
        ndefaulted   = max(0,nnamed - nargs)
        default_map = dict(zip(named[-ndefault:],defaults)) if ndefault>0 else {}
        if ndefaulted>0:
            for k in named[-ndefaulted:]:
                if k in named_store: raise ValueError('Duplicate argument',k)
                named_store[k] = kwargs[k] if k in kwargs else default_map[k]
                if k in kwargs: del kwargs[k]
        # Store excess positional arguments in *vargs if possible
        vargs = None
        if len(args)>nnamed:
            if vargname is None:
                raise ValueError('Excess positional arguments, but function does not accept *vargs!')
            vargs = args[nnamed:]
        # Store excess keyword arguments if the function accepts **kwargs
        if len(kwargs):
            if kwargname is None:
                raise ValueError("Excess keyword arguments, but function does not accept **kwargs!")
            for k in kwargs:
                if k in named_store: raise ValueError('Duplicate argument',k)
                named_store[k] = kwargs[k]
        # Construct a tuple reflecting argument signature
        keys  = sorted(named_store.keys())
        vals  = tuple(named_store[k] for k in keys)
        named = tuple(zip(keys,vals))
        argument_signature = (named,vargs)
        return argument_signature
    
    def print_signature(sig):
        '''Formats the argument signature for printing.'''
        named, vargs = sig
        result = ','.join(['%s=%s'%(k,v) for (k,v) in named])
        if not vargs is None: result += '; ' + ','.join(map(str,vargs))
        return result
    
    def vararg_memoize(f):
        '''Memoization decorator'''
        cache = {}
        @robust_decorator
        def decorated(f,*args,**kwargs):
            sig = argument_signature(f,*args,**kwargs)
            if not sig in cache:  cache[sig] = f(*args,**kwargs)
            else: print('found cached',f.func_name,print_signature(sig))
            return cache[sig]
        return decorated(f)
    
    if __name__=="__main__":
        print("Running example and testing code")
    
        def example_function(a,b,c=1,d=('ok',),*vargs,**kw):
            ''' This docstring should be preserved by the decorator '''
            e,f = vargs if (len(vargs)==2) else (None,None)
            g = kw['k'] if 'k' in kw else None
            print(a,b,c,d,e,f,g)
    
        f = example_function
        g = vararg_memoize(example_function)
    
        for fn in [f,g]:
            print('Testing',fn.__name__)
            fn('a','b','c','d')
            fn('a','b','c','d','e','f')
            fn('a','b',c='c',d='d')
            fn('a','b',**{'c':'c','d':'d'})
            fn('a','b',*['c','d'])
            fn('a','b',d='d',*['c'])
            fn('a','b',*['c'],**{'d':'d'})
            fn('a','b','c','d','e','f')
    

    【讨论】:

      【解决方案2】:
      import inspect
      def memoize(obj):
          cache = obj.cache = {}
          @functools.wraps(obj)
          def memoizer(*args, **kwargs):
              kwargs.update(dict(zip(inspect.getargspec(obj).args, args)))
              key = tuple(kwargs.get(k, None) for k in inspect.getargspec(obj).args)
              if key not in cache:
                  cache[key] = obj(**kwargs)
              return cache[key]
          return memoizer
      

      【讨论】:

      • 这不起作用。 co_varnames 是函数中定义的所有 变量的列表。如果你想要参数的名字,你必须使用inspect.getargspec
      • &gt;&gt;&gt; def my_func(a,b,c,d): e=5 ... &gt;&gt;&gt; print my_func.__code__.co_varnames ('a', 'b', 'c', 'd', 'e') 注意最后的e 根本不是参数!
      • 是的。 co_varnames 列出了函数的所有局部变量的名称。这包括参数,但仅适用于极其简单的函数,它将仅包含参数。
      【解决方案3】:

      如果您始终将参数用作位置或始终用作关键字,Thorsten 解决方案可以正常工作。但是,如果您想考虑为参数赋予相同值的相等调用,而与参数的传递方式无关,那么您必须做一些更复杂的事情:

      import inspect
      
      
      def make_key_maker(func):
          args_spec = inspect.getargspec(func)
      
          def key_maker(*args, **kwargs):
              left_args = args_spec.args[len(args):]
              num_defaults = len(args_spec.defaults or ())
              defaults_names = args_spec.args[-num_defaults:]
      
              if not set(left_args).symmetric_difference(kwargs).issubset(defaults_names):
                  # We got an error in the function call. Let's simply trigger it
                  func(*args, **kwargs)
      
              start = 0
              key = []
              for arg, arg_name in zip(args, args_spec.args):
                  key.append(arg)
                  if arg_name in defaults_names:
                      start += 1
      
              for left_arg in left_args:
                  try:
                      key.append(kwargs[left_arg])
                  except KeyError:
                      key.append(args_spec.defaults[start])
      
                  # Increase index if we used a default, or if the argument was provided
                  if left_arg in defaults_names:
                      start += 1
              return tuple(key)
      
          return key_maker
      

      上述函数尝试将关键字参数(和默认值)映射到位置,并使用结果元组作为键。我对其进行了一些测试,它似乎在大多数情况下都能正常工作。 当目标函数也使用**kwargs 参数时,它会失败。

      >>> def my_function(a,b,c,d,e=True,f="something"): pass
      ... 
      >>> key_maker = make_key_maker(my_function)
      >>> 
      >>> key_maker(1,2,3,4)
      (1, 2, 3, 4, True, 'something')
      >>> key_maker(1,2,3,4, e=True)               # same as before
      (1, 2, 3, 4, True, 'something')
      >>> key_maker(1,2,3,4, True)                 # same as before
      (1, 2, 3, 4, True, 'something')
      >>> key_maker(1,2,3,4, True, f="something")  # same as before
      (1, 2, 3, 4, True, 'something')
      >>> key_maker(1,2,3,4, True, "something")    # same as before
      (1, 2, 3, 4, True, 'something')
      >>> key_maker(1,2,3,d=4)                     # same as before
      (1, 2, 3, 4, True, 'something')
      >>> key_maker(1,2,3,d=4, f="something")      # same as before
      (1, 2, 3, 4, True, 'something')
      

      【讨论】:

      • 所以我想答案是没有简单的方法。如果我错了,请纠正我,但似乎你已经完成了所有肮脏的工作,此时,要处理 kwargs,可以弹出任何使用过的 kwargs,然后在生成的密钥中进行最后一个条目 a剩余键值对的元组,按参数名称排序?
      • @acjohnson55 嗯,这可能更容易,但我不知道它是否总是有效。请记住,您始终必须处理默认值,因此您必须始终采取一些技巧来找出使用了哪些默认值。我不知道生成的代码是否会更小/更简单。
      • 好吧,看来现在我最好保持我想记住的功能简单,但我认为你的方法是在一般情况下可以工作的正确基础。
      【解决方案4】:

      一般来说,不可能推断出两个调用具有相同的参数含义。考虑电话

      func(foo=1)
      func(1)
      func(bar=1)
      

      这些(如果有的话)是等价的取决于位置参数是调用 foo 还是 bar:如果参数调用 foo,那么第一个调用匹配第二个,等等。但是,位置参数也可能有一个完全不同的名字。

      IOW,您需要考虑要调用的函数,而这又可能是不可能的(例如,如果它是用 C 实现的,或者它本身就是一个只处理 *args、**kwargs 的包装器)。

      如果你想走反射路线,类似ndpu的响应是一个好的开始。

      【讨论】:

      • 如果函数是 python 函数,它可以确定如何解析参数。您只需使用inspect.getargspec 即可获取所有必需的信息(参数名称、顺序、默认值、加星标的参数等)
      • 试试 inspect.getargspec("".split)
      • 咳咳。请再次阅读我之前评论的前 7 个字,然后在 python 命令行上尝试 inspect.getargspec("".split) 并阅读异常消息:TypeError: &lt;built-in method split of str object at 0x7fb55abac508&gt; is not a Python function
      • 你是对的。我误读了您的评论,说它可以确定如何为函数解析参数。
      【解决方案5】:

      您只需要找到一种从argskwargs 构建密钥的好方法。也许试试这个:

      import functools
      from collections import OrderedDict
      
      # note that this decorator ignores **kwargs
      def memoize(obj):
          def make_key(args, kwargs):
              ordered_kwargs = OrderedDict(kwargs)
              parameters = tuple([args, 
                                  tuple(ordered_kwargs.keys()), 
                                  tuple(ordered_kwargs.values())])
              return parameters
          cache = obj.cache = {}
      
          @functools.wraps(obj)
          def memoizer(*args, **kwargs):
              key = make_key(args, kwargs)
              if key not in cache:
                  cache[key] = obj(*args, **kwargs)
                  print "Not using cached result for key %s" % str(key)
              else:
                  print "Using cached result for key %s" % str(key)
              return cache[key]
          return memoizer
      
      @memoize
      def calculate_sum(*args, **kwargs):
          return sum(args)
      
      calculate_sum(4,7,9,2,flag=0)
      calculate_sum(4,7,9,3)
      calculate_sum(4,7,9,2,flag=1)
      calculate_sum(4,7,9,2,flag=0)
      

      我将一些打印语句放入memoizer,只是为了证明它有效。输出是:

      Not using cached result for key ((4, 7, 9, 2), ('flag',), (0,))
      Not using cached result for key ((4, 7, 9, 3), (), ())
      Not using cached result for key ((4, 7, 9, 2), ('flag',), (1,))
      Using cached result for key ((4, 7, 9, 2), ('flag',), (0,))
      

      我确定我没有解决所有极端情况,尤其是当作为 kwargs(甚至 args)传入的值不可散列时。但也许它可以作为一个很好的起点。

      【讨论】:

      • 当您将一些参数only作为位置参数访问,而其他参数only作为关键字访问时,这可能会起作用。如果你想考虑等于调用f(a,b,third_param=c)f(a,b,c),那么你必须做一些更复杂的事情来将关键字映射到正确的位置参数。可能需要使用inspect.getargspec
      • 顺便问一下,你为什么使用OrderedDict?根据python的documentation调用items/keys/values没有修改dict都将具有相同的顺序,所以zip(kwargs.keys(), kwargs.values())将始终匹配键到正确的值。
      • (1) 是的,你说得对,我没有解决这个要求,会考虑一下(2) 打电话给calculate_sum(4,6,flag=0,x=7)calculate_sum(4,6,x=7,flag=0) 怎么样?它们是等价的,应该这样对待。
      猜你喜欢
      • 2014-08-10
      • 2014-03-04
      • 2015-03-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-01-27
      • 1970-01-01
      相关资源
      最近更新 更多