I've created a class that implements customized addition, subtraction, etc. But for this class I'd like to have some additional parameters for these methods (__add__
, ...).
So my first thought was: A global dictionary that can be set or altered before I do the operations and internally it just accesses this dictionary and assumes these were given as kwargs
. This obviously solved my problem but another one emerged: I had to keep track of this global dictionary and it's state.
So I thought I can solve this with a dictionary that can be used as context manager that allows me to set them temporarly but cleans up afterwards:
from copy import deepcopy
class Arguments(object):
"""A dictionary container that can be used as context manager.
The context manager allows to modify the dictionary values and after
exiting it resets them to the original state.
Parameters
----------
kwargs :
Initial values for the contained dictionary.
Attributes
----------
defaults : dict
The `dict` containing the defaults as key-value pairs
"""
defaults = {}
def __init__(self, **kwargs):
# Copy the original and update the current dictionary with the values
# passed in.
self.dct_copy = deepcopy(self.defaults)
self.defaults.update(kwargs)
def __enter__(self):
# return the dictionary so one can catch it if one wants don't want to
# always update the class attribute or change some defaults in between
return self.defaults
def __exit__(self, type, value, traceback):
# clear the dictionary (in case someone added a new value) and update
# it with the original values again
self.defaults.clear()
self.defaults.update(self.dct_copy)
Do you think this approach is good? Are there alternatives or even builtin or plugins that do essentially the same (maybe some configuration-like class)? Is the code fairly straightforward without to many problems, is the coding style ok? Do you spot any potential bugs or problems?
The following part is only to illustrate what I want to do and how I think of using it in case I haven't formulated in clearly in the introduction text. This class is obvious nonsense (and only of explanatory purpose) and should not be part of any review:
class Container(object):
def __init__(self, data):
self.data = data
def __add__(self, other):
# Magic methods don't accept args when called as "xxx + yyy"
# so take the global defaults and pass them in.
return self.add(other, **Arguments.defaults)
def add(self, other, **kwargs):
# It could be that some kwargs are not set, then apply the default
# from the global defaults.
for key in Arguments.defaults:
if key not in kwargs: # We don't want to overwrite explicit kwargs
kwargs[key] = Arguments.defaults[key]
print(kwargs) # Just print the arguments we got.
The magic method takes no arguments except both operands:
>>> Container(10) + 2
{}
I have to use a direct method call if I want to provide arguments:
>>> Container(10).add(2, some_argument=10)
{'some_argument': 10}
But with the context manager I would be able to do something like this:
>>> with Arguments(some_argument=10):
... Container(10) + 2 # just one operation, in general I do 100s here.
{'some_argument': 10}
It would allow me to define global defaults: Arguments.defaults[kwarg_name] = kwarg_default
but overwrite them temporary in some context without needing to call the explicit method or subclassing the container just to alter one default value.
It was asked about the context and I have a class that contains some primary data (which is straightforward to add
) but also some other informations, like a dict
which contains information about the data. The defaults contains a function that does basically a merge with conflict resolution (if the key was present in both). I want to be able to give this function some parameters during the operation, for example which keywords must not be present in both and which will be affected by the operation and with add
that's easy but cumbersome to repeat - and with __add__
it's just impossible without hardcoding it somewhere. And it's not only that dict
, I have more attributes, some are trivial to "do" in the operation and don't need any arguments but others do.
In short: I expect this context-dictionary to be a solution to adding arguments to magic methods and quickly reset or alter them if the need arises.
2 Answers 2
What you’re trying to do reminds me a lot of decimal
's contexts. Basically, you have setcontext
and getcontext
to manipulate the current context, they are a wrappers around threading
and thread-local objects to be able to manage a different context per thread if need be. In your case, if you don't plan on supporting threads, a global object can do (as is your Arguments.defaults
).
And then localcontext
which is a thin layer around _ContextManager
which performs pretty much what Arguments
do: save a context using getcontext
on __enter__
and restore it using setcontext
on __exit__
.
For reference, here are the relevant parts of decimal
:
def setcontext(context): if context in (DefaultContext, BasicContext, ExtendedContext): context = context.copy() context.clear_flags() threading.current_thread().__decimal_context__ = context def getcontext(): try: return threading.current_thread().__decimal_context__ except AttributeError: context = Context() threading.current_thread().__decimal_context__ = context return context
I picked the parts not using thread-locals here but the principle is the same if they are enabled.
And for the context manager part:
def localcontext(ctx=None): if ctx is None: ctx = getcontext() return _ContextManager(ctx) class _ContextManager(object): def __init__(self, new_context): self.new_context = new_context.copy() def __enter__(self): self.saved_context = getcontext() setcontext(self.new_context) return self.new_context def __exit__(self, t, v, tb): setcontext(self.saved_context)
Based on that, if you don't plan on using threads, you can simplify the design:
import copy
class Arguments:
def __init__(self, **kwargs):
self.update(kwargs)
def update(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def get_arguments():
global _arguments
try:
return _arguments
except NameError:
_arguments = Arguments()
return _arguments
def set_arguments(arg=None, **kwargs):
global _arguments
if arg is None:
arg = get_arguments()
_arguments = copy.copy(arg)
_arguments.update(kwargs)
class local_arguments:
def __init__(self, arg=None, **kwargs):
if arg is None:
arg = get_arguments()
self.new_arguments = copy.copy(arg)
self.new_arguments.update(kwargs)
def __enter__(self):
self.old_arguments = get_arguments()
set_arguments(self.new_arguments)
return self.new_arguments
def __exit__(self, t, v, tb):
set_arguments(self.old_arguments)
The advantage of such approach is that you can directly use get_arguments
in the magic methods, without having to rely on a delegated one:
class Container(object):
def __init__(self, data):
self.data = data
def __add__(self, other):
args = get_arguments()
print(self, other, args)
But if you want to add extra methods to manualy supply extra arguments, it is simplified thanks to local_argument
:
def add(self, other, **kwargs):
with local_arguments(**kwargs):
return self + other
Global usage is still pretty much the same, though:
set_arguments(foo=42, bar='baz')
with local_arguments(foo=8):
Container(2) + 10
-
\$\begingroup\$ I didn't knew about the decimals context manager but this looks really amazing. I tried implementing it and it works quite different. The fact that the logic is outside the class makes some thing much simpler. I'll wait a few days more until I award the bounty - I hope you don't mind. \$\endgroup\$MSeifert– MSeifert2016年05月28日 14:23:27 +00:00Commented May 28, 2016 at 14:23
It's an interesting solution to that problem. Given that implementation
and the usage example I'd probably change it a bit - at the moment the
Arguments
class is essentially a global singleton and the with
statement temporarily binds some values to that; however if you wanted
to use this from multiple places, it would be better if there are
multiple places to which to bind values to.
I.e. I'd imagine something more like foo = Arguments(x = 1)
and later
with foo(x=2):
to use that place, also possibly with the extension to
make the values directly available via foo.x
etc. That should still
support nesting of course (for reference, this is basically Dynamic Scoping).
There are also performance considerations, so it would be good to measure and compare different strategies.
Explore related questions
See similar questions with these tags.
Arguments
can be a poor choice if other factors in the code that we can't see goes in favor of using alternatives). With the real code available, it is possible to propose alternatives about the big picture that could lead to use a completely different approach \$\endgroup\$Container
information, and you may also want to add more to yourArguments
class. \$\endgroup\$