comment dire qu'une variable est itérable mais pas une chaîne

88

J'ai une fonction qui prend un argument qui peut être un élément simple ou un élément double:

def iterable(arg)
    if #arg is an iterable:
        print "yes"
    else:
        print "no"

pour que:

>>> itérable (("f", "f"))
Oui

>>> itérable (["f", "f"])
Oui

>>> itérable ("ff")
non

Le problème est que la chaîne est techniquement itérable, donc je ne peux pas simplement attraper ValueError en essayant arg[1]. Je ne veux pas utiliser isinstance (), car ce n'est pas une bonne pratique (du moins c'est ce qu'on me dit).

prêtre
la source
1
Quelle version de Python? Je crois que la réponse est différente entre 2. * et 3
Kathy Van Stone
4
On vous a dit incorrectement, ce n'est pas une mauvaise pratique.
Lennart Regebro
3
Oh, attendez, peut-être qu'il se réfère au principe selon lequel il est mauvais de vérifier un type d'objets, et que cela indique que le programme est cassé? Cela est vrai en principe (mais pas toujours en pratique). Cela peut ou non être un tel cas. Mais ce n'est pas la fonction ou l'instance qui pose problème, c'est l'habitude de vérifier les types.
Lennart Regebro
@Lennart: canonical.org/~kragen/isinstance, il est peut-être obsolète
Priestc
@up Cela ne mentionne pas la surcharge de fonctions basée sur le type, mais isinstancec'est la façon de le faire dans des langages typés dynamiquement. Une chose à ne pas utiliser tous les jours, mais OK dans des cas justifiés.
Kos

Réponses:

50

Utilisez une instance (je ne vois pas pourquoi c'est une mauvaise pratique)

import types
if not isinstance(arg, types.StringTypes):

Notez l'utilisation de StringTypes. Cela garantit que nous n'oublions pas un type de chaîne obscur.

En revanche, cela fonctionne également pour les classes de chaînes dérivées.

class MyString(str):
    pass

isinstance(MyString("  "), types.StringTypes) # true

Aussi, vous voudrez peut-être jeter un œil à cette question précédente .

À votre santé.


NB: le comportement a changé dans Python 3 au fur StringTypeset à mesure et basestringne sont plus définis. Selon vos besoins, vous pouvez les remplacer isinstancepar str, ou par un sous-ensemble de (str, bytes, unicode), par exemple pour les utilisateurs Cython. Comme @Theron Luhn l'a mentionné, vous pouvez également utiliser six.

scvalex
la source
Sympa, scvalex. Je retire mon -1 maintenant et en fait un +1 :-).
Tom
2
Je pense que la mauvaise idée de pratique est due au principe de typage du canard . Être membre d'une classe particulière ne signifie ni que c'est le seul objet qui peut être utilisé ni que les méthodes attendues sont disponibles. Mais je pense que parfois vous ne pouvez tout simplement pas déduire ce que fait la méthode même si elle est présente, ce isinstancepourrait donc être le seul moyen.
estani
2
Remarque: types.StringTypes n'est pas disponible dans Python 3. Puisqu'il n'y a qu'un seul type de chaîne dans py3k, je pense que c'est sûr do isinstance(arg, str). Pour une version rétrocompatible, pensez à utiliser pythonhosted.org/six/#six.string_types
Theron Luhn
J'utilise strictement Python3 et j'ai remarqué qu'il types.StringTypesn'est pas disponible dans Python3. Quelle est la valeur de Python2?
kevinarpe
2
2017 : Cette réponse n'est plus valide, voir stackoverflow.com/a/44328500/99834 pour celle qui fonctionne avec toutes les versions de Python.
sorin
26

À partir de 2017, voici une solution portable qui fonctionne avec toutes les versions de Python:

#!/usr/bin/env python
import collections
import six


def iterable(arg):
    return (
        isinstance(arg, collections.Iterable) 
        and not isinstance(arg, six.string_types)
    )


# non-string iterables    
assert iterable(("f", "f"))    # tuple
assert iterable(["f", "f"])    # list
assert iterable(iter("ff"))    # iterator
assert iterable(range(44))     # generator
assert iterable(b"ff")         # bytes (Python 2 calls this a string)

