Исправить изменяемые аргументы по умолчанию через метакласс

Проблема со значениями по умолчанию для изменяемых аргументов хорошо известна в Python. В основном изменяемые значения по умолчанию назначаются один раз в определенное время и затем могут быть изменены в теле функции, что может стать неожиданностью.


Сегодня на работе мы думали о разных способах борьбы с этим (рядом с тестированием против None что, видимо, правильный путь...) и я придумал Metaclass решение, которое вы можете найти здесь или внизу (это несколько строк, поэтому суть может быть более читабельным).

Это в основном работает так:

  1. Для каждой функции объект. в атрибутах дикт.
  2. Функция Introspect для изменяемых аргументов по умолчанию.
  3. Если изменяемые аргументы по умолчанию. найдены, замените функцию декорированной функцией
  4. Декорированная функция была создана с закрытием, которое зарегистрировало аргумент по умолчанию. имя и начальное значение по умолчанию
  5. При каждом вызове функции проверяйте, не является ли kwarg. зарегистрированное имя было дано, и если оно НЕ было дано, повторно инстанцируйте начальное значение, чтобы создать мелкую копию и добавить его в kwargs перед выполнением.

Проблема в том, что этот подход прекрасно работает для list а также dict объекты, но это как-то не работает для других изменяемых значений по умолчанию, таких как set() или же bytearray(), Есть идеи почему?
Не стесняйтесь проверить этот код. Единственный нестандартный деп. это шесть (pip install шесть), поэтому он работает в 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()

1 ответ

Решение

Ваш код на самом деле делает то, что вы ожидаете. Он передает новую копию значения по умолчанию в функцию при вызове. Однако, поскольку вы ничего не делаете с этим новым значением, оно собирает мусор и освобождает память для немедленного перераспределения при следующем вызове.

Таким образом, вы продолжаете получать то же самое 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
Другие вопросы по тегам