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
personfoldercd personthe terminal shows I am in the
personfolder.../pumping_python/personI activate the virtual environment
source .venv/bin/activateAttention
on Windows without Windows Subsystem for Linux use
.venv/bin/activate.ps1NOTsource .venv/bin/activate.venv/scripts/activate.ps1the terminal shows
(.venv) .../pumping_python/personI use
pytest-watchto run the testspytest-watchthe 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.pyto open it in the editorI make another file in the
srcfolder namedclasses.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
classesmodule1import unittest 2import src.classes 3 4 5class TestClasses(unittest.TestCase):I change
test_failuretotest_making_a_class_w_pass5class TestClasses(unittest.TestCase): 6 7 def test_making_a_class_w_pass(self): 8 self.assertIsInstance(src.classes.WPass(), object) 9 10 11# Exceptions seenthe terminal shows AttributeError
AttributeError: module 'src.classes' has no attribute 'WPass'there is no definition for
WPassinclasses.py
GREEN: make it pass
then I add a class definition to
classes.py1class WPass: 2 3 passthe 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 passthe 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.py11class WObject(object): 12 13 pass 14 15 16class Doe(object): 17 18 passTypeError: Doe() takes no argumentsI add the
__init__method16class Doe(object): 17 18 def __init__(self): 19 return NoneTypeError: Doe.__init__() takes 1 positional argument but 2 were givenI add a parameter to the method
16class Doe(object): 17 18 def __init__(self, first_name): 19 return Nonethe 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.py1import src.person 2 3 4class WPass:the terminal still shows AssertionError
I change the “parent” of
Doe19class Doe(src.person.Person): 20 21 def __init__(self, first_name): 22 return Nonethe test passes
REFACTOR: make it better
I add a test for the attributes and methods of the
Doeclass47 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 seenthe 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 seenthe test passes. I do not need to add an import statement because
classes.pyimportssrc.personand I importsrc.classesat the beginning oftest_person.pyI 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 theDoeclass19class Doe(src.person.Person): passthe 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 seenI 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 seenthe 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
Boyclassclass Boy(object): passthe terminal shows AttributeError
I make the
Boyclass with an attribute calledsexclass Boy(object): sexI add a definition for the
sexattributeclass Boy(object): sex = 'M'the test passes
REFACTOR: make it better
I add another assertion to
test_making_a_class_w_initializersthis time for aGirlclass but with a difference, I provide the value for thesexattribute when I call the classdef 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
Boyclass then add a definition for theGirlclass toclasses.pyclass Girl(object): sex = 'M'TypeError: Girl() takes no argumentsI add the initializer method called
__init__to theGirlclassclass Girl(object): sex = 'F' def __init__(self): passTypeError: __init__() got an unexpected keyword argument 'sex'I make the definition of the
__init__method take a keyword argumentdef __init__(self, sex=None): passthe 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.pyclass Other(object): sex = '?' def __init__(self, sex=None): passthe test passes
Wait a minute, I just repeated the same thing twice.
I am going to make it a third repetition by redefining the
Boyclass to match theGirlandOtherclass because it is fun to do bad thingsclass Boy(object): sex = 'M' def __init__(self, sex=None): passthe 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
Humantoclasses.pybefore the definition forBoywith the same attribute and method of the classes I am trying to abstractclass Human(object): sex = 'M' def __init__(self, sex='M'): passthe terminal still shows passing tests
I change the definitions for
Boyto inherit from theHumanclass and all tests are still passingclass Boy(Human): sex = 'M' def __init__(self, sex=None): passI remove the
sexattribute from theBoyclass and the tests continue to passI remove the
__init__method, then add the pass placeholderclass Boy(Human): passall tests are still passing. Lovely
What if I try the same thing with the
Girlclass and change its definition to inherit from theHumanclass?class Girl(Human): sex = 'F' def __init__(self): passI remove the
sexattribute the terminal shows AssertionErrorI make the
Humanclass to set thesexattribute in the parent initializer instead of at the child levelclass Human(object): sex = 'M' def __init__(self, sex='M'): self.sex = sexthe terminal still shows AssertionError
when I remove the
__init__method from theGirlclassclass Girl(Human): passthe test passes. Lovely
I wonder if I can do the same with the
Otherclass? I change the definition to inherit from theHumanclassclass Other(Human): passthe test passes
One More Thing! I remove the
sexattribute from theHumanclassclass Human(object): def __init__(self, sex='M'): self.sex = sexall tests are passing, I have successfully refactored the 3 classes and abstracted a
Humanclass from themthe
Boy,GirlandOtherclass now inherit from theHumanclass which means they all get the same methods and attributes that theHumanclass has, including the__init__methodself.sexin each class is thesexattribute in the class, allowing its definition from inside the__init__methodsince
self.sexis defined as a class attribute, it is accessible from outside the class as I do in the tests i.eclasses.Girl(sex='F').sexandclasses.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
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
instanceof an object
Attention
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
deactivatethe terminal goes back to the command line,
(.venv)is no longer on the left side.../pumping_python/personI change directory to the parent of
personcd ..the terminal shows
.../pumping_pythonI am back in the
pumping_pythondirectory
code from the 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