Instructions

In this lab, you have two tasks:

  • Think about what would Python display in section 3 and verify your answer with Ok.
  • Complete the required problems described in section 4 and submit your code to our OJ website.

The starter code for these problems is provided in lab06.py.

Submission: As instructed before, you need to submit your work with Ok by python ok --submit. You may submit more than once before the deadline, and your score of this assignment will be the highest one of all your submissions.

Readings: You might find the following reference to the textbook useful:

Review

Consult this section if you need a refresher on the material for this lab. It's okay to skip directly to the next section and refer back here when you get stuck.

Object-Oriented Programming

Object-oriented programming (OOP) is a style of programming that allows you to think of code in terms of "objects." Here's an example of a Car class:

#{{}}
class Car(object):
    num_wheels = 4

    def __init__(self, color):
        self.wheels = Car.num_wheels
        self.color = color

    def drive(self):
        if self.wheels <= Car.num_wheels:
            return self.color + ' car cannot drive!'
        return self.color + ' car goes vroom!'

    def pop_tire(self):
        if self.wheels > 0:
            self.wheels -= 1

Here's some terminology:

  • class: a blueprint for how to build a certain type of object. The Car class (shown above) describes the behavior and data that all Car objects have.

  • instance: a particular occurrence of a class. In Python, we create instances of a class like this:

    >>> my_car = Car('red')
    

    my_car is an instance of the Car class.

  • attribute or field: a variable that belongs to the class. Think of an attribute as a quality of the object: cars have wheels and color, so we have given our Car class self.wheels and self.color attributes. We can access attributes using dot notation:

    >>> my_car.color
    'red'
    >>> my_car.wheels
    4
    
  • method: Methods are just like normal functions, except that they are tied to an instance or a class. Think of a method as a "verb" of the class: cars can drive and also pop their tires, so we have given our Car class the methods drive and pop_tire. We call methods using dot notation:

    >>> my_car = Car('red')
    >>> my_car.drive()
    'red car goes vroom!'
    
  • constructor: As with data abstraction, constructors describe how to build an instance of the class. Most classes have a constructor. In Python, the constructor of the class defined as __init__. For example, here is the Car class's constructor:

    def __init__(self, color):
        self.wheels = Car.num_wheels
        self.color = color
    

    The constructor takes in one argument, color. As you can see, the constructor also creates the self.wheels and self.color attributes.

  • self: in Python, self is the first parameter for many methods (in this class, we will only use methods whose first parameter is self). When a method is called, self is bound to an instance of the class. For example:

    >>> my_car = Car('red')
    >>> car.drive()
    

    Notice that the drive method takes in self as an argument, but it looks like we didn't pass one in! This is because the dot notation implicitly passes in car as self for us.

Inheritance

To avoid redefining attributes and methods for similar classes, we can write a single base class from which more specialized classes inherit. For example, we can write a class called Pet and define Dog as a subclass of Pet:

class Pet:

    def __init__(self, name, owner):
        self.is_alive = True    # It's alive!!!
        self.name = name
        self.owner = owner

    def eat(self, thing):
        print(self.name + " ate a " + str(thing) + "!")

    def talk(self):
        print(self.name)

class Dog(Pet):

    def talk(self):
        super().talk()
        print('This Dog says woof!')

Inheritance represents a hierarchical relationship between two or more classes where one class is a more specific version of the other: a dog is a pet (We use is a to describe this sort of relationship in OOP languages, and not to refer to the Python is operator).

Since Dog inherits from Pet, the Dog class will also inherit the Pet class's methods, so we don't have to redefine __init__ or eat. We do want each Dog to talk in a Dog-specific way, so we can override the talk method.

We can use super() to refer to the superclass of self, and access any superclass methods as if we were an instance of the superclass. For example, super().talk() in the Dog class will call the talk method from the Pet class, but passing the Dog instance as the self.

3. What Would Python Display?

In this section, you need to think about what python would display if the code given were input to a python interpreter.

Question 1

Use Ok to test your knowledge with the following "What Would Python Display?" questions:

python ok -q q1 -u

Important: Type Function if you believe the answer is <function...>, Error if it errors, and Nothing if nothing is displayed.

Below is the definition of a Car class that we will be using in the following WWPD questions.

class Car(object):
    num_wheels = 4
    gas = 30
    headlights = 2
    size = 'Tiny'
    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.color = 'No color yet. You need to paint me.'
        self.wheels = Car.num_wheels
        self.gas = Car.gas
    def paint(self, color):
        self.color = color
        return self.make + ' ' + self.model + ' is now ' + color
    def drive(self):
        if self.wheels < Car.num_wheels or self.gas <= 0:
            return 'Cannot drive!'
        self.gas -= 10
        return self.make + ' ' + self.model + ' goes vroom!'
    def pop_tire(self):
        if self.wheels > 0:
            self.wheels -= 1
    def fill_gas(self):
        self.gas += 20
        return 'Gas level: ' + str(self.gas)
