【问题标题】:Why do list/dict attributes retain values outside decorators, but integer attributes do not?为什么 list/dict 属性保留装饰器外部的值,而整数属性不保留?
【发布时间】:2014-01-08 07:37:54
【问题描述】:

在下面的代码中,为什么g.__int 等于0 而不是1 最后,而g.__dictionaryg.__list 都保留了它们在装饰器中的值?

为什么我可以将列表/字典作为属性添加到装饰器内部的函数中,然后在装饰器外部访问它,但我不能对整数做同样的事情?

这里有一些代码来说明:

import functools

def dictify(func):
    func.__dictionary = { 0 : 0 }

    @functools.wraps(func)
    def _func(*args, **kwds):
        func.__dictionary[0] += 1
        print('  Incremented __dictionary, now __dictionary = {0}'.format(str(func.__dictionary)))
        return func(*args, **kwds)
    return _func

def listify(func):
    func.__list = [1, 2, 3]

    @functools.wraps(func)
    def _func(*args, **kwds):
        func.__list.append(0)
        print('  Appended 0 to __list, now __list = {0}'.format(str(func.__list)))
        return func(*args, **kwds)
    return _func

def intify(func):
    func.__int = 0

    @functools.wraps(func)
    def _func(*args, **kwds):
        func.__int += 1
        print('  Incremented __int, now __int = {0}'.format(func.__int))
        return func(*args, **kwds)
    return _func

def g():
    return 'pennyroyal tea'

print('*** UNMODIFIED ***')
print('g() returns \'{0}\''.format(g()))
print('id(g) = {0}'.format(id(g)))

g = dictify(g)
print('*** DICTIFIED ***')
print('g() returns \'{0}\''.format(g()))
print('g.__dictionary = {0}'.format(str(g.__dictionary)))
print('id(g) = {0}'.format(id(g)))

g = listify(g)
print('*** LISTIFIED ***')
print('g() returns \'{0}\''.format(g()))
print('g.__dictionary = {0}'.format(str(g.__dictionary)))
print('g.__list = {0}'.format(str(g.__list)))
print('id(g) = {0}'.format(id(g)))

g = intify(g)
print('*** INTIFIED ***')
print('g() returns \'{0}\''.format(g()))
print('g.__dictionary = {0}'.format(str(g.__dictionary)))
print('g.__list = {0}'.format(str(g.__list)))
print('g.__int = {0}'.format(str(g.__int)))
print('id(g) = {0}'.format(id(g)))

这给出了以下输出:

*** UNMODIFIED ***
g() returns 'pennyroyal tea'
id(g) = 139861398390976
*** DICTIFIED ***
  Incremented __dictionary, now __dictionary = {0: 1}
g() returns 'pennyroyal tea'
g.__dictionary = {0: 1}
id(g) = 139861398391096
*** LISTIFIED ***
  Appended 0 to __list, now __list = [1, 2, 3, 0]
  Incremented __dictionary, now __dictionary = {0: 2}
g() returns 'pennyroyal tea'
g.__dictionary = {0: 2}
g.__list = [1, 2, 3, 0]
id(g) = 139861398391216
*** INTIFIED ***
  Incremented __int, now __int = 1
  Appended 0 to __list, now __list = [1, 2, 3, 0, 0]
  Incremented __dictionary, now __dictionary = {0: 3}
g() returns 'pennyroyal tea'
g.__dictionary = {0: 3}
g.__list = [1, 2, 3, 0, 0]
g.__int = 0
id(g) = 139861398391336

可以看到,在装饰器内部,func.__int 的值打印为1,但在装饰器外部,g.__int 是默认的0,而g.__dictionaryg.__list 保留它们的值在装饰器内部和外部引用时。

注意:id(g) 调用表明用dictify()listify()intify() 进行装饰都会返回新对象,说明函数是不可变的。 (见我的详细解释here

这个问题是基于我之前的一个here。它的答案满足了我的实际需要,但我的“为什么”本能不会把这个放在一边。 :)

【问题讨论】:

    标签: python function attributes decorator


    【解决方案1】:

    您将函数 passed 上的属性分配给装饰器,但返回一个 不同 函数(包装函数)。 functools.wraps shallow-copy 属性从一个复制到另一个,这意味着它复制 list 和 dict 对象。然后你改变这些对象。但是您不能改变 int,所以您所做的只是更改 g 的“未包装”版本的值,同时打印包装版本的值。

    这里有一个说明性的尝试:

    >>> def g():
    ...     return 'pennyroyal tea'
    >>> f = intify(g)
    >>> f()
      Incremented __int, now __int = 1
    'pennyroyal tea'
    >>> f.__int
    0
    >>> g.__int
    1
    

    我通知了g,但将其分配给了f。您可以看到 __int 属性 更新 --- 但在原始函数上,而不是在包装的函数上。

    您看不到 list 和 dict 的区别,因为这些对象是可变的。两个函数共享一个列表和一个字典。但是,如果您将包装的函数一一拆分,您可以再次看到它:

    >>> f = dictify(g)
    ... f2 = listify(f)
    ... f3 = intify(f2)
    >>> f3()
      Incremented __int, now __int = 1
      Appended 0 to __list, now __list = [1, 2, 3, 0]
      Incremented __dictionary, now __dictionary = {0: 1}
    'pennyroyal tea'
    >>> f3.__list is f2.__list
    True
    >>> f3.__dictionary is f2.__dictionary
    True
    >>> f3.__int is f2.__int
    False
    

    您对 __list__dictionary 的修改会改变对象,但您对 __int 的修改会创建一个 new int(因为 int 不能被改变),从而在__int 传递给装饰器的函数的属性以及它返回的包装函数。

    这里的基本问题是你似乎想要在装饰器中做的是thisFuncion.__list.append(0),其中thisFunction 是返回的装饰函数,而不是待装饰的函数功能。也就是说,您希望包装器能够引用自身。但你不能这样做。 Python 中没有通用的方法让函数引用自身。在您的装饰器中,您定义了一个函数_func,它引用了一个函数func。有两个不同的功能,_func 只是在func 上设置属性,而不是在自身上。

    当然,真正的问题是为什么您首先要尝试设置这样的函数属性。但我从您的问题中得知,您只是出于好奇而询问了解正在发生的事情,而不是因为您真的想这样做。

    【讨论】:

    • 查看source code 我认为wraps 只是将相同的对象分配给新函数。所以,我认为从你的答案中删除 shallow 这个词是安全的。
    • @AshwiniChaudhary:但这正是我所说的浅层的意思——也就是说,它不会复制值,它只是重新分配相同的对象。
    • 现在说得通了。很好的例子。
    【解决方案2】:

    添加到@BrenBarn 的答案,如果您编写自己的functools.wraps 版本,那么您将不得不这样做:

    def intify(func):
        print func is g
        func.__int = 0
        def _func(*args, **kwds):
            func.__int += 1
            print('  Incremented __int, now __int = {0}'.format(func.__int))
            return func(*args, **kwds)
        _func.__int = func.__int
        _func.__doc__ = func.__doc__
        #... and some more 
        return _func
    

    即您必须将整数值分配给新的函数对象:

    _func.__int = func.__int
    

    但是,由于整数是不可变的,因此更改一个引用不会影响另一个:

    >>> x = 1
    >>> y = x 
    >>> x += 1
    >>> x       
    2
    >>> y         #y still unchanged
    1
    

    但是当您执行就地操作时,同样的事情不适用于可变对象:

    >>> x = [1]
    >>> y = x
    >>> y.append(10)
    >>> x
    [1, 10]
    >>> y
    [1, 10]
    >>> 
    

    source code 您可以看到wraps 只是将相同的对象分配给新的函数对象,因此当您使用可变对象时会产生副作用。

    """Update a wrapper function to look like the wrapped function
    
       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-04-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-01
      • 2020-03-22
      • 1970-01-01
      • 2011-09-16
      相关资源
      最近更新 更多