Salut, Habr. Récemment, je suis devenu fou de conception - modificateurs d'accès et interfaces, puis je l'ai porté au langage de programmation Python. Je demande sous kat - je partage les résultats et comment cela fonctionne. Pour ceux qui sont intéressés, à la fin de l'article, il y a un lien vers le projet sur Github.
Modificateurs d'accès
Les modificateurs d'accès restreignent l'accès aux objets - aux méthodes de leur classe ou aux classes enfants - aux méthodes de leur classe parente. L'utilisation de modificateurs d'accès permet de masquer les données dans la classe afin que personne à l'extérieur ne puisse interférer avec le travail de cette classe.
les méthodes
privées sont disponibles uniquement à l'intérieur de la classe,
protégées (à l'intérieur) - à l'intérieur de la classe et dans les classes enfants.
Comment les méthodes privées et protégées sont implémentées en Python
Spoiler - au niveau de l'accord selon lequel les adultes ne les appelleront tout simplement pas en dehors de la classe. Avant les méthodes privées, vous devez écrire un double soulignement, avant un protégé. Et vous pouvez toujours accéder aux méthodes, malgré leur accès "limité".
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()
Les inconvénients suivants peuvent être déterminés:
- Si la méthode _start_engine mettait à jour certaines variables de classe ou conservait l'état, et ne renvoyait pas simplement un «calcul stupide», vous auriez pu casser quelque chose pour un travail futur avec la classe. Vous ne vous autorisez pas à réparer quelque chose dans le moteur de votre voiture, car vous n'irez nulle part, non?
- Le point coulant du précédent - pour vous assurer que vous pouvez "en toute sécurité" (appeler la méthode ne nuit pas à la classe elle-même) utiliser une méthode protégée - vous devez examiner son code et passer du temps.
- Les auteurs des bibliothèques espèrent que personne n'utilise les méthodes protégées et privées des classes que vous utilisez dans vos projets. Par conséquent, ils peuvent changer son implémentation dans n'importe quelle version (ce qui n'affectera pas les méthodes publiques en raison de la compatibilité descendante, mais vous en souffrirez).
- L'auteur de la classe, votre collègue, s'attend à ce que vous n'augmentiez pas la dette technique du projet en utilisant une méthode protégée ou privée en dehors de la classe qu'il a créée. Après tout, celui qui le refactorisera ou le modifiera (méthode de classe privée) devra s'assurer (par exemple par des tests) que ses modifications ne casseront pas votre code. Et s'ils le cassent, il devra passer du temps à essayer de résoudre ce problème (avec une béquille, car il en a besoin hier).
- Peut-être vous assurez-vous que les autres programmeurs n'utilisent pas de méthodes protégées ou privées lors de la révision du code et «battent pour ça», alors passez du temps.
Comment implémenter des méthodes protégées à l'aide d'une bibliothèque
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()
Tenter d'appeler la méthode
start_engine en dehors de la classe entraînera l'erreur suivante (la méthode n'est pas disponible selon la stratégie d'accès):
Traceback (most recent call last): File "examples/access/private.py", line 24, in <module> 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
Utilisation de la bibliothèque:
- Vous n'avez pas besoin d'utiliser de soulignements moches (subjectifs) ou de doubles soulignements.
- Vous obtenez une belle méthode (subjective) pour implémenter des modificateurs d'accès dans le code - des décorateurs privés et protégés .
- Transférer la responsabilité de la personne à l'interprète.
Comment ça marche:
- Le décorateur privé ou protégé - le décorateur le plus «haut», se déclenche avant la méthode class , qui a été déclarée modificateur d'accès privé ou protégé.
- À l'aide de la bibliothèque d' inspection intégrée, le décorateur récupère l' objet actuel de la pile d'appels - inspect.currentframe () . Cet objet a les attributs suivants qui nous sont utiles: l'espace de noms (locaux) et le lien vers l'objet précédent de la pile d'appels (l'objet qui appelle la méthode avec le modificateur d'accès).
(Illustration très simplifiée)
- inspect.currentframe (). f_back - utilisez cet attribut pour vérifier si l'objet précédent de la pile d'appels se trouve ou non dans le corps de la classe. Pour ce faire, regardez l'espace de noms - f_locals . S'il y a un auto- attribut dans l'espace de noms, la méthode est appelée à l'intérieur de la classe, sinon, à l'extérieur de la classe. Si vous appelez une méthode avec un modificateur d'accès privé ou protégé en dehors de la classe, il y aura une erreur de stratégie d'accès.
Interfaces
Les interfaces sont un contrat d'interaction avec une classe qui l'implémente. L'interface contient les signatures de méthode (le nom des fonctions, les arguments d'entrée) et la classe qui implémente l'interface, après les signatures, implémente la logique. En résumé, si deux classes implémentent la même interface, vous pouvez être sûr que les deux objets de ces classes ont les mêmes méthodes.
Exemple
Nous avons une classe
User qui utilise l'objet de
stockage pour créer un nouvel utilisateur.
class User: def __init__(self, storage): self.storage = storage def create(self, name): return storage.create_with_name(name=name)
Vous pouvez enregistrer l'utilisateur dans la base de données à l'aide de
DatabaseStorage.create_with_name .
class DatabaseStorage: def create_with_name(self, name): ...
Vous pouvez enregistrer l'utilisateur dans des fichiers à l'aide de
FileStorage.create_with_name .
class FileStorage: def create_with_name(self, name): ...
Étant donné que les signatures des méthodes
create_with_name (nom, arguments) sont les mêmes pour les classes - la classe
User n'a pas à se soucier de l'objet auquel elle a été substituée si les deux ont les mêmes méthodes. Cela peut être réalisé si les classes
FileStorage et
DatabaseStorage implémentent la même interface (c'est-à-dire qu'elles sont liées par le contrat pour définir une méthode avec une logique à l'intérieur).
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=...)
Comment travailler avec des interfaces à l'aide de la bibliothèque
Si une classe implémente une interface, la classe doit contenir
toutes les méthodes de l'interface . Dans l'exemple ci-dessous, l'interface HumanInterface contient la méthode eat et la classe Human l'implémente, mais n'implémente pas la méthode eat.
from accessify import implements class HumanInterface: @staticmethod def eat(food, *args, allergy=None, **kwargs): pass if __name__ == '__main__': @implements(HumanInterface) class Human: pass
Le script se termine avec l'erreur suivante:
Traceback (most recent call last): File "examples/interfaces/single.py", line 18, in <module> @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)
Si une classe implémente une interface, la classe doit contenir toutes les méthodes de l'interface,
y compris tous les arguments entrants . Dans l'exemple ci-dessous, l'interface HumanInterface contient la méthode eat, qui prend 4 arguments en entrée, et la classe Human l'implémente, mais implémente la méthode eat avec seulement 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
Le script se termine avec l'erreur suivante:
Traceback (most recent call last): File "examples/interfaces/single_arguments.py", line 16, in <module> @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
Si une classe implémente une interface, la classe doit contenir toutes les méthodes de l'interface, y compris les arguments entrants et
les modificateurs d'accès . Dans l'exemple ci-dessous, l'interface HumanInterface contient la méthode eat privée et la classe Human l'implémente, mais n'implémente pas le modificateur d'accès privé pour la méthode eat.
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
Le script se termine avec l'erreur suivante:
Traceback (most recent call last): File "examples/interfaces/single_access.py", line 18, in <module> @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.
Une classe peut implémenter plusieurs interfaces (nombre illimité). Si une classe implémente plusieurs interfaces, la classe doit contenir
toutes les méthodes de toutes les interfaces, y compris les arguments entrants et les modificateurs d'accès . Dans l'exemple ci-dessous, la classe Human implémente la méthode eat de l'interface HumanBasicsInterface, mais n'implémente pas la méthode love de l'interface HumanSoulInterface.
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
Le script se termine avec l'erreur suivante:
Traceback (most recent call last): File "examples/interfaces/multiple.py", line 19, in <module> @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)
Fonctionnalité de tueur - une méthode d'interface peut «indiquer» quelles erreurs une méthode d'une classe qui l'implémente doit «lancer». Dans l'exemple ci-dessous, il est «déclaré» que la méthode «love» de l'interface «HumanInterface» doit lever une exception «HumanDoesNotExistError» et
«HumanAlreadyInLoveError», mais la méthode «love» de la classe «Human» n'en «jette» pas une.
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')
Le script se termine avec l'erreur suivante:
Traceback (most recent call last): File "examples/interfaces/throws.py", line 21, in <module> @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)
Pour résumer, en utilisant la bibliothèque:
- Vous pouvez implémenter une ou plusieurs interfaces.
- Les interfaces sont combinées avec des modificateurs d'accès.
- Vous obtiendrez une séparation des interfaces et des classes abstraites ( module abc en Python ), maintenant vous n'avez plus besoin d'utiliser des classes abstraites comme interfaces si vous l'avez fait (je l'ai fait).
- Par rapport aux classes abstraites. Si vous n'avez pas défini tous les arguments de méthode à partir de l'interface, vous obtiendrez une erreur en utilisant une classe abstraite - non.
- Par rapport aux classes abstraites. En utilisant des interfaces, vous obtiendrez une erreur lors de la création de la classe (lorsque vous avez écrit la classe et appelé le fichier * .py ). Dans les classes abstraites, vous obtiendrez déjà une erreur au stade de l'appel d'une méthode d'un objet de classe.
Comment ça marche:
- En utilisant la bibliothèque d' inspection intégrée dans le décorateur d' implémentations , toutes les méthodes de la classe et ses interfaces - inspect.getmembers () sont obtenues . Un index unique d'une méthode est une combinaison de son nom et de son type (méthode statique, propriété, etc.).
- Et avec inspect.signature () , les arguments de la méthode.
- Nous parcourons toutes les méthodes de l'interface et voyons s'il existe une telle méthode (par un index unique) dans la classe qui implémente l'interface, si les arguments entrants sont les mêmes, si les modificateurs d'accès sont les mêmes, si la méthode implémente les erreurs déclarées dans la méthode d'interface.
Merci de votre attention sur l'article.
Lien vers le projet sur Github .