Objects and classes

Introduction to OOP

What is an object?

  • All values in Python are objects

  • All objects are instances of a data type

  • Objects can have properties

    • Methods (functionality)

    • Attributes (data)

What is a class?

  • Classes define data types

    • Classes allow us to combine data and functionality into a single package

  • Classes are used to create (or instantiate) objects

  • Classes let us define how operators and other features of Python behave for the objects we create

You’ve used classes before:

  • int

  • str

  • float

  • list

  • bool

You’ve used methods before:

  • mylist.append("some value")

  • mystring.upper()

  • myfile.close()

Example: pathlib.Path

The Path class from pathlib is an excellent example of a good class.

We’ll look at

  • instantiation

  • some attributes

  • some methods

  • interaction with features of Python

Creating objects

  • To create an instance of a class, invoke the class like a function: cls()

  • Arguments (if any) go inside the parentheses: cls(arg1, arg2, …​)

    • When creating a Path object, you can specify a path as a string

>>> from pathlib import Path
>>>
>>> dir = Path("/home/aric/INST326")
>>> script = Path("/home/aric/INST326/homework1.py")

Attributes

  • Attributes are accessed like this: obj.attribute

    • Path object attributes include

      • name: the name of the file, without the directory

      • suffix: the file extension, if any

      • parent: the directory of the file, as a Path object

>>> script.name
'homework1.py'
>>> script.suffix
'.py'
>>> script.parent
PosixPath('/home/aric/INST326')

Methods

  • Methods are invoked like this: obj.method()

    • Path object methods include

      • exists(): indicates whether or not the file exists

      • read_text(): open the file as a text file and return the contents

      • glob(): search for files in a directory matching a pattern

  • Arguments (if any) go inside the parentheses: obj.method(arg1, arg2, …​)

>>> script.exists()
True
>>> script.read_text(encoding="utf-8")
'print("Hello world!")\n'
>>> for path in dir.glob("*.py"):
...     print(path)
...
/home/aric/INST326/exercise1.py
/home/aric/INST326/homework1.py

Interaction with features of Python

  • Using str() on a Path object gives you the path as a string:

>>> str(dir)
'/home/aric/INST326'
  • Using the / operator on a Path object lets you create child paths:

>>> dir / "homework2.py"
PosixPath('/home/aric/INST326/homework2.py')

Summary

Using pathlib.Path, we looked at examples of

  • how to instantiate a class

  • how to access attributes of an object

  • how to invoke methods of an object

  • how objects can define how they interact with features of Python

Defining classes

  • Classes are defined using a class statement

  • By convention, non-built-in class names use CamelCase

class ClassName:
    body

The simplest class we can write

class Blob:
    pass

Tangent: the pass statement

  • pass is called a no-op: it does nothing

  • pass is used when we need a compound statement, but we have nothing meaningful to put in the body

    • Remember, all compound statements need a body

Where not to use a pass statement

Instead of this:

if today == user_birthday:
    print("Happy birthday!")
else:
    pass

Do this:

if today == user_birthday:
    print("Happy birthday!")

The simplest class we can write, revisited

class Blob:
    pass

Instantiating the class

myblob = Blob()

Setting attributes on our instance

myblob.name = "Desdemona"
myblob.color = "purple"

Accessing our instance’s attributes

print(myblob.name)
print(myblob.color)

Attributes: important points

  • Attributes let us associate data with an object

    • We can use that data inside the object’s methods as well as outside the object

  • Like global variables, attributes persist beyond the lifetime of a method call

  • Unlike global variables, attributes live in a specific object’s namespace (so they’re less likely to be accidentally modified)

  • When you are tempted to use a global variable, consider writing a class and using an attribute instead

Summary

  • Classes are declared using class statements

  • The simplest class we can write has a pass statement for a body and inherits all its functionality from Python

  • To make an instance of a class, we call the class like a function

  • Attributes are variables in the namespace of a class instance

  • Attributes are often a good alternative to global variables

Methods

  • Methods are basically functions declared inside of a class

  • Most methods require a special parameter called self as their first parameter

class Greeter:
    def greet(self):
        print("Hello!")
>>> g = Greeter()
>>> h = Greeter()
>>> g.greet()
Hello!
>>> h.greet()
Hello!

Tangent: objects can have multiple names

  • Names in Python are references to objects

  • It’s possible for more than one name to refer to the same object in Python

>>> b = myblob
>>> b.name
'Desdemona'
>>> b.color
'purple'
>>> b is myblob
True

self

  • self is a reference to whichever class instance is running your code at a given point in time

    • If you have a Greeter object named g and you call g.greet(), self is a reference to g

self demonstration

