Warning

# Creating a class

In [Wentworth2012], there are several chapters dealing with classes and object-oriented programming:

If you have not done so yet, please, read these chapter and try to work out the exercises at the end of the chapters!

In the following we show a solution to exercise 15.6.

## SMS Store

We copy the whole specification of exercise 15.6 here:

Create a new class, SMS_store. The class will instantiate SMS_store objects, similar to an inbox or outbox on a cellphone:

my_inbox = SMS_store()

This store can hold multiple SMS messages (i.e. its internal state will just be a list of messages). Each message will be represented as a tuple:

(has_been_viewed, from_number, time_arrived, text_of_SMS)

The inbox object should provide these methods:

my_inbox.add_new_arrival(from_number, time_arrived, text_of_SMS)
# Makes new SMS tuple, inserts it after other messages
# in the store. When creating this message, its
# has_been_viewed status is set False.

my_inbox.message_count()
# Returns the number of sms messages in my_inbox

# Returns list of indexes of all not-yet-viewed SMS messages

my_inbox.get_message(i)
# Return (from_number, time_arrived, text_of_sms) for message[i]
# Also change its state to "has been viewed".
# If there is no message at position i, return None

my_inbox.delete(i)     # Delete the message at index i
my_inbox.clear()       # Delete all messages from inbox

Write the class, create a message store object, write tests for these methods, and implement the methods.

We shall extend this specification by the following:

str(my_inbox)
# Return a printable representation of all the SMSs in the store

my_other_inbox = SMS_store()
my_inbox + my_other_inbox
# Return a new SMS_store instance containing messages from both inboxes

## Solution

First, we have to decide what the representation of the internal state should be. The individual messages should be 4-tuples, as written in the specifications. The SMS_store is just a collection of such a messages. Let's decide that we shall maintain the state as a list of tuples, and we will store it in the member variable messages.

### Class initialization

When you create an instance of any class, its __init__ method is used to initialize the state of the instance. This can be implemented (including a simple doctest) as follows:

import doctest

class SMS_store:

def __init__(self):
"""Initialize an empty store

>>> my_inbox = SMS_store()
>>> my_inbox.messages
[]
"""
self.messages = []

if __name__=='__main__':
doctest.testmod()

If you run this code, the example inside the initializer shall execute using the doctest module. It shall not produce any output, which means that the examples ran correctly.

### message_count

Method message_count() shall return the number of messages in the store. Since the number of messages is just the length of the messages list, the implementation is easy. Together with a simple test in a docstring:

    def message_count(self):
"""Return the number of messages in the store

>>> my_inbox = SMS_store()
>>> my_inbox.message_count()
0
"""
return len(self.messages)

(You should place this code inside the definition of the SMS_store class, i.e. below the __init__ method, and above if __name__=='__main__', maintaining the indentation. The order of individual methods inside the class is not important.)

Again, the code shall run producing no error output (i.e. success).

Method add_new_arrival shall append a new SMS to our list, messages. We will first construct a tuple representing the SMS, and then we will add it to the list. Again, you can find some tests in the docstring.

    def add_new_arrival(self, from_number, time_arrived, text_of_SMS):
"""Add a new SMS to the store

Insert the new message on the end of messages.
The has_been_viewed status of the new message should be set to False.

>>> my_inbox = SMS_store()
>>> my_inbox.message_count()
1
>>> my_inbox.message_count()
2
"""
sms = (False, from_number, time_arrived, text_of_SMS)
self.messages.append(sms)

### __str__

We would like to have the possibility to easilly print the contents of our SMS store. If we call print(my_inbox) (see the examples in the docstring), it shall nicely format the messages. If function print gets an argument which is not a string, it tries to convert the argument into a string by calling str(object) on that argument. This, in turn translates to the call to a “magic” method object.__str__(). So, if we want the SMS_store to be easilly printable, we shall implement our own __str__ method.

Assuming we have a function format_sms() which converts a single SMS into the desired string, we can implement the __str__ method as follows. We shall go through all messages, we shall convert the message into a string using function format_sms(), and we shall collect all the strings into a list of strings, rows. Finally, we will join all the strings in rows using the new-line character, \n, into one long string.

    def __str__(self):
"""Return a printable representation of all the SMSs in the store

>>> my_inbox = SMS_store()
>>> print(my_inbox)
* | +420123456789 | 12:34 | Hello
* | +420987654321 | 21:43 | Bye
"""
rows = []
for sms in self.messages:
rows.append(format_sms(sms))
return '\n'.join(rows)

Before the code can run, we shall implement also the funtion format_sms(). This function takes a 4-tuple representing a SMS as a parameter. Based on the first item, has_been_read, the function decides whether to print a star or not (star indicates an unread message). Then it joins the star (or space) with the 2nd, 3rd and 4th item of the tuple using the string ' | ' as the glue.

def format_sms(sms):
"""Return a string representation of a SMS

>>> sms_internal = (False, '+420123456789', '12:34', 'Hello, Lucky!')
>>> format_sms(sms_internal)
'* | +420123456789 | 12:34 | Hello, Lucky!'
"""
has_been_read, from_number, time_arrived, text_of_SMS = sms
star = '*'
else:
star = ' '
return ' | '.join([star, from_number, time_arrived, text_of_SMS])        

Note this is a regular function! It shall be placed outside the class definition, before or after it, and there should be no indentation before def.

If you run the module, it executes the doctests and they shall pass with no error message.

As can be seen in the doctest below, we would like to be able to add 2 SMS_stores, like my_new_inbox = my_inbox + my_other_inbox. When Python tries to execute such a statement, it must first evaluate the expression my_inbox + my_other_inbox. Since Python does not know how to add 2 instances of user-defined class, it translates this expression into my_inbox.__add__(my_other_inbox). I.e., if we want to add two SMS stores, we must define how to add them in the “magic” __add__ method.

The implementation is simple. We just create a new instance of SMS_store (with an empty messages list), then we extend this list by messages of the first operand (self, which is populated by my_inbox), and finally we extend it by messages of the second operand (other, populated by my_other_inbox). Then the new instance of SMS_store is returned.

    def __add__(self, other):
"""Return a new SMS_store instance containing messages from both inboxes

>>> my_inbox = SMS_store()
>>> my_other_inbox = SMS_store()
>>> my_new_inbox = my_inbox + my_other_inbox
>>> print(my_new_inbox)
* | +420123456789 | 12:34 | Hello
* | +420987654321 | 21:43 | Bye
* | +420123456789 | 10:00 | Foo
* | +420987654321 | 20:00 | Bah
"""
new = SMS_store()
new.messages.extend(self.messages)
new.messages.extend(other.messages)
return new        

## Exercise

You shall now be in this state: sms.py

Several methods remain to be implemented. Try to implement them yourself as an exercise. You should create your own test scenarios, and you can create doctests for them. Or, you can just try to run the code of the test scenarios you come up with.

    def get_unread_indexes(self):
"""Return list of indexes of all not-yet-viewed SMS messages"""

def get_message(self, i):
"""Return (from_number, time_arrived, text_of_sms) for i-th message.

Set the has_been_viewed status to True.
If no message at position i, return None.
"""

def delete(self, i):
"""Delete message at position i"""

def clear(self, i):
"""Delete all messages"""