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:
- The private or protected decorator - the most “high" decorator, fires before the class method , which was declared a private or protected access modifier.
- 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) - 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:
- 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).
- A via inspect.signature () - method arguments.
- 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 .