Knowledge is the Only Good
  • About

On this page

  • The Empty Class
  • class B derived from class A
    • Super and subclasses
  • More about the `super function
    • Basic Usage
    • Multiple Inheritance
    • super() in Multiple Inheritance
    • super() with Arguments
    • Summary
  • Dynamically extending
    • Key Points:
    • Considerations:
  • __getattribute__ vs __getattr__
    • __getattribute__
    • __getattr__
    • Key Differences

Effective Python: Classes

programming
Python
effective python
llm
Object oriented programming in Python
Author

Stephen J. Mildenhall

Published

2024-02-13

The Empty Class

You can create a class as follows and use it like a dictionary.


pseudo_class = type('typeName', (), {})

pseudo_class.x = 10
pseudo_class.y = 'something else'
pseudo_class.x = range(10)

print(pseudo_class, pseudo_class.x)

class B derived from class A

To call the constructor of class A from the constructor of class B, you use the super() function. Here’s how you can do it:

class A():
    def __init__(self, x):
        self.x = x

class B(A):
    def __init__(self, x, y):
        super().__init__(x)  # This calls the constructor of class A with the argument x
        self.y = y

The super().__init__(x) call within B’s constructor invokes the constructor of the superclass A, passing x as the argument. This way, A’s constructor is responsible for handling the x attribute, while B’s constructor handles the additional attributes or operations specific to B, such as setting the y attribute in this case.

Super and subclasses

In the context of inheritance in object-oriented programming:

  • A is referred to as the superclass (or base class, parent class), because it is the class from which another class (the subclass) inherits.
  • B, which derives from A, is called the subclass (or derived class, child class) because it inherits from the superclass.

The subclass extends or modifies the behavior defined in the superclass. This mechanism allows for code reuse and polymorphism, making it easier to maintain and extend the functionality of your software.

It’s understandable that the terminology might feel counterintuitive at first! The terms “superclass” and “subclass” can indeed seem a bit abstract or even upside down, especially when you’re starting to grasp the concepts of inheritance and object-oriented programming. Here’s a way to think about it that might help:

  • Superclass (Base Class, Parent Class): Think of the superclass as the “foundation” or “base” upon which subclasses are built. It’s “super” in the sense that it provides the basic attributes and methods from which other classes can inherit. It’s a general, broader classification.

  • Subclass (Derived Class, Child Class): The subclass is considered “below” the superclass in the hierarchy, not in importance, but because it specializes or extends the superclass. It inherits from the superclass, adding more specific features or overriding existing ones to differentiate itself. It’s a more specific, detailed classification built on top of the superclass.

In a family tree analogy, the superclass is like a parent, while the subclass is like a child. The child inherits traits from the parent but can also have its unique characteristics or override inherited traits with new ones.

Despite the initial confusion, these terms are widely used in object-oriented programming to describe relationships between classes in a hierarchy, emphasizing the direction of inheritance from the more general to the more specific.

More about the `super function

The super() function in Python is used to give access to methods in a superclass from a subclass that inherits from it. It’s particularly powerful and somewhat complex when dealing with multiple inheritance, where a class is derived from more than one base class.

Basic Usage

In a single inheritance scenario, super() is straightforward: it allows you to call methods of the superclass in your subclass. The common use case is to extend the functionality of the superclass method in the subclass by either calling the superclass method before executing additional logic in the subclass method or doing so afterward.

Multiple Inheritance

Python supports multiple inheritance, where a class can inherit from more than one class. This introduces complexity, especially in how super() behaves. Python’s method resolution order (MRO) comes into play in these scenarios.

Method Resolution Order (MRO)

Python uses the C3 linearization algorithm to establish an MRO in multiple inheritance scenarios. The MRO determines the order in which base classes are searched when executing a method. You can view the MRO of a class using the .__mro__ attribute or the mro() method.

Example with Multiple Inheritance

Consider the following classes:

class A:
    def __init__(self):
        print("A", end=" ")
        super().__init__()

class B(A):
    def __init__(self):
        print("B", end=" ")
        super().__init__()

class C(A):
    def __init__(self):
        print("C", end=" ")
        super().__init__()

class D(B, C):
    def __init__(self):
        print("D", end=" ")
        super().__init__()

In this case, D is derived from both B and C. If you instantiate D, the output will illustrate the MRO:

d = D()

Output might be: D B C A

This output is the result of the C3 linearization that Python uses to resolve the order in which methods should be called. You can check the MRO by:

print(D.mro())

Note it is a function of the class, not an instance.

super() in Multiple Inheritance

When you call super() in a class that’s part of a multiple inheritance hierarchy, Python follows the MRO to determine which superclass method to invoke. This ensures that each method in the hierarchy is called in a specific, predictable order, avoiding the problem of a method being called more than once.

super() with Arguments

In Python 3, super() called without arguments is equivalent to super(ThisClass, self), automatically passing the class and instance to super(). In complex inheritance hierarchies, super() works with the MRO to ensure the correct superclass method is called next.

