I am confused with how Python compares two objects. The code that makes things confusing is roughly like that:
class Window(QtWidgets.QMainWindow):
def __init__(self):
super(Window,self).__init__()
uic.loadUi('window.ui', self)
self.show()
def thread_it(self, func_to_execute):
worker = Worker(func_to_execute)
if func_to_execute == self.mpositioner.movetostart:
worker.signals.progress.connect(self.create_raw_log_line)
self.threadpool.start(worker)
return worker
What is confusing is that the code works only if the line reading if func_to_execute == self.mpositioner.movetostart
uses the ==
operator. When using with is
operator it doesn't work. I was trying the is
approach after it was suggested by VSCode Pylint, but Ruff linter doesn't complain about the ==
.
So the question would be: is there any particular reason why the is
doesn't work?
EDIT:
I made the test using suggested callback_test and the result is the following:
callback: <bound method MotorPositioner.movetostart of <__main__.MotorPositioner object at 0x000001B81B7C60E0>>
callback id: 1890339656832
callback_fun: <bound method MotorPositioner.movetostart of <__main__.MotorPositioner object at 0x000001B81B7C60E0>>
callback_fun id: 1890339659712
Yup same function
The issue here is how methods in Python work. Briefly†:
A method is just a function that exists in the namespace of a class, that when it is accessed through an instance of the class, returns a bound method which is a callable that effectively partially applies the instance to the first positional argument to the original function i.e. it provides the self
parameter (which is just the name we conventionally give to the first positional parameter of a method) with the instance as an argument.
So, consider:
>>> class Foo:
... def bar(self):
... print("barring", self)
...
>>> Foo.bar
<function Foo.bar at 0x111e16fc0>
When accessed on the class itself, it just returns the function object. Note, here, it is always going to be the case that you get the same object back, just the one that was created in the body of the class definition statement.
>>> Foo.bar is Foo.bar # this will always be True
True
But if we create an instance, we get a different kind of objet:
>>> foo = Foo()
>>> foo.bar
<bound method Foo.bar of <__main__.Foo object at 0x11026cad0>>
>>> type(Foo.bar), type(foo.bar)
(<class 'function'>, <class 'method'>)
>>> foo.bar()
barring <__main__.Foo object at 0x11026cad0>
Appreciate again, that when accessed through the class, it is just the regular function you defined before, it works just like any function defined anywhere else, it simply exists in the class namespace:
>>> Foo.bar(42)
baring 42
>>> def baz(self):
... print("bazzing", self)
...
>>> baz(42)
bazzing 42
Note, a bound-method object is created every time you access the object through the instance. So the following will always be evaluate to False
:
>>> foo = Foo()
>>> foo.bar is foo.bar # will always be false
False
Note, however, that bound-method objects will be equal if they are binding the same instance to the same function. So:
>>> foo1 = Foo()
>>> foo2 = Foo()
>>> foo1.bar == foo1.bar # always True
True
>>> foo1.bar == foo2.bar # always False
False
Additionally, these are introspect-able:
>>> foo.bar.__self__
<__main__.Foo object at 0x11026cad0>
>>> foo.bar.__func__
<function Foo.bar at 0x110250180>
A quick gotcha:
If you are following along in a REPL, you may do something like the following, trying to verify that the claims I'm making are true. And you do this reasonable test:
>>> foo = Foo()
>>> for _ in range(5):
... print(id(foo.bar))
...
4581348032
4581348032
4581348032
4581348032
4581348032
"Huh?" You might ask. "What is going on here, why am I getting the same id's for purportedly different bound method objects? But when I check directly"
>>> foo.bar is foo.bar
False
What is going on above is that the bound method gets reclaimed immediately after it is called because it isn't referenced anywhere. Since it is reclaimed, it's lifetime doesn't overlap, so the id
doesn't guaratnee that they are distinct objects. This is because in CPython, the id
is merely the value of the PyObject pointer, that is, the memory address of the object on the privately managed heap. This heap will re-use that memory happily. But if you keep a reference around, it is not reclaimed, so it is forced to use another one:
>>> foo = Foo()
>>> for _ in range(5):
... bound_method = foo.bar
... print(id(bound_method))
...
4581907008
4581530752
4581907008
4581530752
4581907008
You can see a similar phenomenon with this example:
>>> id(object()) == id(object())
True
>>> object() is object()
False
>>> obj1 = object()
>>> obj2 = object()
>>> id(obj1) == id(obj2)
False
Moral of the story here is that you should be careful when using id
.
† if you really want to know how this happens, then you need to understand descriptors. The Descriptor HOWTO goes into the nitty-gritty details, and is worth a read in its entirety. Descriptors are used all over by Python to implement core features.