Max Shouman
Max Shouman

Reputation: 1331

How to determine if a method was called from within the class where it's defined?

I'm trying to implement an (admittedly unPythonic) way of encapsulating a lot of instance variables.

I have these variables' names mapped to the respective values inside a dictionary, so instead of writing a lot of boilerplate (i.e. self.var = val, like times 50), I'm iterating over the dictionary while calling __setattr__(), this way:

class MyClass:
    __slots__ = ("var1", "var2", "var3")
    def __init__(self, data):
        for k, v in data.items():
            self.__setattr__(k, v)

Then I would override __setattr__() in a way that controls access to these properties. From within __setattr__(), I'd check if the object has the property first, in order to allow setattr calls inside __init__():

def __setattr__(self, k, v):
    if k in self.__class__.__slots__:
        if hasattr(self, k):
            return print("Read-only property")
    super().__setattr__(k, v)

The problem is, I also need some of these properties to be writeable elsewhere in myClass, even if they were already initialized in __init__(). So I'm looking for some way to determine if setattr was called inside the class scope or outside of it, e.g.:

class MyClass:
    __slots__ = ("var",)
    def __init__(self):
        self.__setattr__("var", 0)
    def increase_val(self):
        self.var += 1  # THIS SHOULD BE ALLOWED

my_obj = MyClass()
my_obj.var += 1  # THIS SHOULD BE FORBIDDEN

My pseudo definition would be like:

# pseudocode
def setattr:
    if attribute in slots and scope(setattr) != MyClass:
        return print("Read-only property")
    super().setattr

Also, I'd rather not store the entire dictionary in one instance variable, as I need properties to be immutable.

Upvotes: 1

Views: 766

Answers (1)

Max Shouman
Max Shouman

Reputation: 1331

Answering my own question to share with anyone with the same issue.

Thanks to @DeepSpace in the comments I've delved a bit into the frame inspection topic which I totally ignored before.

Since the well known inspect library relies on sys._getframe() in some parts, namely the parts that I'm mainly interested in, I've decided to use sys instead.

The function returns the current frame object in the execution stack, which is equipped with some useful properties.

E.g., f_back allows you to locate the immediate outer frame, which in case __setattr__() was called within the class, is the class itself.

On the outer frame, f_locals returns a dictionary with the variables in the frame's local scope and their respective values.

One can look for self inside f_locals to determine wether the context is a class, although it's a bit 'dirty' since any non-class context could have a self variable too. However, if self is mapped to an object of type MyClass, then there shouldn't be ambiguities.

Here's my final definition of __setattr__()

def __setattr__(self, k, v):
    if k in self.__class__.__slots__:
        self_object = sys._getframe(1).f_back.f_locals.get("self")
        if self_object is None or self_object.__class__ != MyClass:
            return print(k, "is a read-only property")
    super().__setattr__(k, v)

As a conclusion, I feel like pursuing variable privacy in Python is kind of going against the tide; it's definitely a cleaner solution to label variables as 'protected' according to the recognized standard, without bothering too much about the actual accessibility.

Another side note is that frame inspection doesn't look like a very reliable approach for applications meant for production, but more like a debugging tool. As a matter of fact, some inspect functions do not work with some Python implementations, e.g. those lacking stack frame support.

Upvotes: 2

Related Questions