38 votes

Une classe doit-elle convertir les types de paramètres au moment de l’initialisation? Si c'est le cas, comment?

J'ai défini une classe avec 5 variables d'instance

class PassPredictData:
    def __init__(self, rating, name, lat, long, elev):
        self.rating = rating
        # rest of init code

Je veux m'assurer que:

  • rating est un int
  • name est un str
  • lat, long, elev sont des flotteurs

Lors de la lecture de mon fichier d'entrée, tout fonctionne à la création d'une liste d'objets en fonction de ma classe. Quand j'ai commencer à comparer les valeurs que j'ai obtenu des résultats bizarres, depuis les variables d'instance ont été toujours des chaînes de caractères.

"La plus Pythonic façon" jeter les valeurs que l'objet est créé à l'aide d' int(string) et float(string) lors de l'appel du constructeur ou si cette coulée être fait avec la logique à l'intérieur de la classe?

31voto

jdehesa Points 22254

Personnellement, je ferais tout traitement de chaîne avant de passer les valeurs du constructeur, à moins que l'analyse est l'un (ou le) a déclaré explicitement la responsabilité de la classe. Je préfère mon programme à l'échec car je n'ai pas convertir explicitement une valeur que d'être trop souple et se retrouvent dans un Javascript-comme 0 == "0" situation. Cela dit, si vous souhaitez accepter les chaînes de caractères comme des paramètres, vous pouvez simplement appeler int(my_parameter) ou float(my_parameter) comme nécessaire dans le constructeur et qui fera en sorte que ce sont des nombres a pas d'importance que vous passez un nombre, une chaîne de caractères ou même un Booléen.

Dans le cas où vous souhaitez en savoir plus sur le type de sécurité en Python, vous pouvez prendre un coup d'oeil à des annotations de type, qui sont pris en charge par type de pions comme mypy, et les traits de package de sécurité de type dans les attributs de classe.

27voto

David Cullen Points 5279

Si vous tapez import this dans l'interpréteur Python, vous aurez "Le Zen de Python, par Tim Peters". Les trois premières lignes semblent s'appliquent à votre situation:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.

Je recommande la mise en œuvre de votre classe comme ceci:

class PassPredictData:
    def __init__(self, rating, name, lat, long, elev):
        self.rating = int(rating)
        self.name = str(name)
        self.lat = float(lat)
        self.long = float(long)
        self.elev = float(elev)

C'est l'application que vous mentionnez dans votre question. Il est simple et explicite. La beauté est dans l'oeil du spectateur.

Réponses aux Commentaires

La mise en œuvre est explicite à l'auteur de la classe contre une autre solution qui cache la conversion de type derrière certains opaque mécanisme.

Il y a un argument valable qu'il n'est pas évident à partir de la signature de la fonction quels sont les types de paramètres sont. Cependant, la question implique que tous les paramètres sont passés comme des chaînes de caractères. Dans ce cas, le type attendu est - str pour tous les paramètres du constructeur. Peut-être la question du titre ne décrit pas clairement le problème. Peut-être un meilleur titre serait "Appliquer Instance Types de Variables Lors du Passage de Chaînes comme Paramètres au Constructeur".

17voto

OLIVER.KOO Points 2903

EDIT: (edit parce que le sujet de la question a été changé) , je ne recommanderais pas convertir le type de paramètres au moment de l'initialisation. Par exemple:

class PassPredictData:
    def __init__(self, rating, name, lat, long, elev):
        self.rating = int(rating)
        self.name = str(name)
        ...

À mon avis, Ce type de conversion Implicite est dangereux pour plusieurs raisons.

  1. Implicitement convertit le type de paramètre à un autre, sans donner d'avertissement est très trompeur
  2. Il ne soulève aucune des exceptions si les utilisateurs passent dans les indésirables de type. Cela va de pair avec la conversion implicite. Cela pourrait être évité par l'utilisation explicite de la vérification de type.
  3. Silencieusement convertir type viole en tapant duck

Au lieu de convertir le type de paramètres, il est préférable de vérifier le type de paramètre au moment de l'initialisation. Cette approche permettrait d'éviter les trois points. Pour ce faire, vous pouvez utiliser le type de vérification à partir d' typedecorator je l'aime parce qu'il est simple et très lisible

Pour Python2 [edit: cette comme une référence en tant que OP demandé]

from typedecorator import params, returns, setup_typecheck, void, typed

class PassPredictData:
    @void
    @params(self=object, rating = int, name = str, lat = float, long = float, elev = float)
    def __init__(self, rating, name, lat, long, elev):
        self.rating = rating
        self.name = name
        self.lat = lat
        self.long = long
        self.elev = elev

