jak powiedzieć, że zmienna jest iterowalna, ale nie jest łańcuchem

88

Mam funkcję, która przyjmuje argument, który może być pojedynczą lub podwójną pozycją:

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

po to aby:

>>> iterowalne (("f", "f"))
tak

>>> iterowalne (["f", "f"])
tak

>>> iterowalne ("ff")
Nie

Problem polega na tym, że ciąg jest technicznie iterowalny, więc nie mogę po prostu złapać ValueError podczas próby arg[1]. Nie chcę używać isinstance (), ponieważ to nie jest dobra praktyka (a przynajmniej tak mi powiedziano).

ksiądzc
źródło
1
Która wersja Pythona? Uważam, że odpowiedź jest inna między 2. * a 3.
Kathy Van Stone
4
Powiedziano ci niepoprawnie, czy przypadek nie jest złą praktyką.
Lennart Regebro
3
Och, czekaj, może odwołuje się do zasady, że źle jest sprawdzać typ obiektów i że to wskazuje na uszkodzenie programu? W zasadzie jest to prawda (ale nie zawsze w praktyce). Tak może być, ale nie musi. Ale to nie funkcja jest przypadkiem, jest to problem, to zwyczaj sprawdzania typów.
Lennart Regebro
@Lennart: canonical.org/~kragen/isinstance może być jednak nieaktualne
Priestc
@up To nie wspomina jednak o przeciążaniu funkcji opartych na typach i isinstancejest to sposób na zrobienie tego w językach dynamicznie typowanych. Rzecz nie do codziennego użytku, ale OK w uzasadnionych przypadkach.
Kos

Odpowiedzi:

50

Użyj instancji (nie rozumiem, dlaczego jest to zła praktyka)

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

Zwróć uwagę na użycie StringTypes. Zapewnia, że ​​nie zapomnimy o jakimś niejasnym typie łańcucha.

Z drugiej strony działa to również w przypadku pochodnych klas ciągów.

class MyString(str):
    pass

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

Możesz również rzucić okiem na to poprzednie pytanie .

Twoje zdrowie.


Uwaga: zachowanie zmieniło się w Pythonie 3 jako StringTypesi basestringnie są już zdefiniowane. W zależności od potrzeb możesz je zastąpić isinstanceprzez strlub podzbiorem (str, bytes, unicode)np. Dla użytkowników Cythona. Jak wspomniał @Theron Luhn , możesz również użyć six.

scvalex
źródło
Niezłe, scvalex. Usuwam teraz moje -1 i zmieniam je na +1 :-).
Tom
2
Myślę, że zły pomysł na praktykę wynika z zasady pisania na klawiaturze . Bycie członkiem określonej klasy nie oznacza, że ​​jest to jedyny obiekt, którego można użyć, ani że dostępne są oczekiwane metody. Ale myślę, że czasami po prostu nie można wywnioskować, co robi ta metoda, nawet jeśli jest obecna, więc isinstancemoże to być jedyny sposób.
estani
2
Uwaga: types.StringTypes nie jest dostępny w Pythonie 3. Ponieważ w py3k jest tylko jeden typ łańcucha, myślę, że jest to bezpieczne do isinstance(arg, str). W przypadku wersji kompatybilnej wstecz rozważ użycie pythonhosted.org/six/#six.string_types
Theron Luhn
Ściśle używam Python3 i zauważyłem, że types.StringTypesnie jest dostępny w Python3. Jaka jest wartość w Python2?
kevinarpe
2
2017 : Ta odpowiedź nie jest już prawidłowa; zobacz stackoverflow.com/a/44328500/99834, aby znaleźć taki, który działa ze wszystkimi wersjami Pythona.
sorin
26

Od 2017 roku jest to przenośne rozwiązanie, które działa ze wszystkimi wersjami Pythona:

#!/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
źródło
Występują drobne niespójności między 2/3 a bajtestami, ale jeśli używasz natywnego „ciągu znaków”, oba są fałszywe
Nick T
16

Od czasu Pythona 2.6, wraz z wprowadzeniem abstrakcyjnych klas bazowych, isinstance(używane w ABC, a nie w konkretnych klasach) jest teraz uważane za całkowicie akceptowalne. Konkretnie:

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

To jest dokładna kopia (zmieniająca tylko nazwę klasy) Iterablezgodnie z definicją w _abcoll.py(szczegół implementacji collections.py) ... powodem, dla którego działa to zgodnie z życzeniem, a collections.Iterablenie jest, jest to, że ta ostatnia idzie o krok dalej, aby zapewnić, że ciągi są uważane za iterowalne, wywołując Iterable.register(str)jawnie tuż po tej classinstrukcji.

Oczywiście łatwo jest to rozszerzyć __subclasshook__, powracając Falseprzed anywywołaniem innych klas, które chcesz konkretnie wykluczyć ze swojej definicji.

W każdym razie, po zaimportowaniu tego nowego modułu takim myiter, jakim isinstance('ciao', myiter.NonStringIterable)będzie , będzie Falsei isinstance([1,2,3], myiter.NonStringIterable)będzie True, tak jak sobie zażyczysz - aw Pythonie 2.6 i nowszych jest to uważane za właściwy sposób realizacji takich kontroli ... zdefiniuj abstrakcyjną klasę bazową i sprawdź isinstanceto.

