IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Python Discussion :

Typage statique et constructeur privé


Sujet :

Python

  1. #1
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 460
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 460
    Points : 6 064
    Points
    6 064
    Par défaut Typage statique et constructeur privé
    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 : 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
    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
     
    __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()
    Dans mon exemple, pour pouvoir appeler print_something_about_the_client_code, l'utilisateur doit construire un objet de type ClientCode.
    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é.
    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 vérification du format est faite dans as_ClientCode_or_None, qui est appelée par as_ClientCode_or_raise.

    À la place, j'aurais aussi pu faire ça :
    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
    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é.

    Qu'en pensez-vous ?

  2. #2
    Expert éminent
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    3 784
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 3 784
    Points : 7 043
    Points
    7 043
    Par défaut
    Bonsoir,

    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.
    Je ne comprend pas la corrélation entre ta démarche et les constructeurs privés. Si tu pouvais expliquer ?

    Aussi, pour vérifier il existe les tests unitaires, mais je vais attendre la réponse à ma question

    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.)
    Sans regarder la suite, j'utiliserai les décorateurs. C'est du sucre syntaxique, mais ça permet de contrôler les arguments d'une fonction avant son exécution proprement.
    Par exemple,

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    @control
    def maFonction():
        # code
    @control pourra être utilisé sur toutes fonctions où ces mêmes arguments sont à contrôler.

    • 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é.
    Dans tout ça j'ai pas compris grand chose, je ne connais pas Optional, qui apparemment simule Union.
    Pourquoi renommer ClientCode ? Ça sent pas bon la bonne pratique
    Je ne vois toujours pas la liaison avec privé.

    Déjà le principe de privé en python n'existe pas réellement, même si syntaxiquement cela existe, ça ne fonctionne pas de la même manière que dans du C++ ou Java, c'est d'une utilité à juste prévenir l'utilisateur.
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  3. #3
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 240
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 240
    Points : 36 696
    Points
    36 696
    Par défaut
    Salut,

    Citation Envoyé par Pyramidev Voir le message
    Or, en Python, il n'y a pas de moyen standard de signaler qu'une méthode __init__ est privée à un module donné.
    Si on convient que cette méthode doit s'exécuter une fois lors de l'import du module, alors il suffit d'avoir des instructions d'initialisation (celles qui ne sont pas encapsulées dans une fonction)... et si on veut chiader le truc on peut toujours "encapsuler" ces instructions dans une fonction appelée __init__ quitte à la détruire une fois exécutée ou fabriquer un décorateur (qui va poser un label visible) qui fait tout çà.

    Tout çà pour dire que votre solution est probablement fort ingénieuse mais que je n'ai peut être pas bien compris le problème (et donc çà me semble un peu over architected).

    - W
    Architectures post-modernes.
    Python sur DVP c'est aussi des FAQs, des cours et tutoriels

  4. #4
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 460
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 460
    Points : 6 064
    Points
    6 064
    Par défaut
    Pour reformuler le problème, il y a :
    • d'un côté, du code qui manipule des codes clients en supposant qu'ils ont un format valide et
    • d'un autre côté, du code qui récupère des chaînes de caractères qui ont peut-être un format valide de code client, mais peut-être pas.


    Donc, algorithmiquement, pour relier ces codes, il faut vérifier si les chaînes ont un format valide de code client et prendre une décision explicite dans le cas où ce format n'est pas valide.

    Mais, comme l'erreur est humaine, il y a des risques d'oubli ou d'erreurs d'étourderie : on pourrait oublier le cas où le format est invalide ou ne pas le tester correctement (ex : étourderie après un réusinage de code).

    Dans ma solution, ce genre d'erreur est automatiquement détecté lors de l'analyse statique du code, dans tous les chemins d'exécution possibles (sauf ceux où on désactiverait le typage statique), avant même d'écrire le moindre test unitaire.

    Les tests unitaires, c'est bien pour repérer les erreurs qui ne sont pas déjà détectées par l'analyse statique du code.

    Dans mon premier message, pour éviter d'écrire un code de 2 km, j'ai limité le code qui manipule les codes clients à une simple fonction print_something_about_the_client_code et j'ai limité le code qui récupère des chaînes de caractères à la simple lecture d'un argument en ligne de commande.

    EDIT 22h41 : des explications en plus :

    Une piste possible de solution serait de créer une classe ClientCode comme ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     
    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) -> None:
    		if not valid_format_for_ClientCode(string):
    			raise InvalidClientCodeFormatException(f'"{string}" does not have a valid client code format.')
    		self.__string = string
     
            @property
    	def string(self) -> str:
    		return self.__string
    Et de l'utiliser ainsi :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    first_arg = sys.argv[1]
    client_code = ClientCode(first_arg)
    print_something_about_the_client_code(client_code)
    Ici pour appeler print_something_about_the_client_code(client_code), on est obligé de construire un ClientCode dont la méthode __init__ gère automatiquement le cas où la chaîne en entrée a un format invalide.

    Mais, au niveau de l'écriture, le code ClientCode(first_arg) signifie juste « je construis un ClientCode à partir de first_arg ». Il ne signifie pas « j'ai bien pensé au cas limite où first_arg n'a pas le bon format et je décide de lancer une erreur dans ce cas, sans l'intercepter ». Ici, l'oubli n'est pas évité. En cas d'oubli, on a juste un comportement par défaut, qui est celui de lancer une erreur sans l'intercepter.

    Alors, dans mon premier message, j'ai proposé d'obliger l'utilisateur à passer par l'une des fonctions as_ClientCode_or_None et as_ClientCode_or_raise. Or, pour qu'ils ne puisse passer que par l'une d'elles, il faut que le constructeur soit privé.

    À présent, mon premier message devrait être plus facile à comprendre.

  5. #5
    Membre confirmé
    Homme Profil pro
    Développeur banc de test
    Inscrit en
    Mai 2014
    Messages
    199
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur banc de test
    Secteur : High Tech - Électronique et micro-électronique

    Informations forums :
    Inscription : Mai 2014
    Messages : 199
    Points : 482
    Points
    482
    Par défaut
    Bonsoir,

    je rejoins l'idée de fred1599 d'utiliser des décorateurs pour vérifier les arguments passés et pouvoir gérer pourquoi pas des autorisations.

    Par expérience (modeste soit-elle) ce n'est pas forcément la bonne approche que de vouloir raisonner dans un autre langage de programmation, on peut passer à côté de méthodes beaucoup plus simples et tout aussi efficaces.


    Voici un exemple personnel qui montre que des fois on peut se prendre la tête pour pas grande chose.

    Pendant mes débuts en Python j'avais appris à placer mon code dans la condition if __name__ == '__main__': mais sans trop comprendre ce que cela signifiait, si c'était un code magique qui plaçait mon code dans une sorte de fonction main comme en C.

    Puis vient la découverte à mon insu des variables implicites globales qui m'a fait comprendre le risque d'un langage faiblement typé et souple, qui nécessite d'être beaucoup plus rigoureux qu'en C si on ne veut pas se faire avoir.

    J'en étais arrivé à placer à la fin de chaque fonction un code de détection d'appels de variables globales, pour être sûr de ne jamais en utiliser sans le savoir. (Oui c'est ridicule)

    Pour les curieux ça donnait ça :
    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
    # -*- coding: utf-8 -*-
    # Python 3
    # µtf±8
     
    def globals_detection(key_exceptions=tuple()): # {
        """Globals detection to help to track undefined local variables
        """
        import sys
        frame = sys._getframe(1) # depth = 1
        var_list = frame.f_code.co_names # Return called variables and function names
        var_filtered_list = list()
        for key in var_list: # {
            if key not in globals(): continue
            if key in key_exceptions: continue
            if getattr(globals()[key], '__name__', '__main__') == '__main__': # { If in the local file scope
                var_filtered_list.append(key)
            # }
        # } for
        if len(var_filtered_list):
            print(
                "[{}]".format(frame.f_code.co_name),
                "Global variable{} detected:".format(["", "s"][len(var_filtered_list) > 1]),
                ", ".join(var_filtered_list),
                file=sys.stderr,
            )
    # } globals_detection
     
    def function(): # {
        a = 0
        b = a.to_bytes(1, 'big', signed=False)
        c = z
        globals_detection()
    # } function
     
    if __name__ == '__main__': # {
        x = 1
        z = 2
        function()
    # }


    Et plus tard j'ai enfin compris que j'écrivais depuis toujours mon code dans le __main__, en global en somme, et qu'il suffisait simplement de placer mon code dans une fonction pour éviter ces erreurs idiotes.

    Bref j'avais passé mon temps à endurcir mon code d'introspections et de vérifications inutiles, alors qu'il suffisait d'utiliser la bonne approche dès le départ.


    Pour revenir aux décorateurs, si on veut imposer une certaine rigueur dans l'utilisation de fonctions et de classes on peut employer des décorateurs pour vérifier ou même convertir des arguments, et ça tient sur 1 ligne à l'appeler !

    Je n'ai pas beaucoup d'exemples mais en voici un qui permet de convertir des arguments selon les types passés en argument du décorateur.
    Un remplacement par isinstance ou issubclass permettrait de vérifier les arguments.

    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
    # -*- coding: utf-8 -*-
    # Python 3
    # µtf±8
     
    import functools # Higher-order functions and operations on callable objects
     
    def convert_args(*types_args, **kw):
        def decorator(func):
            @functools.wraps(func)
            def newf(*args):
                """newf"""
                return func(*(type_arg(arg) for arg, type_arg in zip(args, types_args)))
            return newf
        return decorator
     
    @convert_args(float, int, str, lambda x:x+1)
    def function(*args):
        """Hello
        """
        return [0, ] + list(args)
     
    print(function(1, 2, 3, 4))
    print(function.__name__)
    print(function.__doc__)
    Mais dans ce cas là autant utiliser le type hint (annotations) et de faire une moulinette qui va vérifier les appels, sans doute que certaines IDE ou modules savent l'analyser.

  6. #6
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 460
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 460
    Points : 6 064
    Points
    6 064
    Par défaut
    Citation Envoyé par YCL-1 Voir le message
    Mais dans ce cas là autant utiliser le type hint (annotations) et de faire une moulinette qui va vérifier les appels, sans doute que certaines IDE ou modules savent l'analyser.
    Je crois qu'il y a eu un problème de communication qui vient du fait que j'ai directement utilisé les termes « typage statique » et « analyse statique du code » sans rappeler leurs définitions.

    Dans mon premier message, quand j'ai parlé d'« analyse statique du code », il s'agissait bien d'analyser le code avec une moulinette qui ne fait que lire le code, sans l'exécuter. Et quand j'ai parlé de « typage statique », il s'agissait bien de vérifier le typage lors de l'analyse statique du code. Quand les vérifications se font au runtime, ce n'est pas « statique ».

    Il existe des outils qui permettent d'analyser le typage statique d'un code Python à partir des annotations de type, dont celui que j'ai cité : mypy.

    Dans le bout de code suivant de mon premier message :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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')
    mypy détecte ces deux erreurs directement en lisant le code, sans l'exécuter.
    Dans le premier cas, il râle car None n'est pas convertible en ClientCode.
    Dans le deuxième cas, il râle car Option[ClientCode] n'est pas convertible en ClientCode.

    as_ClientCode_or_None(first_arg) a pour type Option[ClientCode], c'est à dire qu'il s'agit soit de None, soit d'un objet de type ClientCode. Mais print_something_about_the_client_code(client_code) n'est correct que si client_code est de type ClientCode. En analysant le code, mypy arrive à déduire que, dans le if client_code is not None, client_code est bien un objet de type ClientCode. Par contre, dans les deux autres occurrences de print_something_about_the_client_code(client_code), mypy détecte qu'il y a un problème.

  7. #7
    Expert confirmé Avatar de papajoker
    Homme Profil pro
    Développeur Web
    Inscrit en
    Septembre 2013
    Messages
    2 076
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Nièvre (Bourgogne)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Septembre 2013
    Messages : 2 076
    Points : 4 392
    Points
    4 392
    Par défaut
    bonjour,

    créer ClientCode pour du typage fort pourquoi pas, mais si je comprends bien le problème c'est que tu dois créer 2 constructeurs pour ne pas toujours tester la valeur ?

    Pourquoi ne pas simplement créer une méthode check(default="raise") qui retourne self ou None/Exception en cas de mauvais regex
    parfois tu peux faire un code=ClientCode("") et pour d'autres code=ClientCode("aa").check(None)ou un __init(value, defaut="raise") et faire un code=ClientCode("aa", None)
    $moi= ( !== ) ? : ;

  8. #8
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 460
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 460
    Points : 6 064
    Points
    6 064
    Par défaut
    Je vais faire un récapitulatif pour recoller dans le bon ordre les morceaux éparpillés dans mes trois messages précédents.
    papajoker, je ne suis pas sûr d'avoir entièrement compris ton message, mais je crois que la fin de mon nouveau long message répond à tes questions.

    Il y a :
    • d'un côté, un code qui récupère une chaîne de caractères qui a peut-être un format valide de code client, mais peut-être pas et
    • d'un autre côté, du code qui manipule le code client en supposant que le format est valide.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    ---------------      ----------------
    | Lire une    |      | Utiliser le  |
    | chaîne.     |      | code client. |
    ---------------      ----------------
    Pour relier ces deux morceaux de code existants, je veux obliger le code appelant à vérifier que la chaîne a bien un format valide de code client et je veux obliger le code appelant à décider ce qu'il faut faire quand la chaîne n'a pas un format valide de code client. Donc, quand on lit le code appelant, je veux qu'on puisse le traduire dans sa tête par :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ---------------      --------------------         ----------------
    | Lire une    |      | Format valide    |   Oui   | Utiliser le  |
    | chaîne.     |------| de code client ? |---------| code client. |
    ---------------      --------------------         ----------------
                                |
                                | Non
                                |
                         ----------------------
                         | Faire autre chose. |
                         ----------------------
    Je ne veux pas que le code appelant ressemble à ceci d'un côté :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    ---------------      ----------------
    | Lire une    |      | Utiliser le  |
    | chaîne.     |------| code client. |
    ---------------      ----------------
    et que l'implémentation se comporte comme cela de l'autre :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ---------------      --------------------         ----------------
    | Lire une    |      | Format valide    |   Oui   | Utiliser le  |
    | chaîne.     |------| de code client ? |---------| code client. |
    ---------------      --------------------         ----------------
                                |
                                | Non
                                |
                         -------------------------------------
                         | Faire le comportement par défaut. |
                         -------------------------------------
    avec un comportement par défaut qui risque de ne pas avoir été anticipé par l'auteur du code appelant.

    Comme l'erreur est humaine, il y a des risques d'oubli ou d'erreurs d'étourderie : on pourrait oublier le cas où le format est invalide ou ne pas le tester correctement (ex : étourderie après un réusinage de code). Je veux que ce genre d'erreur soit détecté facilement, idéalement par une analyse statique du code, c'est-à-dire en utilisant un outil qui détecte des problèmes dans le code simplement en le lisant, sans l'exécuter.

    Pour certains développeurs, la manière normale de détecter les erreurs, c'est d'utiliser des tests unitaires. Mais, quand une analyse statique du code regarde tous les chemins d'exécution possibles et garantit qu'un certain type d'erreur est absent, c'est bien plus puissant que des tests unitaires. Normalement, les tests unitaires devraient se concentrer sur les autres erreurs qui peuvent échapper aux outils actuels d'analyse statique du code.

    En Python, on peut créer un type ClientCode qui représente un code client avec un format valide et on peut s'arranger pour que le seul moyen d'obtenir un ClientCode à partir d'une chaîne de caractères soit de vérifier que cette chaîne a un format valide de code client.

    Dès que l'on sait qu'un objet est de type ClientCode, on n'a plus besoin de se demander si le format est valide : c'est garanti par le type.
    Si en plus on utilise le typage statique, alors on peut garantir directement lors de l'analyse statique du code quelles variables ont des formats valides de code client. Il n'y a alors pas besoin d'avoir recours à des tests unitaires pour vérifier que des chaînes ont un format valide : c'est déjà garanti par le type ClientCode.

    Il reste une étape : comment vérifier lors de l'analyse statique du code que toute construction d'un objet de type ClientCode ait un code qui ressemble au schéma ci-dessous ?
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ---------------      --------------------         ----------------
    | Lire une    |      | Format valide    |   Oui   | Utiliser le  |
    | chaîne.     |------| de code client ? |---------| code client. |
    ---------------      --------------------         ----------------
                                |
                                | Non
                                |
                         ----------------------
                         | Faire autre chose. |
                         ----------------------
    Comment vérifier que « Utiliser le code client. » se trouve dans le « Oui » de « Format valide de code client ? »
    Autrement dit, comment obliger le code appelant à séparer le flot de contrôle en deux branche avec « Format valide de code client ? » et comment vérifier lors de l'analyse statique que « Utiliser le code client. » se trouve dans la branche « Oui » ?
    En Python 3, on a deux mécanismes possibles : le type union (par exemple Option[ClientCode]) et la levée d'exception.

    Dans mon code en exemple, pour me limiter à quelque chose de simple :
    • « Lire une chaîne » correspond à first_arg = sys.argv[1].
    • « Format valide de code client ? » correspond à if valid_format_for_ClientCode(first_arg).
    • « Utiliser le code client. » correspond à print_something_about_the_client_code(client_code).
    • « Faire autre chose. » correspond à print('There is nothing to say.').


    Voici le code (déjà publié dans le 1er message) en Python 3.7.2, vérifié avec mypy 0.670, un analyseur de typage statique :

    clientcode.py :
    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
    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
     
    __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()
    Le mécanisme avec le type union est illustré dans mainV1 et mainV3 :
    as_ClientCode_or_None(first_arg) a pour type Option[ClientCode], c'est à dire qu'il s'agit soit de None, soit d'un objet de type 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.
    Pour rappel, mypy détecte les erreurs directement en lisant le code, sans l'exécuter.

    Le mécanisme avec la levée d'exception est illustré dans mainV2 :
    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. On sépare donc bien le flot de contrôle en deux branches : une dans le cas où le format est valide et une autre dans le cas où le format est invalide.
    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 levée, car peut-être qu'il ne faut pas simplement propager l'exception mais écrire un except InvalidClientCodeFormatException. Sans ce suffixe _or_raise, on se retrouverait dans le cas du comportement par défaut (lever une exception) qui risque de ne pas avoir été anticipé par l'auteur du code appelant (en cas de format invalide).

    Il s'agit de deux mécanismes possibles. J'ai mis les deux pour illustrer.

    Pour obliger le code utilisateur à écrire soit as_ClientCode_or_None(first_arg), soit as_ClientCode_or_raise(first_arg), il ne faut pas que l'utilisateur puisse écrire directement ClientCode(first_arg). Alors, j'ai fait un constructeur privé.

    Après avoir lu ceci, vous allez peut-être vous dire « Pourquoi as_ClientCode_or_raise(first_arg) au lieu d'un code du genre ClientCode(first_arg, raise_if_invalid_format=dummy) ? »
    Réponse : une fonction à un seul argument est plus commode. Par exemple, si strings est un itérable de chaînes de caractères, alors on peut écrire directement map(as_ClientCode_or_raise, strings), ce qui est plus pratique que map(lambda s: ClientCode(s, raise_if_invalid_format=dummy), strings).

  9. #9
    Expert confirmé Avatar de papajoker
    Homme Profil pro
    Développeur Web
    Inscrit en
    Septembre 2013
    Messages
    2 076
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Nièvre (Bourgogne)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Septembre 2013
    Messages : 2 076
    Points : 4 392
    Points
    4 392
    Par défaut
    c'est pourtant ce que j'ai écrit mais il me semble que tu désires tout compliquer (déjà dans l'exposition de ton problème)

    mon idée en code "simple" qui doit reprendre tes 3 cas :
    Citation Envoyé par papajoker Voir le message
    créer une méthode check(default="raise") qui retourne self ou None/Exception en cas de mauvais regex
    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
    class ClientCode():
        def __init__(self, value: str):
            self.data = value
            self.check(None)
     
        def __str__(self):
            return self.data
     
        def __bool__(self):
            return self.data is not None
     
        def check(self, default="raise"):
            """regex bidon"""
            if 'b' in self.data:
                if default == "raise":
                    raise Exception('oops')
                else:
                    self.data = None
                    return None
            else:
                return self
     
     
    code = ClientCode("aaa")
    print('code:', code)
     
    #V1 : test not
    code = ClientCode("abba")#.check(None)
    if not code:
        print('erreur V1')
    else:
        print('code:', code)
     
     
    #v2 exception
    try:
        code = ClientCode("abba").check()
        print('code:', code)
    except Exception:
        print('Exception V2')
     
    #v3 test type
    code = ClientCode("abba").check(None) # problème ici il faut impérativement chech(None)
    print(type(code)) # retourne <class 'NoneType'>
    # donc erreur afficherClientCode(code)
    j'ai juste transformé tes fonctions as_ClientCode_or_raise et as_ClientCode_or_none en une méthode check()
    $moi= ( !== ) ? : ;

  10. #10
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 460
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 460
    Points : 6 064
    Points
    6 064
    Par défaut
    @papajoker : dans ta solution, le type ClientCode représente un code client qui peut avoir un format valide ou invalide. Donc, si une fonction a un paramètre de type ClientCode, on n'a pas la garantie que le format est valide. Si un algo part du principe que le format est valide mais que le développeur oublie de vérifier en amont si le format est valide (avec check), cet oubli n'est pas détecté lors de l'analyse statique du code.
    Dans ma solution, le type ClientCode représente un code client qui a toujours un format valide. Donc, si une fonction a un paramètre de type ClientCode, on a la garantie que le format est valide. On peut alors passer cet objet de type ClientCode de paramètre en paramètre sans s'embêter à appeler une méthode check. Dans ma solution, la vérification du format se fait à un seul endroit : celui où on récupère une variable de type ClientCode, soit en appelant as_ClientCode_or_raise, soit en convertissant un objet de type Optional[ClientCode] en ClientCode (via un if).

  11. #11
    Expert éminent
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    3 784
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 3 784
    Points : 7 043
    Points
    7 043
    Par défaut
    Pourquoi tu ne vérifies pas le bon format dans ClientCode,

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    class ClientCode:
        def __init__(self, string):
            assert string
            self.valid_string = string.upper()
    Tu retires l'assert avec l'option -O quand tout est ok dans tes vérifications...
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  12. #12
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 460
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 460
    Points : 6 064
    Points
    6 064
    Par défaut
    Citation Envoyé par fred1599 Voir le message
    Pourquoi tu ne vérifies pas le bon format dans ClientCode,

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    class ClientCode:
        def __init__(self, string):
            assert string
            self.valid_string = string.upper()
    Tu retires l'assert avec l'option -O quand tout est ok dans tes vérifications...
    Parce que les assert ne sont vérifiés qu'au runtime. En cas d'erreur de programmation dans le code appelant, pour détecter l'erreur, il faut exécuter un cas dans lequel le code appelant appelle ClientCode(string) en oubliant de vérifier que string a un format valide et dans lequel string a effectivement un format invalide. Pour augmenter la probabilité que l'erreur de programmation soit détectée avant la mise en prod, on peut blinder le code de tests unitaires, mais ce n'est pas aussi puissant que de garantir que le format est valide directement lors de l'analyse statique du code.

    En utilisant le typage statique et en obligeant le code appelant à passer par as_ClientCode_or_raise ou as_ClientCode_or_None, il est impossible de construire un ClientCode qui contient une chaîne avec un format invalide, sauf si on triche en désactivant le typage statique dans une portion du code. Tous les chemins d'exécution possibles sont vérifiés par l'analyse statique du code, avant même d'écrire le moindre test unitaire.

    Les tests unitaires servent alors à détecter d'autres types d'erreurs qui peuvent échapper aux outils actuels d'analyse statique du code.

    EDIT 13h03 :

    À part ça, avec ton string.upper() appelé dans __init__, on tombe dans le travers du comportement par défaut qui risque de ne pas avoir été anticipé par le code appelant. Si la chaîne en entrée a des minuscules, peut-être qu'il faut la convertir en majuscules, ou peut-être qu'il faut la rejeter. C'est une décision qui doit être prise par le code appelant. Or, quand on lit « ClientCode(string) », si on ne plonge pas dans le code source de __init__, on ne voit pas que __init__ corrige la chaîne à sa manière.

  13. #13
    Expert éminent
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    3 784
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 3 784
    Points : 7 043
    Points
    7 043
    Par défaut
    Ok, cependant je perds le fil de ta question de départ... En gros quelle est ta question, qu'est-ce qu'on peut faire pour t'aider et quel est le résultat attendu ? Parce-que là on se noie dans des syntaxes imbuvables et hyper verbeuses, etc...

    On a bien compris que tu voulais faire de la programmation par contrat, ou quelque chose qui y ressemble, mais je ne vois pas où tu veux en venir au final et comment on peut t'aider.

    EDIT: Si c'est une question MyPy, je pense que tu t'y connaîtras mieux que moi.
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  14. #14
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 460
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 460
    Points : 6 064
    Points
    6 064
    Par défaut
    Citation Envoyé par fred1599 Voir le message
    En gros quelle est ta question, qu'est-ce qu'on peut faire pour t'aider et quel est le résultat attendu ?
    La question de départ tourne autour du constructeur. En gros, je ne veux pas que le code utilisateur puisse écrire directement ClientCode(string), car il pourrait l'écrire sans savoir qu'il y a une contrainte sur le format et, avec cette écriture, cet oubli ne pourrait pas être détecté par une analyse statique du code.
    Il y a plusieurs solutions. La solution qui m'a semblé la plus adaptée à mon besoin était d'avoir un constructeur privé. Mais, pour simuler un constructeur privé, j'ai fait une bidouille dans un style non standard, à coup de paramètres nommés This_constructor_is_private et Do_not_call_it_outside_this_module avec un type int choisi un peu au hasard. Je trouve cela moche et je me demande s'il n'y a pas une meilleure solution.

    Citation Envoyé par fred1599 Voir le message
    On a bien compris que tu voulais faire de la programmation par contrat, ou quelque chose qui y ressemble
    Oui, c'est en quelque sorte de la programmation par contrats, mais avec des contrats vérifiés directement lors de l'analyse statique du code.

  15. #15
    Expert éminent
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    3 784
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 3 784
    Points : 7 043
    Points
    7 043
    Par défaut
    Et si tu mets un underscore,

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    class _ClientCode:
        # suite
    Tu ne veux pas que l'utilisateur utilise la classe finalement, c'est ça ?
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  16. #16
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 460
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 460
    Points : 6 064
    Points
    6 064
    Par défaut
    Je veux que le code utilisateur ait accès à la classe et à tous ses membres publics, sauf le constructeur ClientCode.__call__ (qui appelle __init__).

  17. #17
    Expert confirmé Avatar de papajoker
    Homme Profil pro
    Développeur Web
    Inscrit en
    Septembre 2013
    Messages
    2 076
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Nièvre (Bourgogne)

    Informations professionnelles :
    Activité : Développeur Web
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Septembre 2013
    Messages : 2 076
    Points : 4 392
    Points
    4 392
    Par défaut
    tu peux utiliser inspect dans __init__() pour voir l'appelant et raise si l'appel ne vient pas d'une de tes 2 fabriques
    et je mettrais tes 2 fabriques comme staticmethod de ta classe

    Sinon il y a possibilité d'utiliser __new__ mais pas sur d'avoir tout tes critères ?

    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
    class ClientCode:
        pass
     
    class ClientCode():
     
        def __new__(cls, value, error=None) -> Optional[ClientCode]:
            ret = cls.tester(value, error)
            if ret:
                return super().__new__(cls)
            else:
                return None # retourne mauvais type
     
        @staticmethod
        def tester(value, error="raise"):
            if 'b' in value:
                if error == "raise":
                    raise Exception('oops')
                else:
                    return None
            return True
     
        def __init__(self, value: str):
            self.data = value
     
        def __str__(self):
            return self.data
     
     
    code = ClientCode("aba")
    print(type(code)) #NoneType
    print(code)
     
    code = ClientCode("aba", "raise")
    # exception
    $moi= ( !== ) ? : ;

  18. #18
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 461
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 461
    Points : 9 248
    Points
    9 248
    Billets dans le blog
    6
    Par défaut
    Bonjour,

    J'avoue que le contenu de cette discussion me passe un peu au dessus de la tête. Cependant, s'il s'agit de faire une vérification de conditions et de types sur les arguments passés à une fonction, j'utilise ça depuis longtemps avec un décorateur.

    Mon problème initial était de fournir à une calculatrice des fonctions de calcul, mais au cas où l'utilisateur fournissait de mauvaises données, par exemple un nombre négatif pour une racine carrée réelle, je voulais que chaque fonction concernée renvoie un message d'erreur explicite pour permettre une correction facile. Mais avant, je devais coder plein de tests "if ... raise..." au début de chaque fonction, et ça me semblait pénible (une sorte de pollution du code...). J'ai donc cherché une solution plus pratique.

    J'ai donc fabriqué un décorateur sous forme d'une classe avec arguments, et dont les arguments sont les conditions et les obligations de type sur les arguments de la fonction décorée.

    Voilà un petit exemple sur une fonction qui calcule les mensualités d'un crédit. Mon décorateur s'appelle "verifargs":

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @verifargs("C>=0", "IA>=0", "N>1", C=(int,), IA=(int, float), N=(int,))
    def menscredit(C,IA,N):
        """Retoune la mensualité M d'un prêt C à IA% d'intérêt par an, à rembourser
             en N mois
        """
        # si le capital est nul, il n'y a rien à rembourser
        if C==0:
            return 0
        # si l'intérêt est nul, la mensualité ne dépend plus que de C et N
        if IA==0:
            return C/N
        # calcul de la mensualité dans les autres cas
        I = IA/1200
        return C*I*(1-1/(1-(1+I)**N))
    Les conditions de validité des arguments sont faciles à écrire et les données anormales sont signalées avec un message d'erreur à récupérer:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    try:
        print(menscredit(1000, 5, 24))
    except Exception as msgerr:
        print(msgerr)
    Si je demande: menscredit(1000, 5, 24), ça me répond: 43.87___
    Si je demande: menscredit(-1000, 5, 24), ça me répond: 'Erreur appel "menscredit": échec condition "C>=0"'
    Si je demande: menscredit(1000, "5", 24), ça me répond: 'Erreur appel "menscredit": mauvais type pour "IA"'
    Si je demande: menscredit(1000, 5, 0), ça me répond: 'Erreur appel "menscredit": échec condition "N>1""
    Si je demande: menscredit(1000, 5, [24]), ça me répond: 'Erreur appel "menscredit": mauvais type pour "N"'

    Si ça vous intéresse dans le cadre de ce fil, je peux vous donner le code du décorateur (Python 3.7) et vous expliquer comment il fonctionne.

    A noter que je me suis concentré sur la vérification des arguments avant l'appel à le fonction, mais comme c'est un décorateur, rien n'empêche de tester les données de retour de la fonction décorée.
    Un expert est une personne qui a fait toutes les erreurs qui peuvent être faites, dans un domaine étroit... (Niels Bohr)
    Mes recettes python: http://www.jpvweb.com

  19. #19
    Expert éminent
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    3 784
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 3 784
    Points : 7 043
    Points
    7 043
    Par défaut
    Salut Tyrtamos,

    J'avais déjà proposé la solution ICI, seulement son souhait est de vérifier avant exécution du code que tous les types sont bien à leur place.
    Je crois, parce-que j'utilise très peu ce format, que ta vérification se fait qu'au moment où tu exécutes la fonction menscredit.

    EDIT: 19h11

    @Pyramidev,

    Regarde du côté de ce module, je ne le connais pas...
    Celui qui trouve sans chercher est celui qui a longtemps cherché sans trouver.(Bachelard)
    La connaissance s'acquiert par l'expérience, tout le reste n'est que de l'information.(Einstein)

  20. #20
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 461
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 461
    Points : 9 248
    Points
    9 248
    Billets dans le blog
    6
    Par défaut
    Bonjour fred1599,

    Citation Envoyé par fred1599 Voir le message
    ...ta vérification se fait qu'au moment où tu exécutes la fonction menscredit
    Absolument. Pendant mes développements, je fais les tests habituels basés sur l'exécution avec des choix de données, et je termine systématiquement par de l'analyse de code avec PyLint qui m'a beaucoup apporté pour la qualité du code.

    Avec mon décorateur, je suis bien pendant l'exécution. Ça peut servir aux tests pendant le développement, et aussi après, pendant l'utilisation. Cela permet d'éviter des situations que je déteste absolument:
    - un message d'erreur tellement général qu'on ne sait pas quoi en faire
    - pire: un programme qui crash sans rien dire
    - encore pire: des résultats faux sans aucun message

    Mais faire ça sans exécution, je ne vois pas...
    Un expert est une personne qui a fait toutes les erreurs qui peuvent être faites, dans un domaine étroit... (Niels Bohr)
    Mes recettes python: http://www.jpvweb.com

Discussions similaires

  1. Tableau de template avec constructeur privé
    Par polonain2 dans le forum Langage
    Réponses: 3
    Dernier message: 03/06/2010, 14h24
  2. Extend d'une classe avec un constructeur privé
    Par aelmalki dans le forum Langage
    Réponses: 5
    Dernier message: 13/03/2010, 12h09
  3. Héritage d'une classe avec constructeur privé
    Par Braillane dans le forum Langage
    Réponses: 13
    Dernier message: 02/09/2009, 12h59
  4. [JUnit] [Test][Débutant] Constructeur privé
    Par Shabata dans le forum Tests et Performance
    Réponses: 2
    Dernier message: 12/01/2006, 16h45

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo