python - 通过元类修复可变的默认参数

标签 python decorator mutable metaclass default-arguments

issue with mutable argument default values在 Python 中是众所周知的。基本上可变的默认值在定义时分配一次,然后可以在函数体内进行修改,这可能会让人感到惊讶。

<小时/>

今天在工作中,我们正在考虑处理这个问题的不同方法(接下来针对 None 进行测试,这显然是正确的方法......),我想出了一个 Metaclass您可以找到的解决方案 here 或低于(只有几行,因此要点可能更容易阅读)

它的工作原理基本上是这样的:

  1. 对于每个函数 obj。在属性字典中。
  2. 可变默认参数的内省(introspection)函数。
  3. 如果默认参数可变。找到后,用修饰函数替换该函数
  4. 装饰函数是使用注册了默认参数的闭包创建的。名称和初始默认值
  5. 在每个函数调用中,检查是否有 kwarg。指定的注册名称,如果指定,则重新实例化初始值以创建浅拷贝并在执行前将其添加到 kwargs 中。
<小时/>

现在的问题是这种方法非常适合 listdict对象,但对于其他可变默认值(例如 set()),它会以某种方式失败或bytearray()有什么想法吗?
请随意测试此代码。唯一的非标准部门。是 6(pip install 6),因此它可以在 Py2 和 3 中工作。

# -*- coding: utf-8 -*-
import inspect
import types
from functools import wraps
from collections import(
    MutableMapping,
    MutableSequence,
    MutableSet
)

from six import with_metaclass  # for py2/3 compatibility | pip install six


def mutable_to_immutable_kwargs(names_to_defaults):
    """Decorator to return function that replaces default values for registered
    names with a new instance of default value.
    """
    def closure(func):
        @wraps(func)
        def wrapped_func(*args, **kwargs):

            set_kwarg_names = set(kwargs)
            set_registered_kwarg_names = set(names_to_defaults)
            defaults_to_replace = set_registered_kwarg_names - set_kwarg_names

            for name in defaults_to_replace:
                define_time_object = names_to_defaults[name]
                kwargs[name] = type(define_time_object)(define_time_object)

            return func(*args, **kwargs)
        return wrapped_func
    return closure


class ImmutableDefaultArguments(type):
    """Search through the attrs. dict for functions with mutable default args.
    and replace matching attr. names with a function object from the above
    decorator.
    """

    def __new__(meta, name, bases, attrs):
        mutable_types = (MutableMapping,MutableSequence, MutableSet)

        for function_name, obj in list(attrs.items()):
            # is it a function ?
            if(isinstance(obj, types.FunctionType) is False):
                continue

            function_object = obj
            arg_specs = inspect.getargspec(function_object)
            arg_names = arg_specs.args
            arg_defaults = arg_specs.defaults

            # function contains names and defaults?
            if (None in (arg_names, arg_defaults)):
                continue

            # exclude self and pos. args.
            names_to_defaults = zip(reversed(arg_defaults), reversed(arg_names))

            # sort out mutable defaults and their arg. names
            mutable_names_to_defaults = {}
            for arg_default, arg_name in names_to_defaults:
                if(isinstance(arg_default, mutable_types)):
                    mutable_names_to_defaults[arg_name] = arg_default

            # did we have any args with mutable defaults ?
            if(bool(mutable_names_to_defaults) is False):
                continue

            # replace original function with decorated function
            attrs[function_name] = mutable_to_immutable_kwargs(mutable_names_to_defaults)(function_object)


        return super(ImmutableDefaultArguments, meta).__new__(meta, name, bases, attrs)


class ImmutableDefaultArgumentsBase(with_metaclass(ImmutableDefaultArguments,
                                                   object)):
    """Py2/3 compatible base class created with ImmutableDefaultArguments
    metaclass through six.
    """
    pass


class MutableDefaultArgumentsObject(object):
    """Mutable default arguments of all functions should STAY mutable."""

    def function_a(self, mutable_default_arg=set()):
        print("function_b", mutable_default_arg, id(mutable_default_arg))


class ImmutableDefaultArgumentsObject(ImmutableDefaultArgumentsBase):
    """Mutable default arguments of all functions should become IMMUTABLE.
    through re-instanciation in decorated function."""

    def function_a(self, mutable_default_arg=set()):
        """REPLACE DEFAULT ARGUMENT 'set()' WITH [] AND IT WORKS...!?"""
        print("function_b", mutable_default_arg, id(mutable_default_arg))


if(__name__ == "__main__"):

    # test it
    count = 5

    print('mutable default args. remain with same id on each call')
    mutable_default_args = MutableDefaultArgumentsObject()
    for index in range(count):
        mutable_default_args.function_a()

    print('mutable default args. should have new idea on each call')
    immutable_default_args = ImmutableDefaultArgumentsObject()
    for index in range(count):
        immutable_default_args.function_a()

最佳答案

您的代码实际上正在执行您所期望的操作。它在调用时将默认值的新副本传递给函数。但是,由于您对这个新值不执行任何操作,因此它会被垃圾收集,并且内存可以在您下次调用时立即释放以立即重新分配

因此,您会不断获得相同的 id()

两个对象在不同时间点id()相同这一事实并不表明它们是同一个对象。

要看到此效果,请更改您的函数,以便它使用会增加其引用计数的值执行某些操作,例如:

class ImmutableDefaultArgumentsObject(ImmutableDefaultArgumentsBase):
    cache = []
    def function_a(self, mutable_default_arg=set()):
        print("function_b", mutable_default_arg, id(mutable_default_arg))
        self.cache.append(mutable_default_arg)

现在运行您的代码将提供:

function_b set() 4362897448
function_b set() 4362896776
function_b set() 4362898344
function_b set() 4362899240
function_b set() 4362897672

关于python - 通过元类修复可变的默认参数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37015641/

相关文章:

python - 检测迭代器是否会被消耗

python - 如何在不丢失重复值的情况下从两个列表创建字典?

python - PySpark 在附加超列名称时展平数据框

python - 卡住函数参数

ios - [__NSCFArray insertObject :atIndex:]: mutating method sent to immutable object'

arrays - Project Euler 7 Scala 问题

Python 2.5 subprocess.Popen 问题

python-3.x - 如何检查打印语句中是否存在变量

python - 错误: Decorator takes exactly 2 arguments (1 given)

rust - 获取对 Vec 元素的可变引用或创建新元素并获取该引用