6

Is there any way to block an object instantiation using direct __init__ and only allow instantiation via a factory method?

Task() --> raise NotAllowedDirectInstantiation

Task.factory() --> return Task()

Is this possible or am I just thinking too much? I know I can put some docs and warnings around, But just wanted to know if it could be done.

For eg.

class Task:
    def __init__():
        # Should not allow anyone to directly call Task()
        # but factory can call it

    @classmethod
    def factory_policy1(cls)
        # Do some stuff here
        # Raise events and stuff..
        return Task()

    @classmethod
    def factory_policy2(cls)
        # Do some policy 2 stuff here
        # Raise events and stuff..
        return Task()

I saw this Python - how do I force the use of a factory method to instantiate an object? but it seems to be discussion about singletons.

The use case could be when we want to apply some business rules or raise some events whenever a new Task is instantiated but those events are not closely related to the Task object so we would not want to clutter the __init__ with those. Also this factory doesn't have to be a classmethod. it could be somewhere else. For eg

# factories.py
from tasks.py import Task

def policy1_factory():
    # Do some cross-classes validations
    # Or raise some events like NewTaskCreatedEvent
    return Task()

def policy2_factory():
    # Do some policy 2 cross-classes validations
    # Or raise some events like NewTaskCreatedEvent
    return Task()

What I don't want other developers to do is this:

class TaskService:
    ...
    def create_task():
        # Yeah we could have HUGE warnings in __init__ doc
        # saying that do not initialize it directly, use factory instead
        # but nothing stops them from accidentally doing so.
        # I would rather raise an exception and crash than allow this
        return Task()
        # should have done return Task.create()

The purpose is not to restrict the developers but to put some security checks. It doesn't even have to raise an exception, Even a warning would suffice but there must be a cleaner way to do this other that docs or weird monkey-patching

Hope it makes sense

Two solutions are good:

@spacedman's one https://stackoverflow.com/a/42735574/4985585 is simple and less restrictive which could be used as a soft warning

@marii's one https://stackoverflow.com/a/42735965/4985585 is stricter and would use it if I want a hard block

Thanks

pfabri
  • 885
  • 1
  • 9
  • 25
Kashif Siddiqui
  • 1,476
  • 14
  • 26
  • Maybe about singleton, but first lines are relevant for any python code: *In Python, it's almost never worth trying to "force" anything. Whatever you come up with, someone can get around it by monkeypatching your class, copying and editing the source, fooling around with bytecode, etc.* There is no way to force in python. But a doc could recommand. – aluriak Mar 11 '17 at 12:53
  • Yeah, We could put some warnings but just curious if some combination of `__init__` `__new__` or other could make this – Kashif Siddiqui Mar 11 '17 at 12:57
  • What is your use case actually ? Why would you prevent direct instanciation, and what would your factory method do that can't be done in `__new__` ? – bruno desthuilliers Mar 11 '17 at 13:08
  • @brunodesthuilliers added a use case in the question. Yes possibly in `__new__` if we can check that the caller is type of class Task then we might allow else raise exception. Just not clear about that. Thanks – Kashif Siddiqui Mar 11 '17 at 14:08
  • @KashifSiddiqui can't you just put the factory functions in the `task.py` and put the actual classes elsewhere so someone would have to import from a completely different place to start with... that'd be a sure sign something's probably not going to plan... – Jon Clements Mar 11 '17 at 14:19
  • @JonClements yeah hiding it could be one solution. But would prefer to keep `Task` in `task.py` since what if the `Task` has some class attributes which are also imported. Anyone who reads that import will probably be more confused about the hidden module. A simple warning or exception on instantiation of `Task()` would be less confusing in my opinion :) – Kashif Siddiqui Mar 11 '17 at 14:28
  • It's not unknown within the standard lib/3rd party libs to use `mobule_name` for public facing stuff, which imports from `_module_name` to provide the implementations... If you get a developer importing from `_module_name` they should really know why they're doing it question why they're doing it :) – Jon Clements Mar 11 '17 at 14:30
  • Given your use case I strongly suggest you get a look at Django's `signals`mechanism. – bruno desthuilliers Mar 13 '17 at 06:42
  • Or more generally at the `observer` design pattern FWIW – bruno desthuilliers Mar 13 '17 at 06:44

3 Answers3

10

As others have said, forbidding __init__ isn't the best option. I would not recommend it. However you can achieve this with metaclasses. Consider this example:

def new__init__(self):
    raise ValueError('Cannot invoke init!')

class Meta(type):
    def __new__(cls, name, bases, namespace):
        old_init = namespace.get('__init__')
        namespace['__init__'] = new__init__
        def factory_build_object(cls_obj, *args, **kwargs):
            obj = cls_obj.__new__(cls_obj, *args, **kwargs)
            return old_init(obj, *args, **kwargs)
        namespace['factory_build_object'] = classmethod(factory_build_object)
        return super().__new__(cls, name, bases, namespace)


class Foo(metaclass=Meta):
    def __init__(self, *args, **kwargs):
        print('init')


Foo.factory_build_object()
# Foo()

If you uncomment the last line it will raise ValueError. Here we are adding magic factory method in class construction in Meta's init. We save a reference to old init method and replace it with one which raises ValueError.

Mariy
  • 5,746
  • 4
  • 40
  • 57
5

Here's a trivially circumventable method:

class Task(object):
    def __init__(self, direct=True):
        # Should not allow anyone to directly call Task()
        # but factory can call it
        if direct:
            raise ValueError
    @classmethod
    def factory(cls):
        # Do some stuff here        
        return Task(direct=False)

So with that in classy.py:

>>> import classy

Get an error on direct instantiation:

>>> t = classy.Task()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "classy.py", line 6, in __init__
    raise ValueError
ValueError

No error via factory method:

>>> t = classy.Task.factory()

But circumvent by passing direct=False to the constructor:

>>> t = classy.Task(direct=False)

I suspect any other method will also be circumventable, just slightly less easily. How hard do you want people to try before breaking it? It really doesn't seem to be worth it...

pfabri
  • 885
  • 1
  • 9
  • 25
Spacedman
  • 92,590
  • 12
  • 140
  • 224
  • 1
    This method is able to avoid direct instantiation by mistake (or by misreading of the documentation) and I think can be very useful, yet simple. Think for instance of a parameter like the following: (True and False inverted respect to the example): classy.Task(force_direct_creation_without_the_use_of_the_builder=True) – Ettore Galli May 05 '20 at 10:47
  • This solution adds another level of confusion - when I pass some parameter to constructor I don't expect it directs (naming coincidence intended) object instantiation but it is rather some class property. Programming is about communicating, so factory can expect such parameter, not class. – Bartek Maciejewski Mar 29 '21 at 11:45
2

I don't know if this will fit your need. For what it's worth, you could define the Task class in the scope of the factory function:

def factory():
    class Task:
        pass
    return Task()

As @JonClements pointed out, this ain't bulletproof though:

Of course, once you have an instance of it, you can always hijack its type, eg: Task = type(factory()) then my_task = Task()...

janos
  • 120,954
  • 29
  • 226
  • 236
  • 2
    Of course, once you have an instance of it, you can always hijack its type, eg: `Task = type(factory())` then `my_task = Task()`... – Jon Clements Mar 11 '17 at 13:45
  • @janos I updated the use case. In your example if have a different policy factory I would have to duplicate the `Task` code. – Kashif Siddiqui Mar 11 '17 at 14:14