skifli
Backend Developer
I recently learned about encapsulation in Python, and how you could specify private variables in classes that can't be accessed outside of the class (by prefacing the variable with '__'). However, this actually only mangles the name of the variable to '_{CLASS_NAME}{variable_name_with_two_leading_underscores}'. This means that you can still access the variables out of the class if you were determined enough. I decided to try and find a way to block this by writing a custom class that can prevent this. The code below is my attempt at this, and I would appreciate any feedback on it, as well as if someone managed to find a way to get round it.
To make it work, your class has to inherit from the 'Protected' class. This function overrides the default set / get attribute functions, and also uses a custom dictionary class called 'ProtectedDict', since someone could technically edit the dictionary through 'my_class.__dict__["name"] John'. You also have to add a decorator called 'unlock' to all functions in the class except for '__init__', which unlocks the protected variables. To prevent someone from replicating the code of the 'unlock' function, the 'Protected' class checks if the calling function is the right one when 'allow_private' gets set to 'True'.
If you want me to explain anything, feel free to ask since I haven't commented the code very well. I have also included a dummy class called 'Person' for you to see how it works in action.
To make it work, your class has to inherit from the 'Protected' class. This function overrides the default set / get attribute functions, and also uses a custom dictionary class called 'ProtectedDict', since someone could technically edit the dictionary through 'my_class.__dict__["name"] John'. You also have to add a decorator called 'unlock' to all functions in the class except for '__init__', which unlocks the protected variables. To prevent someone from replicating the code of the 'unlock' function, the 'Protected' class checks if the calling function is the right one when 'allow_private' gets set to 'True'.
If you want me to explain anything, feel free to ask since I haven't commented the code very well. I have also included a dummy class called 'Person' for you to see how it works in action.
Python:
from inspect import currentframe
class ProtectedDict(dict):
def __getitem__(self, key) -> any:
if (
"allow_private" not in self.__dict__
and "initialized" in self.__dict__
and key.startswith("__")
):
raise AttributeError(f"Class has no attribute '{key}'")
return super().__getitem__(key)
def __setitem__(self, key, value) -> None:
if (
"allow_private" not in self.__dict__
and "initialized" in self.__dict__
and key.startswith("__")
):
raise AttributeError(f"Class has no attribute '{key}'")
super().__setitem__(key, value)
def __setattr__(self, key, value) -> None:
if currentframe().f_back.f_code.co_name in ["__init__", "__inner__"]:
super().__setattr__(key, value)
else:
exec(f"super().__getitem__('variables').{key} = value")
class Protected:
def __init__(self, **kwargs) -> None:
self.variables = ProtectedDict()
for key in kwargs:
self.variables[key] = kwargs[key]
self.variables.initialized = True
def __setattr__(self, key, value) -> None:
if currentframe().f_back.f_code.co_name == "__init__":
super().__setattr__(key, value)
else:
self.variables[key[key.find("__") :]] = value
def __getattr__(self, name) -> any:
return self.variables.__getitem__(name[name.find("__") :])
def unlock(func):
def __inner__(*args, **kwargs) -> any:
args[0].variables.allow_private = True
code = func(*args, **kwargs)
del args[0].variables.allow_private
return code
return __inner__
# END PROTECTED CLASS IMPLEMENTATION
class Person(Protected):
def __init__(self, __name: str, age: int) -> None:
super().__init__(__name=__name, age=age)
print(f"Initialized {__name} as {age} years old.")
@unlock
def update_name(self, name: str) -> None:
print(f"Changing name from {self.__name} to {name} in the class.")
self.__name = name
print("Changed the name successfully in the class.")
my_person = Person("John", 20)
my_person.update_name("Jane")
print(f"Changing name from {my_person.__name} to John outside of the class.") # This should error out, saying the '__name' attribute does not exist.