Private methods without underscores and interfaces in Python



    Hi, Habr. Recently I went crazy about design - access modifiers and interfaces, then I ported it to the Python programming language. I ask under kat - I share the results and how it works. For those interested, at the end of the article there is a link to the project on Github.

    Access modifiers


    Access modifiers restrict access to objects - to methods of their class, or to child classes - to methods of their parent class. Using access modifiers helps hide data in the class so that no one outside can interfere with the work of this class.

    private methods are available only inside the class, protected (inside) - inside the class and in child classes.

    How private and protected methods are implemented in Python


    Spoiler - at the level of agreement that adults simply will not call them outside the classroom. Before private methods, you need to write a double underscore, before protected one. And you can still access the methods, despite their "limited" access.

    class Car:
        def _start_engine(self):
            return "Engine's sound."
        def run(self):
            return self._start_engine()
    if __name__ == '__main__':
        car = Car()
        assert "Engine's sound." == car.run()
        assert "Engine's sound." == car._start_engine()
    

    The following disadvantages can be determined:

    • If the _start_engine method updated some class variables or kept state, and not just returned a “dumb calculation”, you could have broken something for future work with the class. You don’t allow yourself to repair something in the engine of your car, because then you won’t go anywhere, right?
    • The flowing point from the previous one - to make sure that you can "safely" (calling the method does not harm the class itself) use a protected method - you need to look into its code and spend time.
    • The authors of the libraries hope that no one uses the protected and private methods of the classes that you use in your projects. Therefore, they can change its implementation in any release (which will not affect public methods due to backward compatibility, but you will suffer).
    • The author of the class, your colleague, expects that you will not increase the technical debt of the project by using a protected or private method outside the class he created. After all, the one who will refactor or modify it (private class method) will have to make sure (for example, through tests) that his changes will not break your code. And if they break it, he will have to spend time trying to solve this problem (with a crutch, because he needs it yesterday).
    • Perhaps you make sure that other programmers do not use protected or private methods on code review and “beat for it”, so spend time.

    How to implement protected methods using a library


    from accessify import protected
    class Car:
        @protected
        def start_engine(self):
            return "Engine's sound."
        def run(self):
            return self.start_engine()
    if __name__ == '__main__':
        car = Car()
        assert "Engine's sound." == car.run()
        car.start_engine()
    

    Attempting to call the start_engine method outside the class will result in the following error (the method is not available according to the access policy):

    Traceback (most recent call last):
      File "examples/access/private.py", line 24, in 
        car.start_engine()
      File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/main.py", line 92, in private_wrapper
        class_name=instance_class.__name__, method_name=method.__name__,
    accessify.errors.InaccessibleDueToItsProtectionLevelException: Car.start_engine() is inaccessible due to its protection level
    

    Using the library:

    • You do not need to use ugly (subjective) underscores or double underscores.
    • You get a beautiful (subjective) method for implementing access modifiers in the code - private and protected decorators .
    • Transfer responsibility from person to interpreter.

    How it works:

    1. The private or protected decorator - the most “high" decorator, fires before the class method , which was declared a private or protected access modifier.


    2. Using the built-in inspect library, the decorator retrieves the current object from the call stack - inspect.currentframe () . This object has the following attributes that are useful to us: the namespace (locals) and the link to the previous object from the call stack (the object that calls the method with the access modifier).


      (Very simplified illustration)
    3. inspect.currentframe (). f_back - use this attribute to check whether the previous object from the call stack is in the class body or not. To do this, look at the namespace - f_locals . If there is a self attribute in the namespace, the method is called inside the class, if not, outside the class. If you call a method with a private or protected access modifier outside the class, there will be an access policy error.

    Interfaces


    Interfaces are a contract of interaction with a class that implements it. The interface contains the method signatures (the name of the functions, the input arguments), and the class that implements the interface, following the signatures, implements the logic. Summing up, if two classes implement the same interface, you can be sure that both objects of these classes have the same methods.

    Example


    We have a User class that uses the storage object to create a new user.

    class User:
        def __init__(self, storage):
            self.storage = storage
        def create(self, name):
            return storage.create_with_name(name=name)
    

    You can save the user to the database using DatabaseStorage.create_with_name .

    class DatabaseStorage:
        def create_with_name(self, name):
            ...
    

    You can save the user to files using FileStorage.create_with_name .

    class FileStorage:
        def create_with_name(self, name):
            ...
    

    Due to the fact that the signatures of the create_with_name methods (name, arguments) are the same for the classes - the User class does not have to worry about which object it was substituted for if both have the same methods. This can be achieved if the FileStorage and DatabaseStorage classes implement the same interface (that is, they are bound by the contract to define some method with logic inside).

    if __name__ == '__main__':
        if settings.config.storage = FileStorage:
            storage = FileStorage()
        if settings.config.storage = DatabaseStorage:
            storage = DatabaseStorage()
        user = User(storage=storage)
        user.create_with_name(name=...)
    

    How to work with interfaces using the library


    If a class implements an interface, the class must contain all the methods of the interface . In the example below, the HumanInterface interface contains the eat method, and the Human class implements it, but does not implement the eat method.

    from accessify import implements
    class HumanInterface:
        @staticmethod
        def eat(food, *args, allergy=None, **kwargs):
            pass
    if __name__ == '__main__':
        @implements(HumanInterface)
        class Human:
            pass
    

    The script will exit with the following error:

    Traceback (most recent call last):
      File "examples/interfaces/single.py", line 18, in 
        @implements(HumanInterface)
      File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 66, in decorator
        interface_method_arguments=interface_method.arguments_as_string,
    accessify.errors.InterfaceMemberHasNotBeenImplementedException: class Human does not implement interface member HumanInterface.eat(food, args, allergy, kwargs)
    

    If a class implements an interface, the class must contain all the methods of the interface, including all incoming arguments . In the example below, the HumanInterface interface contains the eat method, which takes 4 arguments to the input, and the Human class implements it, but implements the eat method with only 1 argument.

    from accessify import implements
    class HumanInterface:
        @staticmethod
        def eat(food, *args, allergy=None, **kwargs):
            pass
    if __name__ == '__main__':
        @implements(HumanInterface)
        class Human:
            @staticmethod
            def eat(food):
                pass
    

    The script will exit with the following error:

    Traceback (most recent call last):
      File "examples/interfaces/single_arguments.py", line 16, in 
        @implements(HumanInterface)
      File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 87, in decorator
        interface_method_arguments=interface_method.arguments_as_string,
    accessify.errors.InterfaceMemberHasNotBeenImplementedWithMismatchedArgumentsException: class Human implements interface member HumanInterface.eat(food, args, allergy, kwargs) with mismatched arguments
    

    If a class implements an interface, the class must contain all the methods of the interface, including incoming arguments and access modifiers . In the example below, the HumanInterface interface contains the private eat method, and the Human class implements it, but does not implement the private access modifier for the eat method.

    from accessify import implements, private
    class HumanInterface:
        @private
        @staticmethod
        def eat(food, *args, allergy=None, **kwargs):
            pass
    if __name__ == '__main__':
        @implements(HumanInterface)
        class Human:
            @staticmethod
            def eat(food, *args, allergy=None, **kwargs):
                pass
    

    The script will exit with the following error:

    Traceback (most recent call last):
      File "examples/interfaces/single_access.py", line 18, in 
        @implements(HumanInterface)
      File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 77, in decorator
        interface_method_name=interface_method.name,
    accessify.errors.ImplementedInterfaceMemberHasIncorrectAccessModifierException: Human.eat(food, args, allergy, kwargs) mismatches HumanInterface.eat() member access modifier.
    

    A class can implement several (unlimited number of) interfaces. If a class implements several interfaces, the class must contain all the methods of all interfaces, including incoming arguments and access modifiers . In the example below, the Human class implements the eat method of the HumanBasicsInterface interface, but does not implement the love method of the HumanSoulInterface interface.

    from accessify import implements
    class HumanSoulInterface:
        def love(self, who, *args, **kwargs):
            pass
    class HumanBasicsInterface:
        @staticmethod
        def eat(food, *args, allergy=None, **kwargs):
            pass
    if __name__ == '__main__':
        @implements(HumanSoulInterface, HumanBasicsInterface)
        class Human:
            def love(self, who, *args, **kwargs):
                pass
    

    The script will exit with the following error:

    Traceback (most recent call last):
      File "examples/interfaces/multiple.py", line 19, in 
        @implements(HumanSoulInterface, HumanBasicsInterface)
      File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 66, in decorator
        interface_method_arguments=interface_method.arguments_as_string,
    accessify.errors.InterfaceMemberHasNotBeenImplementedException: class Human does not implement interface member HumanBasicsInterface.eat(food, args, allergy, kwargs)
    

    Killer feature - an interface method can “state” what errors a method of a class that implements it should “throw”. In the example below, it is “declared” that the “love” method of the “HumanInterface” interface should throw an exception “HumanDoesNotExistError” and
    “HumanAlreadyInLoveError”, but the “love” method of the “Human” class does not “throw” one of them.

    from accessify import implements, throws
    class HumanDoesNotExistError(Exception):
        pass
    class HumanAlreadyInLoveError(Exception):
        pass
    class HumanInterface:
        @throws(HumanDoesNotExistError, HumanAlreadyInLoveError)
        def love(self, who, *args, **kwargs):
            pass
    if __name__ == '__main__':
        @implements(HumanInterface)
        class Human:
            def love(self, who, *args, **kwargs):
                if who is None:
                    raise HumanDoesNotExistError('Human whom need to love does not exist')
    

    The script will exit with the following error:

    Traceback (most recent call last):
      File "examples/interfaces/throws.py", line 21, in 
        @implements(HumanInterface)
      File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/accessify/interfaces.py", line 103, in decorator
        class_method_arguments=class_member.arguments_as_string,
    accessify.errors.DeclaredInterfaceExceptionHasNotBeenImplementedException: Declared exception HumanAlreadyInLoveError by HumanInterface.love() member has not been implemented by Human.love(self, who, args, kwargs)
    

    To summarize, using the library:

    • You can implement one or more interfaces.
    • Interfaces are combined with access modifiers.
    • You will get a separation of interfaces and abstract classes ( abc module in Python ), now you do not need to use abstract classes as interfaces if you did (I did).
    • Compared to abstract classes. If you have not defined all the arguments of the method from the interface, you will get an error using an abstract class - no.
    • Compared to abstract classes. Using interfaces, you will get an error while creating the class (when you wrote the class and called the * .py file ). In abstract classes, you will get an error already at the stage of calling a method of a class object.

    How it works:

    1. Using the built-in inspect library in the implements decorator, all methods of the class and its interfaces - inspect.getmembers () are obtained . A unique index of a method is a combination of its name and type (staticmethod, property, and so on).
    2. A via inspect.signature () - method arguments.
    3. We loop through all the methods of the interface, and see if there is such a method (by a unique index) in the class that implements the interface, whether the incoming arguments are the same, whether the access modifiers are the same, whether the method implements the declared errors in the interface method.

    Thank you for your attention to the article. Link to the project on Github .

    Also popular now: