BurgeoningApe
BurgeoningApe

Reputation: 384

Modify a class value (self.var), that is specified as a parameter to a function

I'm trying to create reusable code that is modifying a class variable, whose name is not known to the method that is doing the logic.

In Example.receive_button_press, I am trying to call a function on an object that I have passed into the method, whose variable(s) would also be provided as parameters.

Can this be done in python? The code below does not quite work, but illustrates what I am trying to achieve.

import tkinter as tk
from tkinter import filedialog

class SomeOtherClass():
    _locked_button = False
    def __init__(self):
        pass
    def do_button_press(self, button):
        if button:
            return
        button = True
        someVal = tk.filedialog.askopenfilename(initialdir='\\')
        button = False
        return someVal 

class Example():
    
    def __init__(self):
        pass

    def receive_button_press(self, obj, func, var):
        return obj.func(var)

if __name__ == "__main__":
    root = tk.Tk()
    toyClass = Example()
    other = SomeOtherClass()

    myButton = tk.Button(text="Press", 
                         command=toyClass.receive_button_press(obj = other,func = other.do_button_press, 
                                                               var=SomeOtherClass._locked_button))
    myButton.pack()
    root.mainloop()

Upvotes: 1

Views: 2690

Answers (1)

Mad Physicist
Mad Physicist

Reputation: 114460

Callable Objects

You need to understand how callables work in python. Specifically, callable objects are regular objects. The only special thing about them is that you can apply the () operator to them. Functions, methods and lambdas are just three of many types of callables.

Here is a callable named x:

x

x might have been defined as

def x():
    return 3

Here is the result of calling x, also known as the "return value":

x()

Hopefully you can see the difference. You can assign x or x() to some other name, like a. If you do a = x, you can later do a() to actually call the object. If you do a = x(), a just refers to the number 3.

The command passed to tk.Button should be a callable object, not the result of calling a function. The following is the result of a function call, since you already applied () to the name receive_button_press:

toyClass.receive_button_press(obj=other, func=other.do_button_press, 
                              var=SomeOtherClass._locked_button)

If this function call were to return another callable, you could use it as an argument to command. Otherwise, you will need to make a callable that performs the function call with no arguments:

lambda: toyClass.receive_button_press(obj=other, func=other.do_button_press, 
                                      var=SomeOtherClass._locked_button)

As an aside, if you add a __call__ method to a class, all if its instances will be callable objects.

Bound Methods

The object other.do_button_press is called a bound method, which is a special type of callable object. When you use the dot operator (.) on an instance (other) to get a function that belongs to a class (do_button_press), the method gets "bound" to the instance. The result is a callable that does not require self to be passed in. In fact, it has a __self__ attribute that encodes other.

Notice that you call

other.do_button_press(button)

not

other.do_button_press(other, button)

That's why you should change Example to read

def receive_button_press(self, func, var):
    return func(var)

References to Attributes

You can access attributes in an object in a couple of different ways, but the simplest is by name. You can access a named attribute using the builtin hasattr and set them with setattr. To do so, you would have to change do_button_press to accept an object and an attribute name:

def do_button_press(self, lock_obj, lock_var):
    if getattr(lock_obj, lock_var, False):
        return
    setattr(lock_obj, lock_var, True)
    someVal = tk.filedialog.askopenfilename(initialdir='\\')
    setattr(lock_obj, lock_var, False)
    return someVal

Interfaces and Mixins

If this seems like a terrible way to do it, you're right. A much better way would be to use a pre-determined interface. In python, you don't make an interface: you just document it. So you could have do_button_press expect an object with an attibute called "locked" as "button", rather than just a reference to an immutable variable:

class SomeOtherClass():
    locked = False

    def do_button_press(self, lock):
        if lock.locked:
            return
        lock.locked = True
        someVal = tk.filedialog.askopenfilename(initialdir='\\')
        lock.locked = False
        return someVal 

At the same time, you can make a "mixin class" to provide a reference implementation that users can just stick into their base class list. Something like this:

class LockMixin:
    locked = False

class SomeOtherClass(LockMixin):
    def do_button_press(self, lock):
        if lock.locked:
            return
        lock.locked = True
        someVal = tk.filedialog.askopenfilename(initialdir='\\')
        lock.locked = False
        return someVal

Locks

Notice that I started throwing the term "Lock" around a lot. That's because the idea you have implemented is called "locking" a segment of code. In python, you have access to thread locks via threading.Lock. Rather than manually setting locked, you use the acquire and release methods.

class SomeOtherClass:
    def do_button_press(self, lock):
        if lock.acquire(blocking=False):
        try:
            someVal = tk.filedialog.askopenfilename(initialdir='\\')
            return someVal
        finally:
            lock.release()

Alternatively, you can use the lock as a context manager. The only catch is that acquire will be called with blocking=True by default, which may not be what you want:

def do_button_press(self, lock):
    with lock:
        someVal = tk.filedialog.askopenfilename(initialdir='\\')
        return someVal

Decorators

Finally, there is one more tool that may apply here. Python lets you apply decorators to functions and classes. A decorator is a callable that accepts a function or class as an argument and returns a replacement. Examples of decorators include staticmethod, classmethod and property. Many decorators return the original function more-or-less untouched. You can write a decorator that acquires a lock and releases it when you're done:

from functools import wraps
from threading import Lock

def locking(func):
    lock = Lock()
    @wraps(func)
    def wrapper(*args, **kwargs):
        if lock.acquire(blocking=False):
            try:
                return func(*args, **kwargs)
            except:
                lock.release()
    return wrapper

Notice that the wrapper function is itself decorated (to forward the name and other attributes of the original func to it). It passes through all the input arguments and return of the original, but inside a lock.

You would use this if you did not care where the lock came from, which is likely what you want here:

class SomeOtherClass:
    @locking
    def do_button_press(self):
        return tk.filedialog.askopenfilename(initialdir='\\')

Conclusion

So putting all of this together, here is how I would rewrite your toy example:

import tkinter as tk
from tkinter import filedialog
from functools import wraps
from threading import Lock

def locking(func):
    lock = Lock()
    @wraps(func)
    def wrapper(*args, **kwargs):
        if lock.acquire(blocking=False):
            try:
                return func(*args, **kwargs)
            finally:
                lock.release()
    return wrapper

class SomeOtherClass():
    @locking
    def do_button_press(self, button):
        return tk.filedialog.askopenfilename(initialdir='\\')

if __name__ == "__main__":
    root = tk.Tk()
    toyClass = SomeOtherClass()

    myButton = tk.Button(text="Press", command=toyClass.do_button_press)

    myButton.pack()
    root.mainloop()

Upvotes: 3

Related Questions