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".
** 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.
** 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.
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
.
Voici comment taper des variables annotées avec des types intégrés comme ʻintet
float`, 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
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.
«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.
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
.
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 classe
Person` 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é.
Je ne l'utilise pas beaucoup, mais cela pourrait être utile
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.
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
** 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.
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.
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é.
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