>>> class Greeter:
...     def greet(self):
...         print(f"Hi, my name is {self.name}!")
...
>>> g = Greeter()
>>> g.name = "Gabrielle"
>>> h = Greeter()
>>> h.name = "Henry"
>>> g.greet()
Hi, my name is Gabrielle!
>>> h.greet()
Hi, my name is Henry!

self is how you access an object’s attributes and methods within the object’s methods

Summary

  • Methods are basically functions declared inside a class

  • self is a (mostly) required parameter that refers to the object that is running your code

  • self gives you access to an object’s attributes and methods within the object’s methods

The __init__() method

  • We often want to set some initial attributes on new objects

  • Less commonly, we may want new objects to perform some action upon creation

  • We can define a "magic" method called __init__() to initialize a new object

  • If we want a class to take arguments when we instantiate it, __init__() is where we declare these parameters

__init__() example

class Greeter:
    def __init__(self, name):
        self.name = name

    def greet(self):
        print(f"Hi, my name is {self.name}!")

__init__(): important points

  • Just because something is a parameter of __init__() doesn’t mean we have to make an attribute for it

  • We can create attributes that don’t correspond to parameters of __init__()

Summary

  • __init__() is a "magic" method that lets us set attributes and perform other actions when a new object is created

  • __init__() is where we define parameters that should be specified when the class is instantiated

    • Not all parameters of __init__() have to correspond to attributes

    • Not all attributes have to correspond to parameters of __init__()

Revisiting docstrings

  • Docstrings give other programmers enough information to use our code

  • Three kinds:

    • Function/method docstrings

    • Class docstrings

    • Module docstrings

  • Docstrings are statements, not comments

  • Docstrings must be the first statement in the body of the thing they are describing

Docstrings are special

>>> def myfunction():
...     """This shows that docstrings are special."""
...     pass
...
>>> help(myfunction)
Help on function myfunction in module __main__:

myfunction()
    This shows that docstrings are special.

>>> myfunction.__doc__
'This shows that docstrings are special.'
def myfunction():
...     pass
...     """This docstring is in the wrong place."""
...
>>> help(myfunction)
Help on function myfunction in module __main__:

myfunction()

>>> myfunction.__doc__
>>>

Class docstrings

Class docstrings should

  • begin with a brief description of the purpose of the class

  • document the data type and meaning of each attribute

class Greeter:
    """ An object that gives users a friendly greeting.

    Attributes:
        name (str): the greeter's name.
    """

Summary

  • Docstrings document how to use our code

  • Functions/methods, classes, and modules can have docstrings

  • Improperly located docstrings aren’t docstrings

  • Class docstrings explain

    • the purpose of the class

    • the data type and meaning of each attribute

Example class

class Memobox:
    """ A mechanism for users to communicate with each other.
    
    Attributes:
        name (str): the user's name.
        contacts (dict of str: Memobox): the people the user
            communicates with and their Memobox objects. Each key is the
            name of a contact; each value is that contact's Memobox.
        memos (list of (tuple of str, str)): unread memos received
            by the user. Each memo is a tuple consisting of a sender
            and a memo string.
    """
    def __init__(self, name):
        """ Initialize new Memobox object.
        
        Side effects:
           Sets attributes name, contacts, and memos.
        """
        self.name = name
        self.contacts = dict()
        self.memos = list()
    
    def add_contact(self, contact):
        """ Add contact to contacts.
        
        Args:
            contact (Memobox): Memobox of a contact to add.
        
        Side effects:
            Modifies contacts attribute.
        """
        self.contacts[contact.name] = contact
    
    def receive_memo(self, sender, memo):
        """ Receive a memo.
        
        Args:
            sender (Memobox): the sender of the memo.
            memo (str): the memo that was received.
        
        Side effects:
            Modifies memos attribute.
        """
        self.memos.append((sender.name, memo))
    
    def send_memo(self, recipient, memo):
        """ Send a memo.
        
        Args:
            recipient (str): the intended recipeint of the memo.
            memo (str): the memo to send.
        
        Side effects:
            Invokes recipient's receive_memo() method.
        
        Raises:
            ValueError: recipient is not listed in contacts.
        """
        if recipient not in self.contacts:
            raise ValueError(f"recipient {recipient} not in contacts")
        self.contacts[recipient].receive_memo(self, memo)
    
    def read_memos(self):
        """ Display all memos and clear memo queue.
        
        Side effects:
            Modifies memos attribute.
            Writes to stdout.
        """
        if len(self.memos) > 0:
            for sender, memo in self.memos:
                print(f"memo from {sender}: {memo}")
            self.memos = list()
        else:
            print("No new memos")