Object Orientation & Update Anomalies#

In our latest micro-training, “Good→Better→Best Python,” we’re talking about object orientation and approaches people take when using it in Python.

(If you’re not already signed up for “Good→Better→Best Python,” it’s not too late! You can join our next workshop on Friday, December 16th by registering here. If you purchase a ticket, we’ll bring you up to speed with a recording from the first workshop along with the notes/work problems to review.)

When discussing the design of large, object-oriented systems, people often ask about “composition” vs “inheritance.” I had a conversation with James last week about this in preparation for the materials, and here are some quick notes we took about one interesting aspect. (If you find this snippet helpful, let us know at learning@dutc.io.)

Update Anomalies & Inheritance vs Composition vs Other#

In languages with strict (nominal) type systems, inheritance can be a mechanism for “subtype” polymorphism, but this isn’t generally relevant to Python code (ignoring MyPy and PEP-484 type hinting).

In Python, inheritance vs composition are typically about code re-use and “normalization” (i.e., the elimination of “update” anomalies in the code development process).

Python is very dynamic: for example, class definition occurs at runtime. As a result, there are options available to us other than inheritance and composition. Additionally, mechanisms like isinstance can be implemented via __instancecheck__ with minor limitations.

Note that “update anomalies” (i.e., a change in the code in one place is not reflected in all relevant places) typically do not occur at runtime.

Inheritance (incl. Mixins)#

With inheritance, you automatically receive new or remove old functionality when the base type changes. It provides all base type functionality in a flattened namespace (indistinguishable from derived type functionality).

On-conflict, it resolves to the functionality available in the most-derived type, and it prevents resolution to base type functionality, except from within the derived type’s methods via super().

Finally, it has a language flaw in it that prevents resolution of certain functionality from within the derived type (e.g., super() does not properly delegate __set__).

class Base:
    def f(self):
        return 1

class Derived(Base):
    def g(self):
        return 20

obj = Derived()
assert obj.f() == 1
assert obj.g() == 20

# changes automatically “pushed” to derived type
Base.f = lambda _: 300
Base.h = lambda _: 4_000
assert obj.f() == 300
assert obj.h() == 4_000
del Base.f
assert not hasattr(obj, 'f')

# manual resolution of conflict
Base.g = lambda _: 50_000
assert obj.g.__func__ is not Base.g and obj.g() != 50_000

Derived.g = lambda self: 20 + super(Derived, self).g()
assert obj.g.__func__ is Derived.g and obj.g() == 50_020

Composition#

With composition, on the other hand, the author needs to manually implement new or manually remove old functionality when the base type changes. Base type functionality can be provided in a namespace (distinguishable from the derived type functionality). Further, the author manually resolves all conflicts; users can resolve to base type functionality by accessing composed objects.

class Base:
    def f(self):
        return 1

class Composed:
    def __init__(self):
        self.base = Base()
    def f(self):
        return self.base.f() + 1
    def g(self):
        return 20

obj = Composed()
assert obj.f() == 2

# additions/removes must be manually “pulled” into composed type
# updates are automatically “pushed”
Base.h = lambda _: 300
assert not hasattr(obj, 'h')

del Base.f
assert hasattr(obj, 'f') # but …
try: obj.f()
except AttributeError: pass
else: raise AssertionError('Base.f should not be available')

Base.f = lambda _: 1
assert obj.f() == 1 + 1
Base.f = lambda _: 1_000
assert obj.f() == 1_000 + 1

Object Construction#

Object construction is a broad category that includes mechanisms such as class decorators, runtime, and programmatic class definition (whether via type or via standard control flow). The author uses programmatic mechanisms with fine control over manual and automatic. While it’s not a very common approach, it may be combined with inheritance or composition.

def f(self):
    return 1
def g(self):
    return 20

class A:
    f = f

class B:
    f = f
    g = g

obj1, obj2 = A(), B()
assert obj1.f.__func__ is obj2.f.__func__ and obj1.f() == obj2.f() == 1
assert not hasattr(obj1, 'g') and hasattr(obj2, 'g') and obj2.g() == 20

Or even…

# rough sketch
class ConstructedMeta(type):
    def __instancecheck__(self, instance):
        return True
    @staticmethod
    def __call__(methods):
        def dec(cls):
            for k, v in methods['via assignment'].items():
                setattr(cls, k, v)
            for k in methods['via dispatch']:
                setattr(cls, k, lambda *a, **kw: methods['via dispatch'][k](*a, **kw))
            subcls = type('sub', (), methods['via inheritance'])
            return type(cls.__name__, (subcls, *cls.__bases__), {**cls.__dict__})
        return dec
class Constructed(metaclass=ConstructedMeta): pass

methods = {
    'via assignment': {
        'f': lambda _: 1,
    },
    'via dispatch': {
        'g': lambda _: 20,
    },
    'via inheritance': {
        'h': lambda _: 300,
    },
}

@Constructed(methods)
class T:
    pass

obj = T()
assert obj.f() == 1
assert obj.g() == 20
assert obj.h() == 300
assert isinstance(T, Constructed)

# no change
methods['via assignment']['f'] = lambda _: 10
methods['via assignment']['f2'] = lambda _: 10
assert obj.f() == 1 and not hasattr(obj, 'f2')

# some changes
methods['via dispatch']['g'] = lambda _: 200
methods['via dispatch']['g2'] = lambda _: 200
assert obj.g() == 200 and not hasattr(obj, 'g2')

# all changes
type(obj).__bases__[0].h = lambda _: 3_000
type(obj).__bases__[0].h2 = lambda _: 3_000
assert obj.h() == 3_000 and obj.h2() == 3_000

Wrap Up#

All in all, a pretty interesting conversation with James about composition vs inheritance.

I can’t wait to share more on this topic during the hands-on portions of the micro-training sessions. I hope to see you there!

Talk you next week!