Summary

  • super() is essential for accessing superclass methods in a subclass.
  • In multiple inheritance scenarios, the MRO determines the order in which superclass methods are called.
  • Python’s use of the C3 linearization algorithm ensures a consistent and predictable MRO.
  • Understanding super() and MRO is crucial for correctly implementing and extending methods in complex inheritance hierarchies.

Dynamically extending

Here is a way to dynamically extend an object’s functionality in a flexible manner, without permanently adding more methods to the base class (A) and while maintaining the ability to call A’s methods directly from an instance of B as if they were its own. This scenario calls for a design pattern that allows for such flexibility, typically achieved through composition or the use of a proxy pattern.

One approach is the Proxy Pattern, specifically a variation known as the Delegation Pattern. Instead of inheriting from class A, class B holds an instance of A and delegates calls to it. You can automate delegation in Python using the __getattr__ method to dynamically forward method calls to the A instance.

A delegation pattern is implemented like so:

class A:
    def __init__(self, x):
        self.x = x

    def method_a(self):
        print(f"Method in A, x = {self.x}")

class B:
    def __init__(self, a_instance, y):
        self._a_instance = a_instance
        self.y = y

    def __getattr__(self, name):
        """Delegate attribute access to the A instance if attribute not found in B."""
        return getattr(self._a_instance, name)

    # B's specific methods can be defined here
    def method_b(self):
        print(f"Method in B, y = {self.y}")

# Usage
a = A("Hello from A")
b = B(a, "Hello from B")

b.method_a()  # This will call A's method_a through B
b.method_b()  # This will directly call B's method_b

In this implementation, B does not inherit from A. Instead, it “wraps” an instance of A. When you try to access an attribute or method on an instance of B that doesn’t exist in B, Python’s __getattr__ method is called. Here, __getattr__ is used to delegate those calls to the wrapped A instance. This allows you to call methods of A directly on an instance of B without the need for B to explicitly define or know about those methods.

Key Points:

  • Flexibility: This approach allows B to extend A dynamically, without hardcoding method names or altering the structure of A.
  • Maintainability: It keeps A’s implementation clean and focused, only adding functionality in B when necessary.
  • Transparency: To the user of class B, it appears as if B has all methods of A, providing a seamless interface.

Considerations:

  • Performance: Using __getattr__ introduces a slight overhead because of the dynamic method resolution.
  • Debugging: Debugging can be slightly more complex due to the indirection added by __getattr__.
  • Method Overriding: If B needs to override a method from A and also call the original method from A, you’ll have to handle that explicitly in B.

This pattern provides a powerful way to dynamically extend objects in Python, accommodating scenarios where inheritance might be too rigid or when you want to avoid bloating base classes with methods that are only occasionally needed.

__getattribute__ vs __getattr__

The delegation pattern uses __getattr__. In Python, both __getattr__ and __getattribute__ methods exist, and they have subtly different behaviors and use cases when it comes to attribute access in classes.

__getattribute__

  • Always Called: __getattribute__ is called for every attempt to access an attribute, regardless of whether the attribute exists or not. It’s a part of the lookup chain for any attribute access on an object.
  • Override with Caution: Because __getattribute__ is called for every attribute access, overriding it requires careful handling to avoid infinite recursion. When you override __getattribute__, you typically use super().__getattribute__(name) within it to safely access attributes of the superclass without recursion.
  • Use Case: You might override __getattribute__ if you need to intercept every attribute access, which could be useful for debugging, logging, or implementing proxies where you need to handle all attribute accesses dynamically.

Example:

class A:
    def __init__(self):
        self.x = 'X'

    def __getattribute__(self, name):
        print(f"Accessing {name}")
        return super().__getattribute__(name)

a = A()
print(a.x)  # This will print "Accessing x" followed by "X"

__getattr__

  • Called as a Fallback: __getattr__ is only called if the attribute was not found by the usual means. It acts as a fallback method for attribute access, which makes it useful for catching attempts to access missing attributes without affecting normal attribute access.
  • Use Case: __getattr__ is suitable when you want to provide a default behavior for missing attributes, such as returning a default value, generating attributes on the fly, or forwarding attribute access to another object (as in delegation or proxy patterns).

Example:

class B:
    def __getattr__(self, name):
        return f"{name} does not exist"

b = B()
print(b.some_missing_attribute)  # This will print "some_missing_attribute does not exist"

This explains why the pattern uses __getattr__.

Key Differences

  • Invocation: __getattribute__ is called for every attribute access, making it very powerful but also risky if not handled correctly due to the potential for infinite recursion. __getattr__ is called only when an attribute is not found by the normal lookup process.
  • Purpose: __getattribute__ can be used to intercept all attribute accesses, which is useful for low-level control or proxying. __getattr__ is more suited for providing fallback behavior for missing attributes or for cases where attributes are to be generated dynamically.

Choosing between __getattribute__ and __getattr__ depends on your specific needs for attribute access and interception, and understanding their differences is key to using them effectively.

Written with help from Chat GPT.

Stephen J. Mildenhall. License: CC BY-SA 2.0.

 

Website made with Quarto