classes

classes are definitions that represent an object. I think of them as attributes and methods (functions) that belong together


requirements

how to make a class in Python

  • use the class keyword

  • use TitleCase for the name

  • use a name that tells what the collection of attributes and methods (functions) does - this is hard to do and is something I am still learning


test_defining_classes_w_pass

red: make it fail

I make a new file called test_classes.py in the tests directory

import unittest
import classes


class TestClasses(unittest.TestCase):

    def test_defining_classes_w_pass(self):
        self.assertIsInstance(classes.ClassWithPass(), object)

the terminal shows ModuleNotFoundError because I have an import statement for a module called classes

green: make it pass

  • I add ModuleNotFoundError to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    
  • I make Python module called classes.py and the terminal shows AttributeError which I add to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # AttributeError
    
  • I then add the name ClassWithPass to the module

    ClassWithPass
    

    and the terminal shows NameError because ClassWithPass is not defined anywhere

  • I add the error to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # AttributeError
    # NameError
    
  • I point the name to None

    ClassWithPass = None
    
  • and then redefine the variable as a class using thePython class keyword

    class ClassWithPass:
    

    the terminal shows IndentationError because I declared a class without adding any indented text

  • I add the new error to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # ModuleNotFoundError
    # AttributeError
    # NameError
    # IndentationError
    
  • Python has the pass keyword to use as a placeholder for moments like this cue Kelly Clarkson

    class ClassWithPass:
    
        pass
    

    and the terminal shows passing tests

refactor: make it better

Here is a quick review of what has happened so far

  • pass is a placeholder

  • self.assertIsInstance is a unittest.TestCase method that checks if the first input to the method is an instance of the second input

  • the test self.assertIsInstance(classes.ClassWithPass(), object) checks if ClassWithPass is an object

  • in Python everything is an object , which means if it is in Python there is a class definition for it somewhere or it inherits from a class


test_defining_classes_w_parentheses

red: make it fail

I add another test to TestClasses in test_classes.py to show another way to make a class

def test_defining_classes_w_parentheses(self):
    self.assertIsInstance(classes.ClassWithParentheses(), object)

the terminal shows AttributeError

green: make it pass

  • I add a class definition like ClassWithPass to classes.py

    class ClassWithParentheses:
    
        pass
    

    the terminal shows passing tests

  • When I make the definition include parentheses

    class ClassWithParentheses():
    
        pass
    

    the terminal shows all tests are still passing.

  • I can confidently say that in Python

    • I can define classes with parentheses

    • I can define classes without parentheses

    • pass is a placeholder


test_defining_classes_w_object

In object oriented programming there is a concept called Inheritance). With Inheritance I can define new objects that inherit from existing objects.

Making new objects is easier because I do not have to reinvent or rewrite things that already exist, I can inherit them instead and change the new objects for my specific use case

To use inheritance I specify the “parent” in parentheses when I define the new object (the child) to make the relationship

red: make it fail

I add another test to TestClasses in test_classes.py

def test_defining_classes_w_object(self):
    self.assertIsInstance(classes.ClassWithObject(), object)

and the terminal shows AttributeError

green: make it pass

  • I add a class definition to classes.py

    class ClassWithObject():
    
        pass
    

    the terminal shows all tests passed

  • then I change the definition to explicitly state the parent object

    class ClassWithObject(object):
    
        pass
    

    and the terminal still shows passing tests

test_classes_w_attributes

I now add some tests for attributes since I know how to define a class for attributes

red: make it fail

  • I add a failing test to TestClasses in classes.py

    def test_classes_w_attributes(self):
        self.assertEqual(classes.ClassWithAttributes.a_boolean, bool)
    

    the terminal shows AttributeError

  • I add a class definition to classes.py

    class ClassWithAttributes(object):
    
        pass
    

    the terminal shows AttributeError for a missing attribute in the newly defined class

green: make it pass

refactor: make it better

red: make it fail

Let us add more tests with the other Python data structures to test_classes_w_attributes

def test_classes_w_attributes(self):
    self.assertEqual(classes.ClassWithAttributes.a_boolean, bool)
    self.assertEqual(classes.ClassWithAttributes.an_integer, int)
    self.assertEqual(classes.ClassWithAttributes.a_float, float)
    self.assertEqual(classes.ClassWithAttributes.a_string, str)
    self.assertEqual(classes.ClassWithAttributes.a_tuple, tuple)
    self.assertEqual(classes.ClassWithAttributes.a_list, list)
    self.assertEqual(classes.ClassWithAttributes.a_set, set)
    self.assertEqual(classes.ClassWithAttributes.a_dictionary, dict)

the terminal shows AttributeError

green: make it pass

I add matching attributes to ClassWithAttributes to make the tests pass

