Is it possible to implement protected
and private
access modifiers for classes with decorators in python? How?
The functionality should be like the code below:
class A:
def public_func(self):
self.protected_func() # Runs without warning and error (Because is called in the owner class)
self.private_func() # Runs without warning and error (Because is called in the owner class)
@protected
def protected_func(self):
print('protected is running')
self.private_func() # Runs without warning and error (Because is called in the owner class)
@private
def private_func(self):
print(f'private is running')
a = A()
a.public_func() # Runs without any warning and error (Because has no access modifier)
a.protected_func() # Runs with protected warning
a.private_func() # Raises Exception
The idea for this question was being accessable private functions as below:
class A:
def __private_func(self):
print('private is running')
a = A()
a._A__private_function()
If we define private
with decorator
, then have not to define it with __name
.
So _A__private_function
will not exist and the private function is really not inaccessible from outside of the owner class.
Is the idea a True solution to solve the problem below?
__name is not realy private
In the other answer of mine, in trying to determine whether a call is made from code defined in the same class as a protected method, I made use of the co_qualname
attribute of a code object, which was only introduced in Python 3.11, making the solution incompatible with earlier Python versions.
Moreover, using the fully qualified name of a function or code for a string-based comparison means that it would be difficult, should there be a need, to allow inheritance to work, where a method in a subclass should be able to call a protected method of the parent class without complaints. It would be difficult because the subclass would have a different name from that of the parent, and because the parent class may be defined in a closure, resulting in a fully qualified name that makes it difficult for string-based introspections to reliably work.
To clarify, if we are to allow protected methods to work from a subclass, the following usage should run with the behaviors as commented:
class C:
def A_factory(self):
class A:
@protected
def protected_func(self):
print('protected is running')
self.private_func()
@private
def private_func(self):
print('private is running')
return A
a = C().A_factory()
class B(a):
def foo(self):
super().private_func()
b = B()
b.foo() # Runs without complaint because of inheritance
b.protected_func() # Runs with protected warning
b.private_func() # Raises Exception
We therefore need a different approach to determining the class in which a protected method is defined. One such approach is to recursively trace the referrer of objects, starting from a given code object, until we obtain a class object.
Tracing recursively the referrers of an object can be potentially costly, however. Given the vast interconnectedness of Python objects, it is important to limit the recursion paths to only referrer types that can possibly lead to a class.
Since we know that the code object of a method is always referenced by a function object, and that a function object is either referenced by the __dict__
of a class (whose type is a subclass of type
) or a cell object in a tuple representing a function closure that leads to another function and so on, we can create a dict that maps the current object type to a list of possible referrer types, so that the function get_class
, which searches for the class closest to a code object, can stay laser-focused:
from gc import get_referrers
from types import FunctionType, CodeType, CellType
referrer_types = {
CodeType: [FunctionType],
FunctionType: [dict, CellType],
CellType: [tuple],
tuple: [FunctionType],
dict: [type]
}
def get_class(obj):
if next_types := referrer_types.get(type(obj)):
for referrer in get_referrers(obj):
if issubclass(referrer_type := type(referrer), type):
return referrer
if referrer_type in next_types and (cls := get_class(referrer)):
return cls
With this utility function in place, we can now create decorators that return a wrapper function that validates that the class defining the decorated function is within the method resolution order of the class defining the caller's code. Use a weakref.WeakKeyDictionary
to cache the code-to-class mapping to avoid a potential memory leak:
import sys
import warnings
from weakref import WeakKeyDictionary
def make_protector(action):
def decorator(func):
def wrapper(*args, **kwargs):
func_code = func.__code__
if func_code not in class_of:
class_of[func_code] = get_class(func_code)
caller_code = sys._getframe(1).f_code
if caller_code not in class_of:
class_of[caller_code] = get_class(caller_code)
if not (class_of[caller_code] and
class_of[func_code] in class_of[caller_code].mro()):
action(func.__qualname__)
return func(*args, **kwargs)
class_of = WeakKeyDictionary()
return wrapper
return decorator
@make_protector
def protected(name):
warnings.warn(f'{name} is protected.', stacklevel=3)
@make_protector
def private(name):
raise Exception(f'{name} is private.')