1

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.

Max Shouman
  • 1,333
  • 1
  • 4
  • 11
  • 1
    Looks like a rare case where [name mangling](https://stackoverflow.com/questions/7456807/python-name-mangling) should be used, however keep in mind it will still allow to modify the variable from outside the class, it will just be a lot harder – DeepSpace Aug 25 '21 at 13:12
  • 3
    Another alternative will be a property with frame inspection – DeepSpace Aug 25 '21 at 13:14
  • @DeepSpace I find the inspection approach interesting actually, thank you for the advice! – Max Shouman Aug 25 '21 at 13:22

1 Answers1

2

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.

Max Shouman
  • 1,333
  • 1
  • 4
  • 11
  • What's with the `return print(...)` you keep using? It also seems like you could just check if `self_object == self`. – martineau Aug 25 '21 at 16:50
  • I use return `print()` just to immediately return the function after printing (since print returns None) and avoid falling into `super().__setattr__()`. I usually prefer returning when a certain 'stopping' conditions is met instead of having if-else blocks (just a coding style no reason behind it). As per self, if I use `self_object == self`, I'm potentially allowing subclasses that inherit the same `__setattr__` to change that attribute, while I don't want to. This I didn't clearly state in my question actually, but I want the class defining the function to be the only one able to do that. – Max Shouman Aug 25 '21 at 17:38
  • 1
    Preventing inheritance makes sense, I suppose. As for the `return print()` rationale, seems like you really ought to be raising some kind of exception. – martineau Aug 25 '21 at 17:44
  • That I've beed told on another occasion too. Still I'm not getting why; I'm relatively new to Python, but isn't return supposed to accept a value or something that evaluates to some result? If that's the case, print() technically evaluates to None. While admittedly suffering from the patology of condensing code to unnecessary extents, I still find interesting to learn why something like this is bad practice. – Max Shouman Aug 25 '21 at 20:18
  • IMO it a poor practice because you're executing the `print` for a side-effect that would likely be unwelcome by most, but more importantly, that the failure to set the attribute is otherwise being ignored and program execution allowed to continue. Seems like not doing what as intended should raise an exception that will be fatal unless there's an `except` handler in place for it so the caller can deal with it happening in some other manner. – martineau Aug 25 '21 at 20:49