4

The context for me is a single int's worth of info I need to retain between calls to a function which modifies that value. I could use a global, but I know that's discouraged. For now I've used a default argument in the form of a list containing the int and taken advantage of mutability so that changes to the value are retained between calls, like so--

 def increment(val, saved=[0]):
    saved[0] += val
    # do stuff

This function is being attached to a button via tkinter, like so~

button0 = Button(root, text="demo", command=lambda: increment(val))

which means there's no return value I can assign to a local variable outside the function.

How do people normally handle this? I mean, sure, the mutability trick works and all, but what if I needed to access and modify that value from multiple functions?

Can this not be done without setting up a class with static methods and internal attributes, etc?

shx2
  • 61,779
  • 13
  • 130
  • 153
temporary_user_name
  • 35,956
  • 47
  • 141
  • 220
  • I'd make a callable object (i.e. object with `__cal__` method so it's indistinguishable from a function). Actually, `increment` is already something with attributes, so you can assign `increment.saved = 0` after defining the function and then access `increment.saved` inside the function. I wouldn't call this Pythonic though, I'd prefer object. – yeputons Feb 03 '17 at 07:42
  • why do you feel you need to save it to a _local_ variable? The scope will have long gone away by the time the button is clicked. The click happens in a private context (eg: inside `mainloop`). You can save it to a global, but to save it to a local variable makes no sense. – Bryan Oakley Feb 03 '17 at 13:44

4 Answers4

5

Use a class. Use an instance member for keeping the state.

class Incrementable:
    def __init__(self, initial_value = 0):
        self.x = initial_value
    def increment(self, val):
        self.x += val
        # do stuff

You can add a __call__ method for simulating a function call (e.g. if you need to be backward-compatible). Whether or not it is a good idea really depends on the context and on your specific use case.

Can this not be done without setting up a class with static methods and internal attributes, etc?

It can, but solutions not involving classes/objects with attributes are not "pythonic". It is so easy to define classes in python (the example above is only 5 simple lines), and it gives you maximal control and flexibility.

Using python's mutable-default-args "weirdness" (I'm not going to call it "a feature") should be considered a hack.

Community
  • 1
  • 1
shx2
  • 61,779
  • 13
  • 130
  • 153
1

Aren't you mixing up model and view?

The UI elements, such as buttons, should just delegate to your data model. As such, if you have a model with a persistent state (i.e. class with attributes), you can just implement a class method there that handles the required things if a button is clicked.

If you try to bind stateful things to your presentation (UI), you will consequently lose the desirable separation between said presentation and your data model.

In case you want to keep your data model access simple, you can think about a singleton instance, such that you don't need to carry a reference to that model as an argument to all UI elements (plus you don't need a global instance, even though this singleton holds some kind of globally available instance):

def singleton(cls):
    instance = cls()
    instance.__call__ = lambda: instance
    return instance

@singleton
class TheDataModel(object):
    def __init__(self):
        self.x = 0

    def on_button_demo(self):
        self.x += 1

if __name__ == '__main__':
    # If an element needs a reference to the model, just get
    # the current instance from the decorated singleton:
    model = TheDataModel
    print('model', model.x)
    model.on_button_demo()
    print('model', model.x)

    # In fact, it is a global instance that is available via
    # the class name; even across imports in the same session
    other = TheDataModel
    print('other', other.x)

    # Consequently, you can easily bind the model's methods
    # to the action of any UI element
    button0 = Button(root, text="demo", command=TheDataModel.on_button_demo)

But, and I have to point this out, be cautious when using singleton instances, as they easily lead to bad design. Set up a proper model and just make the access to the major model compound accessible as a singleton. Such unified access is often referred to as context.

jbndlr
  • 4,965
  • 2
  • 21
  • 31
  • 1
    This doesn't do what you think it does: `button0 = Button(root, text="demo", command=TheDataModel.on_button_demo())` You are immediately calling `on_button_demo` and assigning the result to `command`. – Bryan Oakley Feb 03 '17 at 13:36
  • Well, I'm actually not used to UI elements in python, but sure - it is a call that is immediately evaluated. Don't know whether the UI elements can handle function references, but I'll fix that in my answer. – jbndlr Feb 04 '17 at 16:46
1

If you don't want to set up a class, your only1 other option is a global variable. You can't save it to a local variable because the command runs from within mainloop, not within the local scope in which it was created.

For example:

button0 = Button(root, text="demo", command=lambda: increment_and_save(val))

def increment_and_save(val):
    global saved
    saved = increment(val)

1 not literally true, since you can use all sorts of other ways to persist data, such as a database or a file, but I assume you want an in-memory solution.

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
0

We can make it context oriented by using context managers. Example is not specific to UI element however states general scenario.

class MyContext(object):
    # This is my container 
    # have whatever state to it
    # support different operations

    def __init__(self):
        self.val = 0

    def increament(self, val):
        self.val += val

    def get(self):
        return self.val

    def __enter__(self):
        # do on creation
        return self


    def __exit__(self, type, value, traceback):
        # do on exit
        self.val = 0

def some_func(val, context=None):
    if context:
        context.increament(val)

def some_more(val, context=None):
    if context:
        context.increament(val)

def some_getter(context=None):
    if context:
        print context.get()

with MyContext() as context:
    some_func(5, context=context)
    some_more(10, context=context)
    some_getter(context=context)
Nikhil Rupanawar
  • 4,061
  • 10
  • 35
  • 51