Danger

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

family ties

In test_attributes_and_methods_of_classes I saw the methods I added to the Person class and also saw a lot of attributes and methods that I did not add, which led to the question of where they came from.

In object oriented programming there is a concept called Inheritance, it allows me to define new objects that inherit from other objects.

Making new objects is easier with Inheritance because I do not have to rewrite things that have already been written, I can inherit them instead and change the new objects for what I need

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


preview

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

  1import datetime
  2import random
  3import src.person
  4import unittest
  5
  6
  7def choose(*choices):
  8    return random.choice(choices)
  9
 10
 11def this_year():
 12    return datetime.datetime.now().year
 13
 14
 15def random_year_of_birth():
 16    return random.randint(
 17        this_year()-120, this_year()
 18    )
 19
 20
 21def get_age(year_of_birth):
 22    return this_year() - year_of_birth
 23
 24
 25class TestPerson(unittest.TestCase):
 26
 27    RANDOM_NAMES = (
 28        'jane', 'joe', 'john', 'person',
 29        'doe', 'smith', 'blow', 'public',
 30    )
 31
 32    def setUp(self):
 33        self.random_year_of_birth = random_year_of_birth()
 34        self.random_new_year_of_birth = random_year_of_birth()
 35        self.original_age = get_age(self.random_year_of_birth)
 36        self.new_age = get_age(self.random_new_year_of_birth)
 37        self.random_first_name = choose(*self.RANDOM_NAMES)
 38        self.random_last_name = choose(*self.RANDOM_NAMES)
 39        self.random_sex = choose('M', 'F')
 40        self.random_factory_person = src.person.factory(
 41            first_name=self.random_first_name,
 42            last_name=self.random_last_name,
 43            sex=self.random_sex,
 44            year_of_birth=self.random_year_of_birth,
 45        )
 46        self.random_classy_person = src.person.Person(
 47            first_name=self.random_first_name,
 48            last_name=self.random_last_name,
 49            sex=self.random_sex,
 50            year_of_birth=self.random_year_of_birth,
 51        )
 52
 53    def test_factory_takes_keyword_arguments(self):
 54        self.assertEqual(
 55            src.person.factory(
 56                first_name=self.random_first_name,
 57                last_name=self.random_last_name,
 58                sex=self.random_sex,
 59                year_of_birth=self.random_year_of_birth,
 60            ),
 61            self.random_factory_person
 62        )
 63
 64    def test_factory_w_default_arguments(self):
 65        self.assertEqual(
 66            src.person.factory(
 67                first_name=self.random_first_name,
 68                year_of_birth=self.random_year_of_birth,
 69            ),
 70            dict(
 71                first_name=self.random_first_name,
 72                last_name='doe',
 73                sex='M',
 74                age=self.original_age,
 75            )
 76        )
 77
 78    def expected_greeting(self):
 79        return (
 80            f'Hi, my name is {self.random_first_name} '
 81            f'{self.random_last_name} '
 82            f'and I am {self.original_age}'
 83        )
 84
 85    def test_factory_person_greeting(self):
 86        self.assertEqual(
 87            src.person.hello(self.random_factory_person),
 88            self.expected_greeting()
 89        )
 90
 91    def test_classy_person_greeting(self):
 92        self.assertEqual(
 93            self.random_classy_person.hello(),
 94            self.expected_greeting()
 95        )
 96
 97    def test_update_factory_person_year_of_birth(self):
 98        self.assertEqual(
 99            self.random_factory_person.get('age'),
100            self.original_age
101        )
102
103        with self.assertRaises(KeyError):
104            self.random_factory_person['year_of_birth']
105        self.assertEqual(
106            self.random_factory_person.setdefault(
107                'year_of_birth', self.random_new_year_of_birth
108            ),
109            self.random_new_year_of_birth
110        )
111        self.assertEqual(
112            self.random_factory_person.get('age'),
113            self.original_age
114        )
115
116        self.assertEqual(
117            src.person.update_year_of_birth(
118                self.random_factory_person,
119                self.random_new_year_of_birth
120            ),
121            dict(
122                first_name=self.random_first_name,
123                last_name=self.random_last_name,
124                sex=self.random_sex,
125                age=self.new_age
126            )
127        )
128
129    def test_update_classy_person_year_of_birth(self):
130        self.assertEqual(
131            self.random_classy_person.get_age(),
132            self.original_age
133        )
134
135        self.random_classy_person.year_of_birth = self.random_new_year_of_birth
136        self.assertEqual(
137            self.random_classy_person.get_age(),
138            self.new_age
139        )
140
141
142# Exceptions seen
143# AssertionError
144# NameError
145# AttributeError
146# TypeError
147# SyntaxError

