Bonjour,
L'un des plaisirs de ma vie est de rendre détectable par une analyse statique du code un maximum d'oublis et d'erreurs d'étourderie.
Mais cette démarche m'amène parfois à concevoir des constructeurs privés. Or, en Python, il n'y a pas de moyen standard de signaler qu'une méthode __init__ est privée à un module donné.
Je vais illustrer cela par un exemple.
Admettons que l'on ait du code qui manipule des codes clients. Pour modéliser cela, dans le code ci-dessous, j'ai écrit une bête fonction print_something_about_the_client_code. Mais, dans la réalité, le code qui manipule des codes clients peut être beaucoup plus gros.
Admettons que tout code client doive respecter un certain format, par exemple être une chaîne non vide de lettres majuscules entre A et Z, sans diacritiques (accents, cédilles, etc.) J'ai modélisé cela par une fonction valid_format_for_ClientCode.
Les codes clients sont lus dans une source. Pour modéliser cela, dans le code ci-dessous, j'ai écrit un simple code qui lit le premier argument de la ligne de commande, qui est supposé être un code client.
Entre le moment où on lit la source (sys.argv[1]) et celui où on manipule le code client (print_something_about_the_client_code), je veux obliger l'utilisateur à réfléchir au cas où la source ne serait pas un format valide de code client. En outre, si l'utilisateur appelle print_something_about_the_client_code(something), je veux pouvoir vérifier par une analyse statique du code que le flot de contrôle a bien bifurqué via un if valid_format_for_ClientCode(something) et que l'appel print_something_about_the_client_code(something) se trouve dans la bonne branche.
Pour cela, j'ai écrit le code suivant, en Python 3.7.2, vérifié avec mypy 0.670 :
clientcode.py :
Code utilisateur :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49 from __future__ import annotations __all__ = [ 'as_ClientCode_or_None', 'as_ClientCode_or_raise', 'ClientCode', 'InvalidClientCodeFormatException', 'valid_format_for_ClientCode' ] __author__ = 'Pyramidev' from typing import Optional def as_ClientCode_or_raise(string: str) -> ClientCode: result = as_ClientCode_or_None(string) if result is not None: return result raise InvalidClientCodeFormatException(f'"{string}" does not have a valid client code format.') def as_ClientCode_or_None(string: str) -> Optional[ClientCode]: if valid_format_for_ClientCode(string): return ClientCode(string, This_constructor_is_private=0, Do_not_call_it_outside_this_module=0) return None def valid_format_for_ClientCode(string: str) -> bool: return bool(string) and all(map(lambda char: char >= 'A' and char <= 'Z', string)) class ClientCode: def __init__(self, string: str, *, This_constructor_is_private: int, Do_not_call_it_outside_this_module: int) -> None: self.__string = string @property def string(self) -> str: return self.__string class InvalidClientCodeFormatException(Exception): pass
Dans mon exemple, pour pouvoir appeler print_something_about_the_client_code, l'utilisateur doit construire un objet de type ClientCode.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 __author__ = 'Pyramidev' import os import sys from clientcode import (as_ClientCode_or_None, as_ClientCode_or_raise, ClientCode, InvalidClientCodeFormatException) def print_something_about_the_client_code(client_code: ClientCode) -> None: print(f'"{client_code.string}" has a valid client code format.') def mainV1() -> None: first_arg = sys.argv[1] client_code = as_ClientCode_or_None(first_arg) if client_code is not None: print_something_about_the_client_code(client_code) else: print('There is nothing to say.') os.system('pause') def mainV2() -> None: first_arg = sys.argv[1] try: client_code = as_ClientCode_or_raise(first_arg) print_something_about_the_client_code(client_code) except InvalidClientCodeFormatException: print('There is nothing to say.') os.system('pause') def mainV3() -> None: first_arg = sys.argv[1] client_code = as_ClientCode_or_None(first_arg) if client_code is not None: print_something_about_the_client_code(client_code) else: print('There is nothing to say.') print_something_about_the_client_code(client_code) # Erreur détectée par mypy print_something_about_the_client_code(client_code) # Erreur détectée par mypy os.system('pause') if __name__ == '__main__': mainV1()
Pour cela, il est obligé de passer par une des deux fonctions suivantes : as_ClientCode_or_None (appelée dans mainV1 et mainV3) ou as_ClientCode_or_raise (appelée dans mainV2).
La fonction as_ClientCode_or_None retourne None si le paramètre n'a pas un format valide de code client. Dans mainV1 et mainV3, quand on écrit client_code = as_ClientCode_or_None(first_arg), client_code est est de type Optional[ClientCode]. Mais, à l'intérieur du if client_code is not None, client_code devient de type ClientCode et on peut appeler print_something_about_the_client_code(client_code). Si on appelle print_something_about_the_client_code(client_code) en dehors de ce if, alors mypy signale une erreur.
La fonction as_ClientCode_or_raise lève une exception de type InvalidClientCodeFormatException si le paramètre n'a pas un format valide de code client. Notez que j'ai fait exprès de suffixer la fonction par _or_raise pour inciter l'utilisateur à réfléchir au cas où une exception est lancée, car peut-être qu'il ne faut pas simplement propager l'exception mais écrire un except InvalidClientCodeFormatException.
Le constructeur de ClientCode me pose problème :
- Il retourne forcément un ClientCode, pas un Optional[ClientCode].
- Il s'appelle forcément ClientCode. On ne peut pas le renommer avec un suffixe _or_raise.
- Il n'existe aucune convention officielle pour le rendre privé.
Du coup, j'ai pensé à ajouter des arguments nommés bidons obligatoires à __init__ : This_constructor_is_private et Do_not_call_it_outside_this_module. Cela simule un constructeur privé.
La vérification du format est faite dans as_ClientCode_or_None, qui est appelée par as_ClientCode_or_raise.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 def __init__(self, string: str, *, This_constructor_is_private: int, Do_not_call_it_outside_this_module: int) -> None: self.__string = string
À la place, j'aurais aussi pu faire ça :
Mais je me suis dit qu'une fonction as_ClientCode_or_raise avec un seul paramètre était plus commode pour le code utilisateur. Donc je préfère que le constructeur reste privé.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 def __init__(self, string: str, *, This_constructor_raises_if_the_format_is_invalid: int) -> None: if not valid_format_for_ClientCode(string): raise InvalidClientCodeFormatException(f'"{string}" does not have a valid client code format.') self.__string = string
Qu'en pensez-vous ?
Partager