2016-05-03 22 views
1

Die issue with mutable argument default values ist in Python ziemlich bekannt. Grundsätzlich werden veränderbare Standardwerte einmalig zur definierten Zeit zugewiesen und können dann innerhalb des Funktionskörpers modifiziert werden, was überraschend sein kann.Fix veränderbare Standardargumente via Metaklasse


Heute bei der Arbeit dachten wir über verschiedene Wege mit diesem (neben Tests gegen None die offenbar ist der richtige Weg ...) und ich mit einer Metaclass Lösung kam zu beschäftigen, die Sie finden können here oder unten (es ist ein paar Zeilen, so dass der Kern besser lesbar sein könnte).

Es funktioniert im Grunde wie folgt:

  1. Für jede Funktion Obj. in den Attributen dict.
  2. Introspect-Funktion für veränderbare Standardargumente.
  3. Wenn veränderliche Standardargumente. gefunden werden, ersetzen Sie die Funktion mit einer verzierten Funktion
  4. Die verzierte Funktion wurde mit einer Schließung erstellt, die die Standard-Arg registriert. Name und initialer Standardwert
  5. Überprüfen Sie bei jedem Funktionsaufruf, ob ein kwarg. durch den registrierten Namen wurde gegeben und wenn es NICHT gegeben war, wieder instanziieren Sie den Anfangswert, um eine flache Kopie zu erstellen und fügen Sie es den Kwargs vor der Ausführung hinzu.

Das Problem ist jetzt, dass dieser Ansatz für list und dict Objekte funktioniert gut, aber es funktioniert nicht irgendwie für andere veränderbare Standardwerte wie set() oder bytearray(). Irgendwelche Ideen warum?
Fühlen Sie sich frei, diesen Code zu testen. Die einzige Nicht-Standard-Dep. ist sechs (sechs installieren PIP) so funktioniert es in Py2 und 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

Nichts mit deinem Problem zu tun haben ...Aber ist dir klar, dass du den Klassennamen mit einem Attr-Namen in deiner Metaklasse überschreibst? Die resultierenden Klassen '__name__' werden also nicht das sein, was Sie erwarten ... – donkopotamus

+0

Testen Sie auf Python 2? Die ABCs, die Sie verwenden, verlassen sich auf die manuelle Registrierung von Klassen, die sie unterstützen, und Python 2 macht nur einen halbherzigen Versuch, dies zu tun. Ansonsten gibt es kein 'collections.Mutable' ABC, so dass du nichts fangen wirst, das kein Mapping, keine Sequenz oder kein Set ist. – user2357112

+0

Und Konstruktionen wie 'if (bool (mutable_names_to_defaults) ist False):' werden besser ausgedrückt als einfach 'wenn nicht änderbare_namen_zu_defaults:' – donkopotamus

Antwort

3

Ihr Code wie es tatsächlich tun, was steht Ihnen erwarten. Es ist Übergabe einer neuen Kopie des Standards an die Funktion beim Aufruf. Da Sie jedoch nichts mit diesem neuen Wert tun, ist es Müll gesammelt und der Speicher ist frei für die sofortige Neuzuweisung bei Ihrem nächsten Anruf.

So erhalten Sie immer die gleiche id().

Die Tatsache, dass der id() für zwei Objekte bei verschiedenen Zeitpunkten ist das gleiche zeigt nicht an, dass sie das gleiche Objekt sind.

Um diesen Effekt zu sehen, Ihre Funktion verändern, so dass es etwas mit dem Wert tut, der seine Referenzzahl erhöhen, wie zum Beispiel:

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) 

Jetzt Ihren Code ausgeführt bieten wird:

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

Ha, um ehrlich zu sein, ich war irgendwie misstrauisch, dass es mit Müllsammlung und sofortiger Neuzuteilung in Verbindung gebracht werden könnte ... aber es scheint immer noch fast zu optimisiert um wahr zu sein. Wie/warum scheitert es ** nie mit 'list' und' dict' Objekten? ** Akzeptiert **, * vielen Dank Jungs! * – timmwagener

+0

Wenn du noch eine andere Frage stellst, kann ich das immer für dich beantworten, da es interessant ist :-) ... es bezieht sich auf die Tatsache, dass die Speicherzuordnung unterschiedlich erfolgt Diese Objekte – donkopotamus

+0

Ich verwandelte dies in ein * (sehr) * kleines Paket, das eine Metaklasse und einen * (vielleicht nützlicheren) * Dekorateur bietet. Ich habe auch das Problem angesprochen, das Sie oben erwähnt haben. Es hat eine beträchtliche Testabdeckung, aber bitte fühlen Sie sich eingeladen, es zu versuchen und es irgendwie zu brechen :) Hier ist ein [Link zum Repo] (https://github.com/timmwagener/immutable_default_args) oder [PyPI] (https: // pypi .python.org/pypi/immutable_default_args). – timmwagener