requirements


open the project

  • I change directory to the person folder

    cd person
    

    the terminal shows I am in the person folder

    .../pumping_python/person
    
  • I activate the virtual environment

    source .venv/bin/activate
    

    Attention

    on Windows without Windows Subsystem for Linux use .venv/bin/activate.ps1 NOT source .venv/bin/activate

    .venv/scripts/activate.ps1
    

    the terminal shows

    (.venv) .../pumping_python/person
    
  • I use pytest-watch to run the tests

    pytest-watch
    

    the terminal shows

    rootdir: .../pumping_python/person
    collected 2 items
    
    tests/test_person.py ..                                             [100%]
    
    ============================ 2 passed in X.YZs =============================
    
  • I hold ctrl on the keyboard and click on tests/test_person.py to open it in the editor

  • I make a new file in the tests folder named test_classes.py

  • I make another file in the src folder named classes.py


test_making_a_class_w_pass


to review, I can make a class with the class keyword, use CapWords format for the name and use a name that tells what the group of attributes and methods do


RED: make it fail


  • I add an import statement for the classes module

    1import unittest
    2import src.classes
    3
    4
    5class TestClasses(unittest.TestCase):
    
  • I change test_failure to test_making_a_class_w_pass

     5class TestClasses(unittest.TestCase):
     6
     7    def test_making_a_class_w_pass(self):
     8        self.assertIsInstance(src.classes.WPass(), object)
     9
    10
    11# Exceptions seen
    

    the terminal shows AttributeError

    AttributeError: module 'src.classes' has no attribute 'WPass'
    

    there is no definition for WPass in classes.py


GREEN: make it pass


  • I open classes.py from the src folder in the editor

  • then I add a class definition to classes.py

    1class WPass:
    2
    3    pass
    

    the test passes

pass is a placeholder, it makes sure I am following Python rules and I can make a class with pass


test_making_a_class_w_parentheses


I can also make a class with parentheses.


RED: make it red


I add another test in test_classes.py

 7    def test_making_a_class_w_pass(self):
 8        self.assertIsInstance(src.classes.WPass(), object)
 9
10    def test_making_a_class_w_parentheses(self):
11        self.assertIsInstance(src.classes.WParentheses(), object)
12
13
14# Exceptions seen

the terminal shows AttributeError

E       AttributeError: module 'src.classes' has no attribute 'WParentheses'

GREEN: make it pass


I add a class definition like WPass to classes.py

1class WPass:
2
3    pass
4
5
6class WParentheses:
7
8    pass

the test passes


REFACTOR: make it better


  • I add parentheses to the definition

    6class WParentheses():
    7
    8    pass
    

    the terminal shows all tests are still passing.

pass is a placeholder, it makes sure I am following Python rules, I can make a class with


test_making_a_class_w_object


RED: make it fail


I add another test to TestClasses in test_classes.py

 7    def test_making_a_class_w_parentheses(self):
 8        self.assertIsInstance(src.classes.WParentheses(), object)
 9
10    def test_making_a_class_w_object(self):
11        self.assertIsInstance(src.classes.WObject(), object)
12
13
14# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.classes' has no attribute 'WObject'

GREEN: make it pass


I add a class definition to classes.py

 6class WParentheses():
 7
 8    pass
 9
10
11class WObject():
12
13    pass

the test passes


REFACTOR: make it better


The last two tests pass because everything in Python is an object also known as a class. object is the mother class of all classes. I can use anything in the assertIsInstance method and the test would pass.

I use the examples to show different ways to make a class. I can also say who the parent of a class is when I define it. I add object to the definition

11class WObject(object):
12
13    pass

the test is still green. pass is a placeholder, it makes sure I am following Python rules, I can make a class with


test_attributes_and_methods_of_objects

I add a test to show the attributes and methods of object


RED: make it fail


I add a test to test_classes.py

13    def test_making_a_class_w_object(self):
14        self.assertIsInstance(src.classes.WObject(), object)
15
16    def test_attributes_and_methods_of_objects(self):
17        self.assertEqual(
18            dir(object),
19            []
20        )
21
22
23# Exceptions seen

the terminal shows AssertionError

AssertionError: Lists differ: ['__class__', '__delattr__', '__dir__', '_[272 chars]k__'] != []

GREEN: make it pass


I copy and paste the values from the terminal as the expectation and use the Find and Replace feature of the Integrated Development Environment (IDE) to remove the extra characters

