通过元类修复可变默认参数
Fix mutable default arguments via metaclass
issue with mutable argument default values 在 Python 中非常有名。基本上,可变默认值在定义时分配一次,然后可以在函数体内修改,这可能会令人惊讶。
今天在工作中,我们正在考虑处理此问题的不同方法 (在针对 None
进行测试之后,这显然是正确的方法...) 和我想出了一个 Metaclass
解决方案,你可以找到 here 或低于 (它只有几行,所以要点可能更具可读性) .
它基本上是这样工作的:
- 对于每个函数对象。在属性字典中。
- 可变默认参数的内省函数。
- 如果可变默认参数。找到,将函数替换为装饰函数
- 装饰函数是使用注册默认参数的闭包创建的。名称和初始默认值
- 在每次函数调用时,检查是否有 kwarg。通过注册名称给出,如果它是 NOT 给出,重新实例化初始值以创建浅拷贝并在执行前将其添加到 kwargs。
现在的问题是,这种方法对 list
和 dict
对象非常有效,但它对其他可变默认值(如 set()
或 bytearray()
)却以某种方式失败。 知道为什么吗?
随时测试此代码。唯一的非标准部门。是六(pip 安装六)所以它在 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
issue with mutable argument default values 在 Python 中非常有名。基本上,可变默认值在定义时分配一次,然后可以在函数体内修改,这可能会令人惊讶。
今天在工作中,我们正在考虑处理此问题的不同方法 (在针对 None
进行测试之后,这显然是正确的方法...) 和我想出了一个 Metaclass
解决方案,你可以找到 here 或低于 (它只有几行,所以要点可能更具可读性) .
它基本上是这样工作的:
- 对于每个函数对象。在属性字典中。
- 可变默认参数的内省函数。
- 如果可变默认参数。找到,将函数替换为装饰函数
- 装饰函数是使用注册默认参数的闭包创建的。名称和初始默认值
- 在每次函数调用时,检查是否有 kwarg。通过注册名称给出,如果它是 NOT 给出,重新实例化初始值以创建浅拷贝并在执行前将其添加到 kwargs。
现在的问题是,这种方法对 list
和 dict
对象非常有效,但它对其他可变默认值(如 set()
或 bytearray()
)却以某种方式失败。 知道为什么吗?
随时测试此代码。唯一的非标准部门。是六(pip 安装六)所以它在 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