NumPy·Lesson 14·9 min·0/3 exercises

Vectorisation and ufuncs

The loop doesn’t disappear — it moves somewhere much faster.

elementwise opsnp.sqrtnp.wherenp.maximum+=out=

Watch it happen

Play it through, or step back and forth yourself.

out = []
for value in x:
    out.append(value * 2)
3
1
4
1
5
9
one at a time, in Python

To double every value in a list, Python makes you say so: a loop, one element at a time, with the interpreter doing bookkeeping on every pass. It works. It's just slow, and it buries the intent.

The idea

The single biggest shift from ordinary Python to NumPy is that you stop writing loops. Not because loops are forbidden, but because writing one over an array usually means you've missed the operation that already does it.

out = []                    #  the Python way
for value in x:
    out.append(value * 2)

x * 2                       #  the NumPy way

The second version isn't just shorter — it's a different amount of work. In the loop, Python creates an integer object per element, does a method call per element, and grows a list. In x * 2, NumPy hands a contiguous block to compiled C, which walks it with no Python involvement at all. Typically 10-100x faster, and the gap widens with size.

The loop is still there. It just isn't in your program any more.

Hand-drawn notes contrasting a Python loop stepping through elements one at a time with a single vectorised call handing the whole array to compiled code.

Ufuncs

Functions that work element by element are universal functions, or ufuncs. Arithmetic operators are ufuncs. So are np.sqrt, np.exp, np.log, np.sin, np.abs, np.round. All of them broadcast, so everything from the last lesson still applies.

Some take two arrays: np.maximum(x, y) compares element by element, which is different from np.max(x), which reduces one array to a single value. Similar names, unrelated jobs.

Hand-drawn notes contrasting np.maximum, which compares elementwise and returns an array of the same length, with np.max, which reduces the array to one value.

Watch the dtype

Vectorised arithmetic respects the array's dtype, and integer arrays overflow silently rather than growing:

np.array([200], dtype=np.uint8) + 100    # 44 — wrapped around
np.array([1, 2, 3]) / 2                 # float64, division always is

This matters most with images, which are uint8. Brightening one by adding 60 will wrap the bright pixels to black unless you widen first — you'll do exactly that in the capstone.

In place, when it counts

x = x + 1 allocates a whole new array. x += 1 writes into the existing one. For small arrays it doesn't matter; for large ones it halves your memory traffic. Just remember that in-place writes hit every view onto that memory — the trap from lesson 3.

Practice

Write it yourself. The answer is there when you want it.

Putting the kettle on…

Starting up…

Write it yourself

not graded

With x = np.array([3, 1, 4, 1, 5, 9]), print x * 2, its square roots, and np.maximum(x, 4). Then print np.max(x) straight after, so the difference between elementwise and reducing is on consecutive lines. Finish by adding 100 to a uint8 holding 200, and reading the answer carefully.

Write something and press Run — the output appears here.

Your turn

3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

Square every element of v, without writing a loop.

your answer

Take the square root of every element of cups, rounded to 2 decimal places.

your answer

Return cups with every value below 100 raised up to exactly 100, leaving the rest alone.

your answer