16    def test_attributes_and_methods_of_objects(self):
17        self.assertEqual(
18            dir(object),
19            [
20                '__class__',
21                '__delattr__',
22                '__dir__',
23                '__doc__',
24                '__eq__',
25                '__format__',
26                '__ge__',
27                '__getattribute__',
28                '__getstate__',
29                '__gt__',
30                '__hash__',
31                '__init__',
32                '__init_subclass__',
33                '__le__',
34                '__lt__',
35                '__ne__',
36                '__new__',
37                '__reduce__',
38                '__reduce_ex__',
39                '__repr__',
40                '__setattr__',
41                '__sizeof__',
42                '__str__',
43                '__subclasshook__'
44            ]
45        )
46
47
48# Exceptions seen

and it passes. All classes automatically get these attributes, they inherit them


test_making_classes_w_inheritance


RED: make it fail


I add a new test

43                '__subclasshook__'
44            ]
45        )
46
47    def test_making_classes_w_inheritance(self):
48        self.assertIsInstance(
49            src.classes.Doe('doe'),
50            src.person.Person
51        )
52
53
54# Exceptions seen

the terminal shows AttributeError

AttributeError: module 'src.classes' has no attribute 'Doe'

GREEN: make it pass


  • I add a class definition to classes.py

    11class WObject(object):
    12
    13    pass
    14
    15
    16class Doe(object):
    17
    18    pass
    

    the terminal shows TypeError

    TypeError: Doe() takes no arguments
    
  • I add the __init__ method

    16class Doe(object):
    17
    18    def __init__(self):
    19        return None
    

    the terminal shows TypeError

    TypeError: Doe.__init__() takes 1 positional argument but 2 were given
    
  • I add a parameter to the method

    16class Doe(object):
    17
    18    def __init__(self, first_name):
    19        return None
    

    the terminal shows AssertionError

    AssertionError: <src.classes.Doe object at 0xffff01a2bc34> is not an instance of <class 'src.person.Person'>
    
  • I add an import statement at the top of classes.py

    1import src.person
    2
    3
    4class WPass:
    

    the terminal still shows AssertionError

  • I change the “parent” of Doe

    19class Doe(src.person.Person):
    20
    21    def __init__(self, first_name):
    22        return None
    

    the test passes


REFACTOR: make it better


  • I add a test for the attributes and methods of the Doe class

    47    def test_making_classes_w_inheritance(self):
    48        self.assertIsInstance(
    49            src.classes.Doe('doe'),
    50            src.person.Person
    51        )
    52        self.assertEqual(
    53            dir(src.classes.Doe),
    54            []
    55        )
    56
    57
    58# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: Lists differ: ['__class__', '__delattr__', '__dict__', '[377 chars]llo'] != []
    
  • I change the expectation

    47    def test_making_classes_w_inheritance(self):
    48        self.assertIsInstance(
    49            src.classes.Doe('doe'),
    50            src.person.Person
    51        )
    52        self.assertEqual(
    53            dir(src.classes.Doe),
    54            dir(src.person.Person)
    55        )
    56
    57
    58# Exceptions seen
    

    the test passes. I do not need to add an import statement because classes.py imports src.person and I import src.classes at the beginning of test_person.py

  • I add the import statement to be clearer

    1import unittest
    2import src.classes
    3import src.person
    4
    5
    6class TestClasses(unittest.TestCase):
    

    the test is still green

  • I can remove the __init__ method from the Doe class

    19class Doe(src.person.Person): pass
    

    the test is still green


test_family_ties


RED: make it fail


  • I add a new test for Inheritance

    53        self.assertEqual(
    54            dir(src.classes.Doe),
    55            dir(src.person.Person)
    56        )
    57
    58    def test_family_ties(self):
    59        doe = src.classes.Doe('doe')
    60        jane = src.classes.Doe('jane')
    61        john = src.classes.Doe('john')
    62
    63
    64# Exceptions seen
    
  • I add an assertion for the last names of the people I made

    61        john = src.classes.Doe('john')
    62
    63        for person in (doe, jane, john):
    64            with self.subTest(first_name=person.first_name):
    65                self.assertEqual(
    66                    person.last_name,
    67                    ''
    68                )
    69
    70
    71# Exceptions seen
    

    the terminal shows AssertionError

    AssertionError: 'doe' != ''
    

GREEN: make it pass


I change the expectation

65                  self.assertEqual(
66                      person.last_name,
67                      'doe'
68                  )

all 3 people made with the Doe class have the same last name. They are related.


:refactor:`REFACTOR`: make it better


  • I add a class for another family




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

Attention

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?


rate pumping python

If this has been a 7 star experience for you, please leave a 5 star review. It helps other people get into the book too