>>> deneros_car = Car('Tesla', 'Model S')
>>> deneros_car.model

______
>>> deneros_car.gas = 10
>>> deneros_car.drive()

______
>>> deneros_car.drive()

______
>>> deneros_car.fill_gas()

______
>>> deneros_car.gas

______
>>> Car.gas

______
>>> deneros_car = Car('Tesla', 'Model S')
>>> deneros_car.wheels = 2
>>> deneros_car.wheels

______
>>> Car.num_wheels

______
>>> deneros_car.drive()

______
>>> Car.drive()

______
>>> Car.drive(deneros_car)

______

For the following, we reference the MonsterTruck class below.

class MonsterTruck(Car):
    size = 'Monster'
    def rev(self):
        print('Vroom! This Monster Truck is huge!')
    def drive(self):
        self.rev()
        return Car.drive(self)
>>> deneros_car = MonsterTruck('Monster', 'Batmobile')
>>> deneros_car.drive()

______
>>> Car.drive(deneros_car)

______
>>> MonsterTruck.drive(deneros_car)

______
>>> Car.rev(deneros_car)

______

Question 2

Use Ok to test your knowledge with the following "What Would Python Display?" questions:

python ok -q q2 -u

Important: Type Function if you believe the answer is <function...>, Error if it errors, and Nothing if nothing is displayed.

>>> class A:
...   x, y = 0, 0
...   def __init__(self):
...         return
>>> class B(A):
...   def __init__(self):
...         return
>>> class C(A):
...   def __init__(self):
...         return
>>> print(A.x, B.x, C.x)
______

>>> B.x = 2
>>> print(A.x, B.x, C.x)
______

>>> A.x += 1
>>> print(A.x, B.x, C.x)
______

>>> obj = C()
>>> obj.y = 1
>>> C.y == obj.y
______

>>> A.y = obj.y
>>> print(A.y, B.y, C.y, obj.y)
______

Required Problems

In this section, you are required to complete the problems below and submit your code to OJ website as instructed in lab00 to get your answer scored.

Problem 1: Vending Machine (100pts)

Create a class called VendingMachine that represents a vending machine for some product. A VendingMachine object returns strings describing its interactions. Fill in the VendingMachine class, adding attributes and methods as appropriate, such that its behavior matches the following doctests:

class VendingMachine:
    """A vending machine that vends some product for some price.

    >>> v = VendingMachine('candy', 10)
    >>> v.vend()
    'Machine is out of stock.'
    >>> v.add_funds(15)
    'Machine is out of stock. Here is your $15.'
    >>> v.restock(2)
    'Current candy stock: 2'
    >>> v.vend()
    'You must add $10 more funds.'
    >>> v.add_funds(7)
    'Current balance: $7'
    >>> v.vend()
    'You must add $3 more funds.'
    >>> v.add_funds(5)
    'Current balance: $12'
    >>> v.vend()
    'Here is your candy and $2 change.'
    >>> v.add_funds(10)
    'Current balance: $10'
    >>> v.vend()
    'Here is your candy.'
    >>> v.add_funds(15)
    'Machine is out of stock. Here is your $15.'

    >>> w = VendingMachine('soda', 2)
    >>> w.restock(3)
    'Current soda stock: 3'
    >>> w.restock(3)
    'Current soda stock: 6'
    >>> w.add_funds(2)
    'Current balance: $2'
    >>> w.vend()
    'Here is your soda.'
    """
    "*** YOUR CODE HERE ***"

You may find Python string formatting syntax or f-strings useful. A quick example:

>>> ten, twenty, thirty = 10, 'twenty', [30]
>>> '{0} plus {1} is {2}'.format(ten, twenty, thirty)
'10 plus twenty is [30]'

>>> feeling = 'love'
>>> course = 'SICP'
>>> year = 2024
>>> f'I {feeling} {course}-{year}!'
'I love SICP-2024!'

Problem 2: Cat (100pts)

Below is a skeleton for the Cat class, which inherits from the Pet class. To complete the implementation, override the __init__ and talk methods and add a new lose_life method, such that its behavior matches the following doctests.

We may change the implementation of Pet while testing your code, so make sure you use inheritance correctly.

Hint 1: You can call the __init__ method of Pet to set a cat's name and owner.

Hint 2: The to_str method belongs to Problem 4, you don't need to implement it for the moment.

