Ce à quoi je fais attention dans le codage Python: commentaires, annotations de type, classes de données, énumérations (enum)

Aperçu

Je code généralement en Python. Python fonctionne souvent même si je l'écris grossièrement, donc je l'écris correctement. Cependant, lorsque j'examine ce code plus tard ou que je le montre à d'autres, je me retrouve avec quelque chose comme "Que faites-vous ..." ou "Comment utilisez-vous cette fonction ...". Donc, dans un mémorandum, je vais énumérer les choses que "si vous faites attention à ces choses, ce sera plus facile à comprendre dans une certaine mesure".

commentaire

** Décrivez le but du traitement au lieu de commenter le contenu du traitement **

J'essaie de suivre le contenu du traitement par nom de fonction ou nom de variable, et j'ai conscience de commenter l'objectif du traitement en langage naturel, ce qui est difficile à suivre par lui-même ~~, mais j'oublie souvent ~~.

Par exemple, le code est le suivant.

Exemple de commentaire décrivant le contenu du traitement


# 1/Réduire à 4 ← Peut être imaginé à partir des noms de fonctions et des noms de variables
small_image = resize(image, scale=1/4)

Exemple de commentaire décrivant le but du traitement


#Rétrécissez pour assurer une mémoire stable lors du versement dans le modèle
small_image = resize(image, scale=1/4)

Je ne pense pas qu'il soit mal de commenter ce qui se passe. Je suis très heureux d'avoir des commentaires sur le contenu du traitement lorsque le code s'étend sur plusieurs lignes ou que le code n'est pas compréhensible à première vue en raison de l'optimisation.

Cependant, comme dans l'exemple ci-dessus, je pense qu'il serait redondant de commenter le même contenu que le code lorsque la lecture du code n'est pas un problème.

Si vous écrivez un commentaire avec la conscience que "le motif et le but du traitement sont difficiles à suivre du code", je pense que le code sera facile à retenir même si vous regardez en arrière dans le temps.

Annotation de type

** Il n'y a aucune perte à faire uniquement les arguments de fonction et les valeurs de retour **

Les annotations de type en Python sont des commentaires + α, ce qui ne garantit pas une correspondance de type au moment de l'exécution, mais je pense que vous devriez toujours annoter autant que possible.

Un bon endroit pour taper annoter

Exemple d'annotation


#Très bonnes annotations sur les arguments de méthode et les valeurs de retour (pas besoin de regarder dans la méthode)
class Person:
    def __init__(self, first_name: str, last_name: str, age: int):  #Annotation pour les arguments de méthode
        self._name: str = first_name + ' ' + last_name  #Annotation pour les variables
        self._age = age

    def is_older_than(self, age: int) -> bool:  #Annotation pour la valeur de retour de la méthode
        return self._age > age

En particulier, les arguments et les valeurs de retour des fonctions publiées ne peuvent être utilisés que s'ils sont annotés de type, donc je pense qu'ils sont essentiels. Bien sûr, vous pouvez l'écrire dans docstring. Un éditeur assez sophistiqué devrait également analyser docstring.

Tapez les annotations utilisées quotidiennement

Voici comment taper des variables annotées avec des types intégrés comme ʻintetfloat`, et des variables qui instancient des classes.

Annotation de type quotidien


age: int = 0
weight: float = 0.0
name: str = 'string'
is_student: bool = True

taro: Person = Person('taro', 'suzuki', 20)

Les autres types intégrés souvent utilisés sont «list», «dict» et «tuple». Ce sont les mêmes

python


friends: list = [daisuke, tomoko]
parents: tuple = (mother, father)
contacts: dict = {'email': 'xxx@mail', 'phone_number': 'XXXX'}

Vous pouvez annoter comme ceci, mais vous pouvez utiliser le module de saisie pour annoter plus en détail. Par exemple, dans l'exemple ci-dessus, vous pouvez voir que friends est une liste, mais je ne sais pas quel type d'éléments doit être inclus. Voici comment annoter un élément en utilisant typing.

Annotation détaillée des types quotidiens à l'aide de la saisie

Annotation détaillée des types quotidiens à l'aide de la saisie


from typing import List, Tuple, Dict  #Importer pour l'annotation de type

friends: List[Person] = [daisuke, tomoko]  #Liste avec les instances Person comme éléments
parents: Tuple[Person, Person] = (mother, father)  #Un taple avec une instance Person comme deux éléments
contacts: Dict[str, str] = {'email': 'xxx@mail', 'phone_number': 'XXXX'}  #la clé est str,Dictionnaire où la valeur est str

typing permet des annotations de type plus détaillées. Personnellement, je suis très soulagé car je peux comprendre quel type de dictionnaire j'attends, surtout s'il y a des annotations pour la clé et la valeur dans l'annotation de type de Dict.

Ceux-ci peuvent également être imbriqués dans une structure imbriquée. Par exemple, certaines personnes peuvent avoir plusieurs adresses e-mail et numéros de téléphone. Ensuite, vous voudrez que la valeur de contacts soit List [str] ʻau lieu de str`. À ce moment-là