class ClassWithAttributes(object):

    a_boolean = bool
    an_integer = int
    a_float = float
    a_string = str
    a_tuple = tuple
    a_list = list
    a_set = set
    a_dictionary = dict

and the terminal shows all tests pass


test_classes_w_methods

I can also define classes with methods which are function definitions that belong to the class

red: make it fail

I add some tests for class methods to TestClasses in classes.py

def test_classes_w_methods(self):
    self.assertEqual(
        classes.ClassWithMethods.method_a(),
        'You called MethodA'
    )

and the terminal shows AttributeError

green: make it pass

  • I add a class definition to classes.py

    class ClassWithMethods(object):
    
        pass
    

    the terminal now gives AttributeError with a different error

  • When I add the missing attribute to the ClassWithMethods class

    class ClassWithMethods(object):
    
        method_a
    

    the terminal shows NameError because there is no definition for method_a

  • I define method_a as an attribute by pointing it to None

    class ClassWithMethods(object):
    
        method_a = None
    

    the terminal shows TypeError since method_a is None which is not callable

  • I change the definition of method_a to make it a function which makes it callable

    class ClassWithMethods(object):
    
        def method_a():
            return None
    

    and the terminal shows AssertionError. Progress!

  • I then change the value that method_a returns to match the expectation of the test

    def method_a():
        return 'You called MethodA'
    

    and the test passes

refactor: make it better

  • I can “make this better” by adding a few more tests to test_classes_w_methods for fun

    def test_classes_w_methods(self):
        self.assertEqual(
            classes.ClassWithMethods.method_a(),
            'You called MethodA'
        )
        self.assertEqual(
            classes.ClassWithMethods.method_b(),
            'You called MethodB'
        )
        self.assertEqual(
            classes.ClassWithMethods.method_c(),
            'You called MethodC'
        )
        self.assertEqual(
            classes.ClassWithMethods.method_d(),
            'You called MethodD'
        )
    

    the terminal shows AttributeError

  • and I change each assertion to the right value until they all pass


test_attributes_and_methods_of_classes

Since I know how to define classes with methods and how to define classes with attributes, what happens when I define a class with both?

red: make it fail

I add another test for a class that has both attributes and methods

def test_attributes_and_methods_of_classes(self):
    self.assertEqual(
        classes.ClassWithAttributesAndMethods.attribute,
        'attribute'
    )
    self.assertEqual(
        classes.ClassWithAttributesAndMethods.method(),
        'you called a method'
    )

the terminal shows AttributeError

green: make it pass

I make classes.py to make the tests pass by defining the class, attribute and methods

class ClassWithAttributesAndMethods(object):

    attribute = 'attribute'

    def method():
        return 'you called a method'

test_attributes_and_methods_of_objects

To view what attributes and methods are defined for any object I can call dir on the object.

The dir method returns a list of all attributes and methods of the object provided to it as input

red: make it fail

I add a test to test_classes.py

def test_attributes_and_methods_of_objects(self):
  self.assertEqual(
      dir(classes.ClassWithAttributesAndMethods),
      []
  )

the terminal shows AssertionError as the expected and real values do not match

green: make it pass

I copy the values from the terminal to change the expectation of the test

def test_attributes_and_methods_of_objects(self):
    self.assertEqual(
        dir(classes.ClassWithAttributesAndMethods),
        [
            '__class__',
            '__delattr__',
            '__dict__',
            '__dir__',
            '__doc__',
            '__eq__',
            '__format__',
            '__ge__',
            '__getattribute__',
            '__gt__',
            '__hash__',
            '__init__',
            '__init_subclass__',
            '__le__',
            '__lt__',
            '__module__',
            '__ne__',
            '__new__',
            '__reduce__',
            '__reduce_ex__',
            '__repr__',
            '__setattr__',
            '__sizeof__',
            '__str__',
            '__subclasshook__',
            '__weakref__',
            'attribute',
            'method',
        ]
    )

and it passes, the last 2 values in the list are attribute and method which I defined earlier


test_classes_w_initializer

When making a new class, we can define an initializer which is a method that can receive inputs to be used to customize instances/copies of the class

red: make it fail

I add a failing test to test_classes.py

def test_classes_w_initializers(self):
    self.assertEqual(classes.Boy().sex, 'M')

the terminal shows AttributeError

green: make it pass

  • I add a definition for the Boy class

    class Boy(object):
    
        pass
    

    the terminal shows another AttributeError

  • I make the Boy class with an attribute called sex

    class Boy(object):
    
        sex
    

    the terminal produces NameError

  • I add a definition for the sex attribute

    class Boy(object):
    
        sex = 'M'
    

    the terminal shows passing tests

