Day 11

Today

For Next time

Reading Journal Debrief

The Reading Journal included this list of terms and concepts:

Which of these should we discuss more? Think back to your work on the last two Reading Journals and geometry.py. What Class concepts are still confusing?


Bad Kangaroos

In this Reading Journal we looked at a very common error that can be very frustrating to debug:

class Kangaroo:
    def __init__(self, name, contents=[]):	# Warning: mutable default argument bug!
        self.pouch_contents = contents

The core “gotcha” here is that default argument values are evaluated once when the function is defined, not every time it is called.

In this case, it means that the new list is created one time, and each new Kangaroo instance (that did not provide contents to override the default) has a reference to the same list.

This would be true of any mutable object as a default argument, not just list (e.g. dict, or your custom classes). Here is another implementation, showing a Python idiom commonly used to avoid this bug:

class Kangaroo:
    def __init__(self, name, contents=None):
        if contents == None:
            contents = []
        self.pouch_contents = contents

Here we’ve used None (immutable) as a stand-in for the case when no contents have been passed and we should create an empty list. The new list is created inside the body of the __init__ method when it is called, so each Kangaroo instance gets its own empty list vs a shared reference.

General advice: avoid using mutable objects as default values of function arguments.


Agenda: sorting objects

For this Reading Journal, you had to implement a print_agenda method that displays Event objects in chronological order. The key challenge here was sorting the Events: Python provides many techniques for sorting, but they don’t know how to sensibly sort your custom objects without some help from you.

Let’s imagine that you have a list of Events. The first design choice to make is whether you should maintain the list in sorted order at all times, or sort it only when required (e.g. whenever print_agenda is called). There are pros and cons to each style, depending on application:

Always sorted

In this case the burden falls on your add_event method to ensure that you insert new Events in chronological order (perhaps using your is_after method). It takes a little more time to add an Event, but you afterwards you can rely on the list being sorted at no extra cost.

Sort on demand

With this approach you have a simpler insertion process, but you must sort the list every time you need it to be in order. This may be more expensive depending on whether adding new Events or printing the Agenda is more common.

Python knows how to sort built-in types (e.g. integers), but not your custom classes. Let’s say you have a Time class:

class Time:
    def __init__(self, hour=0, minute=0, second=0):
        self.hour = hour
        self.minute = minute
        self.second = second

    def to_int(self):
        return 60*(60*self.hour + self.minute) + self.second

Sorting key function

Python’s list.sort method and sorted function both take an optional key parameter, which expects a function that takes the object to be sorted and returns a key used to sort it. We could write such a function for Events:

def event_start(event):
    """Given Event object containing a start Time, return time as sortable key"""
    return event.start.to_int()

and then use it to sort a list of Events:

for event in sorted(some_events, key=event_start):
    ... do something

Anonymous lambda functions

If you just want to sort Events, it can be a bit clunky to write the event_start function solely to pass as a key parameter. You’ll often see people use an anonymous lambda function (a function defined on the fly that is not bound to a name) for this purpose. Equivalently:

for event in sorted(some_events, key=lambda e: e.start.to_int())
    ... do something

Implement comparison operator(s)

If you implement the less-than __lt__ method for your classes (so that time1 < time2 works), then Python now knows how to sort your custom objects.

# inside class Time:
    def __lt__(self, other):
        return self.to_int() < other.to_int()

Agenda: __str__ vs __repr__

You might have observed the following behavior when trying to print your Time class using the __str__ method:

>>> times = [Time(6,20), Time(2,45), Time(4)]
>>> print(times[0])
06:20:00
>>> print(times)
[<__main__.Time object at 0x105b1d0b8>, <__main__.Time object at 0x105b1d0f0>, <__main__.Time object at 0x105b1d128>]

This arises because Python has two ways of converting an object into a string that are used differently. Read about each: __str__ and __repr__.

When printing collections like lists, the __repr__ method is used for the contained objects. As an example of why, let’s imagine we have a TicTacToe class with a pretty __str__ method that gives:

X| |O
-----
 |X|O
-----
O|X|

Very nice way to visualize the state of one Tic Tac Toe board - who needs a Graphical User Interface?

But, if we used that same representation for the __repr__ and printed a list of two TicTacToe objects, it would be a mess!:

[X| |O
-----
 |X|O
-----
O|X| , X| |O
-----
 |X|O
-----
O|X| ]

vs

[<__main__.TicTacToe object at 0x105b1d160>, <__main__.TicTacToe object at 0x105b1d168>]

When writing __repr__ methods, a great goal is for the result to be a valid Python expression that could recreate the object (e.g. Time(6,20,0) not 06:20:00). In Python speak, ideally eval(repr(obj)) == obj.

If a class does not define a __str__ method, the __repr__ method is used instead (if it exists). If neither exists, Python defaults to <ClassName object at 0x(memory address)>.

In short:

  • output of __str__ should be “nicely printable
  • output of __repr__ should be “information-rich and unambiguous”, and if possible should be valid Python to recreate the object

Text Mining consulting

By this point in MP3 we hope that you’ve thought of an interesting topic to investigate and tried out a couple of relevant text data sources (note that these don’t have to be the ones that you stick with for the project).

A great next step if you haven’t done it already would be to generate an extensive list of interesting questions you might explore via your data set. Using this list, you can prioritize different analysis techniques that can help you address your questions. It’s helpful to have a longer list of topics than you’ll actually pursue, since as you begin to investigate you’re likely to find that some may not pan out and others may have disappointing results.

We strongly suggest that you consult with course staff to share your project idea, potential data sources, list of research questions, and analysis strategies. We can help by suggesting related alternatives you may not have considered and by estimating the relative difficulty of potential approaches.


Text caching class example

We’ve created an example program to demonstrate a) caching text data as local files, and b) the utility of custom classes.

Caching text rather than re-downloading lots of times is a smart idea for many reasons, not least of which is that it is more respectful to the host of the data. For example, check out the Project Gutenberg terms of use, which lay out how you may/may not access their collection and the consequences of misuse (which include getting all of Olin banned from PG - not a strong move during MP3).

You are free to build on this program in your MP3, but as with all code you didn’t write you must make sure you understand how it works (and ask questions if you don’t). Since this code was provided by course staff, you don’t need to cite its source.

Exercise: Try adding a lines method to the class that returns a list of all the individual lines in the text file, so that you can use it to write code like:

example = Text(my_url)
for line in example.lines():
    do_something(line)