Search code examples
pythonooppython-decoratorsaccess-modifiers

Python access modifiers as a decorator


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


Solution

  • 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.')
    

    Demo: https://ideone.com/o5aQae