Build and Test a Sensor Class

EE1021 Programming for Engineers · Lecture 7 Lab · 2026–2027

Basri Erdogan

Lab Outcomes

By the end of the lab, you should be able to:

  1. Initialize independent objects from one class
  2. Trace which instance becomes self
  3. Implement methods that change or report state
  4. Use __str__ to return a useful representation
  5. Enforce a small class contract with exceptions
  6. Verify normal, boundary, invalid, and independent-instance behavior
  7. Explain one method during an individual checkoff

Working Method

For every task:

  1. Predict object state or output.
  2. Implement one method or correction.
  3. Run one focused test.
  4. Explain what self refers to.
  5. Retest a nearby case.

Open starter/lecture7_lab.py and keep the supplied public method names unchanged.

Pre-Lab Diagnostic

class Sensor:
    readings = []

    def add_reading(self, value):
        self.readings.append(value)

Why could two Sensor instances unexpectedly contain the same readings?

readings is attached to the class and refers to one shared list. Create self.readings = [] inside __init__.

Stage 1: Construct Independent Sensors

Task 1: Complete __init__

The constructor receives:

  • sensor_id, a non-empty string;
  • unit, a non-empty string.

It must create three instance attributes:

self.sensor_id
self.unit
self.readings

Create a new empty readings list for every instance.

Task 2: Test Two Instances

t1 = Sensor("T1", "C")
p1 = Sensor("P1", "kPa")

Verify:

  • their IDs and units differ;
  • both reading lists begin empty;
  • t1.readings is not p1.readings.

Record the expected state before running.

Check-Off A: Construction

Explain:

  1. which object becomes self in each constructor call;
  2. why self.sensor_id = sensor_id is not circular;
  3. why the readings list belongs inside __init__.

Stage 2: Add and Inspect Readings

Task 3: Implement add_reading

Contract:

def add_reading(self, value):
    """Append one numeric reading and return None."""

Raise TypeError("reading must be numeric") for a nonnumeric value.

Test the method on both objects and verify that only the receiver changes.

Trace the Receiver

t1.add_reading(20.0)
p1.add_reading(100.0)
t1.add_reading(22.0)

Complete:

call receiver passed as self affected list

The receivers are t1, p1, and t1; the final lists are [20.0, 22.0] and [100.0].

Task 4: Implement Query Methods

Implement:

reading_count()
latest()
average()
  • reading_count returns an integer.
  • latest returns the final reading.
  • average returns the arithmetic mean.
  • latest and average raise ValueError("sensor has no readings") when empty.

Command or Query?

Classify each method:

  • add_reading
  • reading_count
  • latest
  • average

add_reading is a command that changes state. The other three are queries that return information.

Stage 3: Classify the Latest Reading

Task 5: Implement status

def status(self, low, high):
    """Return low, normal, or high for the latest reading."""

Rules:

  • below low → "low";
  • above high → "high";
  • otherwise → "normal";
  • low > high raises ValueError("low must not exceed high").

Reuse latest() rather than indexing the list again.

Boundary Check

For limits low=20 and high=30, predict the status of:

  • 19.9;
  • 20.0;
  • 25.0;
  • 30.0;
  • 30.1.

Low, normal, normal, normal, high.

Stage 4: Create a Useful String Interface

Task 6: Implement __str__

Required format:

T1: 2 readings (C)

The method must return a string. It must not print.

Verify both:

str(t1)
print(t1)

Why Return Instead of Print?

What happens if __str__ prints the ID but returns nothing?

The method implicitly returns None, so str() and print(object) cannot obtain the required string and Python raises TypeError.

Stage 5: Integrate and Verify

Task 7: Build a Sensor Report

Implement the supplied standalone function:

def sensor_report(sensor, low, high):
    """Return a dictionary summarizing one Sensor."""

Return:

  • description;
  • count;
  • latest;
  • average;
  • status.

Use the object’s methods instead of duplicating formulas.

Task 8: Process Test Data

The starter folder includes starter/sensor_test_data.txt.

Each valid line has:

SENSOR_ID,UNIT,VALUE

Optional extension: parse the file, build one Sensor object per ID, and add each reading to the appropriate object.

Reuse Lecture 5 dictionaries and Lecture 6 parsing principles.

Required Regression Tests

Your tests must cover:

  • two independent instances;
  • one and several readings;
  • empty latest and average calls;
  • nonnumeric input;
  • values below, at, and above status boundaries;
  • reversed limits;
  • the exact __str__ result.

Final Checkpoint

Choose one completed method and explain:

  1. its contract;
  2. which object becomes self;
  3. whether it changes or reports state;
  4. one normal test;
  5. one boundary or invalid test.

Individual Checkoff

Instructor-Selected Demonstration

Each student independently completes one action:

  • repair an initializer that creates shared state;
  • trace self through two method calls;
  • implement a query method;
  • implement a state-changing method;
  • repair __str__;
  • add one boundary or invalid-input assertion.

One retry is available after focused feedback.

Submission Checklist

  • Constructor inputs become instance attributes.
  • Every instance receives its own readings list.
  • Public methods match their documented contracts.
  • No user input or printing occurs inside reusable methods.
  • __str__ returns the required string.
  • Methods reuse other methods where appropriate.
  • Normal, boundary, invalid, and independent-instance tests pass.
  • Each partner can explain what self refers to.