-1

"Man" is a subclass of "Person" with property gender='male '. How can I determine if an instance of Person is an instance of Man? Assuming that Bob=Person (name='bob ', gender='male'), how do I obtain isinstance (Bob, Man)=True?

class Person:
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender

    def __repr__(self):
        return f"Person(name={self.name}, gender={self.gender})"

    @classmethod
    def create_person(cls, name, gender):
        if gender == 'male':
            return Man(name, gender)
        elif gender == 'female':
            return Woman(name, gender)
        else:
            raise ValueError("Invalid gender")

class Man(Person):
    def __init__(self, name, gender):
        super().__init__(name, gender)

class Woman(Person):
    def __init__(self, name, gender):
        super().__init__(name, gender)

bob = Person("bob", "male")
print(isinstance(bob, Man))

I get result which is 'False' but not 'True',how should I correct it?

joylix
  • 95
  • 7
  • 2
    well, you created "bob" as a Person, using Person's constructor, not a create_person class method. Why would you expect him to be a Man? – pneuma Aug 01 '23 at 11:54
  • 1
    But `bob` is a `Person`. Did you mean to write: `bob = Person.create_person("bob", "male")`? – quamrana Aug 01 '23 at 11:54
  • 3
    Better not having a `Woman` and `Man` subclasses, but rather a `is_man()` and `is_woman()` methods on the `Person` class. Using your `create_person()` method would solve the problem also, but it is strange for a parent class to know about its subclasses. – paime Aug 01 '23 at 12:01
  • Why do you need a create_person function? – DarkKnight Aug 01 '23 at 12:17
  • 1
    why do the `__init__`s of `Man` and `Woman` take a `gender` parameter? they could just call `super().__init__(name, "male")` (or `"female"` in `Woman.__init__`) – Aemyl Aug 01 '23 at 12:25
  • another improvement would be to use an enumeration for gender – Aemyl Aug 01 '23 at 12:27
  • @Aemyl See my answer for an alternative to passing a second parameter to the constructor – DarkKnight Aug 01 '23 at 12:49

3 Answers3

4

Person is your superclass. Man and Woman are subclasses of Person.

Therefore, you should construct either Man or Woman - not Person

Perhaps this will make matters clearer:

class Person:
    def __init__(self, name: str):
        self._name = name
    def __str__(self) -> str:
        return f'My name is {self._name} and I am a {type(self).__name__}'

class Man(Person):
    def __init__(self, name: str):
        super().__init__(name)

class Woman(Person):
    def __init__(self, name: str):
        super().__init__(name)

man = Man('Dave')
woman = Woman('Linda')

print(man)
print(woman)

Output:

My name is Dave and I am a Man
My name is Linda and I am a Woman

Of course, you may want an alternative to using the class name so by utilising a class attribute we can do this:

class Person:
    def __init__(self, name: str):
        self._name = name
    def __str__(self) -> str:
        gender = getattr(self, '_gender', 'Unknown')
        return f'My name is {self._name} and I am a {gender}'

class Man(Person):
    _gender = 'man'
    def __init__(self, name: str):
        super().__init__(name)

class Woman(Person):
    _gender = 'woman'
    def __init__(self, name: str):
        super().__init__(name)

man = Man('Dave')
woman = Woman('Linda')

print(man)
print(woman)

Output:

My name is Dave and I am a man
My name is Linda and I am a woman
DarkKnight
  • 19,739
  • 3
  • 6
  • 22
  • My original intention was not to use man to instantiate Bob, but to automatically run the class method create_person when Bob=Person ('bob ','male')_ Person to make Bob an instance of man – joylix Aug 01 '23 at 13:34
  • 1
    @joylix I don't understand your use-case. It breaks the entire super/sub-class paradigm – DarkKnight Aug 01 '23 at 13:46
  • What I mean is, if Person (Bob. gender=='male'), then Bob isinstance of Man, But I don't know how to implement this inference through a Python program – joylix Aug 01 '23 at 14:16
  • @joylix What you're describing sounds like using the type system as a proxy for checking the runtime state of an object. That's not going to play nice in Python or any other strongly typed language. If you can describe what your actual use case is, we may be able to recommend an approach that doesn't fundamentally violate the type system – Brian61354270 Aug 01 '23 at 14:30
1

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)

Hoodlum
  • 950
  • 2
  • 13
  • Although I may not fully understand this code, it does implement the functionality I mentioned, which is to instantiate a Person (name='bob ', gender='male') and then isinstance (bob, Man)=True .Thank you very much, hoodlum.I'm not sure if there is any simpler implementation method – joylix Aug 01 '23 at 14:54
  • Yes. In general, you're better off not using tricks like this -- it'll mostly just lead to confusion. I'll try to update the answer with a few more comments and explanations though. Please let me know what parts of the code you're having trouble understanding, so I can focus my attention there. In the mean time, you can have a look at [this answer](https://stackoverflow.com/questions/45400284/understanding-init-subclass) to demystify `__init_subclass__` a bit – Hoodlum Aug 01 '23 at 15:02
  • I think this use case is very universal: if there is a person named Bob in a group of people whose gender is male, then it is not a normal idea to assert that he is an instance of Man? – joylix Aug 01 '23 at 15:09
  • The use case is indeed quite common, it's just that this solution to it breaks a pretty fundamental assumption on super- and subclass relationships : super classes should be totally independent of subclasses. The usual way of addressing this would be to create a "factory" -- kind of like what you did with your `create_person` method. However, this method should not be part of the `Person` class, ideally, as that would again break the superclass-subclass relationship. – Hoodlum Aug 01 '23 at 15:22
  • In reality, there are many examples of dynamic classification: for example, when it is continuously monitored that a person's blood sugar value is higher than 11, he will be included in the group of diabetes patients – joylix Aug 01 '23 at 15:29
  • Don't confuse classification and "object class". An "object class" is fundamentally just a way of structuring data in a computer's memory and keeping track of a set of methods (functions) associated with it. It is **not** an absolute classification. If you are unfamiliar with the concept, you may want to look into Object-Oriented Programming. – Hoodlum Aug 01 '23 at 15:50
0
class Person:
    def __init__(self, name, gender):
        self.name = name
        self.gender = gender
        if gender == 'male':
            self.__class__ = Man

class Man(Person):
    def __init__(self, name, gender):
        super().__init__(name, gender)

bob = Person('bob', 'male')
print(isinstance(bob, Man))

The above code seems to yield the correct results, but I am not sure if it complies with Python programming conventions

joylix
  • 95
  • 7