Python closure, local variable scope error

2019-01-20 18:37发布

问题:

My function throw me with the local variable 'pt' referenced before assignment error:

Traceback (most recent call last):
  File "/home/solesschong/Workspace/PenPal/python/main.py", line 126, in callback
    ind = (i+pt) % n
UnboundLocalError: local variable 'pt' referenced before assignment

the code is as follows

def get_audio_callback(pt):

    def callback(in_data, frame_count, time_info, status):

        for i in range(frame_count):
            ind = (i+pt) % n

        return (a, b)

    return callback

and in global scope,

pt = 0
stream = p.open(stream_callback=get_audio_callback(pt))

I cannot figure out why the error occurs, since I've checked with some examples on closure and find no difference.

Edit

The reason why you cannot reproduce the error might because of the over-simplify, as mentioned by @Martijn Pieters. Hence the original code.

Further I've solved this problem by passing by reference, plz see my own answer.

"""
Sound API
"""
def get_audio_callback(pt):

    def callback(in_data, frame_count, time_info, status):
        """
        This is the callback function for sound API
        In each call, synthesized data is dumpped into the sound buffer
        """        

        wave = np.ndarray((frame_count, 2))
        for i in range(frame_count):
            ind = (i+pt) % n
            wave[i,0] = float(x[ind]) * 2
            wave[i,1] = float(y[ind]) * 2
        pt = pt + frame_count

        return (encode(wave), pyaudio.paContinue)

    return callback


p = pyaudio.PyAudio()
pt = 0

stream = p.open(format=pyaudio.paFloat32,
                channels=2,
                rate=RATE,
                output=True,
                stream_callback=get_audio_callback(pt))

回答1:

Your code assigns to pt in callback; Python determines the scope of a name at compile time and assignment makes this a local name.

pt = pt + frame_count

Unless you tell Python otherwise, that is. In Python 2, you can only mark a name explicitly as a global instead, you need Python 3 to be able to use the nonlocal keyword:

def callback(in_data, frame_count, time_info, status):
    """
    This is the callback function for sound API
    In each call, synthesized data is dumpped into the sound buffer
    """        

    nonlocal pt

    wave = np.ndarray((frame_count, 2))
    for i in range(frame_count):
        ind = (i+pt) % n
        wave[i,0] = float(x[ind]) * 2
        wave[i,1] = float(y[ind]) * 2
    pt = pt + frame_count

    return (encode(wave), pyaudio.paContinue)

With the nonlocal pt line Python is explicitly told not to treat pt as a local name but to take it from the enclosing scope of get_audio_callback instead.

In Python 2, you can just create a local that takes its value from the closure:

def callback(in_data, frame_count, time_info, status):
    """
    This is the callback function for sound API
    In each call, synthesized data is dumpped into the sound buffer
    """        

    pt_local = pt

    wave = np.ndarray((frame_count, 2))
    for i in range(frame_count):
        ind = (i+pt_local) % n
        wave[i,0] = float(x[ind]) * 2
        wave[i,1] = float(y[ind]) * 2
    pt_local = pt_local + frame_count

    return (encode(wave), pyaudio.paContinue)

because your enclosing get_audio_callback scope doesn't appear to use pt anyway and won't need access to the updated pt_local value.

If you do need pt to update at the get_audio_callback scope (if, say, callback is called multiple times and you need pt to be updated from call to call), you need to avoid using pt as a local inside the callback function altogether.

One effective work-around for that is to wrap the value in a mutable object or assign it as a mutable attribute somewhere that both the enclosing scope and the local scope can access it without it ever being seen as a local assignment. Setting an attribute on the callback function is a good way to do that:

def get_audio_callback(pt):

    def callback(in_data, frame_count, time_info, status):
        """
        This is the callback function for sound API
        In each call, synthesized data is dumpped into the sound buffer
        """        

        wave = np.ndarray((frame_count, 2))
        for i in range(frame_count):
            ind = (i+callback.pt) % n
            wave[i,0] = float(x[ind]) * 2
            wave[i,1] = float(y[ind]) * 2
        callback.pt = callback.pt + frame_count

        return (encode(wave), pyaudio.paContinue)

    callback.pt = pt

    return callback

Here callback.pt is no longer a local name; it is an attribute on the callback function object instead.



回答2:

It turned out to be a problem about 'reference'

I changed my code into passing pt by variable, and it worked out fine.

pt = [0]

def get_audio_callback(pt_ref):

    def callback(in_data, frame_count, time_info, status):

        pt = pt_ref[0]

        for i in range(frame_count):
            ind = (i+pt) % n

        return (a, b)

    return callback