Give a Reusable Calculation a Clear Contract
Copying a calculation into every exercise makes mistakes hard to repair. We will put the rule in a function, state what its inputs mean, and check it on more than one report.
10 sections · 4 questions · 2 coding challenges
The arrow keys move between sections too.
Making Numerical Routines Reusable Without Losing Their Assumptions
BOTIE
NumPy's array conversion and testing interfaces support those contracts. They let a Python function accept array-like input, validate its layout, and compare a result with a small independently checked example. The function remains responsible for its scientific assumptions.
Sources for this history
Can We Explain the Rule Once and Use It Again?
Botie keeps converting new greenhouse reports. If we copy the same calculation into each program, one copy may use a different offset or quietly change the original data. A named function gives the rule one place to live.
The name alone is not enough. The function needs a clear agreement about its inputs, output shape, units, and treatment of invalid data. We will build that agreement as part of the code.
Name the input meaning
The function receives Celsius readings in their original greenhouse order.
Input
- 12
- 18
- 24
What we can now calculate
The same fitted reference explains every output. Collecting both results mirrors a function returning its reference and transformed array.
Explore the full example
Choose an example, then change a value. Follow the calculation below to see which result changes and why. These are small teaching examples; the construction later asks you to choose the operations yourself.
BOTIE
Result from these values
[12,[0,6]]
Follow the operations
1. Training readings
- 010
- 114
2. Mean
12
3. New report
- 012
- 118
4. Subtract
- 00
- 16
5. Collect two results
[12,[0,6]]
6. Result
[12,[0,6]]
Workshop cases satisfy the finite, nonempty vector contract. Python is where we define functions and handle invalid inputs. Editing this example does not change the workshop tests.
1. Separate a Function Definition From a Call
def introduces a function. Its parameters name the inputs inside the function; the indented body describes the work. Defining it does not yet perform the calculation. A call supplies actual arguments. return sends a result back to the caller.
Printing and returning are different: print displays something; return makes the result available to the next calculation. A function that only prints cannot be used as a numerical result in the same way.
Define the conversion, then use its returned array
asarray accepts a list or array and requests floating-point values. The arithmetic creates the output without modifying the incoming readings.
import numpy as np
def to_fahrenheit(celsius):
values = np.asarray(celsius, dtype=float)
return values * 1.8 + 32
first = to_fahrenheit([12,18,24])
reference = to_fahrenheit([0,100])
print(np.round(first,1).tolist())
print(reference.tolist())[53.6, 64.4, 75.2]
[32.0, 212.0]The function has one local parameter, but two calls provide different data. Neither call needs a separate copy of the conversion rule.
2. Reject an Input That Does Not Match the Promise
Suppose the function promises one nonempty vector of finite readings. A matrix, an empty vector, or a NaN should produce a useful error. Check these conditions before performing the calculation.
A raise statement stops the function with an exception. ValueError is appropriate when an argument's value or arrangement is unsuitable. A caller can catch a particular exception with try and except when it has a meaningful response.
Make the failed assumption visible
The three checks are separate because their error messages describe different problems. len is not a substitute for ndim: a matrix also has a length.
import numpy as np
def checked_readings(values):
array = np.asarray(values, dtype=float)
if array.ndim != 1:
raise ValueError("Supply one vector of readings.")
if array.size == 0:
raise ValueError("Supply at least one reading.")
if not np.isfinite(array).all():
raise ValueError("Every reading must be finite.")
return array
print(checked_readings([0,18]).tolist())
try:
checked_readings([[12,18]])
except ValueError as error:
print(str(error))[0.0, 18.0]
Supply one vector of readings.The all reduction turns the finite mask into one Boolean, so Python's not is appropriate here. The error describes the input contract instead of letting a later shape failure obscure it.
3. Return the Reference Along With the Result
Some calculations first learn a reference, then apply it elsewhere. If Botie centres each new report on its own average, comparisons no longer share the same reference. Learn from the training readings once and pass that value to the next operation.
A function can return more than one result in a tuple. The caller can unpack that tuple into names that keep their meanings distinct.
Keep the fitted reference separate from the new report
This example accepts already validated, nonempty finite vectors. The training average is learned before the new report is transformed.
import numpy as np
def centre_from_training(training, report):
training = np.asarray(training, dtype=float)
report = np.asarray(report, dtype=float)
reference = training.mean()
return reference, report - reference
reference, centred = centre_from_training([10,14], [12,18])
print(reference)
print(centred.tolist())12.0
[0.0, 6.0]The reference is one scalar and the centred report is an array. Returning both makes it possible to inspect the fitted value and reuse it without recalculating it from later observations.
4. Test More Than the First Demonstration
A check should describe what the function must do, not merely repeat the same implementation. Use reference cases with independently known answers, then include a changed input and a boundary case. Check shapes and whether the input was preserved.
A loop is useful here because we want to run a collection of separate test cases. It is not the numerical operation we are trying to vectorise.
Check familiar reference temperatures and input preservation
A test case is a pair of input and expected-output lists. The loop unpacks each pair, calls the function, and checks its contract. The final report only prints if every assertion succeeds.
import numpy as np
def to_fahrenheit(celsius):
return np.asarray(celsius, dtype=float) * 1.8 + 32
cases = [([0,100],[32,212]), ([-40],[-40])]
for values, expected in cases:
original = np.array(values, dtype=float)
saved = original.copy()
result = to_fahrenheit(original)
assert result.shape == original.shape
np.testing.assert_allclose(result, expected, rtol=0, atol=1e-10)
assert np.array_equal(original, saved)
print("2 cases passed; inputs preserved.")2 cases passed; inputs preserved.The tests do not fit a model or prove the conversion for every conceivable input. They check concrete expectations that would catch common mistakes in the rule and its handling of arrays.
Check yourself
Questions on Parts 1 to 4
Question 1 of 4
What does the caller receive?
Use this setup. The code is given; its result is for you to work out.
import numpy as np
def convert(values):
return np.asarray(values, dtype=float) * 1.8 + 32
result = convert([0,100])Try it with Botie
Build with Botie
Write and run Python