Numpy: Création "contrôlée" de tableaux d'objets

Créé le 4 déc. 2019  ·  45Commentaires  ·  Source: numpy/numpy

La création automatique de tableaux d'objets a récemment été dépréciée dans numpy. Je suis d'accord avec le changement, mais il semble un peu difficile d'écrire certains types de code générique qui déterminent si un argument fourni par l'utilisateur est convertible en un tableau non-objet.

Exemple de code de reproduction :

Matplotlib contient l'extrait suivant :

    # <named ("string") colors are handled earlier>
    # tuple color.
    c = np.array(c)
    if not np.can_cast(c.dtype, float, "same_kind") or c.ndim != 1:
        # Test the dtype explicitly as `map(float, ...)`, `np.array(...,
        # float)` and `np.array(...).astype(float)` all convert "0.5" to 0.5.
        # Test dimensionality to reject single floats.
        raise ValueError(f"Invalid RGBA argument: {orig_c!r}")

mais parfois, la fonction est appelée avec un tableau de couleurs dans différents formats (par exemple ["red", (0.5, 0.5, 0.5), "blue"] ) -- nous attrapons la ValueError et convertissons chaque élément un à la fois.

Maintenant, l'appel à np.array(c) émettra un DeprecationWarning. Comment pouvons-nous contourner cela? Même quelque chose comme np.min_scalar_type(c) émet un avertissement (ce que je suppose qu'il ne devrait pas le faire ?), il n'est donc pas évident pour moi de savoir comment vérifier « si nous avons converti cette chose en un tableau, quel serait le type ? »

Informations sur la version Numpy/Python :


1.19.0.dev0+bd1adc3 3.8.0 (par défaut, 6 novembre 2019, 21:49:08)
[CCG 7.3.0]

57 - Close?

Commentaire le plus utile

Quelqu'un pourrait-il pointer vers l'exemple operator.mod ?

Quant à l'opérateur == , celui que j'ai vu faisait quelque chose comme np.array(vals, dtype=object) == valsvals=[1, [2, 3]] (en paraphrasant le code), donc la solution est de créer de manière proactive le tableau sur la droite côté.

La plupart des échecs scipy semblent être de la forme np.array([0.25, np.array([0.3])]) , où le mélange de scalaires et de ndarray avec shape==(1,) enfreindra la découverte de la dimension et créera un tableau d'objets. xréf gh-15075

Tous les 45 commentaires

Une option serait
```python
essayer:
# prendre de l'avance et promouvoir la dépréciation à une erreur qui la remplacera
avec warnings.catch_warnings() :
warnings.filterwarnings('augmenter', DeprecationWarning, message="...")
c_arr = np.asarray(c)
sauf (DeprecationWarning, ValueError) :
# tout ce que vous faites actuellement pour ValueError

Je suppose que cela, et le test d'échec mentionné dans gh-15045 sont des cas où l'émission d'un DeprecationWarning pendant quelques années au lieu d'émettre directement un ValueError provoque plus de perte de code que nécessaire.

Notez que warnings.catch_warnings n'est pas threadsafe. Cela rend la solution de contournement un peu sujette à des problèmes de suivi sur toute la ligne.

Je pense que le code-churn vaut la période de dépréciation.

Matplotlib exécute sa suite de tests avec des avertissements en cas d'échec pour détecter exactement ce type de changement au début, donc cela semble être le système qui fonctionne pour moi :).

Mais AFAICT il n'y a même pas de solution raisonnablement simple (comme indiqué ci-dessus, la solution proposée n'est pas threadsafe) pour cela :/

Je pense que je vois le point de @anntzer ici. Nous sommes dans un pétrin où la bibliothèque en aval veut échouer rapidement afin de pouvoir essayer autre chose, tandis que les utilisateurs devraient recevoir un message plus doux.

Le problème est aujourd'hui qu'il n'y a aucun moyen pour l'auteur de la bibliothèque de demander "cela émettrait-il un avertissement" sans réellement... émettre l'avertissement et le supprimer n'est pas threadsafe.

Concernant la sécurité des threads d'avertissement : https://bugs.python.org/issue37604

AFAIK, la dépréciation est dans la branche de publication. Voulons-nous le revenir? Sinon, les correctifs auront besoin de rétroportages. Je ne comprends toujours pas pourquoi les avertissements n'ont pas été émis dans les roues de la branche de publication et ne sont pas apparus dans les versions nocturnes avant les deux dernières versions. Je n'ai rien changé après la branche et rien ne semble très suspect dans les commits depuis lors dans la branche master sauf, peut-être, #15040.

À mon humble avis (et en accord avec le point de

Ou peut-être que multibuild traite les branches différemment de master.

FWIW J'ai toujours été au moins -1 sur ce changement, en particulier en tant qu'utilisateur passionné de structures de données irrégulières, mais de toute façon, maintenant, je dois savoir quoi faire à propos des centaines d'échecs de test pour la préparation de SciPy 1.4.0rc2 dans https://github.com/scipy/scipy/pull/11161

maintenant je dois trouver quoi faire à propos des centaines d'échecs de test

Une option facile serait :

  • Supprimez l'avertissement dans votre configuration pytest
  • Ouvrir un problème pour le résoudre plus tard

L'intérêt pour nous d'utiliser DeprecationWarning au lieu de ValueError était de donner aux projets en aval et aux utilisateurs une période de grâce pour faire exactement cela.

AFAIK, la dépréciation est dans la branche de publication. Voulons-nous le revenir?

Je pense que nous le faisons, il pleut des problèmes. Nous avons maintenant une liste de ce qui se passe dans Pandas, Matplotlib, SciPy, à l'intérieur de numpy.testing et NumPy ufuncs, == , etc. Je pense que nous devrions annuler le changement maintenant et aller évaluer/réparer tous ces choses, puis réintroduire la dépréciation.

Pouvons-nous faire un compromis sur un avertissement en attente de dépréciation ?

De cette façon, les projets en aval peuvent l'ajouter à leurs listes d'ignorés, et lorsque nous revenons à DeprecationWarning, ils peuvent à nouveau prendre la décision.

Nous semblons avoir divergé du problème d'origine, qui semble être "étant donné une séquence de valeurs, comment matplotlib peut-il déterminer s'il s'agit d'une seule couleur ou d'une liste de couleurs". Je pense qu'il devrait y avoir une solution qui ne nécessite pas de convertir les valeurs dans un ndarray et de vérifier le dtype de ce tableau. Une sorte de fonction is_a_color() récursive pourrait être une meilleure solution.

J'ai annulé le changement pour 1.18.x dans #15053.

Le sentiment est que briser scipy et pandas CI est suffisamment ennuyeux pour le rétablir temporairement dans le maître également. J'aimerais cependant qu'il revienne dans l'ordre prévu (disons dans un mois). Nous devrons peut-être trouver une solution cependant. De plus, les corrections que font les pandas m'inquiètent légèrement, car ils utilisent catch_warnings .

S'il n'y a vraiment aucun moyen, et nous avons besoin d'une suppression d'avertissement thread-safe. np.seterr pourrait éventuellement contenir un emplacement pour cela :/.

Nous semblons avoir divergé du problème d'origine, qui semble être "étant donné une séquence de valeurs, comment matplotlib peut-il déterminer s'il s'agit d'une seule couleur ou d'une liste de couleurs".

Je pense que le problème soulevé par

  • créer ndarray(flexible_input)
  • if `new_ndarray.dtype.kind == 'O' : gérer cela
  • sinon : use_the_array

puisqu'on ne peut pas ajouter dtype=object à un tel code, que faut-il faire ?

De plus, les corrections que font les pandas m'inquiètent légèrement, car ils utilisent catch_warnings .

@seberg n'était-il pas suppress_warnings meilleur pour ça ?

@rgommers non, suppress_warnings résolu le problème de la suppression permanente des avertissements alors qu'elle ne devrait pas l'être. Cela a été corrigé sur les nouvelles versions de python, de sorte que nous n'en avons plus vraiment besoin (il a de meilleures propriétés, car il prend en charge l'imbrication, mais il ne prend pas en charge la sécurité des threads. Je ne suis pas sûr que ce soit possible en dehors de python, et même si c'était le cas, ce n'est probablement pas souhaitable)

Pas tout à fait sûr si les cas problématiques vont à l'encontre de l'intention initiale (https://numpy.org/neps/nep-0034.html) d'eux, nous ne sommes tout simplement pas anticipés.

Quoi qu'il en soit, une solution serait d'activer explicitement l'ancien comportement du type "apprécier votre préoccupation, mais nous voulons explicitement le type d'objet dépendant du contexte et gérerons nous-mêmes les entrées problématiques". Quelque chose comme l'un des

~~~
np.array(données, dtype='allow_object')

np.array(données, allow_object_dtype=True)

avec np.array_create_allow_object_dtype() :
np.array(données)
~~~

le tout pas très joli et le nommage à coup sûr à améliorer. Mais cela donne une issue propre aux bibliothèques qui se sont appuyées sur le comportement et qui souhaitent le conserver (au moins pour le moment).

Le cas matplotlib n'est-il pas réellement :

with np.forbid_ragged_arrays_immediately():
    np.array(data)

puisque vous voulez vraiment attraper l'erreur, plutôt que d'obtenir un type d'objet ?

Il n'y a pas d'annulation de la dépréciation actuellement en attente pour le maître. Je ne pense pas qu'il devrait être rétabli en gros comme c'était le cas dans la version 1.18, car cela a également supprimé les correctifs, que je pense que nous voulons conserver. @mattip Une réversion plus ciblée serait appréciée jusqu'à ce que nous décidions quoi faire à long terme.

FWIW Je pense que la plupart des endroits dans mpl qui ont touché cela peuvent être corrigés (avec plus ou moins de restructuration -- dans un cas, il s'avère que le code est beaucoup plus rapide après...).
Je pense que l'API proposée par with np.forbid_ragged_arrays_immediately: car cette dernière peut facilement être écrite en fonction de la première (augmenter si np.array(..., allow_object=True).dtype == object ) alors que l'inverse ( try: with np.forbid: ... except ValueError: ... ) serait moins efficace si nous voulons toujours créer un tableau d'objets après tout. Mais un CM (juste "dépassant localement la période de dépréciation") serait mieux que rien.

(Encore une fois, je pense que le changement est bon, c'est juste une question de comment il est exécuté.)

Oui, nous devons juste comprendre à quoi devrait ressembler l'API. Comme beaucoup l'ont souligné, il y a actuellement deux problèmes principaux :

  1. Confondre object et "allow ragged" . Si les objets ont un type raisonnable (disons Decimal ), vous voulez réellement obtenir l'avertissement/l'erreur, mais vous devrez peut-être également transmettre dtype=object
  2. Il n'y a aucun moyen de s'inscrire au nouveau comportement ou de continuer à utiliser l'ancien (sans avertissement). Il semble au moins que l'Opt-In soit probablement nécessaire pour un usage interne, si nous ne le fournissons pas, nous supposons fondamentalement que (peut-être indirectement) seuls les utilisateurs finaux rencontrent ces cas ?

Enfin, nous devons trouver comment le caser dans notre code :). ndmin peut être une autre cible à fourrer dans des drapeaux contrôlant au moins le comportement irrégulier.

Il n'y a pas d'annulation de la dépréciation actuellement en attente pour le maître. Je ne pense pas qu'il devrait être rétabli en gros comme c'était le cas dans la version 1.18, car cela a également supprimé les correctifs, que je pense que nous voulons conserver. @mattip Une réversion plus ciblée serait appréciée jusqu'à ce que nous décidions quoi faire à long terme.

Je ne vois pas de problème avec un retour complet et ensuite réintroduire toutes les parties qui ont du sens maintenant. Encore une fois, revenir sur quelque chose n'est pas un jugement de valeur sur ce qui est bon ou mauvais, c'est juste un moyen pragmatique de défaire un tas de choses que nous venons de casser en appuyant sur le bouton de fusion. Il y a clairement un impact et des problèmes non résolus qui n'étaient pas prévus dans le PEN, donc revenir en arrière en premier est la bonne chose à faire.

Un argument pour ne pas encore revenir en arrière - pendant que le changement est dans le maître, nous pouvons tirer parti des exécutions de CI en aval pour essayer de déterminer à quoi ressembleraient leurs solutions de contournement

Le CI en aval est rouge, c'est _très_ inutile. Nous avons maintenant leur liste d'échecs, nous n'avons pas besoin de garder leur CI rouge pour nous rendre la vie un peu plus facile ici.

Et au moins le CI de Matplotlib s'exécute contre pip install --pre pas la branche principale

Et au moins le CI de Matplotlib s'exécute contre pip install --pre pas la branche principale

Cela tire des roues nocturnes auxquelles il ressemble. Le changement a déjà été annulé pour 1.18.0rc1, vous ne devriez donc pas le voir si vous installiez avec --pre de PyPI.

Certains des commentaires ci-dessus reviennent à repenser les changements proposés dans le NEP 34. Je ne sais pas si ce fil est l'endroit approprié pour continuer cette discussion, mais voilà. (Pas de mal si cela doit être discuté ailleurs - copier et coller des commentaires est facile. :smile: De plus, certains d'entre vous ont vu une variation de ces commentaires dans une discussion sur Slack.)

Après y avoir réfléchi récemment, je me suis retrouvé avec la même idée que la première suggestion de @timhoffm (et l'idée a probablement été proposée à d'autres moments au cours des derniers mois) : définir une chaîne ou un objet singleton spécifique qui, lorsqu'il est donné comme l'argument dtype de array , permet à la fonction de gérer une entrée de forme irrégulière en créant un tableau d'objets 1D. En effet, cela active le comportement pré-NEP-34 de dtype=None dans lequel l'entrée de forme irrégulière est automatiquement convertie en un tableau d'objets. Si une autre valeur pour dtype est donnée (y compris None ou object ), un avertissement de dépréciation est donné si l'entrée est en forme de guenilles. Dans une future version de NumPy, cet avertissement sera converti en erreur.

Je pense qu'il est clair maintenant que l'utilisation de dtype=object pour permettre la gestion des entrées de forme irrégulière n'est pas une bonne solution au problème. Idéalement, nous découplerions les notions de « tableau d'objets » de « tableau déchiqueté ». Mais nous ne pouvons pas complètement les découpler, car lorsque nous voulons gérer un tableau irrégulier, le seul choix que nous avons est de créer un tableau d'objets. D'un autre côté, nous voulons parfois un tableau d'objets, mais nous ne voulons pas la conversion automatique d'une entrée de forme irrégulière en un tableau d'objets de séquences.

Par exemple (cf. élément 1 dans le dernier commentaire de @seberg ), supposons que f1 , f2 , f3 et f4 soient Fraction objets, et je travaille avec des tableaux d'objets de Fraction s. Je ne suis pas intéressé par la création d'un tableau en lambeaux. Si j'écris accidentellement a = np.array([f1, f2, [f3, f4]], dtype=object) , je veux que cela génère une erreur, pour toutes les raisons pour lesquelles nous avons NEP 34. Avec NEP 34, cependant, cela créera un tableau 1D de longueur 3.

Les alternatives qui ajoutent un nouvel argument de mot-clé, telles que la deuxième suggestion de @timhoffm , semblent plus compliquées que nécessaire. Le problème que nous essayons de résoudre est le "pistolet à pied" où l'entrée irrégulière est automatiquement convertie en un tableau d'objets 1D. Le problème ne se pose que lorsque dtype=None est passé à array . Exiger des utilisateurs qu'ils remplacent dtype=None par dtype=<special-value-that-enables-ragged-handling> pour maintenir l'ancien comportement gênant est un simple changement de l'API qui est facile à expliquer. Avons-nous vraiment besoin de plus que cela ?

Je pense qu'il est clair maintenant que l'utilisation de dtype=object pour permettre la gestion des entrées de forme irrégulière n'est pas une bonne solution au problème. Idéalement, nous découplerions les notions de « tableau d'objets » de « tableau déchiqueté ».

Cela semble raisonnable, peut-être. Il est également bon de souligner qu'il n'y a pas de véritable concept de "tableau déchiqueté" dans NumPy . C'est quelque chose que nous ne prenons pas en charge fondamentalement (recherchez "ragged" dans la documentation, sur le suivi des problèmes ou la liste de diffusion pour confirmer si vous le souhaitez), c'est quelque chose que DyND et XND prennent en charge, et nous n'avons commencé à en parler que pour avoir un phrase pour discuter de « nous voulons supprimer le comportement np.array([1, [2, 3]]) qui fait trébucher les utilisateurs ». Par conséquent, la cuisson dans des "tableaux déchiquetés" en tant que nouvelle API doit être effectuée avec une extrême prudence, ce n'est absolument pas quelque chose que nous voulons promouvoir. Il serait donc bon de préciser cela dans la dénomination de tout ce que nous pouvons ajouter à dtype=some_workaround .

Il semble que l'opinion générale se rassemble autour d'une solution consistant à étendre la dépréciation (peut-être indéfiniment) en autorisant np.array(vals, dtype=special) qui se comportera comme avant NEP 34. Je préfère un singleton plutôt qu'une chaîne, car cela signifie que les utilisations de la bibliothèque peuvent faire special = getattr(np.special, None) et leur code fonctionnera dans toutes les versions.

Maintenant, nous devons décider du nom et de l'endroit où il doit être exposé. Peut-être never_fail ou guess_dimensions ? Quant à savoir où l'exposer, je préférerais ne pas le suspendre np plutôt qu'un autre module interne, peut-être avec un _ pour indiquer qu'il s'agit vraiment d'une interface privée.

Je pense que la voie à suivre consiste à amender le NEP 34, puis à exposer la discussion sur la liste de diffusion.

Notez qu'il y a eu aussi quelques rapports de problèmes avec l'utilisation des opérateurs ( == et operator.mod au moins). Proposez-vous d'ignorer cela ou de stocker d'une manière ou d'une autre cet état sur le tableau ?

Dans presque tous les cas, il est probablement connu que l'un des opérandes est un tableau numpy. Il devrait donc probablement être possible d'obtenir un comportement bien défini en convertissant manuellement en un tableau numpy.

Quelqu'un pourrait-il pointer vers l'exemple operator.mod ?

Quant à l'opérateur == , celui que j'ai vu faisait quelque chose comme np.array(vals, dtype=object) == valsvals=[1, [2, 3]] (en paraphrasant le code), donc la solution est de créer de manière proactive le tableau sur la droite côté.

La plupart des échecs scipy semblent être de la forme np.array([0.25, np.array([0.3])]) , où le mélange de scalaires et de ndarray avec shape==(1,) enfreindra la découverte de la dimension et créera un tableau d'objets. xréf gh-15075

Quelqu'un pourrait-il pointer vers l'exemple operator.mod ?

J'ai vu cela dans le PR Pandas de @jbrockmendel , mais je pense que cela a changé depuis (ne voyez plus de operator.mod explicite dans les commentaires).

Quant à l'opérateur == , celui que j'ai vu faisait quelque chose comme np.array(vals, dtype=object) == valsvals=[1, [2, 3]] (en paraphrasant le code), donc la solution est de créer de manière proactive le tableau sur la droite côté.

À ce stade, cela devient np.array(vals, dtype=object) == np.array(vals, dtype=object) , alors mieux vaut simplement supprimer le test :)

@mattip a écrit :

Je préfère un singleton plutôt qu'une chaîne, car cela signifie que les utilisations de la bibliothèque peuvent faire special = getattr(np.special, None) et que leur code fonctionnera dans toutes les versions.

Cela me semble OK.

Maintenant, nous devons décider du nom et de l'endroit où il doit être exposé. Peut-être never_fail ou guess_dimensions ? Quant à savoir où l'exposer, je préférerais ne pas le suspendre à np plutôt qu'à un autre module interne, peut-être avec un _ pour indiquer qu'il s'agit vraiment d'une interface privée.

Mon nom de travail actuel pour cela est legacy_auto_dtype , mais il y a probablement beaucoup d'autres noms pour lesquels je ne me plaindrais pas.

Je ne suis pas sûr que le nom doive être privé. Par n'importe quelle définition pratique de _private_ et _public_, ce sera un objet _public_. Il fournit aux utilisateurs les moyens de préserver le comportement hérité de, par exemple, array(data) en le réécrivant sous la forme array(data, dtype=legacy_auto_dtype) . J'imagine que le NEP mis à jour expliquera que c'est ainsi que le code doit être modifié pour maintenir le comportement hérité (pour ceux qui doivent le faire). Si tel est le cas, l'objet n'est certainement pas privé. En fait, il semble que ce soit un objet public qui restera indéfiniment dans NumPy. Mais peut-être que ma compréhension de la façon dont le NEP 34 modifié se déroulera est fausse.

D'accord avec la description de @WarrenWeckesser de public/privé ; soit il est public, soit il ne doit être utilisé par personne en dehors de NumPy.

Re nom : veuillez choisir un nom qui décrit la fonctionnalité. Des choses comme « l'héritage » ne sont presque jamais une bonne idée.

veuillez choisir un nom qui décrit la fonctionnalité.

auto_object , auto_dtype , auto ?

Réfléchir un peu à voix haute...

A quoi sert cet objet ?

Actuellement, lorsque NumPy reçoit un objet Python qui contient des sous-séquences dont les longueurs ne sont pas cohérentes avec un tableau nd régulier, NumPy créera un tableau avec le type de données object , avec les objets au premier niveau où l'incohérence de forme se produit laissés en tant qu'objets Python. Par exemple, array([[1, 2], [1, 2, 3]]) a la forme (2,) , np.array([[1, 2], [3, [99]]]) a la forme (2, 2) , etc. Avec NEP 34, nous déprécions ce comportement, nous essayons donc de créer un tableau avec une entrée "ragged" entraînera éventuellement une erreur, à moins qu'il ne soit explicitement activé. La valeur spéciale dont nous parlons active l'ancien comportement.

C'est quoi un bon nom pour ça ? ragged_as_object ? inconsistent_shapes_as_object ?

À ce stade, cela devient np.array(vals, dtype=object) == np.array(vals, dtype=object) , alors mieux vaut simplement supprimer le test :)

Eh bien, je paraphrasais. Le test réel ressemble plus à my_func(vals) == vals devrait devenir my_func(vals) == np.array(vals, dtype=object)

Je proposerai une extension à NEP 34 pour permettre une valeur spéciale pour dtype.

Notez qu'il semble que scipy n'ait pas besoin de cette sentinelle pour réussir les tests avec scipy/scipy#11310 et scipy/scipy#11308

gh-15119 a été fusionné, ce qui a réimplémenté le NEP. S'il n'est pas rétabli, nous pouvons fermer ce problème

Je vais fermer ceci, car nous n'avons pas fait de suivi avant la version 1.19. Et j'espère au moins que la raison en est que la discussion s'est éteinte puisque tous les grands projets ont pu trouver des solutions raisonnables aux problèmes qu'elle a créés.
Veuillez me corriger si je me trompe, surtout si cela est toujours sujet à des problèmes avec les pandas, matplotlib, etc. Mais je suppose que nous en aurions entendu parler pendant le cycle de candidature de la version 1.19.x.

Cette page vous a été utile?
0 / 5 - 0 notes