class Pet:
    """A pet.

    >>> kyubey = Pet('Kyubey', 'Incubator')
    >>> kyubey.talk()
    Kyubey
    >>> kyubey.eat('Grief Seed')
    Kyubey ate a Grief Seed!
    """
    def __init__(self, name, owner):
        self.is_alive = True    # It's alive!!!
        self.name = name
        self.owner = owner
    
    def eat(self, thing):
        print(self.name + " ate a " + str(thing) + "!")
    
    def talk(self):
        print(self.name)

    def to_str(self):
        "*** YOUR CODE HERE ***"


class Cat(Pet):
    """A cat.

    >>> vanilla = Cat('Vanilla', 'Minazuki Kashou')
    >>> isinstance(vanilla, Pet) # check if vanilla is an instance of Pet.
    True
    >>> vanilla.talk()
    Vanilla says meow!
    >>> vanilla.eat('fish')
    Vanilla ate a fish!
    >>> vanilla.lose_life()
    >>> vanilla.lives
    8
    >>> vanilla.is_alive
    True
    >>> for i in range(8):
    ...     vanilla.lose_life()
    >>> vanilla.lives
    0
    >>> vanilla.is_alive
    False
    >>> vanilla.lose_life()
    Vanilla has no more lives to lose.
    """
    def __init__(self, name, owner, lives=9):
        "*** YOUR CODE HERE ***"

    def talk(self):
        """ Print out a cat's greeting.
        """
        "*** YOUR CODE HERE ***"

    def lose_life(self):
        """Decrements a cat's life by 1. When lives reaches zero, 'is_alive'
        becomes False. If this is called after lives has reached zero, print out
        that the cat has no more lives to lose.
        """
        "*** YOUR CODE HERE ***"

    def to_str(self):
        "*** YOUR CODE HERE ***"

Problem 3: Noisy Cat (100pts)

More cats! Fill in this implementation of a class called NoisyCat, which is just like a normal Cat. However, NoisyCat talks a lot -- twice as much as a regular Cat!

We may change the implementation of Pet and Cat while testing your code, so make sure you use inheritance correctly.

class NoisyCat: # Dose this line need to change?
    """A Cat that repeats things twice.

    >>> chocola = NoisyCat('Chocola', 'Minazuki Kashou')
    >>> isinstance(chocola, Cat) # check if chocola is an instance of Cat.
    True
    >>> chocola.talk()
    Chocola says meow!
    Chocola says meow!
    """
    def __init__(self, name, owner, lives=9):
        # Is this method necessary? If not, feel free to remove it.
        "*** YOUR CODE HERE ***"
    
    def talk(self):
        """Talks twice as much as a regular cat.
        """
        "*** YOUR CODE HERE ***"

Problem 4: Pretty Printer (100pts)

Part 1

Your first task is to define pretty printer methods for Pet, Cat and NoisyCat.

We start with implementing functions named to_str for Pet, Cat, and NoisyCat classes.

For Pet, the output should be:

>>> kyubey = Pet('Kyubey', 'Incubator')
>>> kyubey.to_str()
'(Kyubey, Incubator)'

For Cat and NoisyCat, the output should be:

>>> vanilla = Cat('Vanilla', 'Minazuki Kashou')
>>> vanilla.to_str()
'(Vanilla, Minazuki Kashou, 9)'
>>> vanilla.lose_life()
>>> vanilla.to_str()
'(Vanilla, Minazuki Kashou, 8)'

Now, we could define a function pretty_print, which accepts an object and print it prettily (i.e. with color).

To colorfully print something in terminal, this simplest way is to use ANSI escape code.

class Colors:
 HEADER     = '\033[95m'
 OKBLUE     = '\033[34m'
 OKCYAN     = '\033[35m'
 OKGREEN    = '\033[96m'
 WARNING    = '\033[93m'
 FAIL       = '\033[91m'
 ENDC       = '\033[0m'
 BOLD       = '\033[1m'
 UNDERLINE  = '\033[4m'


>>> print(f"{Colors.OKBLUE}Hello, World{Colors.ENDC}")
Hello, World # this line should be blue
>>> print(f"{Colors.OKCYAN}Hello, World{Colors.ENDC}")
Hello, World # this line should be red
>>> print(f"{Colors.UNDERLINE}Hello, World{Colors.ENDC}")
Hello, World # this line should be underlined 

Windows Console may not be able to display that. Use vscode console or windows terminal instead.

For real world program, you should use a library to display colorful output. E.g. termcolor

pretty_print should print object with form type(to_str). The type is the class name of the input object, the to_str is the return value of object.to_str(). For example:

>>> kyubey = Pet('Kyubey', 'Incubator')
>>> pretty_print(kyubey)
Pet(Kyubey, Incubator)

example

