Is it possible to deconstruct `self` when using class methods in Python?

Viewed 353

I want to clean up my code so it uses less self.attribute instances and is easier to read.

Example of regular functionality:

class Car:
    brand = None
    color = None
    model = None

    def __init__(self, color, brand, model):
        self.brand = brand
        self.color = color
        self.model = model
    
    def print_car(self):
        print(f"This is a {self.color} {self.brand} manufactured in {self.model}"
car_a = Car(color='red', brand='audi', model=2001)
car_a.print_car()
# output: This is a red audi manufactured in 2001

How I would like to change it:

I would like to deconstruct the self argument on print_car so it would be a bit cleaner, but achieve the same functionality.something like this:

def print_car({color, brand, model}):
    print(f"This is a {color} {brand} manufactured in {model}"

Is that possible? Common? It's mainly just cleaner code but maybe it's not a good approach?

1 Answers

You can use the builtin function vars to get the attributes of most instances as a dictionary. This allows to invoke callables with the attributes, e.g. by using **-unpacking to use them as keyword arguments:

class Car:
    def __init__(self, color=None, brand=None, model=None):
        self.brand = brand
        self.color = color
        self.model = model

    def print_car(self):
        print(
           "This is a {color} {brand} manufactured in {model}".format(
               **vars(self)  # get and unpack attributes to keyword arguments
        ))

This is not common usage in methods, though it does have its uses. Classes have many capabilities that conflict with explicit destructuring – most importantly subclassing and properties making attributes not strictly defined.

However, classes solely intended to hold a specific set of attributes can be safely destructured. For example, dataclasses directly support converting instances to mappings and sequences. This allows to destructure attributes by name or order.


If destructuring is needed often, it can be automatised using decorators. A simple decorator to destructure the first argument looks like this:

def destruct(method):
    """Mark a method to destructure ``self`` into its arguments"""
    @wraps(method)
    def wrapper(self, **kwargs):
        return method(**vars(self), **kwargs)
    return wrapper

It can be applied like other method decorators, e.g. classmethod, directly in the class definition. The method then takes each attribute as a separate argument, instead of the single self argument.

class Car:
    def __init__(self, color=None, brand=None, model=None):
        self.brand = brand
        self.color = color
        self.model = model

    @destruct
    def print_car(color, brand, model):
        print(f"This is a {color} {brand} manufactured in {model}")


Car(color='red', brand='audi', model=2001).print_car()
# This is a red audi manufactured in 2001

Note that destruct is a demonstrator. For example, vars does not work for classes using __slots__ – some fallbacks can be used if needed or inspecting the class' __annotations__. Also, the method does not support positional arguments anymore – inspecting the signature or otherwise normalising arguments would allow this.

Related