setup_typecheck()     
x = PassPredictData(1, "derp" , 6.8 , 9.8, 7.6) #works fine
x1 = PassPredictData(1.8, "derp" , 6.8 , 9.8, 7.6) #TypeError: argument rating = 1.8 doesn't match signature int
x2 = PassPredictData(1, "derp" , "gagaga" , 9.8, 7.6) #TypeError: argument lat = 'gagaga' doesn't match signature float
x3 = PassPredictData(1, 5 , 6.8 , 9.8, 7.6) #TypeError: argument name = 5 doesn't match signature str

Pour Python3 vous pouvez utiliser l' annotation de la syntaxe:

class PassPredictData1:
    @typed
    def __init__(self : object, rating : int, name : str, lat : float, long : float, elev : float):
        self.rating = rating

setup_typecheck()    
x = PassPredictData1(1, 5, 4, 9.8, 7.6)

renvoie une erreur:

TypeError: l'argument name = 5 ne correspond pas à la signature str

6voto

pixels Points 135

On dirait qu'il y a un million de façons de faire cela, mais voici la formule que j'utilise:

 class PassPredictData(object):
    types = {'lat'   : float,
             'long'  : float,
             'elev'  : float,
             'rating': int,
             'name'  : str,
             }

    def __init__(self, rating, name, lat, long, elev):
        self.rating = rating
        [rest of init code]

    @classmethod
    def from_string(cls, string):
        [code to parse your string into a dict]

        typed = {k: cls.types[k](v) for k, v in parsed.items()}

        return cls(**typed)
 

Une chose intéressante à ce sujet: vous pouvez directement utiliser un re.groupdict() pour produire votre dict (par exemple):

parsed = re.search('(?P<name>\w): Latitude: (?P<lat>\d+), Longitude: (?P<long>\d+), Elevation: (?P<elev>\d+) meters. (?P<rating>\d)', some_string).groupdict()

6voto

Ashwini Chaudhary Points 94431

Définir des types de champ personnalisés

Une façon est de définir vos propres types de champ et de faire la conversion et le traitement des erreurs dans les. Les champs vont être fondées sur des descripteurs. C'est quelque chose que vous allez trouver dans les modèles Django, Flask-SQLAlchemy, DRF-Champs etc

Avoir de tels champs personnalisés vous permettront de les lancer, de les valider et cela ne fonctionne pas seulement en __init__, mais partout où nous essayons de lui affecter une valeur.

class Field:
    type = None

    def __init__(self, default=None):
        self.value = default

    def __get__(self, instance, cls):
        if instance is None:
            return self
        return self.value

    def __set__(self, instance, value):
        # Here we could either try to cast the value to
        # desired type or validate it and throw an error
        # depending on the requirement.
        try:
            self.value = self.type(value)
        except Exception:
            raise ValueError('Failed to cast {value!r} to {type}'.format(
                value=value, type=self.type
            ))

class IntField(Field):
    type = int


class FloatField(Field):
    type = float


class StrField(Field):
    type = str


class PassPredictData:
    rating = IntField()
    name = StrField()
    lat = FloatField()
    long = FloatField()
    elev = FloatField()

    def __init__(self, rating, name, lat, long, elev):
        self.rating = rating
        self.name = name
        self.lat = lat
        self.long = long
        self.elev = elev

Démo:

>>> p = PassPredictData(1.2, 'foo', 1.1, 1.2, 1.3)
>>> p.lat = '123'
>>> p.lat
123.0
>>> p.lat = 'foo'
...
ValueError: Failed to cast 'foo' to <class 'float'>
>>> p.name = 123
>>> p.name
'123'

L'utilisation d'un analyseur statique

Une autre option est l'utilisation d'analyseurs statiques comme Mypy et rattraper les erreurs avant que le programme est exécuté. Le code ci-dessous à l'aide de Python 3.6 syntaxe, mais vous pouvez le faire fonctionner avec d'autres versions en effectuant quelques modifications.

class PassPredictData:
    rating: int
    name: str
    lat: float
    long: float
    elev: float

    def __init__(self, rating: int, name: str, lat: float, long: float, elev: float) -> None:
        self.rating = rating
        self.name = name
        self.lat = lat
        self.long = long
        self.elev = elev

PassPredictData(1, 2, 3, 4, 5)
PassPredictData(1, 'spam', 3.1, 4.2, 5.3)
PassPredictData(1.2, 'spam', 3.1, 4.2, 5)

Lorsque nous Mypy sur ce que nous obtenons:

/so.py:15: error: Argument 2 to "PassPredictData" has incompatible type "int"; expected "str"
/so.py:17: error: Argument 1 to "PassPredictData" has incompatible type "float"; expected "int"

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X