It is technically possible to do this, and can be quite useful under certain circumstances. That being said, this is something of an anti-pattern as has been stated in several other answers, and will break a bunch of stuff considered "standard" in Python, so you should probably prefer the Factory Design Pattern if you can.
So enjoy the following responsibly.
The easiest way to do this is to make use of the __init_subclass__
method, to register subclasses to the super-class when they are created (as shown in this answer), and then to dynamically change the instance class during the __init__
.
Dynamically changing classes is really confusing to someone who might be unfamiliar with your code, and will look like black magic, so you'll need to DOCUMENT IT PROPERLY. It also means that you are pretty limited in what you can do in any subclass' __init__
. You need to create a __post_init__
which can be specialized by subclass (analogous to the dataclasses.dataclass
structure).
class Person:
gender = 'unknown' # this is a class variable
'''the default gender for this class'''
_REGISTERED_GENDERS = {}
'''
a dictionary of all genders known to `Person`,
and the respective `Person` subclasses which
should be instantiated from these genders
'''
def __init__(self, name: str, gender: str = None):
'''
initialize a person. if no gender is given,
this method will default to the class' gender.
NOTE:
`__init__` should not be subclassed. Instead, for
subclass specific setup, you should subclass the
`__post_init__` method
'''
self.name = name # this is an instance variable
'''the name of this instance of `Person`'''
# gender is an instance variable. We instantiate it from
# the `gender` argument, if it is provided, or from the
# class' default `gender` variable if it is not
self.gender = gender or self.__class__.gender
'''the gender of this instance of `Person`'''
# dynamically change this object's class based on gender
self.__class__ = __class__._REGISTERED_GENDERS.get(
self.gender, __class__)
# Do subclass-specific setup stuff.
# this will now call the `__post_init__` method
# of the relevant subclass.
self.__post_init__()
def __post_init__(self):
'''do sub-class specific setup stuff here'''
print(f"A person was born today. Their name is {self.name}.")
def __init_subclass__(cls, gender=None):
'''register a new subclass of a given gender'''
# set the subclass' default `gender`
if gender is not None:
cls.gender = gender
# register the subclass to this super-class
__class__._REGISTERED_GENDERS[cls.gender] = cls
def __repr__(self):
return f"{self.__class__.__name__}(name={self.name}, gender={self.gender})"
class Man(Person, gender='man'):
def __post_init__(self):
'''do sub-class specific setup stuff here'''
print(f"A man was born today. His name is {self.name}.")
class Woman(Person, gender='woman'):
def __post_init__(self):
'''do sub-class specific setup stuff here'''
print(f"A woman was born today. Her name is {self.name}.")
If we do things the "usual" way, we get the expected output:
dave = Man('Dave')
print(dave)
A man was born today. His name is Dave.
Man(name=Dave, gender=man)
If we do things the "new"/"unconventional"/"wrong" way, we also get the desired output :
tom = Person('Tom', 'man')
print(tom)
A man was born today. His name is Tom.
Man(name=Tom, gender=man)
As a bonus, we can instantiate a non-gendered person, and they will have the default Person
class.
robot = Person("Isaac")
print(robot)
A person was born today. Their name is Isaac.
Person(name=Isaac, gender=unknown)