2016-05-03 7 views
1

issue with mutable argument default values довольно хорошо известен в Python. В основном изменяемые значения по умолчанию назначаются один раз во время определения и затем могут быть изменены внутри тела функции, что может стать неожиданностью.Исправить изменчивые аргументы по умолчанию через metaclass


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

Это в основном работает следующим образом:

  1. Для каждой функции OBJ. в атрибутах dict.
  2. Функция Introspect для изменяемых аргументов по умолчанию.
  3. Если изменяемые аргументы по умолчанию. найдены, замените функцию на украшенную функцию
  4. Декорированная функция была создана с закрытием, которое зарегистрировало аргумент по умолчанию. имя и начальное значение по умолчанию
  5. При каждом вызове функции проверьте, есть ли 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() 
+0

Nothing делать с вашей проблемой ...но понимаете ли вы, что вы переписываете имя класса с именем attr в вашем метаклассе? Таким образом, полученные классы '__name__' не будут вообще такими, какие вы ожидаете ... – donkopotamus

+0

Вы тестируете на Python 2? ABC, которые вы используете, в большой степени полагаются на ручную регистрацию классов, которые их поддерживают, а Python 2 делает лишь половинчатую попытку сделать это. Кроме этого, нет ABC 'collections.Mutable', поэтому вы не поймаете ничего, что не является отображением, последовательностью или множеством. – user2357112

+0

И конструкции типа 'if (bool (mutable_names_to_defaults) являются False):' лучше выражаются как просто 'if not mutable_names_to_defaults:' – donkopotamus

ответ

3

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

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

Ха, если честно, я был подозрительным, это могло быть связано с сбором мусора и немедленным перераспределением ...... однако все еще кажется, что он слишком оптимизирован, чтобы быть правдой. Как/почему это ** никогда не терпит неудачу с объектами 'list' и' dict'? ** Принято **, * Большое спасибо, ребята! * – timmwagener

+0

Если вы зададите другой вопрос, я всегда могу ответить на него за вас, поскольку это интересно :-) ... это связано с тем, что распределение памяти выполняется по-разному для эти объекты – donkopotamus

+0

Я превратил это в * (очень) * маленький пакет, который предлагает метакласс и * (возможно, более полезный) * декоратор. Я также затронул вопрос, который вы упомянули выше, с позициями, указанными в позициях. Он имеет значительное покрытие для тестирования, но, пожалуйста, попросите его как-то сломать :) Вот ссылка [ссылка на репо] (https://github.com/timmwagener/immutable_default_args) или [PyPI] (https: // pypi .python.org/PyPI/immutable_default_args). – timmwagener