refactor: make it better

  • I add another assertion to test_classes_w_initializers this time for a Girl class but with a difference, I provide the value for the sex attribute when I call the class

    def test_classes_w_initializers(self):
        self.assertEqual(classes.Boy().sex, 'M')
        self.assertEqual(classes.Girl(sex='F').sex, 'F')
    

    the terminal shows AttributeError

  • I try the same solution I used for the Boy class then add a definition for the Girl class to classes.py

    class Girl(object):
    
        sex = 'M'
    

    and the terminal shows TypeError

    TypeError: Girl() takes no arguments
    
    • classes.Girl(sex='F') looks like a call to a function

    • I can define classes that take values by using an initializer

    • An initializer is a class method that allows customization of instances/copies of a class

  • I add the initializer method called __init__ to the Girl class

    class Girl(object):
    
        sex = 'F'
    
        def __init__(self):
            pass
    

    and the terminal shows TypeError

    TypeError: __init__() got an unexpected keyword argument 'sex'
    
  • I make the signature of the __init__ method take a keyword argument

    def __init__(self, sex=None):
        pass
    

    and the terminal shows passing tests

  • I add another assertion

    def test_classes_w_initializers(self):
        self.assertEqual(classes.Boy().sex, 'M')
        self.assertEqual(classes.Girl(sex='F').sex, 'F')
        self.assertEqual(classes.Other(sex='?').sex, '?')
    

    and the terminal shows AttributeError

  • I add a class definition to classes.py

    class Other(object):
    
        sex = '?'
    
        def __init__(self, sex=None):
            pass
    

    the terminal shows passing tests

  • Wait a minute, I just repeated the same thing twice.

    • I defined a class with a name

    • I defined an attribute called sex

    • I defined an __init__ method which takes in a sex keyword argument

  • I am going to make it a third repetition by redefining the Boy class to match the Girl and Other class because it is fun to do bad things

    class Boy(object):
    
        sex = 'M'
    
        def __init__(self, sex=None):
            pass
    

    the terminal shows all tests still passing and I have now written the same thing 3 times. Earlier on I mentioned inheritance, and will now try to use it to remove this duplication so I do not repeat myself

  • I add a new class called Human to classes.py before the definition for Boy with the same attribute and method of the classes I am trying to abstract

    class Human(object):
    
        sex = 'M'
    
        def __init__(self, sex='M'):
            pass
    

    the terminal still shows passing tests

  • I change the definitions for Boy to inherit from the Human class and all tests are still passing

    class Boy(Human):
    
        sex = 'M'
    
        def __init__(self, sex=None):
            pass
    
  • I remove the sex attribute from the Boy class and the tests continue to pass

  • I remove the __init__ method, then add the pass placeholder

    class Boy(Human):
    
        pass
    

    all tests are still passing. Lovely

  • What if I try the same thing with the Girl class and change its definition to inherit from the Human class?

    class Girl(Human):
    
        sex = 'F'
    
        def __init__(self):
            pass
    
  • I remove the sex attribute and the terminal shows AssertionError

  • I make the Human class to set the sex attribute in the parent initializer instead of at the child level

    class Human(object):
    
        sex = 'M'
    
        def __init__(self, sex='M'):
            self.sex = sex
    

    the terminal still shows AssertionError

  • when I remove the __init__ method from the Girl class

    class Girl(Human):
    
        pass
    

    the terminal shows passing tests. Lovely

  • I wonder if I can do the same with the Other class? I change the definition to inherit from the Human class

    class Other(Human):
    
        pass
    

    the terminal shows passing tests

  • One More Thing! I remove the sex attribute from the Human class

    class Human(object):
    
      def __init__(self, sex='M'):
          self.sex = sex
    

    all tests are passing, I have successfully refactored the 3 classes and abstracted a Human class from them

  • the Boy, Girl and Other class now inherit from the Human class which means they all get the same methods and attributes that the Human class has, including the __init__ method

  • self.sex in each class is the sex attribute in the class, allowing its definition from inside the __init__ method

  • since self.sex is defined as a class attribute, it is accessible from outside the class as I do in the tests i.e classes.Girl(sex='F').sex and classes.Other(sex='?').sex


review

the tests show

  • how to define a class with an attribute

  • how to define a class with a method

  • how to define a class with an initializer

  • how to view the attributes and methods of a class

  • classes can be defined

    • with parentheses stating what object the class inherits from

    • with parentheses without stating what object the class inherits from

    • without parentheses

    • pass is a placeholder

  • classes by default inherit from the object class, because in each of the tests, whether the parent is stated or not, each class I defined is an instance of an object

Zen of Python (PEP 20)

I prefer to use the explicit form of class definitions with the parent object in parentheses, from the Zen of Python: Explicit is better than implicit


classes: tests and solutions