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
TitleCase
for 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_defining_classes_w_pass¶
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_defining_classes_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
# Exceptions Encountered # AssertionError # ModuleNotFoundError
I make Python module called
classes.py
and the terminal shows AttributeError which I add to the list of Exceptions encountered# Exceptions Encountered # AssertionError # ModuleNotFoundError # AttributeError
I then add the name
ClassWithPass
to the moduleClassWithPass
and the terminal shows NameError because
ClassWithPass
is not defined anywhereI add the error to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # ModuleNotFoundError # AttributeError # NameError
I point the name to None
ClassWithPass = None
and then redefine the variable as a class using thePython 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
# 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
and the terminal shows passing tests
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 an instance of the second inputthe test
self.assertIsInstance(classes.ClassWithPass(), object)
checks ifClassWithPass
is 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_defining_classes_w_parentheses¶
red: make it fail¶
I add another test to TestClasses
in test_classes.py
to show another way to make a class
def test_defining_classes_w_parentheses(self):
self.assertIsInstance(classes.ClassWithParentheses(), object)
the terminal shows AttributeError
green: make it pass¶
I add a class definition like
ClassWithPass
toclasses.py
class ClassWithParentheses: pass
the terminal shows passing tests
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 parenthesesI can define
classes
without parenthesespass is a placeholder
test_defining_classes_w_object¶
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_defining_classes_w_object(self):
self.assertIsInstance(classes.ClassWithObject(), object)
and 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_classes_w_attributes¶
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
inclasses.py
def test_classes_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
and the terminal shows a ` <https://docs.python.org/3/library/exceptions.html?highlight=exceptions#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
and 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_classes_w_attributes
def test_classes_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
and the terminal shows all tests pass
test_classes_w_methods¶
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_classes_w_methods(self):
self.assertEqual(
classes.ClassWithMethods.method_a(),
'You called MethodA'
)
and 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
classclass 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 Noneclass ClassWithMethods(object): method_a = None
the terminal shows TypeError since
method_a
is None which is not callableI change the definition of
method_a
to make it a function which makes it callableclass ClassWithMethods(object): def method_a(): return None
and the terminal shows AssertionError. Progress!
I then change the value that
method_a
returns to match the expectation of the testdef method_a(): return 'You called MethodA'
and the test passes
refactor: make it better¶
I can “make this better” by adding a few more tests to
test_classes_w_methods
for fundef test_classes_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_attributes_and_methods_of_classes¶
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_attributes_and_methods_of_classes(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_classes_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_classes_w_initializers(self):
self.assertEqual(classes.Boy().sex, 'M')
the terminal shows AttributeError
green: make it pass¶
I add a definition for the
Boy
classclass Boy(object): pass
the terminal shows another AttributeError
I make the
Boy
class with an attribute calledsex
class Boy(object): sex
the terminal produces NameError
I add a definition for the
sex
attributeclass Boy(object): sex = 'M'
the terminal shows passing tests
refactor: make it better¶
I add another assertion to
test_classes_w_initializers
this time for aGirl
class but with a difference, I provide the value for thesex
attribute when I call the classdef test_classes_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 theGirl
class toclasses.py
class Girl(object): sex = 'M'
and the terminal shows TypeError
TypeError: Girl() takes no arguments
I add the initializer method called
__init__
to theGirl
classclass Girl(object): sex = 'F' def __init__(self): pass
and the terminal shows TypeError
TypeError: __init__() got an unexpected keyword argument 'sex'
I make the signature of the
__init__
method take a keyword argumentdef __init__(self, sex=None): pass
and the terminal shows passing tests
I add another assertion
def test_classes_w_initializers(self): self.assertEqual(classes.Boy().sex, 'M') self.assertEqual(classes.Girl(sex='F').sex, 'F') self.assertEqual(classes.Other(sex='?').sex, '?')
and the terminal shows AttributeError
I add a class definition to
classes.py
class Other(object): sex = '?' def __init__(self, sex=None): pass
the terminal shows passing tests
Wait a minute, I just repeated the same thing twice.
I am going to make it a third repetition by redefining the
Boy
class to match theGirl
andOther
class 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
Human
toclasses.py
before the definition forBoy
with 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
Boy
to inherit from theHuman
class and all tests are still passingclass Boy(Human): sex = 'M' def __init__(self, sex=None): pass
I remove the
sex
attribute from theBoy
class 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
Girl
class and change its definition to inherit from theHuman
class?class Girl(Human): sex = 'F' def __init__(self): pass
I remove the
sex
attribute and the terminal shows AssertionErrorI make the
Human
class to set thesex
attribute 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 theGirl
classclass Girl(Human): pass
the terminal shows passing tests. Lovely
I wonder if I can do the same with the
Other
class? I change the definition to inherit from theHuman
classclass Other(Human): pass
the terminal shows passing tests
One More Thing! I remove the
sex
attribute from theHuman
classclass 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 themthe
Boy
,Girl
andOther
class now inherit from theHuman
class which means they all get the same methods and attributes that theHuman
class has, including the__init__
methodself.sex
in each class is thesex
attribute in the class, allowing its definition from inside the__init__
methodsince
self.sex
is defined as a class attribute, it is accessible from outside the class as I do in the tests i.eclasses.Girl(sex='F').sex
andclasses.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
instance
of an object
Zen of Python (PEP 20)
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