Effective Python #5 | Classes and Interfaces


Introduction

  • As an oop language, Python supports a full range of features, such as inheritance, polymorphism, and encapsulation

  • Python’s classes and inheritance make it easy to express a program’s intended behaviors with objects.

    • They allow you to improve and expand functionality over time.

    • They provide flexibility in an environment of changing requirements.



Item 37. Compose Classes Instead of Nesting Many Levels of Build-in Types

  1. Avoid making dictionaries with values that are dictionaries, long tuples, or complex nestings of other build-in types.

  2. Use namedtuple for lightweight, immutalbe data containers before you need the flexibility of a full class.

  3. Move your bookkeeping code to using multiple classes when your internal state dictionaries get complicated

# Before : nested build-in types in dictionary
class BySubjectGradeBook:
    def __init__(self):
        self._grades = {}

    def add_student(self, name):
        self._grades[name] = defaultdict(list)

    def report_grade(self, name, subject, grade):
        by_subject = self._grades[name]
        grade_list = by_subject[subject]
        grade_list.append(grade)

    def average_grade(self, name):
        by_subject = self._grades[name]
        total, count = 0, 0    
        for grades in by_subject.values():
            total += sum(grades)
            coutn += len(grades)
        return total/count
# After : Use multiple classes
class Subject:
    def __init__(self):
        self._grades = []

    def report_grades(self, score, weight):
        self._grades.append(Grade(score, weight))

    def average_grade(self):
        return total / total_weight # omitted details

class Student:
    def __init__(self):
        self._subjects = defaultdict(Subject)

    def get_subject(self, name):
        return self._subjects[name]

    def average_grade(self):
        return total / count # omitted details 

class Gradebook:
    def __init__(self):
        self._students = defaultdict(Student)

    def get_student(self, name):
        return self._students[name]


Item 39. Use @classmethod Polymorphism to Construct Object Generically

  1. Python only supports a single constructor per class: the __init__ method.

  2. Use @classmethod to define alternative constructors for your classes.

  3. Use class method polymorphism to provide generic ways to build and connect many concrete subclasses.



Item 40. Initialize Parent Classes with super

  1. Python’s standard method resolution order solves the problems of superclass initialization order and diamond inheritance

  2. Use the super built-in function with zero arguments to initialize parent classes

# Diamond inheritance
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) # This line resets the value to 5

# >> Should be (5 * 7) + 9 = 44 but is 14
# The call to the second parent class’s constructor, PlusNine.__init__, causes self.value to be reset back to 5 when MyBaseClass
# To solve these problems, Python has the super built-in function and standard method resolution order (MRO). 
# super ensures that common superclasses in diamond hierarchies are run only once
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

# Now, the top part of the diamond, MyBaseClass.__init__, is run only a single time. The other parent classes are run in the order specified in the class statement
class GoodWay(TimesSevenCorrect, PlusNineCorrect):
    def __init__(self, value):
        super().__init__(value)


Item41. Consider Composing Functionality with Mix-in Classes

  1. Avoid using multiple inheritance with instance attributes and __init__ if mix-in classes can achieve the same outcome.

  2. Use pluggable behaviors at the instance level to provide per-class customization when mix-in classes may require it.

  3. Mix-ins can include instance methods or class methods, depending on your needs.

  4. Compose mix-ins to create complex functionality from simple behaviors.

# Define an example mix-in that accomplishes 
class ToDictMixin:
    def to_dict(self):
        return self._traverse_dict(self.__dict__)

    def _traverse_dict(self, instance_dict):
        output = {}
        for k,v in instance_dict.items()
            output[key] = self._traverse(key, value)
        return output

    def _traverse(self, key, value):
        if isinstance(value, ToDictMixin):
            return value.to_dict()
        elif isinstance(value, dict):
            return self._traverse_dict(value)
        elif isinstance(value, list):
            return [self.traverse(key, i) for i in value]
        else:
            return value

# Define an example class that uses the mix-in to make a dictionary representation of a binary tree
class BinaryTree(ToDictMixin):
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

# Translating a large number of related Python objects into a dict becomes easy:
tree = BinaryTree(10,
    left = BinaryTree(7, right=BinaryTree(9)),
    right = BinaryTree(13, left=BinaryTree(11)))


Item42. Prefer Public Attributes Over Private Ones

  1. Private attributes aren’t regorously enforced by the Python complier

  2. Plan from the beginning to allow subclasses to do more with your internal APIs and attributes instead of choosing to lock them out.

  3. Use documentation of protected fields to guide subclasses instead of trying to force access control with private attributes

  4. Only consider using private attributes to avoid naming conflicts with subclasses that are out of your control

# In Python, there are only two types of visibility for a class’s attributes: public and private
Class MyObject:
    def __init__(self):
        self.public_field = 5
        self.__private_field = 10

    def get_private_field(self):
        return self.__privage_field

    # class methdos also have access to private attributes    
    @classmethod
    def get_private_field_of_instance(cls, instance):
        return instance.__private_field

Class MyChildObject(MyObject):
    def get_private_field(self):
        return self.__private_field


Item43. Inherit from collections.abc for Custom Container Types

  • Inherit dicectly from Python’s container types (like list or dict) for simple use cases

  • Beware of the large number of methods required to implement custom container types correctly

  • Have yout custom container types inherit from the interfaces defined in collections.abc to ensure that your classes match required interfaces and behaviors

# When you access a sequence item by index:
bar = [1,2,3]
bar[0] 
# above line will be interpreted as : 
bar.__getitem__(0)
# So, to make your own class act like a sequence, you can provide a custom implementation of __getitem__
class MyClass():
    def __init__(self, values):
        self.values = values

    def __getitem__(self, index):
        for i, item in enumerate(values):
            if i == index:
                return item
        raies IndexError(f'Inted {index} is out of range')
# But it's not enough to provide all of the sequence semantics you'd expect from a list instance
test = MyClass(values=[1,2,3])
len(test) # >> MyClass has no len()
# The built-in collections.abc module defines a set of abstract base classes that provide all of the typical methods for each container type
from collections.abc import Sequence
class BadType(Sequence):
    pass

foo = BadType()
>>>
Traceback ...
TypeError: Can't instantiate abstract class BadType with  ̄abstract methods __getitem__, __len__

# When you do implement all the methods required by an abstract base class from collections.abc, it provides all of the additional methods, like index, count, ... for free.