Danger

DANGER WILL ROBINSON! Though the code works, this chapter is still UNDER CONSTRUCTION it may look completely different when I am done

classes

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

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


preview

Here are the tests I have by the end of the chapter

  1import datetime
  2import random
  3import src.person
  4import unittest
  5
  6
  7def this_year():
  8    return datetime.datetime.now().year
  9
 10
 11def random_second_numberear_of_birth():
 12    return random.randint(
 13        this_year()-120, this_year()
 14    )
 15
 16
 17def get_age(year_of_birth):
 18    return this_year() - year_of_birth
 19
 20
 21class TestPerson(unittest.TestCase):
 22
 23    def setUp(self):
 24        self.random_first_name = random.choice((
 25            'jane', 'joe', 'john', 'person',
 26        ))
 27        self.random_second_numberear_of_birth = random_second_numberear_of_birth()
 28        self.random_new_year_of_birth = random_second_numberear_of_birth()
 29        self.original_age = get_age(self.random_second_numberear_of_birth)
 30        self.new_age = get_age(self.random_new_year_of_birth)
 31        self.random_last_name = random.choice((
 32            'doe', 'smith', 'blow', 'public',
 33        ))
 34        self.random_sex = random.choice(('F', 'M'))
 35        self.random_factory_person = src.person.factory(
 36            first_name=self.random_first_name,
 37            last_name=self.random_last_name,
 38            sex=self.random_sex,
 39            year_of_birth=self.random_second_numberear_of_birth,
 40        )
 41        self.random_classy_person = src.person.Person(
 42            first_name=self.random_first_name,
 43            last_name=self.random_last_name,
 44            sex=self.random_sex,
 45            year_of_birth=self.random_second_numberear_of_birth,
 46        )
 47
 48    def test_takes_keyword_arguments(self):
 49        self.assertEqual(
 50            self.random_factory_person,
 51            dict(
 52                first_name=self.random_first_name,
 53                last_name=self.random_last_name,
 54                sex=self.random_sex,
 55                age=self.original_age,
 56            )
 57        )
 58
 59    def test_function_w_default_keyword_arguments(self):
 60        self.assertEqual(
 61            src.person.factory(self.random_first_name),
 62            dict(
 63                first_name=self.random_first_name,
 64                last_name='doe',
 65                sex='M',
 66                age=0,
 67            )
 68        )
 69
 70    def test_factory_person_introduction(self):
 71        self.assertEqual(
 72            src.person.say_hello(self.random_factory_person),
 73            (
 74                f'Hello, my name is {self.random_first_name} '
 75                f'{self.random_last_name} '
 76                f'and I am {self.original_age}'
 77            )
 78        )
 79
 80    def test_classy_person_introduction(self):
 81        self.assertEqual(
 82            self.random_classy_person.say_hello(),
 83            (
 84                f'Hello, my name is {self.random_first_name} '
 85                f'{self.random_last_name} '
 86                f'and I am {self.original_age}'
 87            )
 88        )
 89
 90    def test_update_factory_person_year_of_birth(self):
 91        self.assertEqual(
 92            self.random_factory_person.get('age'),
 93            self.original_age
 94        )
 95
 96        with self.assertRaises(KeyError):
 97            self.random_factory_person['year_of_birth']
 98        self.assertEqual(
 99            self.random_factory_person.setdefault(
100                'year_of_birth', self.random_new_year_of_birth
101            ),
102            self.random_new_year_of_birth
103        )
104        self.assertEqual(
105            self.random_factory_person.get('age'),
106            self.original_age
107        )
108
109        self.assertEqual(
110            src.person.update_year_of_birth(
111                self.random_factory_person,
112                self.random_new_year_of_birth
113            ),
114            dict(
115                first_name=self.random_factory_person.get('first_name'),
116                last_name=self.random_factory_person.get('last_name'),
117                sex=self.random_factory_person.get('sex'),
118                age=self.new_age,
119            )
120        )
121
122    def test_update_classy_person_year_of_birth(self):
123        self.assertEqual(
124            self.random_classy_person.get_age(),
125            self.original_age
126        )
127
128        self.random_classy_person.year_of_birth = self.random_new_year_of_birth
129        self.assertEqual(
130            self.random_classy_person.get_age(),
131            self.new_age
132        )
133
134    def test_attributes_and_methods_of_classes(self):
135        self.assertEqual(
136            dir(src.person.Person),
137            [
138                '__class__',
139                '__delattr__',
140                '__dict__',
141                '__dir__',
142                '__doc__',
143                '__eq__',
144                '__firstlineno__',
145                '__format__',
146                '__ge__',
147                '__getattribute__',
148                '__getstate__',
149                '__gt__',
150                '__hash__',
151                '__init__',
152                '__init_subclass__',
153                '__le__',
154                '__lt__',
155                '__module__',
156                '__ne__',
157                '__new__',
158                '__reduce__',
159                '__reduce_ex__',
160                '__repr__',
161                '__setattr__',
162                '__sizeof__',
163                '__static_attributes__',
164                '__str__',
165                '__subclasshook__',
166                '__weakref__',
167                'get_age',
168                'say_hello'
169            ]
170        )
171
172
173# Exceptions seen
174# AssertionError
175# NameError
176# AttributeError
177# TypeError
178# SyntaxError
179# KeyError

requirements

how to make a person


test_factory_person_introduction

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_making_a_class_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 seen in test_classes.py

    # Exceptions seen
    # AssertionError
    # ModuleNotFoundError
    
  • I make Python module called classes.py the terminal shows AttributeError which I add to the list of Exceptions seen in test_classes.py

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

    ClassWithPass
    

    the terminal shows NameError because ClassWithPass is not defined anywhere

  • I add the error to the list of Exceptions seen in test_classes.py

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

    ClassWithPass = None
    
  • and then redefine the variable as a class using the Python 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 seen in test_classes.py

    # Exceptions seen
    # 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
    

    the test passes

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 a child 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_classy_person_introduction

RED: make it fail

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

def test_making_a_class_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 test passes

  • 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_update_factory_person_year_of_birth

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_making_a_class_w_object(self):
    self.assertIsInstance(classes.ClassWithObject(), object)

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_update_classy_person_year_of_birth

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_making_a_class_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

  • I add an attribute to ClassWithAttributes

    class ClassWithAttributes(object):
    
        a_boolean
    

    the terminal shows NameError

  • after I point the name to None

    class ClassWithAttributes(object):
    
        a_boolean = None
    

    the terminal shows AssertionError

  • I redefine the attribute to make the test pass

    class ClassWithAttributes(object):
    
        a_boolean = bool
    

    the terminal shows all tests passed

REFACTOR: make it better

RED: make it fail

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

def test_making_a_class_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

the terminal shows all tests pass


test_attributes_and_methods_of_classes

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_making_a_class_w_methods(self):
    self.assertEqual(
        classes.ClassWithMethods.method_a(),
        'You called MethodA'
    )

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
    

    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'
    

    the test passes

REFACTOR: make it better

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

    def test_making_a_class_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_making_a_class_w_attributes_and_methods

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_making_a_class_w_attributes_and_methods(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 built-in function 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_making_a_class_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_making_a_class_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 AttributeError

  • I make the Boy class with an attribute called sex

    class Boy(object):
    
        sex
    

    the terminal shows NameError

  • I add a definition for the sex attribute

    class Boy(object):
    
        sex = 'M'
    

    the test passes

REFACTOR: make it better

  • I add another assertion to test_making_a_class_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_making_a_class_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'
    

    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
    

    the terminal shows TypeError

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

    def __init__(self, sex=None):
        pass
    

    the test passes

  • I add another assertion

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

    the terminal shows AttributeError

  • I add a class definition to classes.py

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

    the test passes

  • 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 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 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 test passes. 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 test passes

  • 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

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


close the project

  • I close the file(s) I have open in the editor(s)

  • I click in the terminal and exit the tests with ctrl+c on the keyboard

  • I deactivate the virtual environment

    deactivate
    

    the terminal goes back to the command line, (.venv) is no longer on the left side

    .../pumping_python/person
    
  • I change directory to the parent of person

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory


code from the chapter

Do you want to see all the CODE I typed in this chapter?


what is next?

you have gone through a lot of things and know

Would you like to test ModuleNotFoundError?


please leave a review