We ignore the ascii escape code in examples, you can check the real output in doctest

Note:

  • type could be obtained by python builtin type(obj).__name__. E.g. type(kyubey).__name__ is Pet
  • The type part of the printed str should be displayed with color defined by Colors.OKBLUE. For most device/terminal, this color should be blue.
  • The to_str part of the printed str should be displayed with color defined by Colors.OKCYAN. For most device/terminal, this color should be like red.

Part 2

Your second task is to inject the defined pretty_print method to all Pet, Cat and NoisyCat classes. For example:

>>> kyubey = Pet('Kyubey', 'Incubator')
>>> kyubey.pp() # the same result as `pretty_print`
Pet(Kyubey, Incubator)

To smoothly support such method call, edit the declaration of the Pet class.

Hint1: You may not need to edit more than one line of code for this task.

Hint2: You should use the provided PrintModule class.

Test your code using python -i lab06.py with:

>>> kyubey = Pet('Kyubey', 'Incubator')
>>> kyubey.pp()
Pet(Kyubey, Incubator)
>>> vanilla = Cat('Vanilla', 'Minazuki Kashou')
>>> vanilla.pp()
Cat(Vanilla, Minazuki Kashou, 9)

This is an ugly implementation of mixin. You can check this page for ruby style mixin, which is clear and elegant.

Problem 5: Time (100pts)

In everyday programming, we often need to store dates. Typically, this involves storing three separate values: the year, the month, and the day. However, manually handling these values in separate variables each time can be cumbersome. To make it easier to store and display dates, we want to implement a class Time that will help us manage these values more conveniently.

We create a class called Time that represents a specific date. The class should have a constructor that accepts a year, month, and day. Additionally, it should include the following methods:

  1. setyear(self, year): Sets the year of the time object self to year.
  2. setmonth(self, month): Sets the month of the time object self to month.
  3. setday(self, day): Sets the day of the time object self to day.
  4. setformat(self, format_str): Accepts a string format_str that includes 'yy', 'mm', and 'dd'. The format string specifies how the date should be displayed when the to_str method is called. The default format string should be 'yy.mm.dd'.

The format_str parameter in the setformat method allows you to specify how the date should be displayed as a string. It is a string that defines the order and format in which the year (yy), month (mm), and day (dd) should appear when the to_str method is called.

  • yy represents the two-digit year.
  • mm represents the two-digit month.
  • dd represents the two-digit day.

You can arrange these placeholders in any order you like and separate them with any delimiter, such as a dot (.), slash (/), or hyphen (-). For example:

  • "yy.mm.dd" will display the date as 25.12.25 for the year 2025, month December, and day 25.
  • "dd-mm-yy" will display the date as 25-12-25.
  • "mm/dd/yy" will display the date as 12/25/25.
  • "yy.mm" will display the date as 25.12.

The initial format_str will be 'yy.mm.dd' when a Time object is created. yy, mm, and dd can appear multiple times or not appear at all. For example, 'yy/mm', 'yy.mm.dd yy/mm/dd', or even 'yyy' are all valid formats (you only need to treat the first two y as the year).

Hint1: When the parameter given to setyear, setmonth, or setday is invalid (for example, if the month is 13 or the month is given as a string like "a"), you need to print Parameter Error! and do not perform any other operations.

Hint2: To simplify the problem, you can assume that every month has 31 days.

Hint3: You can use isinstance(x, int) to check if x is an integer.

Hint4: You might find some built-in string methods helpful, like replace, zfill, etc.

class Time:
    """ A class that can store and display the date.
    >>> time = Time(2024, 11, 20)
    >>> print(time.to_str())
    24.11.20
    >>> time.setyear(2023)
    >>> time.setmonth(2)
    >>> time.setday(5)
    >>> time.setformat("dd-mm-yy")
    >>> time.to_str()
    '05-02-23'
    >>> time.setformat("yy/mm/dd")
    >>> time.to_str()
    '23/02/05'
    >>> time.setyear(-1)
    Parameter Error!
    >>> time.to_str()
    '23/02/05'
    """
    def __init__(self, year, month, day):
        """Initialize a Time object."""
        "*** YOUR CODE HERE ***"

    def setyear(self, year):
        """Set the year of the Time object."""
        "*** YOUR CODE HERE ***"

    def setmonth(self, month):
        """Set the month of the Time object."""
        "*** YOUR CODE HERE ***"

    def setday(self, day):
        """Set the day of the Time object."""
        "*** YOUR CODE HERE ***"

    def setformat(self, format):
        """Set the format of the Time object."""
        "*** YOUR CODE HERE ***"

    def to_str(self):
        """Return the formatted date."""
        "*** YOUR CODE HERE ***"