We closed the post More state power with a function that collects n random numbers between start and end into a list. Here is the function again:

def n_uniq_random(n, start, end):
    """Return a list of n unique random numbers between start and end. """

    from random import randint
    from funcutils import proc

    def update(s):
        x = randint(start, end)
        if x not in s['uniq_nums']:
            s['uniq_nums'] = s['uniq_nums'] + [x]
            s['count'] = s['count'] + 1
        return s

    return proc(state={'uniq_nums': [], 'count': 0},
                alive=lambda s: s['count'] < n,
                update=update
                )['uniq_nums']

If we wanted to send our update functions as a lambda expression, this would have been impossile because our function is full of assignments. One for the random number x, and two for updating the dictionary s. The if statement is also a problem, and we cannot turn it into an expression if since its arguments are assignments, not expressions.

Actually, dictionary key assignment is not a problem, because it is totally unnecessary. You can get rid of it by:1

def update(s):
    x = randint(start, end)
    if x not in s['uniq_nums']:
        return {'uniq_nums' : s['uniq_nums'] + [x],
                'count' : s['count'] + 1}
    else:
        return s

Once you get rid of assignments inside the if statement, you can turn the if into an expression:

def update(s):
    x = randint(start, end)
    return ({'uniq_nums' : s['uniq_nums'] + [x], 'count' : s['count'] + 1}
            if x not in s['uniq_nums']
            else s)

In this solution you have to re-write the dictionary s you received as argument. It is fine for this case but this may be inefficient especially in cases where you have many keys in the state dictionary and you update only one or two of them. You will then have to write the whole dictionary again. Furthermore, imagine you have 3 conditions according to which you change a different key in the dictionary. You will then have to write the whole dictionary 3 times, once for each condition. This is tedious and, more importantly, error-prone.

Actually you do not have to write the entire dictionary. We can use the dictionary merging operator ‘|’ to create a new dictionary with the updated values instead of modifying the existing one in place. Here is how merge works:

>>> d = {'a': 1, 'b': 2, 'c': 3}
>>> d
{'a': 1, 'b': 2, 'c': 3}
>>> d | {'b': 5}  # new dictionary with 'b' mapped to 5
{'a': 1, 'b': 5, 'c': 3}
>>> d # original dictionary remains unchanged
{'a': 1, 'b': 2, 'c': 3}
>>> d | {'d': 7, 'c':9}  # multiple changes in a single stroke 
{'a': 1, 'b': 2, 'c': 9, 'd': 7}
>>> d | {'b': d['b'] + 3}  # updating 'b' based on its previous value
{'a': 1, 'b': 5, 'c': 3}
>>> d | {'b': d['b'] + 3, 'e': 10}  # updates and additions
{'a': 1, 'b': 5, 'c': 3, 'e': 10}

The update function above can be re-written, of course unnecessarily in this case, as:

def update(s):
    x = randint(start, end)
    return (s | {'uniq_nums' : s['uniq_nums'] + [x], 'count' : s['count'] + 1}
            if x not in s['uniq_nums']
            else s)

This would work perfectly fine in a setting where there are other keys in your state dictionary which you want to leave untouched when updating 'uniq_nums' and 'count'.

Having handled dictionary manipulation without using assignment, what do we now do with the variable assignment x = randint(start, end)? We simply cannot do:

def update(s):
    return (s | {'uniq_nums' : s['uniq_nums'] + [randint(start, end)], 
                 'count' : s['count'] + 1}
            if randint(start, end) not in s['uniq_nums']
            else s)

since the two calls of randint will give different values. We need to store the value of randint(start, end) somewhere to use it twice.

Let’s simplify the problem a little bit. Suppose we want to write a function that generates a random integer between 1 and 10, and returns it if it is even, otherwise returns -1. Using assignment, we can write:

from random import randint
def random_even(start,end):
    """Pick a random integer between start and end, and return it if even or -1 if odd."""
    x = randint(start, end)
    return x if x % 2 == 0 else -1

If you want to write this as a lambda expression, we saw above that the following will not work:

from random import randint
random_even = lambda start, end: (randint(start, end)
                                  if randint(start, end) % 2 == 0
                                  else -1)

as this picks two different random numbers, one used for evenness check and other as return value.

As lambda expressions are expressions, it shouldn’t be hard to make sense of this:

>>> from random import randint
>>> (lambda x: (x if x % 2 == 0 else -1))(8)
8
>>> (lambda x: (x if x % 2 == 0 else -1))(7)
-1

Lambda expressions are simply names of functions, whenever you have a name a you can apply it to its argument x by a(x). What we did above is exactly this.

I guess this also makes sense:

>>> from random import randint
>>> (lambda x: (x if x % 2 == 0 else -1))(randint(1,10))
# some return value according to the result of randint(1,10)

I couldn’t write the return value as it is random. But we know that the value returned by randint will be bound to x and used both in the test and the success expression. With all this in place, here is our random_even function as a lambda expression:

>>> from random import randint
>>> random_even = lambda start, end: (lambda x: (x if x % 2 == 0 else -1))(randint(start, end))

And, here is our update:

update = lambda s:
                  (lambda x:  (s | {'uniq_nums' : s['uniq_nums'] + [x], 'count' : s['count'] + 1}
                               if x not in s['uniq_nums']
                               else s))(randint(start, end))

Thankfully, the developers of Python have found a way to simplify this somewhat cumbersome syntax by introducing the “walrus operator” :=. Using this operator, you can perform assignments inside expressions. First, see it in action in random_even:

>>> from random import randint
>>> random_even = lambda start, end: x if (x := random(start,end)) % 2 == 0 else -1

And here is our final update function using the walrus operator:

update = lambda s: (s | {'uniq_nums' : s['uniq_nums'] + [x], 'count' : s['count'] + 1}
            if (x := randint(start,end)) not in s['uniq_nums']
            else s)

The walrus operator is not restricted to lambda expressions, you can use it in regular function as well. We will make good use of it when we deal with comprehensions.

  1. From here on the code blocks of isolated update are meant to be placed inside n_uniq_random function.