python


#la clé est str,Un dictionnaire où valeur est une liste de str
contacts: Dict[str, List[str]] = 
    {
        'email': 
            ['xxx@mail', 'yyy@mail'], 
        'phone_number':
            ['XXXX', 'YYYY']
     }

Il est possible d'annoter comme suit.

Annotation de type pratique à l'aide de la saisie

«typing» permet diverses annotations autres que celles listées ci-dessus. Je présenterai «Union» et «Optionnel» comme étant les plus fréquemment utilisés.

Union [A, B, C]: A, B, C

Pensez à écrire une fonction qui change le poids d'une instance Person. C'est une mise en œuvre très simple, mais cela change le poids par le poids reçu.

python


class Person:
    ...
    def update_weight(self, weight: float) -> float
        self.weight += weight
        return self.weight

Cela semble bon à première vue, mais c'est un peu triste de n'accepter que «float» comme un changement. Cela peut être un peu pratique si vous recevez également ʻint. Je veux faire une annotation de type qui dit OK pour ʻint ou float. ʻUnion` peut être utilisé lorsque vous dites "l'un de ces types est OK".

python


class Person:
    ...
    def update_weight(self, weight: Union[int, float]) -> float
        self.weight += weight
        return self.weight

Cela permet à la fois ʻint et float d'être inclus dans l'argument de ʻupdate_weight.

Facultatif [A]: A ou Aucun

Lorsque vous commencez à utiliser ʻUnion, vous pouvez constater que vous utilisez souvent la notation ʻUnion [A, None] dans votre code.

Par exemple, supposons que vous définissiez une classe ʻOccupationqui représente votre profession. Donnons à la classePerson` une profession pour cette personne. Cependant, peut-être que «Person» est un étudiant et n'a pas de profession. Je veux dire que la profession est «Occupation» ou «Aucune». Vous pouvez utiliser «Union» dans un tel cas.

python


class Person:
    def __init__(..., occupation: Union[Occupation, None]):
        self._occupation = occupation

Comme autre exemple, disons que vous voulez avoir une fonction qui obtient l'ID de passeport de cette personne sous forme de chaîne. Mais vous n'avez peut-être pas de passeport. Une façon consiste à renvoyer une chaîne vide. Cependant, si vous voulez indiquer clairement qu'il n'existe pas, vous pouvez envisager de renvoyer «Aucun».

python


class Person:
    def get_passport_id(self) -> Union[str, None]:
        if self.has_passport:
            return self._passport._id
        else:
            return None

Plus les opportunités pour ces annotations de type sont fréquentes, plus cela devient ennuyeux. Pour un tel cas, «Facultatif» est préparé. «Optionnel» est utilisé comme «Optionnel [A]» et signifie «A» ou «Aucun». ʻOptionnel [A] = Union [A, Aucun] `.

En utilisant ʻOptional`, l'exemple précédent ressemble à ceci:

python


class Person:
    def __init__(..., occupation: Optional[Occupation]):
        self._occupation = occupation

    def get_passport_id(self) -> Optional[str]:
        if self.has_passport:
            return self._passport._id
        else:
            return None

Je pense qu'il est un peu plus facile d'exprimer l'intention du code, probablement parce que le mot «facultatif» a été attribué.

Tapez des annotations qui peuvent être utiles en utilisant la saisie

Je ne l'utilise pas beaucoup, mais cela pourrait être utile

NewType: créer un nouveau type

Vous pouvez définir un nouveau type. Par exemple, en tant que fonction qui recherche l'instance Person correspondante à partir du person_id de type ʻint`