Alex Martelli
źródło
W Pythonie 3 isinstance('spam', NonStringIterable)zwraca True.
Nick T
1
(...) iw Pythonie 2.6 i późniejszych jest to uważane za właściwy sposób ucieleśnienia takich kontroli (...) To, jak nadużywanie dobrze znanej koncepcji klasy abstrakcyjnej w taki sposób może być kiedykolwiek uznane za właściwe, jest poza moim zrozumieniem. Właściwym sposobem byłoby wprowadzenie zamiast tego operatora przypominającego wyglądem .
Piotr Dobrogost
Alex, czy możesz odnieść się do stwierdzenia Nicka, że ​​to nie działa w Pythonie 3? Podoba mi się odpowiedź, ale chciałbym się upewnić, że piszę kod przyszłościowy.
Merlyn Morgan-Graham
@ MerlynMorgan-Graham, to prawda, ponieważ __iter__ jest teraz zaimplementowany w łańcuchach w Pythonie 3. Tak więc mój „łatwy do rozszerzenia” akapit ma zastosowanie i np. if issublass(cls, str): return FalseMusi zostać dodany na początku __subclasshook__(jak również wszelkie inne klasy, które definiują, __iter__ale w twoim sposobu myślenia nie można akceptować jako „iterowalnych elementów niebędących ciągami”).
Alex Martelli
@AlexMartelli W przypadku Pythona 3, czy nie masz na myśli tego, że if issublass(C, str): return Falsenależy go dodać?
Rob Smallshire,
4

Zdaję sobie sprawę, że to stary post, ale pomyślałem, że warto dodać moje podejście do potomności Internetu. Poniższa funkcja wydaje się działać w większości przypadków zarówno z Pythonem 2, jak i 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

Sprawdza, czy nie jest ciągiem iterowalnym przez (mis) przy użyciu funkcji wbudowanej, hasattrktóra zgłosi a, TypeErrorgdy jego drugi argument nie jest łańcuchem ani łańcuchem Unicode.

Nigel Small
źródło
3

Łącząc poprzednie odpowiedzi, używam:

import types
import collections

#[...]

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

#[Do stuff...]

Nie jest to w 100% dowód na głupców, ale jeśli obiekt nie jest iterowalny, nadal możesz pozwolić mu przejść i wrócić do pisania na klawiaturze.


Edycja: Python3

types.StringTypes == (str, unicode). Odpowiednik Phythona3 to:

if isinstance(var, str ) \
    or not isinstance(var, collections.Iterable):
xvan
źródło
Deklaracja dotycząca importu powinna mieć
postać
3

2.x

Zasugerowałbym:

hasattr(x, '__iter__')

lub w związku z komentarzem Davida Charlesa, który poprawił to dla Python3, co z:

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

3.x

wbudowany basestringtyp abstrakcyjny został usunięty . Użyj strzamiast tego. Te stri bytestypy nie mają tyle wspólnego, funkcjonalność, aby uzasadnić wspólną klasę bazową.

mike gryzoń
źródło
3
Być może dlatego, __iter__że w Pythonie 3 są łańcuchy?
davidrmcharles
@DavidCharles Och, naprawdę? Mój błąd. Jestem użytkownikiem Jythona, a Jython obecnie nie ma wersji 3.
mike gryzoń
To nie jest tak naprawdę odpowiedź, a raczej komentarz / pytanie, a to jest złe dla 3.x. Czy możesz to posprzątać? Czy możesz dodać uzasadnienie roszczenia „Typy„ str ”i„ bajty ”nie mają wystarczająco wspólnych funkcji, aby zagwarantować współdzieloną klasę bazową? Jednym z kluczowych punktów 3.x było uczynienie bajtów Unicode obywatelem pierwszej klasy.
smci
Nie mam pojęcia, dlaczego napisałem którekolwiek z powyższych. Proponuję usunąć cały tekst z „3.x” ... chociaż moja odpowiedź została już edytowana. Edytuj go bardziej, jeśli chcesz.
mike gryzoń
0

Jak słusznie zauważyłeś, pojedynczy ciąg to sekwencja znaków.

Więc to, co naprawdę chcesz zrobić, to dowiedzieć się, jaki rodzaj sekwencji argjest przy użyciu isinstance lub type (a) == str.

Jeśli chcesz zrealizować funkcję, która przyjmuje zmienną ilość parametrów, powinieneś to zrobić w następujący sposób:

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

function („ff”) i function („ff”, „ff”) będą działać.

Nie widzę scenariusza, w którym potrzebna jest funkcja isiterable () taka jak Twoja. To nie isinstance () jest złym stylem, ale sytuacjami, w których musisz użyć isinstance ().

Otto Allmendinger
źródło
4
type(a) == strNależy unikać używania . Jest to zła praktyka, ponieważ nie bierze pod uwagę podobnych typów ani typów pochodzących z str. typenie wspina się po hierarchii typów, podczas gdy isinstancetak, dlatego lepiej jest użyć isinstance.
AkiRoss
0

Aby wyraźnie rozwinąć świetną sztuczkę Alexa Martelliego collections.pyi odpowiedzieć na niektóre związane z nim pytania: Aktualnym działającym rozwiązaniem w Pythonie 3.6+ jest

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

i zademonstrowane

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

Jeśli chcesz iter(''), na przykład, dodać do wykluczeń, zmodyfikuj wiersz

            if issubclass(c, str):
                return False

być

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

dostać

[False, False, True, True, True, True]
Alexander McFarlane
źródło