Reputation: 384
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
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