Or a class about classes!
Object-oriented programming is about using classes to define data abstraction.
A strong abstraction should make use of encapsulation, modularity, and inheritance.
class Simple(object):
pass
Actually it can be simpler
class Simple:
pass
# %load ../code/image.py
class Image(object):
"""Base class for images."""
def __init__(self, height, width):
self.height, self.width = height, width
def dimensions(self):
return (self.height, self.width)
When a new instance of a class is created, the __init__
method is called.
image = Image(100, 100)
image.dimensions()
(100, 100)
The self
is the first argument to all class methods. self
is similar to this
in C++ or Java. Unlike this
in C++ or Java, self
is not a keyword only a strong convention. It represents the current class instance. It is automatically passed when the object calls the method.
While can self
be used to store information about the current instance, you can also use class level attributes to maintain information about all class instances. To do this we can use the __class__
attribute which is an attribute on all class instances.
# %load ../code/counter.py
class Counter(object):
count = 0
def __init__(self):
self.__class__.count += 1
first = Counter()
first.count
1
second = Counter()
second.count
2
Part of encapsulation is being able to keep information hidden. To mark a method as private you prefix the name with a double underscore. When this is done you can only call the method from inside the class.
# %load ../code/private.py
class Hidden(object):
def __init__(self, data):
self.data = data
self.__print_data()
def __print_data(self):
print(self.data)
example = Hidden(4)
4
# Will raise an error
example.__print_data()
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-32-3c699316bc1e> in <module>() 1 # Will raise an error ----> 2 example.__print_data() AttributeError: 'Hidden' object has no attribute '__print_data'
Classes do not always have to extend from object
. They can extend from one or more base classes. Doing this we can extend the functionality provided by the base class.
The super
function delegates method calls to a parent or sibling class of type. This is useful for accessing inherited methods that have been overridden in a class.
class SquareImage(Image):
def __init__(self, side):
super().__init__(side, side)
square = SquareImage(20)
square.dimensions()
(20, 20)
You can also extend built in Python types such as int
, float
, dict
, list
.
Possible extensions:
__init__
is called after the object is constructed. For immutable types (int
, string
, tuple
) it is too late to modify the value. If you want to change how they are created you need to override the class constructor __new__
.
# %load ../code/newstring.py
class LanguageString(str):
def __new__(cls, value, lang='en'):
obj = str.__new__(cls, value)
obj.lang = lang
return obj
english_string = LanguageString('Hello')
spanish_string = LanguageString('Hola', lang='sp')
__new__
vs __init__
¶__new__
takes the class (cls
) as its first argument. If it returns a new instance of the class then __init__
is called passing the new instance and the rest of the arguments. If __new__
doesn't return a new instance then __init__
will not be called.
If you are extending object or a subclass of object will typically not need to change this method.
One use of overriding __new__
for a mutable type is the Singleton pattern.
The Singleton pattern is a common programming pattern for having a class which only allows for a single instance.
# %load ../code/singleton.py
class Singleton(object):
__instance = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = object.__new__(cls, *args, **kwargs)
return cls.__instance
class ExampleSingleton(Singleton):
pass
x = ExampleSingleton()
y = ExampleSingleton()
x is y
True
Some critics of the Singleton pattern have noted that it is often used in cases where you don't really care about object identity but really all you care about is shared state (such as global configuration).
# %load ../code/borg.py
class Borg(object):
_state = {}
def __new__(cls, *args, **kwargs):
self = object.__new__(cls, *args, **kwargs)
# override instance namespace with shared state
self.__dict__ = cls._state
return self
config1 = Borg()
config1.debug = True
config2 = Borg()
config2.debug
True
config1 is config2
False
Python supports multiple inheritance.
Multiple inheritance can cause some ambiguity in what version of the method will be called. In Python methods are resolved depth-first from left to right in the defined parent classes. This means the order of base classes matter in the class definition.
class TypeA(object):
def name(self):
print('Type A')
class TypeB(object):
def name(self):
print('Type B')
class TypeC(TypeA, TypeB):
pass
class TypeD(TypeB, TypeA):
pass
c = TypeC()
c.name()
c.__class__.__mro__
Type A
(__main__.TypeC, __main__.TypeA, __main__.TypeB, object)
d = TypeD()
d.name()
d.__class__.__mro__
Type B
(__main__.TypeD, __main__.TypeB, __main__.TypeA, object)
Mixins are a sytle of using multiple inheritance. Each mixin class adds a small and specific piece of functionality.
This would be similar to interfaces in Java or C#.
A great example comes right from the Python standard library: SocketServer.py
class ForkingUDPServer(ForkingMixIn, UDPServer): pass
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
Comparison methods __lt__
, __le__
, __eq__
, __ne__
, __gt__
, and __ge__
can be used to define object comparisons. They should return either True
or False
but can return any value which will be converted to a bool.
class Student(object):
def __init__(self, name, grade):
self.name, self.grade = name, grade
def __lt__(self, other):
return self.name < other.name
def __gt__(self, other):
return self.grade > other.grade
sally = Student('Sally', 90)
susan = Student('Susan', 90)
sally < susan
True
sally > susan
False
sally >= susan
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-52-ada2b62cc324> in <module>() ----> 1 sally >= susan TypeError: '>=' not supported between instances of 'Student' and 'Student'
If you define all of these comparisons then you can potentially create a non-well defined set of ordering. To simplify the creation a well ordered type you can use functools.total_ordering
. In that case you only need to define one of __lt__()
, __le__()
, __gt__()
, or __ge__()
along with __eq__
. The rest of the comparisons will be derived from those.
from functools import total_ordering
@total_ordering
class Student(object):
def __init__(self, name, grade):
self.name, self.grade = name, grade
def __lt__(self, other):
return self.grade < other.grade
def __eq__(self, other):
return self.grade == other.grade
sally = Student('Sally', 90)
susan = Student('Susan', 80)
sally >= susan
True
students = [sally, susan]
students.sort() # Ascending sort is the default
[s.name for s in students]
['Susan', 'Sally']
students.sort(reverse=True) # Now desending sort
[s.name for s in students]
['Sally', 'Susan']
You can define common operators by defining __add__
, __sub__
, __mul__
, __floordiv__
, __mod__
, __pow__
, __lshift__
, __rshift__
, __and__
, __or__
, and __xor__
.
class Media(object):
def __init__(self, css=None, js=None):
self.css = set(css or [])
self.js = set(js or [])
def __add__(self, other):
css = self.css | other.css
js = self.js | other.js
return Media(css=css, js=js)
base = Media(css=['base.css'])
forms = Media(css=['base.css', 'forms.css'], js=['forms.js'])
new = base + forms
new.js
{'forms.js'}
new.css
{'base.css', 'forms.css'}
Using the file system and handling problems.