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:
- Für jede Funktion Obj. in den Attributen dict.
- Introspect-Funktion für veränderbare Standardargumente.
- Wenn veränderliche Standardargumente. gefunden werden, ersetzen Sie die Funktion mit einer verzierten Funktion
- Die verzierte Funktion wurde mit einer Schließung erstellt, die die Standard-Arg registriert. Name und initialer Standardwert
- Ü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()
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
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
Und Konstruktionen wie 'if (bool (mutable_names_to_defaults) ist False):' werden besser ausgedrückt als einfach 'wenn nicht änderbare_namen_zu_defaults:' – donkopotamus