====== Creating a class ====== In {[a4b99rph:Wentworth2012]}, there are several chapters dealing with classes and object-oriented programming: * [[http://openbookproject.net/thinkcs/python/english3e/classes_and_objects_I.html|15. Classes and Objects — the Basics]] * [[http://openbookproject.net/thinkcs/python/english3e/classes_and_objects_II.html|16. Classes and Objects — Digging a little deeper]] * [[http://openbookproject.net/thinkcs/python/english3e/even_more_oop.html|21. Even more OOP]] * [[http://openbookproject.net/thinkcs/python/english3e/collections.html|22. Collections of objects]] 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 my_inbox.get_unread_indexes() # 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). ==== add_new_arrival ==== 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.add_new_arrival('+420123456789', '12:34', 'Hello') >>> my_inbox.message_count() 1 >>> my_inbox.add_new_arrival('+420987654321', '21:43', 'Bye') >>> 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() >>> my_inbox.add_new_arrival('+420123456789', '12:34', 'Hello') >>> my_inbox.add_new_arrival('+420987654321', '21:43', 'Bye') >>> 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 if not has_been_read: 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. ==== __add__ ==== As can be seen in the doctest below, we would like to be able to add 2 ''SMS_store''s, 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_inbox.add_new_arrival('+420123456789', '12:34', 'Hello') >>> my_inbox.add_new_arrival('+420987654321', '21:43', 'Bye') >>> my_other_inbox = SMS_store() >>> my_inbox.add_new_arrival('+420123456789', '10:00', 'Foo') >>> my_inbox.add_new_arrival('+420987654321', '20:00', 'Bah') >>> 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: {{:courses:be5b33prg:tutorials: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"""