python


def find_by_id(person_id: int) -> Person:
    ...

Je pense que vous pouvez écrire quelque chose comme ça. Ici, le nom de l'argument est un nom descriptif de person_id, donc il peut ne pas être trop confus, mais vous pouvez par inadvertance faire une erreur en passant ʻoccupation_id défini avec le même type ʻint comme argument. ne pas. Pour éviter de telles erreurs par inadvertance, osez définir la classe PersonId

python


class PersonId(int):
    pass


def find_by_id(person_id: PersonId) -> Person:
    ...

p = find_by_id(PersonId(10))

Peut-être bien, mais cela entraîne la surcharge de l'instanciation. Avec NewType

python


from typing import NewType


PersonId = NewType('PersonId', int)

def find_by_id(person_id: PersonId) -> Person:
    ...

p = find_by_id(PersonId(10))

Cependant, cela a une petite surcharge car il appelle uniquement une fonction qui ne fait rien et renvoie 10 au moment de l'exécution. En outre, la signification du code est «int», mais je pense qu'il est facile de voir que l'erreur humaine est évitée en redéfinissant cela comme un type différent.

TypeAlias: Alias pour les types complexes

Par exemple, s'il s'agit de Dict [str, List [str]], vous pouvez lire "Eh bien, str est une clé et str est un dictionnaire dont la liste d'éléments est value", mais c'est List [Dict] [str, Union [int, float, None]]] C'est difficile à comprendre quand il s'agit de , et c'est pénible d'ajouter autant d'annotations de type à chaque fois avec une fonction qui échange ce type. Dans ce cas, si vous utilisez TypeAlias,

TypeAlias


TypeReportCard = List[Dict[str, Union[int, float, None]]]]

def set_report_card(report_card: TypeReportCard):
    ...

set_report_card([{'math': 9.5}, {'english': 7}, {'science': None}])
#Exemple d'erreur-> set_report_card(TypeReportCard([{'math': 9.5}, {'english': 7}, {'science': None}])) 

Vous pouvez écrire clairement comme. Créez simplement un alias et aucune importation spéciale n'est requise. Contrairement à NewType, c'est juste un alias, donc lorsque vous l'utilisez réellement comme argument, vous n'avez pas besoin de l'envelopper dans un nom d'alias.

Il peut être intéressant de lire PEP 484

Classe de données

** Lorsque vous utilisez le type de dictionnaire, considérez la classe de données une fois **

Je voulais avoir les informations de contact d'une certaine personne sous forme de données, je vais donc l'exprimer dans un type de dictionnaire.

contacts_dictionnaire



contacts: Dict[str, Optional[str]] = 
    {
        'email': 'xxx@mail',
        'phone_number': None
     }

Si vous regardez cela, c'est un code très courant. Un jour, vous voulez savoir "si un contact a un numéro de téléphone".

python


def has_phone_number(contacts: Dict[str, Optional[str]]) -> bool:
    return contacts.get('phone_number', None) is not None
#    return 'phone_number' in contacts and contacts['phone_number']n'est pas Aucun n'est bien

Je pense que cela fonctionne sans aucun problème. Deux semaines plus tard, j'avais à nouveau besoin de cette fonction. Grâce à l'annotation de type, je peux me souvenir à quoi ressemblent les contacts et appeler la fonction avec succès.

has_phone_number({'email': 'xxx@mail', 'phone-number': 'XXXX'})

Cependant, la valeur de retour de cette fonction sera «False». Si vous regardez attentivement, vous pouvez voir que phone_number a été changé en phone-number. Par conséquent, «phone_number» n'existe pas et le résultat est «False». L'annotation de type Dict [str, Optional [str]] ne connaissait pas le nom de clé requis, donc je ne me souvenais pas exactement du nom de clé que j'avais décidé il y a deux semaines.

Cet exemple peut être facile à comprendre car l'implémentation de has_phone_number est écrite juste au-dessus. Mais que se passe-t-il si cette fonction est implémentée loin? Que faire si vous ne remarquez pas immédiatement que le résultat est «Faux»? Je pense que le débogage sera difficile.

Il est standard d'éviter autant que possible les constantes directement intégrées qui apparaissent dans le code, mais vous devez également faire attention aux clés de type dictionnaire.

Dans un tel cas, vous voudrez peut-être considérer Data Class une fois.

Remplacement du dictionnaire par la classe de données

Classe de données


#Remplacer par la classe de données
import dataclasses


@dataclasses.dataclass
class Contacts:
    email: Optional[str]
    phone_number: Optional[str]

dataclasses.dataclass générera automatiquement __init __, donc la notation en tant que classe est suffisante ci-dessus. De plus, il a diverses fonctions telles que la génération automatique de «eq» et la génération automatique de «repr», mais je vais l'omettre cette fois.

Si vous définissez le contact comme une classe de données comme ci-dessus, has_phone_number peut être implémenté comme ci-dessous.

c = Contacts(email='xxx@mail', phone_number='XXXX')

def has_phone_number(contacts: Contacts) -> bool:
    return contacts.phone_number is not None

Accédé en tant que champ dans la classe de données de cette manière, vous ne ferez pas de faute de frappe (car l'éditeur prend en charge la vérification et la suggestion).

De plus, contrairement à quand il était défini dans Dict [str, Optional [str]], le nom de la clé (nom du champ) est fixe, et chaque clé reçoit un type, alors de quel type de données s'agit-il? Il est plus concret que vous le demandiez.

Remarque: remplacement du dictionnaire par NamedTuple

La classe de données est une fonctionnalité de Python 3.7, donc si vous utilisez 3.6 ou moins, vous pouvez envisager NamedTuple dans typage. Utilisez NamedTuple comme ceci.

NamedTuple


#Remplacer par NamedTuple
from typing import NamedTuple


class Contacts(NamedTuple):
    email: Optional[str]
    phone_number: Optional[str]

c = Contacts(email='xxx@mail', phone_number='XXXX')

def has_phone_number(contacts: Contacts) -> bool:
    return contacts.phone_number is not None

La description est presque la même que celle de «classe de données», mais alors que «classe de données» a le pouvoir de définir des paramètres plus détaillés, «Named Tuple» est juste un taple nommé.

Type d'énumération (enum)

Le dernier est Type d'énumération.

C'est très utile lorsqu'une variable ne peut prendre que A, B ou C ... Par exemple, j'écrase souvent ce genre de code.

def display(object_shape: str):
    if object_shape == 'circle':
        ...
    elif object_shape == 'rectangle':
        ...
    elif object_shape == 'triangle':
        ...
    else:
        raise NotImplementedError

display('circle')

Lors de la division du processus en fonction de l'état, il est écrit comme ceci, et la gestion de l'état se complique plus tard, ou le nom de l'état n'est pas connu et l'implémentation est recherchée. De plus, si vous ne regardez que le type, il semble que tout va bien avec str, mais en réalité, vous obtiendrez une erreur à l'exception de certaines chaînes.

Dans un tel cas, vous pourrez peut-être écrire clairement en utilisant le type d'énumération (ʻenum`).

Type d'énumération


from enum import Enum


class ObjectShape(Enum):
    CIRCLE = 0
    RECTANGLE = 1
    TRIANGLE = 2

def display(object_shape: ObjectShape):
    if object_shape is ObjectShape.CIRCLE:
        ...
    elif object_shape is ObjectShape.RECTANGLE:
        ...
    elif object_shape is ObjectShape.TRIANGLE:
        ...
    else:
        raise NotImplementedError

display(ObjectShape.CIRCLE)

Désormais, vous n'avez plus à vous soucier des fautes de frappe et vous pouvez voir en un coup d'œil ce qui est autorisé en regardant le moule. De plus, cette fois, j'ai attribué manuellement tous les identifiants de ʻEnum`, mais ce numéro ne devrait pas avoir de signification particulière, donc

auto


import enum


class ObjectShape(enum.Enum):
    CIRCLE = enum.auto()
    RECTANGLE = enum.auto()
    TRIANGLE = enum.auto()

Je pense qu'il est préférable d'avoir une valeur unique attribuée automatiquement de cette manière.

Recommended Posts

Ce à quoi je fais attention dans le codage Python: commentaires, annotations de type, classes de données, énumérations (enum)
Ce qui était surprenant dans les classes Python
Tapez les annotations pour Python2 dans les fichiers stub!
Analyse de données en Python: une note sur line_profiler
À propos de __all__ en python