# strings or non-iterables
assert not iterable(u"ff")     # string
assert not iterable(44)        # integer
assert not iterable(iterable)  # function
Sorin
la source
Il y a quelques incohérences mineures entre 2/3 avec des chaînes d'octets, mais si vous utilisez la "chaîne" native, elles sont toutes les deux fausses
Nick T
16

Depuis Python 2.6, avec l'introduction de classes de base abstraites, isinstance(utilisé sur ABC, pas sur des classes concrètes) est maintenant considéré comme parfaitement acceptable. Plus précisément:

from abc import ABCMeta, abstractmethod

class NonStringIterable:
    __metaclass__ = ABCMeta

    @abstractmethod
    def __iter__(self):
        while False:
            yield None

    @classmethod
    def __subclasshook__(cls, C):
        if cls is NonStringIterable:
            if any("__iter__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

Ceci est une copie exacte (en changeant uniquement le nom de la classe) de Iterabletel que défini dans _abcoll.py(un détail d'implémentation de collections.py) ... la raison pour laquelle cela fonctionne comme vous le souhaitez, alors que ce collections.Iterablen'est pas le cas, est que ce dernier fait un effort supplémentaire pour s'assurer que les chaînes sont considéré comme itérable, en appelant Iterable.register(str)explicitement juste après cette classinstruction.

Bien sûr, il est facile d'augmenter __subclasshook__en retournant Falseavant l' anyappel pour d'autres classes que vous souhaitez exclure spécifiquement de votre définition.

Dans tous les cas, après avoir importé ce nouveau module tel que myiter, isinstance('ciao', myiter.NonStringIterable)sera Falseet isinstance([1,2,3], myiter.NonStringIterable)sera True, comme vous le demandez - et dans Python 2.6 et versions ultérieures, cela est considéré comme la bonne façon d'incarner de telles vérifications ... définissez une classe de base abstraite et vérifiez- isinstancele.

Alex Martelli
la source
Dans Python 3 isinstance('spam', NonStringIterable)renvoie True.
Nick T
1
(...) et dans Python 2.6 et les versions ultérieures, cela est considéré comme la bonne façon d'incarner de telles vérifications (...) Comment abuser du concept bien connu de classe abstraite d'une telle manière pourrait jamais être considéré comme approprié est au-delà de ma compréhension. La bonne manière serait d'introduire un opérateur ressemblant à la place.
Piotr Dobrogost
Alex, pouvez-vous répondre à l'affirmation de Nick selon laquelle cela ne fonctionne pas dans Python 3? J'aime la réponse, mais j'aimerais m'assurer que j'écris du code à l'épreuve du temps.
Merlyn Morgan-Graham
@ MerlynMorgan-Graham, c'est correct, car il __iter__ est maintenant implémenté dans des chaînes en Python 3. Donc mon paragraphe "facile à augmenter" devient applicable et if issublass(cls, str): return Falsedoit par exemple être ajouté au début de __subclasshook__(ainsi que toute autre classe qui définit __iter__mais dans votre l'état d'esprit ne doit pas être accepté comme des «itérables sans chaîne»).
Alex Martelli le
@AlexMartelli Pour Python 3, ne voulez-vous pas dire que cela if issublass(C, str): return Falsedevrait être ajouté?
Rob Smallshire
4

Je me rends compte que c'est un ancien article, mais j'ai pensé qu'il valait la peine d'ajouter mon approche pour la postérité sur Internet. La fonction ci-dessous semble fonctionner pour moi dans la plupart des cas avec Python 2 et 3:

def is_collection(obj):
    """ Returns true for any iterable which is not a string or byte sequence.
    """
    try:
        if isinstance(obj, unicode):
            return False
    except NameError:
        pass
    if isinstance(obj, bytes):
        return False
    try:
        iter(obj)
    except TypeError:
        return False
    try:
        hasattr(None, obj)
    except TypeError:
        return True
    return False

Cela vérifie une non-chaîne itérable par (mis) en utilisant le intégré hasattrqui lèvera a TypeErrorlorsque son deuxième argument n'est pas une chaîne ou une chaîne Unicode.

Nigel petit
la source
3

En combinant les réponses précédentes, j'utilise:

import types
import collections

#[...]

if isinstance(var, types.StringTypes ) \
    or not isinstance(var, collections.Iterable):

#[Do stuff...]

Ce n'est pas une preuve à 100%, mais si un objet n'est pas un itérable, vous pouvez toujours le laisser passer et retomber dans la frappe de canard.


Edit: Python3

types.StringTypes == (str, unicode). L'équivalent Phython3 est:

if isinstance(var, str ) \
    or not isinstance(var, collections.Iterable):
Xvan
la source
Votre déclaration d'importation doit être «types» et non «type»
PaulR
3

2.x

J'aurais suggéré:

hasattr(x, '__iter__')

ou au vu du commentaire de David Charles modifiant ceci pour Python3, qu'en est-il:

hasattr(x, '__iter__') and not isinstance(x, (str, bytes))

3.x

le basestringtype abstrait intégré a été supprimé . Utilisez strplutôt. Les types stret bytesn'ont pas suffisamment de fonctionnalités en commun pour justifier une classe de base partagée.

Mike rongeur
la source
3
Peut-être parce __iter__qu'il y a des chaînes en Python 3?
davidrmcharles
@DavidCharles Oh, vraiment? Ma faute. Je suis un utilisateur de Jython et Jython n'a actuellement pas de version 3.
mike rodent
Ce n'est pas vraiment une réponse, plus un commentaire / une question, et c'est faux pour 3.x. Pourriez-vous le nettoyer? Pouvez-vous ajouter une justification pour réclamer "Les types 'str' et 'bytes' n'ont pas de fonctionnalités suffisamment communes pour justifier une classe de base partagée. L'un des points clés de la 3.x était de faire des octets Unicode un citoyen de premier ordre.
smci
Je n'ai aucune idée de la raison pour laquelle j'ai écrit l'un de ces articles Je propose de supprimer tout le texte sous "3.x" ... bien que vous ayez déjà édité ma réponse. Modifiez-le davantage si vous le souhaitez.
mike rodent
0

Comme vous le faites remarquer correctement, une seule chaîne est une séquence de caractères.

Donc, la chose que vous voulez vraiment faire est de savoir quel type de séquence argest en utilisant isinstance ou type (a) == str.

Si vous voulez réaliser une fonction qui prend une quantité variable de paramètres, vous devriez le faire comme ceci:

def function(*args):
    # args is a tuple
    for arg in args:
        do_something(arg)

function ("ff") et function ("ff", "ff") fonctionneront.

Je ne vois pas de scénario où une fonction isiterable () comme la vôtre est nécessaire. Ce n'est pas isinstance () qui est de mauvais style mais des situations où vous devez utiliser isinstance ().

Otto Allmendinger
la source
4
L'utilisation type(a) == strdoit être évitée. C'est une mauvaise pratique car elle ne prend pas en compte des types similaires ou des types dérivés de str. typene monte pas dans la hiérarchie des types, alors qu'il le isinstancefait, il est donc préférable d'utiliser isinstance.
AkiRoss
0

Pour développer explicitement l'excellent hack d'Alex Martelli collections.pyet répondre à certaines des questions qui l'entourent: La solution de travail actuelle dans python 3.6+ est

import collections
import _collections_abc as cabc
import abc


class NonStringIterable(metaclass=abc.ABCMeta):

    __slots__ = ()

    @abc.abstractmethod
    def __iter__(self):
        while False:
            yield None

    @classmethod
    def __subclasshook__(cls, c):
        if cls is NonStringIterable:
            if issubclass(c, str):
                return False
            return cabc._check_methods(c, "__iter__")
        return NotImplemented

et démontré

>>> typs = ['string', iter(''), list(), dict(), tuple(), set()]
>>> [isinstance(o, NonStringIterable) for o in typs]
[False, True, True, True, True, True]

Si vous souhaitez ajouter iter('')dans les exclusions, par exemple, modifiez la ligne

            if issubclass(c, str):
                return False

être

            # `str_iterator` is just a shortcut for `type(iter(''))`*
            if issubclass(c, (str, cabc.str_iterator)):
                return False

obtenir

[False, False, True, True, True, True]
Alexander McFarlane
la source