简体   繁体   中英

How do I create a class where instantiation only happens if certain conditions are met?

Let's say I have this class:

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

If I want to instantiate Person I can do:

me = Person("António")

But what if I only want to instantiate Person if name has type str ?
I tried this:

class Person:
    def __init__(self, name):
        if type(name) == str:
            self.name = name

But then when I do:

me = Person("António")
print(me.name)

you = Person(1)
print(you.name)

I get this:

在此处输入图片说明

So all that's happening is:

  • If name is str , the instance has a .name method
  • If name is not str , the instance has no .name method

But what I actually want, is to stop instantiation all together if name is not an str .
In other words, I want it to be impossible to create an object from the Person class with a non str name .

How can I do that?

You could use a factory that checks the parameters, and returns a Person object if everything is fine, or raises an error:

maybe something line this:

class PersonNameError(Exception):
    pass

class Person:
    def __init__(self):
        self.name = None

def person_from_name(name: str) -> Person:
    """Person factory that checks if the parameter name is valid
    returns a Person object if it is, or raises an error without 
    creating an instance of Person if not.
    """
    if isinstance(name, str):
        p = Person()
        p.name = name
        return p
    raise PersonNameError('a name must be a string')

p = person_from_name('Antonio') 

Whereas:

p = person_from_name(123)   # <-- parameter name is not a string

throws an exception:

PersonNameError                           Traceback (most recent call last)
<ipython-input-41-a23e22774881> in <module>
     14 
     15 p = person_from_name('Antonio')
---> 16 p = person_from_name(123)

<ipython-input-41-a23e22774881> in person_from_name(name)
     11         p.name = name
     12         return p
---> 13     raise PersonNameError('a name must be a string')
     14 
     15 p = person_from_name('Antonio')

PersonNameError: a name must be a string

How about :

class Person:
    def __init__(self, name):
        if type(name) == str:
            self.name = name
        else: 
            raise Exception("name attribute should be a string")

You should use factory design pattern . You can read more about it here . To put it simple:

Create Class/method that will check for the conditions and return new class instance only if those conditions are met.

If you want to modify instantiation behaviour, You can create a constructor, using a class method.

class Person:
    def __init__(self, name):
        self.name = name
        print("ok")

    @classmethod
    def create(cls, name):
        if not isinstance(name, str):
            raise ValueError(f"Expected name to be a string, got {type(name)}")
        return cls(name)
           
me = Person.create("António")
print(me.name)

you = Person.create(1)
print(you.name)

OK prints once proving only once instantiation

ok
António
Traceback (most recent call last):
  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 31, in <module>
    start(fakepyfile,mainpyfile)  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 30, in start
    exec(open(mainpyfile).read(),  __main__.__dict__)
  File "<string>", line 17, in <module>
  File "<string>", line 11, in create
ValueError: Expected name to be a string, got <class 'int'>

[Program finished]

Here, it's an explicit test that's being done. Overriding new is very rarely needed and for everyday normal classes I think it should be avoided. Doing so keeps the class implementation simple.

class Test(object):
     print("ok")
     def __new__(cls, x):
         if isinstance(x, str) :           
             print(x)
         else:
             raise ValueError(f"Expected name to be a string, got {type(x)}")
 
obj1 = Test("António")

obj2 = Test(1)
ok
António
Traceback (most recent call last):
  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 31, in <module>
    start(fakepyfile,mainpyfile)  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 30, in start
    exec(open(mainpyfile).read(),  __main__.__dict__)
  File "<string>", line 14, in <module>
  File "<string>", line 10, in __new__
ValueError: Expected name to be a string, got <class 'int'>

[Program finished]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM