Et Guido a créé des chaînes à l'image de C, à l'image de tableaux de personnages qui les ont créés. Et Guido a vu que c'était bon.
Ou pas?Imaginez que vous écrivez du code complètement idiomatique pour contourner certaines données avec l'imbrication. Beau vaut mieux que laid, simple vaut mieux que complexe, vous vous arrêtez donc à la version suivante du code:
from collections.abc import Iterable def traverse(list_or_value, callback): if isinstance(list_or_value, Iterable): for item in list_or_value: traverse(item, callback) else: callback(list_or_value)
Vous écrivez un test unitaire, et qu'en pensez-vous? Cela ne fonctionne pas, et non seulement ne fonctionne pas, mais
>>> traverse({"status": "ok"}, print) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in traverse File "<stdin>", line 4, in traverse File "<stdin>", line 4, in traverse [Previous line repeated 989 more times] File "<stdin>", line 2, in traverse File "/usr/local/opt/python/libexec/bin/../../Frameworks/Python.framework/Versions/3.7/lib/python3.7/abc.py", line 139, in __instancecheck__ return _abc_instancecheck(cls, instance) RecursionError: maximum recursion depth exceeded in comparison
Comment? Pourquoi? À la recherche d'une réponse, vous plongerez dans le monde merveilleux des collections d'une profondeur infinie.
En fait, une chaîne est le seul
Iterable
intégré qui renvoie toujours
Iterable
en tant qu'élément! Nous pouvons bien sûr construire un autre exemple en créant une liste et en l'ajoutant à nous-mêmes une ou deux fois, mais voyez-vous souvent cela dans votre code? Et la ligne est
Iterable
une profondeur infinie, se glissant sous le couvert de la nuit jusque dans votre production.
Un autre exemple. Quelque part dans le code, vous devez vérifier à plusieurs reprises la présence d'éléments dans des conteneurs. Vous décidez d'écrire un assistant qui l'accélère de plusieurs façons. Vous écrivez une solution universelle qui utilise uniquement la méthode
__contains__
(la seule méthode de la classe de base abstraite du
Container
), mais vous décidez ensuite d'ajouter une super-optimisation pour un cas spécial - une collection. Après tout, vous pouvez simplement marcher le long et faire un
set
!
import functools from typing import Collection, Container def faster_container(c: Container) -> Container: if isinstance(c, Collection): return set(c) return CachedContainer(c) class CachedContainer(object): def __init__(self, c: Container): self._contains = functools.lru_cache()(c.__contains__) def __contains__(self, stuff): return self._contains(stuff)
III ... votre solution ne fonctionne pas! Eh bien ici! Encore une fois!
>>> c = faster_container(othello_text) >>> "Have you pray'd to-night, Desdemona?" in c False
(Mais la mauvaise réponse a été émise très rapidement ...)
Pourquoi? Parce qu'une chaîne en Python est une collection étonnante dans laquelle la sémantique de la méthode
__contains__
pas cohérente avec la sémantique de
__iter__
et
__len__
.
En fait, une chaîne est une collection:
>>> from collections.abc import Collection >>> issubclass(str, Collection) True
Mais la collection ... quoi?
__iter__
et
__len__
considèrent qu'il s'agit d'une collection de caractères:
>>> s = "foo" >>> len(s) 3 >>> list(s) ['f', 'o', 'o']
Mais
__contains__
pense que c'est une collection de sous-chaînes!
>>> "oo" in s True >>> "oo" in list(s) False
Que peut-on faire?
Bien que le comportement de
str.__contains__
puisse sembler étrange dans le contexte des implémentations
__contains__
autres types standard, ce comportement est l'une des nombreuses petites choses qui rendent Python aussi pratique qu'un langage de script; vous permettant d'écrire du code rapide et littéraire dessus. Je ne suggérerais pas de changer le comportement de cette méthode, d'autant plus que nous ne l'utilisons presque jamais pour vérifier la présence d'un seul caractère dans une chaîne.
Et au fait, savez-vous pourquoi? Parce que nous n'utilisons presque jamais une chaîne comme une collection de caractères dans un langage de script! Manipulation de caractères spécifiques dans une chaîne, accès par index - le plus souvent le destin des tâches dans les entretiens. Donc, vous devriez peut-être supprimer
__iter__
de la chaîne, le cacher derrière une méthode comme
.chars()
? Cela résoudrait ces deux problèmes.
Il est temps de discuter vendredi dans les commentaires!