5 Classes and Inhertiance
Last edited
37 Compose classes instead of nesting builtin-types
This is a walk through of how you go from using simple dicts to a full class for managing data.
start with a
dictfor key/valuesWhen to move on: - once you have a dict in a dictcollections.namedtuple- you get to name attributes thus stablizing the API for a future class
- no types
- no default attributes
- no mutables
- cant control init or repr
When to move on: - once you need types or default attributestyping.NamedTuple- default attributes
- types
- some of the same issues as above
When to move on: - once you need the missing parts of tuples resource: https://peps.python.org/pep-0557/#why-not-just-use-namedtuple@dataclassWhen to move on: - you want real control __init__ at construction time (__post_init__ only patches the tail end). - args that don't map 1:1 to storage. - or the class is more behavior than data so dataclasses' default __eq__ is wrong/irrelevant.class
38 Accept functions instead of classes for simple interfaces
Question
What does
defdo?
Answer
defcreates a function object, storing the compiled body in code. The object is callable because its type(types.FunctionType)defines call, which inturn calls__code__So if want to make something that isn’t a function ‘callable’ we just need to meet the interface of
__call__
We all know you can pass a generic function into a function for it to be run.
The point of this is you can pass in a class AND STORE STATE.
class BetterCountMissing:
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
counter = BetterCountMissing()
assert counter() == 0
assert callable(counter)counter = BetterCountMissing()
result = defaultdict(counter, current) # Relies on __call__
for key, amount in increments:
result[key] += amount
assert counter.added == 2So we emulated a function to meet the interface and snuck in our state tracking.
40 The purpose of super
The main takeaway: always use super() when calling a parent class, rather than the name directly.
The problem
The old way to initialize a parent class from a child:
Note
This example outlines the issue of not using
super()and how fragile calling parent’s__init__is.
class MyBaseClass:
def __init__(self, value):
self.value = value
class TimesSeven(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value *= 7
class PlusNine(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value += 9
class ThisWay(TimesSeven, PlusNine):
def __init__(self, value):
TimesSeven.__init__(self, value)
PlusNine.__init__(self, value)
foo = ThisWay(5)
print('Should be (5 * 7) + 9 = 44 but is', foo.value) # 14PlusNine.__init__ calls MyBaseClass.__init__ a second time, resetting value back to 5. So TimesSeven’s *7 is wiped out and you get 5 + 9 = 14.

The super function
You should instead use super(), as it allows python to MRO (method resolution order)
to ensure methods are called in the correct order
class MyBaseClass:
def __init__(self, value):
self.value = value
class TimesSevenCorrect(MyBaseClass):
def __init__(self, value):
super().__init__(value)
self.value *= 7
class PlusNineCorrect(MyBaseClass):
def __init__(self, value):
super().__init__(value)
self.value += 9
class GoodWay(TimesSevenCorrect, PlusNineCorrect):
def __init__(self, value):
super().__init__(value)
foo = GoodWay(5)
print('Should be 7 * (5 + 9) = 98 and is', foo.value) # 98Calling specific parents
You can still refer to specific parents, but make sure to use super(Parent)
Note
This is rare and I have no idea a use case, but basically always use super, and pass in a specific parent if you need to for MRO safety.
class MyBaseClass:
def __init__(self, value):
self.value = value
def describe(self):
return f"value={self.value}"
# ... [snip same as above]
class GoodWay(TimesSevenCorrect, PlusNineCorrect):
def __init__(self, value):
super().__init__(value) # normal: MRO drives init
def describe(self):
base = super(PlusNineCorrect, self).describe() # call a specific implementation
return f"GoodWay({base})"
foo = GoodWay(5)
print(foo.value) # 98 (normal cooperative init, untouched)
print(foo.describe()) # GoodWay(value=98)41 Mix-in Classes instead of Multiple inhertance
40 showed how to use multiple inhertiance safely, although it’s better to avoid multiple inhertiance all together.
Definition
A mix-in is a class that defines only a small set of additional methods for its child classes to provide. Mix-in classes don’t define their own instance attributes nor require their
__init__constructor to be called.
Example
class ToDictMixin:
def to_dict(self):
return self._traverse_dict(self.__dict__)
def _traverse_dict(self, instance_dict)
... # implementation
class User(ToDictMixin):
def __init__(self, username: str):
self.username = username
user = User("mj")
user.to_dict()This could also be used to implement a sort of interface, where it gives you some methods while requiring the client provide some.
class CRUDMixin:
def _store(self):
raise NotImplementedError # child provides
def create(self, **data):
# ideally this would do some heavy lifting to justify it
self._store().update(data)
return data
class Book(CRUDMixin):
_books = {}
def _store(self):
return self._books
Book().create(id=1, title="Dune")42 Prefer public attributes over private
Lean towards using protected attributes when you don’t want an attribute to be accessed not private, one day someone may have a reason to access that attribute, don’t make them use jank to access it.
The only time you should use private is to avoid naming conflicts with child classes.
__fooprivate - name mangled to_ClassName__foodiscourages access and prvents subclass clashes but not enforced, reachable by the mangled name_fooprotected - you can use these but proceed with cautionfoopublic - you can use these
43 Inherit from collections.abc for Custom Container Types
Much of programming in Python is defining classes that contain data and describing how such objects relate to each other. Every Python class is a container of some kind, encapsulating attributes and functionality together.
Use collections.abc’s base classes to help you implement interfaces, they’ll do the heavy lifting and tell what you must provide to meet their requirements.
Example, I want to make my own sequence, I could do guess work and try to implement all the dunder methods, or I could have a friendly abstract base class from collections tell what I need.
Definition
Interfaces define methods that are typically abstract, which means that the interface declares them, but doesn’t implement them.
from collections.abc import Sequence
class BadType(Sequence):
pass
foo = BadType()
# Traceback ...
# TypeError: Can't instantiate abstract class BadType with
# abstract methods __getitem__, __len__
#################################################
# Now that I know what I need I can implement it.
class MySequence(Sequence):
def __init__(self, members):
self._members = members
def __getitem__(self, index):
return self._members[index]
def __len__(self):
return len(self._members)
foo = MySequence([10, 5, 6, 7, 15, 11])
print('Index of 7 is', foo.index(7)) # free from Sequence
print('Count of 10 is', foo.count(10)) # free from Sequence
print('11 in seq', 11 in foo) # free (__contains__)
print('As list:', list(foo)) # free (__iter__)
>>>
# Index of 7 is 3
# Count of 10 is 1
# 11 in seq True
# As list: [10, 5, 6, 7, 15, 11]
We are all consenting adults here. We don’t need the language to prevent us from doing what we want to do. It’s our individual choice to extend functionality as we wish and to take responsibility for the consequences of such a risk