Search code examples
pythonsocket.iodecorator

Python Decorated Function Can't Update Attribute from Another Class


I have an issue using a decorator from a imported package in a class I created. I created two classes and a main(). An instance of class A is created in main() and an instance of class B is created in class A. Class B needs to update attributes of the instance created in main(). Furthermore, I need to use decorators from the imported package.

I don’t know how to get around the inability to reference the attributes of the the instance created in main() from the decorated confirm_connect function in the instance of class B. The only way I have made it work is to declare the instance created in main() as global and remove all the self references in class B. However, making it global causes other issues in my application. (Making the instance of socketio as global within class B is tolerable, but I prefer not having it either.)

The function confirm_connect receives a message from the server. If I define the function as def conform_connect(self, data), I get the error message connect_confirm() missing 1 required positional argument: 'data'/. If I remove self from the declaration, then I get the error NameError: name 'self' is not defined.

Here is my script. How can I make my script do what I need it to do?

import socketio

class A():

    def __init__(self):
        self.pin = None
        
    def connect_to_server(self):
        self.io = B()
        self.io.connect_to_server()

    def start_the_process(self):
        self.b = B(self)
        self.b.connect_to_server()
        
    def do_something_with_pin(self):
        print(self.pin)

class B():
    
    global sio
    sio = socketio.Client()
    
    def __init__(self, a):
        self.a = a
        
    def connect_to_server(self):    
        sio.connect('http://my_url_is_this.org')    
        sio.emit('manual_connection_parameter', {'foo': 'bar'})
            
    @sio.event
    def connect_confirm(data):
        self.a.pin = data
        self.a.do_something_with_pin()
        
def main():
    a = A()
    a.start_the_process()

if __name__ == '__main__':
    main()

Solution

  • If you understand how decorators work, then you understand that

    @sio.event
    def connect_confirm(self, data):
        self.a.pin = data
        self.a.do_something_with_pin()
    

    is simply syntactic sugar for

    def connect_confirm(self, data):
        self.a.pin = data
        self.a.do_something_with_pin()
    connect_confirm = sio.event(connect_confirm)
    

    And the reported problem is that sio.event expects a 1-argument, plain callback function that will receive the data; so with a self parameter it doesn't meet those expectations (and without a self parameter, the expectations of the method aren't met).

    The insight is that (since 3.x; 2.x did things differently under the hood) a method defined in a class is just a function; it's the process of looking up that method from an instance that makes methods do the special things with self that they do.

    So when you decorate the method, you end up registering totally the wrong thing as a callback. The socketio.Client doesn't know anything about your B instance and can't work with it, no matter what you do.

    The solution is to instead use the bound instance method of your instance for the callback, which requires us to invoke the decorator manually as described at the beginning.

    In the __init__, we can do something like:

    def __init__(self, a):
        self.a = a
        sio.event(self.connect_confirm)
    

    And then we can define that method normally:

    def connect_confirm(self, data):
        self.a.pin = data
        self.a.do_something_with_pin()
    

    Notice how in the __init__ context we can now write self. when we do the "decoration", so we tell the socketio.Client to use the connect_confirm of this instance as a callback. We don't need to do anything with the "decorated" result, so we don't assign it anywhere.


    It's worth considering what this kind of thing looks like from the API's point of view. The socketio.Client class implementation presumably includes something like:

    class Client:
        # ... lots of other stuff...
        def event(self, callback):
            self._event_callback = callback
            return callback
        def _some_internal_logic(self):
            if _special_situation_occurs():
                self._event_callback(self._get_data_for_callback())
    

    If the implementation didn't return callback, it would be obvious what you needed to do in your situation, since the decorator syntax wouldn't have been available. (Well, you can use anything as a decorator; but getting back None or another non-function is not very useful most of the time.)