issue with mutable argument default values довольно хорошо известен в Python. В основном изменяемые значения по умолчанию назначаются один раз во время определения и затем могут быть изменены внутри тела функции, что может стать неожиданностью.Исправить изменчивые аргументы по умолчанию через metaclass
Сегодня на работе мы думали о различных способах борьбы с этим (рядом тестированием против None
, который очевидно является правильным путем ...) и я пришел с Metaclass
решения, которое можно найти here или ниже (это несколько строк, поэтому сущность может быть более читаемой).
Это в основном работает следующим образом:
- Для каждой функции OBJ. в атрибутах dict.
- Функция Introspect для изменяемых аргументов по умолчанию.
- Если изменяемые аргументы по умолчанию. найдены, замените функцию на украшенную функцию
- Декорированная функция была создана с закрытием, которое зарегистрировало аргумент по умолчанию. имя и начальное значение по умолчанию
- При каждом вызове функции проверьте, есть ли kwarg. по зарегистрированному названию, и если это было NOT, повторно инициализируйте начальное значение для создания мелкой копии и добавьте ее в kwargs перед выполнением.
Сейчас проблема заключается в том, что этот подход работает отлично подходит для list
и dict
объектов, но это как-то не выполняется для других изменяемых значений по умолчанию, как set()
или bytearray()
. Любые идеи, почему?
Не стесняйтесь протестировать этот код. Единственный нестандартный dep. есть шесть (ПГИ установить шесть), поэтому он работает в 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()
Nothing делать с вашей проблемой ...но понимаете ли вы, что вы переписываете имя класса с именем attr в вашем метаклассе? Таким образом, полученные классы '__name__' не будут вообще такими, какие вы ожидаете ... – donkopotamus
Вы тестируете на Python 2? ABC, которые вы используете, в большой степени полагаются на ручную регистрацию классов, которые их поддерживают, а Python 2 делает лишь половинчатую попытку сделать это. Кроме этого, нет ABC 'collections.Mutable', поэтому вы не поймаете ничего, что не является отображением, последовательностью или множеством. – user2357112
И конструкции типа 'if (bool (mutable_names_to_defaults) являются False):' лучше выражаются как просто 'if not mutable_names_to_defaults:' – donkopotamus