2

I have some trouble with Python class creations. My task is to create objects using some parse method. But I want to turn off basic class creation using __init__

For example, I have

class A:
   @classmethod
   def create_from_file(cls, file):
     # Create instance form file...
     return self

This gives me an opportunity to create object using command like this

a = A.create_from_file()

But this code provides me a chance to create instance using __init__

a = A() won't raise an exception...

When I try to add own __init__ method, my parse function also raises an exception.

 class A:
   def __init__(self):
      raise NotImplementedError

   @classmethod
   def create_from_file(cls, file):
     # Create instance form file...
     return self

How I can fix this trouble, and what is the most Pythonic way to write this classes?

Artem Kryvonis
  • 128
  • 2
  • 13
  • 6
    `__init__` doesn't create instances. It is a hook method called **after** the instance is already created. You want to look at `__new__` instead. – Martijn Pieters Jul 19 '17 at 08:18
  • 1
    In this case, I'd have the class method pass in a 'secret' sentinel object (`_token = object()` and `return cls(..., _from_factory=_token)`) then have `__init__` check for this: `def __init__(..., _from_factory=None):` and `if _from_factory is not _token: raise TypeError('Can only be created with A.create_from_file()`)`. – Martijn Pieters Jul 19 '17 at 08:22
  • @MartijnPieters Can you please describe more or named this trick that I can google it? – Artem Kryvonis Jul 19 '17 at 08:38
  • What I would do is overwrite the `__init__` method after parsing/setup/whatever, once you're sure you won't ever need to create an instance again. For instance, at the end of your parsing, define a `fail` function that only raises a `NotImplementedError`: `def fail(*args, **kwargs): raise NotImplementedError`. Then, change the value of the `__init__` attribute of class `A`: `A.__init__ = fail`. This is valid only if there's a precise point after which you absolutely want to disallow any instanciation of the object. – Right leg Jul 19 '17 at 09:00
  • Maybe I do not describe the problem at all, but in future, I have more than one method for creating, like `from_file`, `from_object` etc. But I need to close any other instance creation methods. – Artem Kryvonis Jul 19 '17 at 09:05

2 Answers2

2

__init__ is not responsible for creating a instance. It is a hook method that Python calls for you after the instance is already created. You can't prevent instance creation from there. Besides, you don't want to prevent all instance creation, even your classmethod has to create an instance at some point.

Since all you want to do is raise an exception when your factory method is not used to create the instance, it's still fine to raise an exception in __init__ method. That'll prevent the new instance from being assigned anywhere. What you need to do then is distinguish between direct access, and your factory method being used.

You could achieve this is several different ways. You could use a "secret" token that only the factory method passes in:

_token = object()  # unique token to flag factory use

class A:
    def __init__(self, data, _from_factory=None):
        if _from_factory is not _token:
            raise TypeError(f"Can't create {type(self).__name__!r} objects directly")
        self._data = data

    @classmethod
    def create_from_file(cls, file):
        data = file.read()
        return cls(data, _from_factory=_token)

The classmethod still creates an instance, the __init__ is still called for that instance, and no exception is raised because the right token was passed in.

You could make your class an implementation detail of the module and only provide a public factory function:

def create_from_file(cls, file):
    data = file.read()
    return _A(data)

class _A:
    def __init__(self, data):
        self._data = data

Now the public API only gives you create_from_file(), the leading underscore tells developers that _A() is an internal name and should not be relied on outside of the module.

Actual instance creation is the responsibility of the object.__new__ method; you could also use that method to prevent new instances to be created. You could use the same token approach as I showed above, or you could bypass it altogether by using super() to call the original overridden implementation:

class A:
    def __new__(cls, *args, **kwargs):
        raise TypeError(f"Can't create {cls.__name__!r} objects directly")

    def __init__(self, data):
        self._data = data

    @classmethod
    def create_from_file(cls, file):
        data = file.read()
        # Don't use __new__ *on this class*, but on the next one in the
        # MRO. We'll have to manually apply __init__ now.
        instance = super().__new__(cls)
        instance.__init__(data)
        return instance

Here a direct call to A() will raise an exception, but by using super().__new__ in the classmethod we bypass the A.__new__ implementation.

Note: __new__ is implicitly made a staticmethod, so we have to manually pass in the cls argument when we call it from the classmethod.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
0

If you only have one method to create the object, just make that the constructor. That is, instead of

@classmethod
def create_from_file(cls, file):
  # Create instance form file...
  return self

you would have

def __init__(self, file):
  # Create instance form file...

If there are several different methods of creating an object, it's still typically going to be the case that one of them is more fundamental than any other - in other words, that the arguments to other methods can be "converted" into the arguments to the one method. For example, you might have create_from_file(), create_from_url(), and create_from_string(), where the former two methods basically read the content of the file or URL and then do the same thing with it that create_from_string() does. So just turn create_from_string() into __init__(), and have the other two methods read the file or URL and call the constructor with the content.

If you really don't have a single "fundamental" way of creating the object, it's probably worth considering whether you should have different subclasses of a base class.

David Z
  • 128,184
  • 27
  • 255
  • 279
  • Thanks for this answer it's really helpful. In my case, this code didn't raise an exception if you want to create an instance using simple form `A()` , only that you need is to add the parameter into `__init__` -> `A(some_file)`.In my case, I need to exclude this situation – Artem Kryvonis Jul 19 '17 at 09:22
  • @ArtemKryvonis If you add a parameter to the constructor, as I was saying to do, it _will_ raise an exception when you call `A()`. (Note that you still need the object parameter `self` as well as the passed parameter `file`.) – David Z Jul 19 '17 at 09:25