come dire che una variabile è iterabile ma non una stringa


92

Ho una funzione che accetta un argomento che può essere un elemento singolo o un elemento doppio:

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

così che:

>>> iterabile (("f", "f"))
sì

>>> iterabile (["f", "f"])
sì

>>> iterabile ("ff")
no

Il problema è che la stringa è tecnicamente iterabile, quindi non posso semplicemente rilevare ValueError quando provo arg[1]. Non voglio usare isinstance (), perché non è una buona pratica (o almeno così mi è stato detto).


1
Quale versione di Python? Credo che la risposta sia diversa tra 2. * e 3
Kathy Van Stone,

4
Ti è stato detto in modo errato, l'istanza non è una cattiva pratica.
Lennart Regebro

3
Oh, aspetta, forse si riferisce al principio che è sbagliato controllare un tipo di oggetto e che questa è un'indicazione che il programma è rotto? Questo è vero in linea di principio (ma non sempre in pratica). Questo può o non può essere un caso del genere. Ma non è la funzione è l'istanza che è il problema, è l'abitudine di controllare i tipi.
Lennart Regebro

@Lennart: canonical.org/~kragen/isinstance potrebbe essere obsoleto però
priestc

@up Ciò non menziona il sovraccarico di funzioni basato sul tipo, ed isinstanceè il modo per farlo nei linguaggi tipizzati dinamicamente. Una cosa da non usare tutti i giorni, ma va bene in casi giustificati.
Kos

Risposte:


50

Usa un'istanza (non vedo perché sia ​​una cattiva pratica)

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

Nota l'uso di StringTypes. Assicura che non ci dimentichiamo di qualche tipo oscuro di stringa.

Al rialzo, questo funziona anche per le classi di stringhe derivate.

class MyString(str):
    pass

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

Inoltre, potresti voler dare un'occhiata a questa domanda precedente .

Saluti.


NB: comportamento cambiato in Python 3 in quanto StringTypese basestringnon sono più definiti. A seconda delle proprie esigenze, è possibile sostituirli in isinstanceby stro in una tupla di sottoinsiemi (str, bytes, unicode), ad esempio per gli utenti Cython. Come menzionato da @Theron Luhn , puoi anche usare six.


Bello, scvalex. Sto rimuovendo il mio -1 ora e lo sto rendendo un +1 :-).
Tom

2
Penso che l'idea di cattiva pratica sia dovuta al principio di battitura a papera . Essere un membro di una particolare classe non significa che sia l' unico oggetto che può essere utilizzato né che i metodi previsti siano disponibili. Ma penso che a volte non puoi dedurre cosa fa il metodo anche se è presente, quindi isinstancepotrebbe essere l'unico modo.
estani

2
Nota: types.StringTypes non è disponibile in Python 3. Dato che c'è un solo tipo di stringa in py3k, penso che sia sicuro do isinstance(arg, str). Per una versione compatibile con le versioni precedenti, considera l'utilizzo di pythonhosted.org/six/#six.string_types
Theron Luhn

Uso rigorosamente Python3 e ho notato che types.StringTypesnon è disponibile in Python3. Qual è il valore in Python2?
kevinarpe

2
2017 : questa risposta non è più valida, vedere stackoverflow.com/a/44328500/99834 per uno che funziona con tutte le versioni di Python.
sorin

26

A partire dal 2017, ecco una soluzione portatile che funziona con tutte le versioni di 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

Ci sono alcune piccole incongruenze tra 2/3 con bytestrings, ma se usi la "stringa" nativa allora sono entrambe false
Nick T

16

Dal momento che Python 2.6, con l'introduzione delle classi base astratte, isinstance(usate su ABC, non classi concrete) è ora considerato perfettamente accettabile. Nello specifico:

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

Questa è una copia esatta (cambiando solo il nome della classe) di Iterablecome definito in _abcoll.py(un dettaglio di implementazione di collections.py) ... il motivo per cui funziona come desideri, mentre collections.Iterablenon lo fa, è che quest'ultimo fa il possibile per garantire che le stringhe siano considerato iterabile, chiamando Iterable.register(str)esplicitamente subito dopo questa classistruzione.

Ovviamente è facile aumentare __subclasshook__tornando Falseprima della anychiamata per altre classi che desideri escludere specificamente dalla tua definizione.

In ogni caso, dopo aver importato questo nuovo modulo come myiter, isinstance('ciao', myiter.NonStringIterable)sarà Falsee isinstance([1,2,3], myiter.NonStringIterable)sarà True, proprio come richiesto - e in Python 2.6 e versioni successive questo è considerato il modo corretto per incorporare tali controlli ... definire una classe base astratta e controllalo isinstance.


In Python 3 isinstance('spam', NonStringIterable)ritorna True.
Nick T

1
(...) e in Python 2.6 e versioni successive questo è considerato il modo appropriato per incorporare tali controlli (...) Come l'abuso del ben noto concetto di classe astratta in un tale modo possa essere considerato il modo corretto è al di là della mia comprensione. Il modo corretto sarebbe invece introdurre alcuni operatori di aspetto .
Piotr Dobrogost

Alex, puoi rispondere all'affermazione di Nick secondo cui questo non funziona in Python 3? Mi piace la risposta, ma vorrei assicurarmi di scrivere codice a prova di futuro.
Merlyn Morgan-Graham

@ MerlynMorgan-Graham, è corretto, perché ora __iter__ è implementato in stringhe in Python 3. Quindi il mio paragrafo "facile da aumentare" diventa applicabile e ad esempio if issublass(cls, str): return Falsedeve essere aggiunto all'inizio di __subclasshook__(così come qualsiasi altra classe che definisce __iter__ma nel tuo la mentalità non deve essere accettata come "iterabili non stringa").
Alex Martelli

@AlexMartelli per Python 3, non intendi che if issublass(C, str): return Falsedovrebbe essere aggiunto?
Rob Smallshire il

4

Mi rendo conto che questo è un vecchio post, ma ho pensato che valesse la pena aggiungere il mio approccio per i posteri di Internet. La funzione di seguito sembra funzionare per me nella maggior parte dei casi con Python 2 e 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

Questo verifica la presenza di una stringa non iterabile da (mis) utilizzando l'integrato hasattrche solleverà a TypeErrorquando il suo secondo argomento non è una stringa o una stringa Unicode.


3

Combinando le risposte precedenti, sto usando:

import types
import collections

#[...]

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

#[Do stuff...]

Non a prova di stupido al 100%, ma se un oggetto non è un iterabile puoi comunque lasciarlo passare e ricadere nella digitazione anatra.


Modifica: Python3

types.StringTypes == (str, unicode). L'equivalente Phython3 è:

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

La tua dichiarazione di importazione dovrebbe essere "tipi" non "tipo"
PaulR

3

2.x

Avrei suggerito:

hasattr(x, '__iter__')

o in vista del commento di David Charles che modifica questo per Python3, che dire di:

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

3.x

il basestringtipo astratto incorporato è stato rimosso . Usa strinvece. I tipi stre bytesnon hanno abbastanza funzionalità in comune per garantire una classe base condivisa.


3
Forse perché __iter__c'è sulle stringhe in Python 3?
davidrmcharles

@DavidCharles Oh, davvero? Colpa mia. Sono un utente Jython e Jython attualmente non ha la versione 3.
mike rodent

Questa non è davvero una risposta, più un commento / domanda, ed è sbagliato per 3.x. Potresti ripulirlo? Puoi aggiungere una giustificazione per affermare che "I tipi" str "e" byte "non hanno abbastanza funzionalità in comune per garantire una classe di base condivisa." Uno dei punti chiave di 3.x era rendere Unicode bytes un cittadino di prima classe.
smci

Non ho la più pallida idea del motivo per cui ho scritto quanto sopra. Propongo di cancellare tutto il testo sotto "3.x" ... anche se hai già modificato la mia risposta. Modificalo di più, se lo desideri.
mike rodent il

0

Come hai sottolineato correttamente, una singola stringa è una sequenza di caratteri.

Quindi la cosa che vuoi veramente fare è scoprire che tipo di sequenza argè usando isinstance o digita (a) == str.

Se vuoi realizzare una funzione che accetta una quantità variabile di parametri, dovresti farlo in questo modo:

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

function ("ff") e function ("ff", "ff") funzioneranno.

Non riesco a vedere uno scenario in cui è necessaria una funzione isiterable () come la tua. Non è isinstance () che è cattivo stile, ma situazioni in cui è necessario utilizzare isinstance ().


4
L'uso type(a) == strdeve essere evitato. È una cattiva pratica perché non prende in considerazione tipi simili o tipi derivati ​​da str. typenon scala la gerarchia dei tipi, mentre lo isinstancefa, quindi è meglio usare isinstance.
AkiRoss

0

Per espandere esplicitamente l'eccellente hack di Alex Martelli collections.pye rispondere ad alcune delle domande che lo circondano: L'attuale soluzione di lavoro in Python 3.6+ è

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

e dimostrato

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

Se vuoi aggiungere iter('')delle esclusioni, ad esempio, modifica la riga

            if issubclass(c, str):
                return False

essere

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

ottenere

[False, False, True, True, True, True]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.