2

I have a class and a normal constructor but I wish to preprocess the parameters and postprocess the result so I provide a mandated Factory constructor. Yes, I know that this is an unusual meaning for Factory and I also know that I could use memoization to do my processing but I had problems with extending a memoized class.

I wish to prevent myself from accidentally using the normal constructor and this is one way of doing it.

import inspect

   class Foo():
       def __init__(self):
           actual_class_method = Foo.Factory
           # [surely there's a way to do this without introspection?]
           allowed_called_from = {name:method for name,method in inspect.getmembers(Foo, inspect.ismethod)}['Factory']

           actual_called_from = inspect.currentframe().f_back.f_code # .co_name)

           print("actual class method = ",actual_class_method," id = ",id(actual_class_method),",name = ",actual_class_method.__name__)
           print("allowed called from = ",allowed_called_from,", id = ",id(allowed_called_from),", name =",allowed_called_from.__name__)
           print()
           print("actual called from = ",actual_called_from,", id = ",id(actual_called_from),", name =",actual_called_from.co_name)
       @classmethod
       def Factory(cls):
           Foo()

   Foo.Factory()

produces output

actual class method =  <bound method Foo.Factory of <class '__main__.Foo'>>  id =  3071817836 ,name =  Factory
allowed called from =  <bound method Foo.Factory of <class '__main__.Foo'>> , id =  3072138412 , name = Factory

actual called from =  <code object Factory at 0xb7118f70, file "/home/david/Projects/Shapes/rebuild-v0/foo.py", line 15> , id =  3071381360 , name = Factory

Suppose I wished to check that the constructor to Foo() had been called from its Factory. I can find various things about the method whence Foo() was called such as its name and the filename where it was compiled, and this would be sufficient to stop me accidentally calling it directly, but I can't see a way of saying (the method that Foo() was called from) is (the method Factory() in the class Foo). Is there a way of doing this?

Haydon Berrow
  • 485
  • 2
  • 6
  • 14
  • I'm confused. According to your output, `actual_called_from.co_name == 'Factory'`, so it seems like you have all of the pieces in place already – Paul H Jan 29 '18 at 15:20
  • As far as I'm concerned I wouldn't bother preventing direct instanciation of the class. Could you expand a bit on your real use case ? – bruno desthuilliers Jan 29 '18 at 15:27
  • This question: https://stackoverflow.com/questions/2168964/python-creating-class-instance-without-calling-initializer and answers have various ways of differentiating between instantiating a class directly, and indirectly thru' a factory. – quamrana Jan 29 '18 at 15:34
  • @PaulH I know it has been called from a method called 'Factory' but this might be a totally different method of the same name. I want to check it's /my/ method called 'Factory' – Haydon Berrow Jan 29 '18 at 16:07
  • @brunodesthuilliers I didn't want to explain why I wanted to do this because the original question would get lost. Basically, Foo has a parameter and there is a unique Foo for each value of the parameter. The Factory method checks whether there is already a foo for the given parameter, creates it if not, and returns the pre-constructed object. I know this is memoization of the class (see by brandizzi) but I ran into problems when I wanted to extend Foo by class Bar(Foo) and eventually gave up. – Haydon Berrow Jan 29 '18 at 16:54
  • @HaydonBerrow there's certainly a way to make the "memoized" solution (actually more of a singleton variant) to work with inheritance. Typical XY problem here, please explain your _real_ use case (I mean: including the expected behaviour for Foo subclasses , ie should they have their own cache or a distinct one etc) and someone will certainly post a nice pythonic solution. – bruno desthuilliers Jan 30 '18 at 08:34

2 Answers2

0

without inspect, you could provide a default argument to your constructor and check that the value passed is the one you're expecting (with default value at 0)

only the factory has a chance to pass the correct initialization value (or someone really wanting to call the constructor, but not by accident)

class Foo():
    __MAGIC_INIT = 12345678
    def __init__(self,magic=0):
        if magic != self.__MAGIC_INIT:
            raise Exception("Cannot call constructor, use the factory")

    @classmethod
    def Factory(cls):
       return Foo(magic=Foo.__MAGIC_INIT)

f = Foo.Factory()   # works

f = Foo()           # exception

another variation would be to toggle a private "locking" boolean. If set to True, crash when entering constructor, else let it do its job, then reset to True.

Of course the factor has access to this boolean, and can set it to False before calling the constructor:

class Foo():
    __FORBID_CONSTRUCTION = True
    def __init__(self):
        if self.__FORBID_CONSTRUCTION:
            raise Exception("Cannot call constructor, use the factory")
        Foo.__FORBID_CONSTRUCTION = True        # reset to True

    @classmethod
    def Factory(cls):
        Foo.__FORBID_CONSTRUCTION = False
        return Foo()

f = Foo.Factory()
print("OK")
f = Foo()

not an issue even if multithreaded thanks to python GIL.

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
  • The magic number is quite clever and I might use it. It doesn't quite fit the bill because in the last half hour I've been experimenting with a Factory class that automatically adds the factory method and implementing Foo and Bar as Foo(Factory) and Bar(Factory). Both of the latter would have the same magic number so I couldn't distinguish a call to Foo() from within Foo.Factory() (legal) from a call to Foo() from within Bar.Factory() (illegal) – Haydon Berrow Jan 29 '18 at 16:18
  • so don't hardcode the magic number. Use a hash from the class name. – Jean-François Fabre Jan 29 '18 at 16:21
  • Oh, yes, or perhaps even just use the class-name. Actually, I just realised that I don't fully understand what is going on. Why is the call within the factory "return Foo(Foo.__MAGIC_INIT)" instead of "return Foo(magic=__MAGIC_INIT)"? – Haydon Berrow Jan 29 '18 at 16:34
  • it's the same thing. positional vs by parameter name. – Jean-François Fabre Jan 29 '18 at 16:37
0

Alex Martelli posted an answer.

This might get you what you want:

class Foo:
    def __init__(self):
        print('Foo.__init__')  # might consider throwing an exception

    @classmethod 
    def makeit(cls):
        self = cls.__new__(cls)
        self.foo = 'foo'
        return self

f = Foo()    # accidentally create Foo in the usual way

g = Foo.makeit()  # use the 'One True Way' of making a Foo
print(g.foo)
print(f.foo)

Output:

Foo.__init__
foo
Traceback (most recent call last):
  File "D:\python\soMetaClassWorkAroundInit.py", line 19, in <module>
    print(f.foo)
AttributeError: 'Foo' object has no attribute 'foo'
quamrana
  • 37,849
  • 12
  • 53
  • 71