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
TitleCasefor the nameuse 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
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 # ModuleNotFoundErrorI make Python module called
classes.pythe terminal shows AttributeError which I add to the list of Exceptions seen intest_classes.py# Exceptions seen # AssertionError # ModuleNotFoundError # AttributeErrorI then add the name
ClassWithPassto the moduleClassWithPassthe terminal shows NameError because
ClassWithPassis not defined anywhereI add the error to the list of Exceptions seen in
test_classes.py# Exceptions seen # AssertionError # ModuleNotFoundError # AttributeError # NameErrorI point the name to None
ClassWithPass = Noneand 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 # IndentationErrorPython has the pass keyword to use as a placeholder for moments like this cue Kelly Clarkson
class ClassWithPass: passthe test passes
REFACTOR: make it better
Here is a quick review of what has happened so far
pass is a placeholder
self.assertIsInstanceis a unittest.TestCase method that checks if the first input to the method is a child of the second inputthe test
self.assertIsInstance(classes.ClassWithPass(), object)checks ifClassWithPassis an objectin 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
ClassWithPasstoclasses.pyclass ClassWithParentheses: passthe test passes
When I make the definition include parentheses
class ClassWithParentheses(): passthe terminal shows all tests are still passing.
I can confidently say that in Python
I can define
classeswith parenthesesI can define
classeswithout parenthesespass 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
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
TestClassesinclasses.pydef 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.pyclass ClassWithAttributes(object): passthe terminal shows AttributeError for a missing attribute in the newly defined class
GREEN: make it pass
I add an attribute to
ClassWithAttributesclass ClassWithAttributes(object): a_booleanafter I point the name to None
class ClassWithAttributes(object): a_boolean = Nonethe terminal shows AssertionError
I redefine the attribute to make the test pass
class ClassWithAttributes(object): a_boolean = boolthe 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.pyclass ClassWithMethods(object): passthe terminal now gives AttributeError with a different error
When I add the missing attribute to the
ClassWithMethodsclassclass ClassWithMethods(object): method_athe terminal shows NameError because there is no definition for
method_aI define
method_aas an attribute by pointing it to Noneclass ClassWithMethods(object): method_a = Nonethe terminal shows TypeError since
method_ais None which is not callableI change the definition of
method_ato make it a function which makes it callableclass ClassWithMethods(object): def method_a(): return Nonethe terminal shows AssertionError. Progress!
I then change the value that
method_areturns to match the expectation of the testdef 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_methodsfor fundef 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
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
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?