Lecture 4

Types, Classes, & Objects

Announcements

  • Assignment 1 is due tomorrow (Wednesday, February 16, before midnight)

A quick refresher on types...

primitive types

  • int
  • float
  • str
  • bool
  • None

composite types

  • list
  • dict
  • set
  • tuple*

Up until this point, you have been introduced to data types as they grew in the woods and just exist as a fact of the universe.

This is actually not the case. Data types are something that programmers create to organize the way that their code is used.

Let's take a closer look at the humble tuple

  • It has a fixed size
  • It is immutable
  • It is designed to hold heterogeneous types
  • You access its constituent parts based on their position in the tuple

Let's say we're building a Venmo-like app and we need some way to represent a transfer of money in our code. There are 4 attributes that represent a transfer:

  1. The user the transfer is coming from (str)
  2. The user the transfer is going to (???)
  3. The amount being transferred (???)
  4. The date of the transfer (???)
  1. The user the transfer is coming from (str)
  2. The user the transfer is going to (str)
  3. The amount being transferred (Decimal)
  4. The date of the transfer (datetime)

What will this expression evaluate to?


        1.2 - 1.0
Tuple[str, str, Decimal, datetime]

Constructing values of our type


        t1 = ("Sally", "Bob", Decimal("25.50"), datetime(2020, 8, 5))
        t2 = ("Bob", "Fran", Decimal("15.20"), datetime(2020, 8, 6))
        t3 = ("Bob", "Matt", Decimal("34.29"), datetime(2020, 8, 26))
        t4 = ("Matt", "Bob", Decimal("34.29"), datetime(2020, 9, 1))
    

Extracting values from our type


        sender = transfer[0]
        receiver = transfer[1]
        amount = transfer[2]
        date = transfer[3]
        # Or use tuple unpacking...
        sender, receiver, amount, date = transfer
    

Advantages to using tuples to model our transfer type:

  • They are simple and small (not much memory)
  • You can't accidentally mutate your data: transfer.append([1, 2, 3]) will error
  • You just need to remember the order of your parameters

Tuples have some disadvantages too:

  • Having to remember the order of parameters is cumbersome (and error-prone)
  • It also gets unwieldy when you start to have lots of attributes
  • Updating a value is even worse:
    
                    transfer = ("Bob", "Sally", Decimal(35.50), datetime.now())
                    new_transfer = transfer[:2] + (Decimal(36.50), ) + transfer[3:]
                

Another option: we could use a dictionary


        t1 = {"from": "Sally",
              "to": "Bob",
              "amount": Decimal("25.50"),
              "date": datetime(2020, 8, 5)
        }
        t2 = {"from": "Bob",
              "to": "Fran",
              "amount": Decimal("15.20"),
              "date": datetime(2020, 8, 6)
        }
    

Advantages to using dictionaries to model our transfer type:

  • You can update them in-place (mutable)
  • You reference values within the type by name rather than position (easier to read)

Dictionaries have some disadvantages too:

  • You can add things to the type that aren't supposed to be there:
    t2["invalid_property"] = True
  • Uses more memory than a tuple...but that's only any issue if you have a lot of objects.

Classes to the rescue!


        t1 = Transfer("Bob", "Sally", Decimal(35.50), datetime(2020, 10, 1))
        # alternative method, with keyword arguments
        t1 = Transfer(sender="Bob",
                      receiver="Sally",
                      amount=Decimal(35.50),
                      date=datetime(2020, 10, 1))
        t1.sender  # "Bob"
        t1.date  # datetime(2020, 10, 1)
        # Assignment update the value!
        t1.amount  = Decimal(36.50)
        t1.amount  # Is now Decimal(36.50)
    

Classes act a little bit like dictionaries:


        t1 = Transfer(sender="Bob",
                      receiver="Sally",
                      amount=Decimal(35.50),
                      date=datetime(2020, 10, 1))
        # Gives you "Bob"
        t1.sender
    

        t2 = {"sender": "Bob",
              "receiver": "Sally",
              "amount": Decimal(35.50),
              "date": datetime(2020, 10, 1)}
        # Also gives you "Bob"
        t2["sender"]
    

