Point 17 de la note Python efficace Respect de la certitude lors de l'utilisation d'itérateurs pour les arguments

Il s'agit d'un mémo écrit du livre efficace python d'O'Reilly Japan. https://www.oreilly.co.jp/books/9784873117560/ P38~42

Notez que l'appel de l'itérateur est avec état

Dans une fonction qui traite plusieurs fois, lorsqu'un itérateur est utilisé comme argument, il peut se comporter de manière inattendue.

** Considérez une fonction qui calcule le ratio de visiteurs dans chaque ville par rapport au nombre total de visiteurs **

def normalize(numbers):
    total = sum(numbers)
    result = []
    for value in numbers:
        persent = 100 * value / total
        result.append(persent)
    return result

visits = [15, 35, 80]
percentages = normalize(visits)
print(percentages)

>>>
[11.538461538461538, 26.923076923076923, 61.53846153846154]

C'est bien, mais essayez d'utiliser le générateur au cas où la quantité de données deviendrait importante (la quantité de données qui provoque une panne de mémoire).

def read_visits(data_path):
    with open(data_path) as f:
        for line in f:
            yield int(line)

it = read_visits("visits.txt") #En supposant un fichier avec un grand nombre de nombres
percentages = normalize(it)
print(percentages[:3])

>>>
[]

Nous nous attendons à obtenir des résultats similaires au code ci-dessus, mais dans ce cas, une liste vide est renvoyée. La raison en est que le résultat de l'itérateur n'est généré qu'une seule fois.

Si vous exprimez le flux comme un flux

  1. Tout d'abord, read_visits () le crée, qui est un générateur.
  2. normalize () prend le générateur comme argument, et sum () est calculé en premier.
  3. ** À ce stade, le générateur qu'il compte est déjà épuisé! ** **
  4. Après cela, le traitement de la boucle for est effectué dans normalize, mais ** l'instruction for ne fonctionne pas car le nombre de générateurs qu'il a déjà épuisé **
  5. le résultat reste vide tel qu'il a été déclaré

Dans ce cas, ce qui est particulièrement déroutant, c'est qu'aucune exception n'est renvoyée lorsque l'itérateur est épuisé. Dans le traitement itératif python, il n'est pas possible de déterminer s'il n'y a pas de sortie d'itérateur ou s'il est déjà attaché et StopIteration, donc aucune exception n'est retournée.

Pour résoudre ce problème, copiez l'itérateur dans la liste afin de pouvoir l'appeler encore et encore.

def normalize_copy(numbers):
    numbers = list(numbers) #Faites une liste de copies de l'itérateur ici
    total = sum(numbers)
    result = []
    for value in numbers:
        persent = 100 * value / total
        result.append(persent)
    return result

it = read_visits("visits.txt")
persentage = normalize_copy(it)
print(persentage)

>>>
[11.538461538461538, 26.923076923076923, 61.53846153846154]

Le résultat est comme prévu, mais en générant une liste de nombres dans la fonction normalize_copy, une nouvelle zone mémoire sera utilisée. Cela élimine les avantages des itérateurs. Au lieu de créer une liste, envisagez d'utiliser une fonction qui renvoie un nouvel itérateur.

Définissez maintenant une fonction pour passer un nouvel itérateur

def normalize_func(get_iter):
    total = sum(get_iter()) #Nouvel itérateur
    result = []
    for value in get_iter():  #Nouvel itérateur
        persent = 100 * value / total
        result.append(persent)
    return result

persentages = normalize_func(lambda: read_visits("visits.txt"))
print(list(persentages))

>>>
[11.538461538461538, 26.923076923076923, 61.53846153846154]

Cela fonctionne comme prévu, mais l'utilisation de lambda est un problème. Mettez en œuvre le ** protocole Iterator ** d'origine pour obtenir le même résultat.

Le protocole itérateur est en charge de traiter les appels répétés dans le conteneur dans une boucle telle qu'une instruction for. (Appelez next () jusqu'à ce que StopIteration se produise) Créons notre propre classe de conteneur pour ce processus

class ReadVisits(object):
    def __init__(self, data_path):
        self.data_path = data_path
        
    def __iter__(self):
        with open(self.data_path) as f:
            for line in f:
                yield int(line)

visits = ReadVisits("visits.txt")
percentages = normalize(visits)
print(percentages)

>>>
[11.538461538461538, 26.923076923076923, 61.53846153846154]

La différence par rapport aux read_visits d'origine est qu'en raison de la classe de conteneur ReadVisit nouvellement implémentée, elle est traitée deux fois dans la fonction de normalisation. En effet, chaque nouvel objet itérateur est créé. Cela permet aux visites d'être appelées un nombre illimité de fois. (Cependant, cela a aussi son inconvénient: cela implique de lire plusieurs fois les données d'entrée.)

Fournir une fonction pour vérifier s'il s'agit d'un type de conteneur pour assurer un traitement correct

def normalize_defensive(numbers):
    if iter(numbers) is iter(numbers):
        raise TypeError('Must supply a container')
    total = sum(numbers)
    result = []
    for value in numbers:
        percent = 100 * value / total
        result.append(percent)
    return result

visits = [15, 35, 80]
normalize_defensive(visits)

>>>
[11.538461538461538, 26.923076923076923, 61.53846153846154]

Pas d'erreur

visits = ReadVisits("visits.txt")
normalize_defensive(visits)

>>>
[11.538461538461538, 26.923076923076923, 61.53846153846154]

Pas d'erreur

Une erreur se produit si l'entrée n'est pas de type conteneur

it = iter(visits)
normalize_defensive(it)

>>>
TypeError: Must supply a container

Résumé

Recommended Posts

Point 17 de la note Python efficace Respect de la certitude lors de l'utilisation d'itérateurs pour les arguments
Élément de note Python efficace 12 Évitez d'utiliser des blocs else après les boucles for et while
Élément de note Python efficace 20 Utilisez Aucun et la chaîne de documentation lors de la spécification d'arguments par défaut dynamiques
Choses à surveiller lors de l'utilisation d'arguments par défaut en Python
[python, multitraitement] Comportement des exceptions lors de l'utilisation du multitraitement
Une note utile lors de l'utilisation de Python après une longue période
Obtenir des informations sur les notes à l'aide du SDK Evernote pour Python 3
Élément de mémo Python efficace 3
Élément de mémo Python efficace 9 Considérons une expression de générateur pour la notation d'inclusion de grande
Élément de mémo Python efficace 19 Donner un comportement facultatif aux arguments de mot-clé
Notez les liens qui peuvent être utiles lors de l'utilisation de Python, Selenium2
Élément de note Python efficace 16 Envisagez de renvoyer un générateur sans renvoyer une liste
Notes depuis 3 mois depuis que j'ai commencé Python
Arguments de mots-clés pour les fonctions Python
Élément de mémo Python efficace 11 Utilisez zip pour traiter les itérateurs en parallèle
Note Python efficace Item 15 Savoir comment les fermetures sont liées à la portée de la fonction
Atom: Remarque pour l'erreur d'indentation lors de la copie du script Python dans le shell
Remarques sur l'utilisation de la saisie semi-automatique lors de l'exécution interactive de Python sous Windows
Remarque Python: à propos de la comparaison en utilisant is
Précautions lors de l'utilisation de Pit avec Python
Remarque sur boto3 (AWS SDK for Python)
[TouchDesigner] Conseils pour la déclaration par python
[Python] Soyez prudent lorsque vous utilisez print
[Python] Raison du remplacement à l'aide de super ()
[Python] Quatre-vingt-dix-neuf tables utilisant des instructions for
Précautions lors de l'utilisation de phantomjs de python
Lors de l'utilisation de MeCab avec python dans virtualenv
Précautions lors de l'utilisation de six avec Python 2.5
Lors de l'utilisation d'expressions régulières en Python
Remarques sur la configuration d'un conteneur Docker pour l'utilisation de JUMAN ++, KNP, python