1

I have this situation:

module1.py:

class AudioEngine:
    def __init__(self):
        self.liverecording = False

    def getaudiocallback(self):

        def audiocallback(in_data, frame_count, time_info, status):  # these 4 parameters are requested by pyaudio
            data = None      # normally here we process the audio data
            if self.liverecording:
                print("Recording...")
            return data

        return audiocallback

main.py:

import module1

a = module1.AudioEngine()
f = a.getaudiocallback()

f(0, 0, 0, 0)

a.liverecording = True

f(0, 0, 0, 0)  # prints "Recording...", which is the expected behaviour, but why does it work?

Question: How to make the callback function audiocallback(...) change accordingly to the new value of a.liverecording? If it works out of the box, why does it?

More specifically, does f, once created with f = a.getaudiocallback(), keep in his code a pointer to a.liverecording (so if the latter is modified, this will be taken into consideration), or a copy of the value of a.liverecording (i.e. False) at the time of the creation of f?

Basj
  • 41,386
  • 99
  • 383
  • 673
  • 2
    The reason why the 2nd snippet doesn't work is because you're assigning to `a.recording` instead of `a.liverecording`. The first one is probably related to pyAudio. – Aran-Fey Apr 29 '18 at 23:23
  • What you've done is entirely correct as far as classes and closures go. As @Aran-Fey points out, it actually works just fine in your non-pyaudio example if you use the right variable name. So the actual problem (assuming it isn't a similar typo) probably _is_ pyaudio-specific. For example, do you even know your callback is being called? (What happens if you add `else: print('not recording')`?) – abarnert Apr 29 '18 at 23:25
  • @Basj Are you asking how `f` ends up as a closure that captures `self`, or do you need a complete tutorial on what closures are and how they work? – abarnert Apr 29 '18 at 23:27
  • It doesn't answer why the pyaudio code doesn't work, and anyway the problem in the 2nd snippet is just a typo. Typos don't get answered, they get closed. I'd consider removing the typo part of the question and just asking about pyaudio. – Aran-Fey Apr 29 '18 at 23:27
  • If your problem is just a typo, and it's now gone, but meanwhile you have a question about closures that has nothing to do with pyaudio and only tangentially about this existing code, I'd close this question and write a new one with a standalone [mcve] asking whatever you want to know. (But there's probably already a much better answer for any general question on closures than anyone's likely to whip up on the fly, so search first.) – abarnert Apr 29 '18 at 23:29
  • @abarnert: I cleaned and fixed the question, so now it's unrelated to pyaudio and not linked to any typo. – Basj Apr 29 '18 at 23:32
  • Your question needs to say specifically what you want explained, and at what level. "How does it work" questions can be very good questions, but not when it's not clear whether you need the basic idea of what closures are, or how closures interact with objects, or how CPython implements the cell objects under the covers, or the theory behind closures, or… – abarnert Apr 29 '18 at 23:32
  • 1
    Does [this](https://stackoverflow.com/questions/2005956/how-do-nested-functions-work-in-python) answer your question? – Aran-Fey Apr 29 '18 at 23:34
  • @Aran-Fey not exactly, because in your link, the parameter is passed as a constant, and not linked to another class – Basj Apr 29 '18 at 23:35
  • 1
    I don't see the difference. The `n` is your `self`, and `x` is your `in_data, frame_count, time_info, status`. – Aran-Fey Apr 29 '18 at 23:36
  • It's probably obvious once you know it @Aran-Fey, but here the fact there's a class adds another layer of complexity for someone who is not familiar to this, so I was not sure if it behaves the same with self's attributes. – Basj Apr 29 '18 at 23:40
  • @abarnert the key question is: does `f`, once created with `f = a.getaudiocallback()`, keep in his code **a pointer to `a.liverecording`** (so if the latter is modified, this will be taken into consideration), or a copy of the value of `a.liverecording` (i.e. `False`) at the time of the creation of `f`? – Basj Apr 29 '18 at 23:41
  • Yes, it's exactly the same thing. The class has no effect on the behavior of the function at all. The only thing the class does is make the `self` parameter implicit. – Aran-Fey Apr 29 '18 at 23:41
  • @Aran-Fey so does `f` internally keeps a pointer to `a.liverecording` instead of the value? – Basj Apr 29 '18 at 23:43
  • No, it keeps a pointer to `self`. You can see it if you take a look at `f.__closure__`. – Aran-Fey Apr 29 '18 at 23:47
  • Oh okey, I see now. Thank you @Aran-Fey. – Basj Apr 29 '18 at 23:58

1 Answers1

2

If you understand closures, the only trick here is that the local variable you're capturing in your closure is the self parameter inside getaudiocallback.

Inside that method, the self is of course the AudioEngine instance a. So, the value of the variable that you've captured is that same instance.

In fact, Python lets you reflect on almost everything at runtime, so you can see this directly:

>>> f = a.getaudiocallback()
>>> f
<function __main__.AudioEngine.getaudiocallback.<locals>.audiocallback(in_data, frame_count, time_info, status)>
>>> f.__closure__[0].cell_contents
<__main__.AudioEngine at 0x11772b3c8>
>>> f.__closure__[0].cell_contents is a
True

If getaudiocallback were still live, and it rebound self to some other value, that f.__closure__[0] would update to point to the new value of self. Since it's already exited, that's never going to happen; the cell will always be pointing at the instance that was in a at the time the method was called.

But if that instance later gets mutated, as when you write a.liverecording = True, of course you can see that.

abarnert
  • 354,177
  • 51
  • 601
  • 671