All courses › Intermediate Programming
Intermediate Programming: free practice, theory and problems
Object-oriented programming (OOP) lets you bundle data together with the functions that belong to it, in one unit: an object. A class is the blueprint, and an object is a concrete instance of it. As an engineer you use this to model real things – a sensor, a component, a simulation – so the code mirrors the problem and is easier to build on.
Contents
1. Object orientation
What is it about?
Object-oriented programming (OOP) lets you bundle data together with the functions that belong to it, in one unit: an object. A class is the blueprint, and an object is a concrete instance of it. As an engineer you use this to model real things – a sensor, a component, a simulation – so the code mirrors the problem and is easier to build on.
Concepts and formulas
- Class vs object: the class
class Car:is the blueprint,c = Car()creates an object (an instance). - Instance attribute: set with
self.x = ...in__init__, unique to each object. - Class variable: set directly in the class body (outside any method), shared by all objects of the class, unless an object gets its own version via
self.name = .... - Magic methods (dunder):
__init__(constructor),__str__(the text used byprint(obj)),__eq__(==),__len__(len(obj)). @staticmethod: a method withoutself, logically belongs to the class but does not need an object.@classmethod: takesclsinstead ofselfand operates on the class itself (e.g. an alternative constructor).- Inheritance and overriding: a subclass can define a method with the same name as the parent class; then the subclass's version runs when the method is called (polymorphism).
How to solve the problems
- Work out whether an attribute is a class variable (shared) or an instance variable (set with
self.in__init__). - Follow each object separately: which methods are called on which object, and in what order?
- With inheritance: check which class's version of the method actually runs (the most specific one defined for that object).
- With a class variable: remember that a change made through one object can affect all others, unless a new instance variable with the same name is created.
Example
class Account:
rate = 0.02 # class variable, shared by all accounts
def __init__(self, balance):
self.balance = balance # instance variable
def add_interest(self):
self.balance += self.balance * Account.rate
a = Account(1000)
b = Account(500)
Account.rate = 0.05
a.add_interest()
print(round(a.balance), round(b.balance))
aandbeach get their ownbalance(instance variables): 1000 and 500.Account.rate = 0.05changes the class variable for every object, since neither of them has its ownrate.- Only
a.add_interest()is called: . b.balanceis unchanged sinceadd_interestwas never called onb.
The output is 1050 500.
Common mistakes
- Believing a class variable is private to each object. It is shared until someone sets an instance variable with the same name.
- Forgetting
selfas the first parameter of a regular method. - Forgetting the parentheses for inheritance:
class ElectricCar(Car):, notclass ElectricCar: Car. - Confusing
__init__(called automatically when the object is created) with a regular method you must call yourself. - Believing that
@staticmethodhas access toselfor the object's attributes – it does not.
Concepts in this part
2. Algorithms and data structures
What is it about?
When data grows large, it is not enough that the code works – it also has to be fast enough. Complexity analysis (Big-O) gives you a language for describing how much work an algorithm does as the amount of data grows, independent of the computer. As an engineer you need this to choose the right data structure and algorithm before the code becomes slow on real, large datasets – for example measurement series with millions of points.
Concepts and formulas
- Amortized complexity:
my_list.append(x)is on average, even though the list occasionally has to be copied to a larger block of memory. my_list.insert(0, x)andmy_list.pop(0)are : every element behind the insertion point has to be shifted.- A recursive function needs a base case to stop, otherwise it raises
RecursionError. The number of recursive calls is often tied to the size of the input. - Inversion: a pair of elements that are in the wrong order in a list. Insertion sort swaps neighboring elements until every inversion is gone.
- Common complexity classes from fastest to slowest: .
- Two nested loops that both run times together give (quadratic).
How to solve the problems
- Find which part of the code repeats the most as grows – that is what dominates the complexity.
- Count nested loops: one loop over gives , a loop inside a loop gives .
- For recursion: write out the calls until the base case is reached, and count them.
- For sorting: follow the algorithm step by step with a concrete list, noting every comparison and every swap.
Example
Insertion sort swaps an element backward until it is in the right place, one step at a time:
def sort(a):
swaps = 0
for i in range(1, len(a)):
j = i
while j > 0 and a[j-1] > a[j]:
a[j-1], a[j] = a[j], a[j-1]
j -= 1
swaps += 1
return swaps
print(sort([3, 1, 2]))
- : compare and . , so they are swapped: , swaps = 1.
- : compare and . , swap: , swaps = 2. Now , so the inner loop stops.
- The list is sorted, and the function returns 2.
The output is 2, which is the number of inversions in the original list (the pairs 3–1 and 3–2).
Common mistakes
- Believing that
appendandinsert(0, x)are equally fast. They are and respectively. - Forgetting the base case in a recursive function.
- Counting only the outer loop and forgetting that an inner loop multiplies the work.
- Mixing up the number of comparisons and the number of swaps in a sorting algorithm – they are not always equal.
- Believing that and are about equally fast for large . The difference becomes enormous.
Concepts in this part
3. Numerics with NumPy
What is it about?
NumPy gives Python fast, vectorized computation with numeric arrays – essential for engineering calculations on large datasets, such as measurement series and simulations. You will also learn basic numerical methods: Newton's method for finding the roots of a function, and Euler's method for solving differential equations approximately, step by step. try/except lets the program handle bad data without crashing.
Concepts and formulas
np.array: the operators+ - * /act elementwise, not as matrix multiplication. Use@ornp.dotfor a matrix product.- Boolean indexing:
a[a > k]gives a new array with only the elements that satisfy the condition. - Broadcasting: arrays with the same shape, or where one dimension is 1, can be combined elementwise. Different, incompatible shapes raise
ValueError. np.mean,np.std,np.sum,a.min()/a.max(): statistics over an array.- Newton's method: , follows the tangent line down to the root.
- Euler's explicit method: for .
try: ... except SomeError: ...catches one specific error type. A bareexcept:catches everything, including errors you should really have noticed.
How to solve the problems
- Check whether the code computes elementwise or does matrix arithmetic – look for
*versus@/np.dot. - With boolean indexing: work out the condition for every element first, then keep only the ones that are
True. - For Newton/Euler: plug the numbers into the formula, one step at a time, and keep track of which value is / for the next step.
- Check the shapes of the arrays before adding them – do they match, or can they broadcast?
Example
A sensor logs 5 measurements. We want the mean of the measurements above 20:
import numpy as np
a = np.array([15, 24, 31, 18, 27])
above = a[a > 20]
print(above, round(above.mean(), 2))
a > 20gives a boolean array[False, True, True, False, True].a[a > 20]uses it to pick out the values where the condition is true:[24, 31, 27].- The mean is .
The output is [24 31 27] 27.33.
Common mistakes
- Confusing
*(elementwise) with@/np.dot(matrix multiplication). - Believing that boolean indexing returns
True/Falsevalues instead of the actual numbers. - Adding arrays with incompatible shapes and being surprised by
ValueError. - Using a bare
except:that catches and hides errors you should really have noticed. - Dividing by
f'(x)without checking that it is not zero or very small in Newton's method.
.mean()/.sum() is the most common way to filter and summarize measurement data.Concepts in this part
Example problems with solutions
Here are some of the problems in intermediate Programming. In the app, calculation problems get new numbers every time, so you can practise until it sticks – and take a graded practice exam before the real one.
Object orientation: What is __init__ in a Python class?
Answer: The constructor that runs when an object is created
It sets up the object's attributes.
Algorithms and data structures: What is the time complexity of binary search in a sorted list?
Answer:
The search range halves at every step.
Numerics with NumPy: What does np.linspace(0, 1, 5) give?
Answer: [0, 0.25, 0.5, 0.75, 1]
Five evenly spaced points, including both end points.
Object orientation: What does self refer to in a method?
Answer: The object (instance) the method is called on
obj.method() becomes Class.method(obj).
Matches these university courses
The content covers the syllabus found in engineering degrees, for example:
- MEK3100 (OsloMet)
- TDT4100 (NTNU)
- INF200 (NMBU)