5
\$\begingroup\$

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.

\$\endgroup\$
  • 1
    \$\begingroup\$ Hi! Simplifying your code that way makes it too much on the hypothetical side (which is off-topic). We prefer to see real code so we can review any facet of it. Simplifying your code also lead you to disconnect these snippets from what your code does, so we don't even know what that structure is used for. \$\endgroup\$ – Mathias Ettinger May 22 '16 at 8:44
  • \$\begingroup\$ @MathiasEttinger The relevant code isn't simplified (the context manager) all I simplified was the class and that's just because I didn't implement it - mostly because I wanted to have some feedback on the default-argument-context-manager-for-python-operators before I choose to use it or don't. But the real container contains some meta informations and other attributes that in different contexts need different merge strategies or replacement values. For normal methods I could use parameters but for the magic methods... well that's where this context manager idea came from. \$\endgroup\$ – MSeifert May 22 '16 at 9:32
  • \$\begingroup\$ (cont'd) If you think it's off-topic could you be more specific what I should include to make it on-topic? Or should this question be on another SE site? Your text reads more like a copied and pasted text without going into details why it applies to my question... \$\endgroup\$ – MSeifert May 22 '16 at 9:39
  • 2
    \$\begingroup\$ It's actually not copy-pasted, but for a selected few reasons: generic 1-letter variable names are probably the #1 topic of answers (if it appears it is just because of simplification, then it's wasting everyone's time); lack of context (random container doesn't help us get your use case, optimizing 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\$ – Mathias Ettinger May 22 '16 at 9:59
  • 3
    \$\begingroup\$ @MSeifert I don't think this is a bad question, where some others do. To make this question more on-topic, you could remove the Container information, and you may also want to add more to your Arguments class. \$\endgroup\$ – Peilonrayz May 22 '16 at 14:16
1
+50
\$\begingroup\$

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
\$\endgroup\$
  • \$\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 May 28 '16 at 14:23
0
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.