2

Is there any way to know the context in which an object is instantiated? So far I've been searching and tried inspect module (currentcontext) with poor results.

For example

class Item:
    pass

class BagOfItems:
    def __init__(self):
        item_1 = Item()
        item_2 = Item()
item_3 = Item()

I'd want to raise an exception in the instantiation of item_3 (because its outside a BagOfItems), while not doing so in item_1 and item_2. I dont know if a metaclass could be a solution to this, since the problem occurs at instantiation not at declaration.

The holder class (BagOfItems) can't implement the check because when Item intantiation happens outside it there would be no check.

Jose A. García
  • 888
  • 5
  • 13
  • You'd probably want to inspect the stack to get the first element in the call hierarchy for `__init__` that is not part of your class or metaclass. – Mad Physicist Dec 14 '17 at 21:22
  • On an unrelated note, instantiating items directly in the bag class like that may be a poor idea for a similar reason that using globals is a poor idea in most cases. – Mad Physicist Dec 14 '17 at 21:23
  • Class `__init__` is executed in metaclass `__call__` usually, which is why you want to skip that in the calling sequence as well. – Mad Physicist Dec 14 '17 at 21:24
  • @MadPhysicist `inspect.stack()` ? I dont really know how to get the information from it. Would this also work in a new class inheriting from `BagOfItems` ? – Jose A. García Dec 14 '17 at 21:44
  • I can probably come up with a solution as asked, but I have to ask why? It sounds like such a hacky and irresponsible thing to do that I feel like there must be a better way to accomplish your true goal. – Mad Physicist Dec 15 '17 at 01:34
  • 1
    One reason why this is a bit nonsensical without further explanation: once you instantiate some items, you can access them anywhere. In fact, you can probably make an exact deep copy of the object without directly invoking the constructor. You can copy a reference and delete the one in the bag, effectively bypassing your restriction. – Mad Physicist Dec 15 '17 at 04:57
  • @MadPhysicist There isn't a true goal actually. I'm trying to make a game and since I'm much more experienced with Python than C++ (The language I'm trying to learn with this game thing) I'm working on the skeleton and structure with python. I came up with this Item base class which is supposed to only be in players or chestlike objects. Since I noticed, as you wisely said, this is hacky and irresponsible I'll find other solution. But I still wanted to know, just for the knowledge itself. – Jose A. García Dec 15 '17 at 07:59
  • I'll try to find a solution for you because it's an interesting problem. The thing to keep in mind is that `Item` is a programming construct. Only allowing it to be instantiated in a certain scope is not necessarily a good way to model what it does as a game element. You are writing the code, so just don't write something like `item3`. I'll hack something together in a couple of hours if I can. – Mad Physicist Dec 15 '17 at 12:37

1 Answers1

1

When you instantiate an object with something like Item(), you are basically doing type(Item).__call__(), which will call Item.__new__() and Item.__init__() at some point in the calling sequence. That means that if you browse up the sequence of calls that led to Item.__init__(), you will eventually find code that does not live in Item or in type(Item). Your requirement is that the first such "context" up the stack belong to BagOfItem somehow.

In the general case, you can not determine the class that contains the method responsible for a stack frame1. However, if you make your requirement that you can only instantiate in a class method, you are no longer working with the "general case". The first argument to a method is always an instance of the class. We can therefore move up the stack trace until we find a method call whose first argument is neither an instance of Item nor a subclass of type(Item). If the frame has arguments (i.e., it is not a module or class body), and the first argument is an instance of BagOfItems, proceed. Otherwise, raise an error.

Keep in mind that the non-obvious calls like type(Item).__call__() may not appear in the stack trace at all. I just want to be prepared for them.

The check can be written something like this:

import inspect

def check_context(base, restriction):
    it = iter(inspect.stack())
    next(it)  # Skip this function, jump to caller
    for f in it:
        args = inspect.getargvalues(f.frame)
        self = args.locals[args.args[0]] if args.args else None
        # Skip the instantiating calling stack
        if self is not None and isinstance(self, (base, type(base))):
            continue
        if self is None or not isinstance(self, restriction):
            raise ValueError('Attempting to instantiate {} outside of {}'.format(base.__name__, restriction.__name__))
        break

You can then embed it in Item.__init__:

class Item:
    def __init__(self):
        check_context(Item, BagOfItems)
        print('Made an item')

class BagOfItems:
    def __init__(self):
        self.items = [Item(), Item()]

boi = BagOfItems()
i = Item()

The result will be:

Made an item
Made an item
Traceback (most recent call last):
    ...
ValueError: Attempting to instantiate Item outside of BagOfItems

Caveats

All this prevents you from calling methods of one class outside the methods of another class. It will not work properly in a staticmethod or classmethod, or in the module scope. You could probably work around that if you had the motivation. I have already learned more about introspection and stack tracing than I wanted to, so I will call it a day. This should be enough to get you started, or better yet, show you why you should not continue down this path.

The functions used here might be CPython-specific. I really don't know enough about inspection to be able to tell for sure. I did try to stay away from the CPython-specific features as much as I could based on the docs.

References

1. Python: How to retrieve class information from a 'frame' object?
2. How to get value of arguments passed to functions on the stack?
3. Check if a function is a method of some object
4. Get class that defined method
5. Python docs: inspect.getargvalues
6. Python docs: inspect.stack

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • I really appreciate your effort, I haven't had time to test it out yet, once I do I will comment here again! But, fear not, I will not keep my project this way! :) – Jose A. García Dec 21 '17 at 13:06
  • Glad you like it. Let your game interface impose these restrictions. It's much easier to tell the user "don't try putting items in your pants" directly than parsing some obscure low level exception like this code raises and coming back with an obscure generic message. – Mad Physicist Dec 21 '17 at 15:03