Search code examples
pythonwalrus-operator

Walrus Operator Doesn't Assign Variable?


Playing with the walrus operator, I have this implementation of merge sort:

def mergesort(array):
    if len(array) == 1:
        output = array
    else:
        pivot = len(array) // 2
        left = mergesort(array[pivot:])
        right = mergesort(array[:pivot])
        output = []
        while (l := len(left)) or (r := len(right)):
            if l and r and left[0] < right[0]:
                output.append(left.pop(0))
            elif r:
                output.append(right.pop(0))
            else:
                output.append(left.pop(0))
        
    return output

mergesort([66, 93, 85, 46, 56, 88, 56, 75, 55, 99, 87])

But this returns the error UnboundLocalError: local variable 'r' referenced before assignment:

---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
/tmp/ipykernel_134/1678992391.py in <module>
----> 1 mergesort(array)

/tmp/ipykernel_134/4030760045.py in mergesort(array)
      5     else:
      6         pivot = len(array) // 2
----> 7         left = mergesort(array[pivot:])
      8         right = mergesort(array[:pivot])
      9         

...

/tmp/ipykernel_134/4030760045.py in mergesort(array)
     10         output = []
     11         while (l := len(left)) or (r := len(right)):
---> 12             if l and r and left[0] < right[0]:
     13                 output.append(left.pop(0))
     14             elif r:

UnboundLocalError: local variable 'r' referenced before assignment

Why isn't r included in my for loop, while l is?


Solution

  • Boolean OR or is lazy, so when l turns out to be true, (r := len(right)) doesn't even get executed.

    You could use the non-lazy bitwise OR | in this case, although it's a bit of an abuse.

    Or just use the truth value of the lists instead of their lengths:

    while left or right:
        if left and right and left[0] < right[0]:
            output.append(left.pop(0))
        elif right:
            output.append(right.pop(0))
        else:
            output.append(left.pop(0))
    

    Btw better use <= instead of <, so that it's a stable sort as mergesort ought to be.

    Addendum: Having fun with laziness:

    while left or right:
        which = (left or right)[0] <= (right or left)[0] and left or right
        output.append(which.pop(0))
    

    Another, note I switched to while ... and ... and append the remaining non-empty one after the loop:

    while left and right:
        which = left if left[0] <= right[0] else right
        output.append(which.pop(0))
    output += left or right
    

    Or back to your style:

    while left and right:
        if left[0] <= right[0]:
            output.append(left.pop(0))
        else:
            output.append(right.pop(0))
    output.extend(left or right)