3

I have the following class, which acts as a collection of people:

class Person:
    PERSONS = dict() # name ==> instance
    def __new__(cls, *args, **kwargs):
        name = kwargs.get('name') or '' if not args else args[0]
        print ('Name: %s' % name)
        if name in cls.PERSONS:
            print ('Returning found person!')
            return cls.PERSONS[name]
        else:
            print ('Initializing new person')
            return super(Person, cls).__new__(cls)
    def __init__(self, name):
        print ("Running init")
        self.name = name
        Person.PERSONS[name] = self

If a person is found, it returns that person, otherwise it creates a new one. And when I run it it works:

>>> p1 = Person('Julia')
Name: Julia
Initializing new person
Running init
>>> p2 = Person('Julia')
Name: Julia
Returning found person!
Running init # <== how to get this not to run?
>>> p1 is p2
True

However, if the person is found, I don't want the __init__ method to run. How would I "skip" the init method based on the return of the __new__ ?

One option is to add a conditional in the __init__, such as:

def __init__(self, name):
    if name in Person.PERSONS: return # don't double-initialize
    print ("Running init")
    self.name = name
    Person.PERSONS[name] = self

But I was hoping there might be a cleaner approach.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
David542
  • 104,438
  • 178
  • 489
  • 842

3 Answers3

3

@MadPhysicist's idea of using a metaclass with a custom __call__ method is correct but the implementation included in the answer is quite off. Instead, the custom __call__ method should use the name of the person, rather than a new Person object, to check if a given name has an existing entry in the PERSONS dict:

class PersonMeta(type):
    def __call__(cls, name):
        print ('Name: %s' % name)
        if name in cls.PERSONS:
            print ('Returning found person!')
            return cls.PERSONS[name]
        print('Initializing new person')
        obj = cls.__new__(cls, name)
        cls.__init__(obj, name)
        cls.PERSONS[name] = obj
        return obj

class Person(metaclass=PersonMeta):
    PERSONS = dict() # name ==> instance
    def __init__(self, name):
        print ("Running init")
        self.name=name

p1=Person('Julia')
p2=Person('Julia')
print(p1 is p2)

This outputs:

Name: Julia
Initializing new person
Running init
Name: Julia
Returning found person!
True
blhsing
  • 91,368
  • 6
  • 71
  • 106
  • thanks. What's the difference between doing `class PersonMeta(type)` and just `class PersonMeta`? Or are they the same and yours is just more explicit ? – David542 Oct 15 '19 at 02:58
  • I still wouldn't recommend using a metaclass for this, but at least this code works. – user2357112 Oct 15 '19 at 02:59
  • @David542: `class PersonMeta(type)` does what providing a superclass always does: it makes `PersonMeta` a subclass of the given superclass, `type` in this case. `class PersonMeta` would not be a subclass of `type`. (Both versions would be *instances* of `type`, but that's not what we need here.) – user2357112 Oct 15 '19 at 03:00
  • @David542 `class PersonMeta(type)` makes `PersonMeta` a subclass of `type`, the class of a class object, otherwise known as a metaclass. – blhsing Oct 15 '19 at 03:01
  • @user2357112 Agreed. Using a metaclass is the only way to achieve exactly what the OP wants, but to serve the purpose, a class method as an alternative constructor is the more readable approach. – blhsing Oct 15 '19 at 03:04
  • @blhsing to clarify: is `type` an argument (such as, it could be of any type passed to it)? Or is `type` the actual type `` ? – David542 Oct 15 '19 at 03:39
  • 1
    @David542 Yes, `type` refers to ``, which implements the default behavior of all classes. You can read more about it at https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python . – blhsing Oct 15 '19 at 03:40
  • @blhsing got it -- thanks for the link. If a `type` isn't explicitly passed to the class, what type is it, `object` ? – David542 Oct 15 '19 at 03:42
  • @David542 Yes, the default base of a class is indeed `object`, which implements descriptors and a number of common methods. You can read more about the `object` class at https://stackoverflow.com/questions/4015417/python-class-inherits-object – blhsing Oct 15 '19 at 03:47
  • 1
    @blhsing awesome, thanks so much for all the links and suggestions here. – David542 Oct 15 '19 at 03:49
1

Instead of trying to skip __init__, put your initialization in __new__. In general, most classes should only implement one of __new__ and __init__, or things get messy.

Also, trying to have a class act as a collection of anything is usually a bad idea. Instead of trying to make your class itself manage its instances, it tends to be a better idea to give that role to a dedicated collection object. This makes it easier to manage object lifetimes, have multiple containers, avoid weird __new__ problems, etc.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • I thought `__new__` is basically like memory allocation -- so that you'd either create a new object or return an existing one. And based on that you'd call the init? – David542 Oct 15 '19 at 02:09
  • thanks for the updated explanation that's very helpful. – David542 Oct 15 '19 at 02:13
  • 1
    @David542: `__new__` is a weird hybrid allocator/secondary constructor thing they had to add to the language when they added the ability to subclass immutable types. You shouldn't think of it as only memory allocation; it's designed to be where initialization happens for immutable types, and for most other types that need to override `__new__`. – user2357112 Oct 15 '19 at 02:19
1

The problem I find in your approach is that the __new__ dunder method is triggered just before the __init__. Once said that, it's not that easy to change that behavior.

Instead of handling the new Person's creation inside __new__, create a class method (e.g. create_person) and update the PERSONS dict if needed.

class Person:
    def __init__(self, name):
        print("Running init\n")
        self.name = name


class PersonFactory:
    PERSONS = dict()

    @classmethod
    def create_person(cls, name):
        print('Name: %s' % name)
        if name in cls.PERSONS:
            print ('Returning found person!')
            return cls.PERSONS[name]

        print('Initializing new person')
        cls.PERSONS[name] = Person(name)
        return cls.PERSONS[name]


if __name__ == '__main__':
    PersonFactory.create_person('Julia')
    PersonFactory.create_person('Julia')
slackmart
  • 4,754
  • 3
  • 25
  • 39