Skip to content

Instantly share code, notes, and snippets.

@fabianbaechli
Last active January 7, 2026 12:46
Show Gist options
  • Select an option

  • Save fabianbaechli/b86b47729443734d0a8b3e66fd543b68 to your computer and use it in GitHub Desktop.

Select an option

Save fabianbaechli/b86b47729443734d0a8b3e66fd543b68 to your computer and use it in GitHub Desktop.

Assignment 1

smart_house.py

How does SmartHouseManagement have access to all current instantiated objects?

  • Is not done via introspection (first approach)
    • We couldn’t use the if __name__ == "__main__" guard this way (objects instantiated inside of guard aren’t in globals)
  • We added a hook in make that adds new objects (just dicts) to a global instances list, SmartHouseManagement class uses this list to iterate over all objects

What tradeoffs were taken during the project?

  • find function has no guard for circular dependencies
    • If A inherits from B and B inherits from A, the function will loop forever
    • How to solve?
      • By adding a flag visited to classes

In what order does find find implementations of a function?

Class A: _parents   = ["Foo", "Bar"]
Class Foo: _parents = ["Foobar", "Baz"]
Class Bar: _parents = ["Foobaz", "Bazbar"]

Lookup Order:
1. Class A
2. Class Foo
3. Class Foobar
4. Class Baz
5. Class Bar
6. Class Foobaz
7. Class Bazbar
  • Lookup order is DFS, left to right (order of _parents array matters!)
  • This doesn’t guarantee finding the “most specific” implementation of a function in the whole inheritance tree → First occurrence of function is returned
  • Modern languages use more sophisticated approaches

What happens if order of _parents array changes?

⇒ Lookup order changes!

What happens if a function isn’t found in find?

NotImplementedError is raised (same as when calling an abstract function)

What is stored inside an instance? How are multiple inheritance instances constructed?

Inside an instance, only instance-specific-properties are stored PLUS the link to the respective _class dictionary which holds all methods a class implements (are the same for all instances of said class)

Constructing a multiple inheritance instance is done via the | operator (merges dictionaries)

  • The constructor method of a class merges the returned dictionaries of all make calls of the parent classes of a class with the properties of the constructed class
  • It overwrites the _class property of the returned dictionaries, that’s why the class-specific dictionary has to come at the end of the | chain ⇒ later dicts in | chain override previous values!
# Thermostat inherits from both Device and Connectable
def thermostat_new(name: str, location: str, base_power: float, status: str, room_temperature: int, target_temperature: int, connected: bool = False,ip: str = "",):
  return (
    make(Device, name, location, base_power, status)
    | make(Connectable, connected, ip)
    # Merge of class specific properties at the end!
    # Overrides of _class
    | {
      "_class": Thermostat,
      "room_temperature": room_temperature,
      "target_temperature": target_temperature,
    }
  )

How does the inheritance look like for Device, Connectable, Light, Thermostat, Camera, SmartHouseManagement?

  • Device: _parents = []
  • Connectable: _parents = []
  • Light: _parents = [Device]
  • Thermostat: _parents = [Device, Connectable]
  • Camera: _parents = [Device, Connectable]
  • SmartHouseManagement: _parents = []

Device and Connectable are abstract parent classes, not to be actually constructed

  • Their constructors are only used inside Light, Thermostat and Camera that’s why the constructors of Device and Connectable don’t raise NotImplementedError even though they are abstract
  • Constructing Device and Connectable is not added to global instances list and thus not regarded by SmartHouseManagement

What’s the difference between is and == and what’s the implication for the code?

  • is (and is not) checks whether two objects are stored at the same memory address
    • This is why you could have two identical instances of a class and is would return False when comparing them that way
  • == checks whether two objects or numbers are the same (disregarding whether they point to different locations)

⇒ Implication: When filtering in SmartHouseManagement via search_type for example, the global class dictionary has to be passed in, a copy would not be found, because SmartHouseManagement checks device["_class"] is not search_type :

# Creates a separate object at a different memory address
Light2 = Light.copy()

# False
Light2 is Light

# True
Light2 == Light

How does the call function make sure it is able to call all functions of all instances even if they require different amounts of arguments?

It handles different numbers of arguments via *call is called via an arbitrary amount of parameters

  • It passes these parameters to the respective method via *args (or **kwargs)
  • This means that the arguments passed to call must match the method signature of the method we want to call

What is inheritsFrom and is there a tradeoff there?

inheritsFrom is just a simple helper function designed to make the testing in the main guard read a bit easier

  • Its tradeoff is, that it only checks single-depth inheritance!!

What are the rules for calculating power consumption?

  • 0 when off for all devices
  • Light: round(device["base_power"] * device["brightness"] / 100)
  • Thermostat: return device["base_power"] * abs(device["target_temperature"] - device["room_temperature"]
  • Camera: device["base_power"] * device["resolution_factor"]

Requirement for a device to be found in get_all_connected_devices and what does the function return?

  1. Connectable as parent
  2. connected == True
  3. status != off
  4. Optional IP match

get_all_connected_devices returns:

A list of: {"description": description, "power": power} dicts

test_smart_house.py

How does test function discovery work?

  • globals.items() is iterated ⇒ introspection
  • Prefix is stripped ⇒ test_toggle_status stored as toggle_status in dict with all test functions
  • Only objects which are callable (= functions) are stored

How does the --select parameter work and how is it implemented?

It runs only those functions whose names contain the value passed into select: python test_smart_house.py --select light runs only functions containing light

Implementation:

  1. When starting the test suite, if "--select" in sys.argv is checked (sys.argv is a list)
  2. If so, the index of the --select flag is extracted index = sys.argv.index("--select")
  3. The value passed in for --select must naturally be the next element in sys.argv: keyword = sys.argv[index + 1]
    • If no value is passed, IndexError is caught and program ends
    • If value of --select starts with -- also, there is something wrong (flag passed as value), so program returns
  4. After keyword has been successfully extracted, actual running of tests begins. Function calls skip if: keyword is not None and keyword.lower() not in testFunctionName.lower():

How is the test suite timing the function calls?

  • setUp and tearDown called before each function call
  • Both functions return the current time, the delta is added to the total execution time and printed in the end

What’s a side effect that setUp has?

  • Side Effect: anything a function does to anything outside its body ⇒ anything different than calculating a return value and returning it
  • The important side effect in setUp is, that it clears the instances array in smart_house.py so that every test function starts out with a “fresh” smart_house.py

How are pass, fail and error tracked inside the suite?

  • Done via the dictionary results = {"pass": 0, "fail": 0, "error": 0}
    • When a function raises an AssertionError, fail is incremented by one
    • When a function raises an Exception of any other kind, error is incremented
    • When the function runs normally, pass is incremented by one

How does testing of SmartHouseManagement differ from other tests?

  • Tests for SmartHouseManagement use mock objects and classes, not constructed via make!
  • The classes point to _fixed_power and _fixed_desc for get_power_consumption and describe_device
    • _fixed_power and _fixed_desc both return obj["__power"] and obj["__desc"]

What does the --verbose flag do?

If set, it prints all variables starting with test

  • Potential problem here: This block runs every time because it is not protected by main guard ⇒ would also print when just importing the module!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment