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
requirements¶
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
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 encountered in
test_classes.py# Exceptions Encountered # AssertionError # ModuleNotFoundError
I make Python module called
classes.pythe terminal shows AttributeError which I add to the list of Exceptions encountered intest_classes.py# Exceptions Encountered # AssertionError # ModuleNotFoundError # AttributeError
I then add the name
ClassWithPassto the moduleClassWithPassthe terminal shows NameError because
ClassWithPassis not defined anywhereI add the error to the list of Exceptions encountered in
test_classes.py# Exceptions Encountered # 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 encountered in
test_classes.py# 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
the 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: 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
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): pass
the 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_boolean
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.pyclass ClassWithMethods(object): pass
the terminal now gives AttributeError with a different error
When I add the missing attribute to the
ClassWithMethodsclassclass ClassWithMethods(object): method_a
the 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 = None
the 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 None
the 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 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_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): pass
the terminal shows AttributeError
I make the
Boyclass with an attribute calledsexclass Boy(object): sex
I 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 arguments
I add the initializer method called
__init__to theGirlclassclass Girl(object): sex = 'F' def __init__(self): pass
TypeError: __init__() got an unexpected keyword argument 'sex'
I make the definition of the
__init__method take a keyword argumentdef __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.pyclass Other(object): sex = '?' def __init__(self, sex=None): pass
the 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): 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
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'): pass
the 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): pass
I remove the
sexattribute from theBoyclass and the tests continue to passI remove the
__init__method, then add the pass placeholderclass Boy(Human): pass
all 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): pass
I 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 = sex
the terminal still shows AssertionError
when I remove the
__init__method from theGirlclassclass Girl(Human): pass
the test passes. Lovely
I wonder if I can do the same with the
Otherclass? I change the definition to inherit from theHumanclassclass Other(Human): pass
the test passes
One More Thing! I remove the
sexattribute from theHumanclassclass Human(object): def __init__(self, sex='M'): self.sex = sex
all 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
Click Here to see the code from this chapter