A brief digression...

If you squint hard, Python's tuples and classes and dictionaries are the same kind of thing. In type theory, this is called a product type.

\[\begin{aligned} str \times str \times \Bbb{Q} \times date \end{aligned} \]

The product of all possible strings, all possible strings, all possible rationals, and all possible dates makes up the domain of this type.

In cases where you have a collection of heterogeneous values that have meaning together, a tuple or a class or a dictionary is useful.

The semantics may vary:


        # With tuple:
        transfer[0]
        # With dictionary:
        transfer["sender"]
        # With class:
        transfer.sender
    

...but each of these give us the same thing: the username of the person that sent the transfer.

Think of classes as tuples/dictionaries+++.

Minimal, complete definition of our transfer type as a class:


        class Transfer:

            def __init__(self, sender, receiver, amount, date):
                self.sender = sender
                self.receiver = receiver
                self.amount = amount
                self.date = date

    

Given:


        class Transfer:

            def __init__(self, sender, receiver, amount, date):
                self.sender = sender
                self.receiver = receiver
                self.amount = amount
                self.date = date
    

We construct a transfer this way:


        t1 = Transfer("Bob", "Matt", Decimal(5), datetime(2020, 2, 1))
    

Given:


        class Transfer:

            def __init__(self, sender, receiver, amount, date):
                self.sender = sender
                self.receiver = receiver
                self.amount = amount
                self.date = date

        t1 = Transfer("Bob", "Matt", Decimal(5), datetime(2020, 2, 1))
    
  • Transfer is a class
  • t1 is an object that is an instance of a Transfer
  • The process of creating an instance of a class is called constructing or instantiating

The class is not the thing. It's a blueprint for making the thing.
Or making many things.


        class Transfer:

            def __init__(self, sender, receiver, amount, date):
                self.sender = sender
                self.receiver = receiver
                self.amount = amount
                self.date = date
    
  • init as in initialize.
  • Notice the indentation: the __init__ function is inside the class and belongs to it.
  • The function is called only once when constructing:
    
                Transfer("Bob", "Matt", Decimal(5), datetime(2020, 2, 1))
                
  • Method names surrounded by __ are magic

What is this self thing about?


        class Transfer:

            def __init__(self, sender, receiver, amount, date):
                self.sender = sender
                self.receiver = receiver
                self.amount = amount
                self.date = date

    
  • A function belonging to a class is called a class method.
  • All methods should start with an argument called self.
  • self refers to the object itself that has been constructed from the class.

You can write your own methods too!


        class Transfer:

            def __init__(self, sender, receiver, amount, date):
                self.sender = sender
                self.receiver = receiver
                self.amount = amount
                self.date = date

            def days_ago(self):
                time_delta = datetime.now() - self.date
                return time_delta.days

            def apply_processing_fee(self):
                self.amount = self.amount + Decimal("0.25")
    

We can make this a bit better...


        class Transfer:

            def __init__(self, sender, receiver, amount, date=datetime.now()):
                self.sender = sender
                self.receiver = receiver
                self.amount = amount
                self.date = date
                self.processing_fee_applied = False

            def days_ago(self):
                time_delta = datetime.now() - self.date
                return time_delta.days

            def apply_processing_fee(self, fee=Decimal("0.25")):
                self.amount = self.amount + fee
                self.processing_fee_applied = True
    

Using our class:


        t1 = Transfer("Bob", "Sally", Decimal(35.50), datetime(2019, 10, 1))

        t2 = Transfer(sender="Sally",
                      receiver="Matt",
                      amount=Decimal(5.25))

        # This will be "Sally":
        t1.receiver
        # This will be True:
        t1.date < t2.date
        # This will be 0:
        t2.days_ago()

        t1.apply_processing_fee():
        # This will be $35.50 + $0.25 = $35.75:
        t1.amount
    

But wait! There's more...

The next time we discuss classes, we'll do so in the context of Object Oriented Programming (OOP). OOP is a style of programming where the vast majority of processing takes place as the interaction between objects and their methods.