yona
yona

Reputation: 434

Python - Override parent class attribute without instantiation

How do I go about overriding a parent class attribute in a child class without using object instances of either class? Coming from the world of Java/C++ and their strict structural designs, I'm finding myself challenged by Python's way of doing things. I'd like to stay relatively static.

Example:

from urllib.parse import urljoin

class base:
    host = "/host/"
    path = "Override this in child classes"
    url = urljoin(host, path)

class config(base):
    path = "config"

    @classmethod
    def print_url(cls):
        print(cls.url) # Currently prints "/host/Override this in child classes"
                       # Would like to print "/host/config" instead

class log(base):
    path = "log"

    @classmethod
    def print_url(cls):
        print(cls.url) # Currently prints "/host/Override this in child classes"
                       # Would like to print "/host/log" instead

Desired Usage:

>>> config.print_url()
/host/config

>>> log.print_url()
/host/log

I'd like the config.path and log.path attributes to override base.path. This is so I can use url = urljoin(host, path) once and for all in the base class (and avoid having to copy/paste that same attribute/calculation in every single derived class).

I can't figure out how to accomplish this without constructing objects (which I'm hoping to avoid). Anyone have any advice? Thanks in advance!

Upvotes: 4

Views: 3017

Answers (1)

Mad Physicist
Mad Physicist

Reputation: 114518

The child path attributes do override base.path. What you don't override is the url attribute. That gets computed once when the body of base is run to create the class object.

You have a couple of options going forward. Either way you need to make url compute dynamically, either every time it's accessed, or at least once per child class.

The simplest way is to make url into a classmethod:

class base:
    host = "/host/"
    path = "Override this in child classes"

    @classmethod
    def url(cls):
        return urljoin(cls.host, cls.path)

    @classmethod
    def print_url(cls):
        print(cls.url())

class config(base):
    path = "config"

class log(base):
    path = "log"

Notice that you're referring to the actual classe's host and path on the fly now. You also only need one print_url method in base instead of a different one in each class.

Another option is to give base, and therefore all its children, a metaclass with url as a property:

class url_meta(type):
    @property
    def url(cls):
        return urljoin(cls.host, cls.path)

class base(metaclass=url_meta):
    host = "/host/"
    path = "Override this in child classes"

    @classmethod
    def print_url(cls):
        print(cls.url)

class config(base):
    path = "config"

class log(base):
    path = "log"

This works because python classes are objects too. You can define a property in the class of a class (the metaclass), and it will behave as any property does with respect to an instance. Just this time the instance is a class itself.

A third option is to make sure that url is defined statically but correctly in each child. The __init_subclass__ method allows you to do that very conveniently directly from base:

class base:
    host = "/host/"
    path = "Override this in child classes"
    url = urljoin(host, path)

    @classmethod
    def __init_subclass__(cls):
        cls.url = urljoin(cls.host, cls.path)

    @classmethod
    def print_url(cls):
        print(cls.url)

class config(base):
    path = "config"

class log(base):
    path = "log"

You can accomplish the same thing with a metaclass as well:

class url_meta2(type):
    def __init__(cls, *args, **kwargs):
        cls.url = urljoin(cls.host, cls.path)

class base(metaclass=url_meta2):
    host = "/host/"
    path = "Override this in child classes"

    @classmethod
    def print_url(cls):
        print(cls.url)

class config(base):
    path = "config"

class log(base):
    path = "log"

Upvotes: 4

Related Questions