як визначити, що змінна є ітераційною, але не рядком


88

У мене є функція, яка приймає аргумент, який може бути як одиничним, так і подвійним елементом:

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

так що:

>>> ітерабельний (("f", "f"))
так

>>> ітерабельний (["f", "f"])
так

>>> ітерабельний ("ff")
немає

Проблема в тому, що рядок технічно повторюваний, тому я не можу просто вловити ValueError при спробі arg[1]. Я не хочу використовувати isinstance (), тому що це погана практика (або, так мені сказали).


1
Яка версія Python? Я вважаю, що відповідь відрізняється між 2. * і 3
Кеті Ван Стоун

4
Вам сказали неправильно, тобто обставини - це не погана практика.
Леннарт Регебро

3
О, почекайте, можливо, він посилається на принцип, що погано перевіряти тип об’єкта і що це свідчить про роботу програми? Це в принципі так (але не завжди на практиці). Це може бути і не бути таким випадком. Але проблема полягає не в функції, а в звичці перевіряти типи.
Леннарт Регебро

@Lennart: canonical.org/~kragen/isinstance це може бути застарілим, хоча
священик

@up Це, однак, не згадує про перевантаження функції на основі типу, і isinstanceце спосіб зробити це на динамічно набраних мовах. Річ, яку не слід використовувати щодня, але в обґрунтованих випадках добре.
Кос

Відповіді:


50

Використовуйте isinstance (я не розумію, чому це погана практика)

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

Зверніть увагу на використання StringTypes. Це гарантує, що ми не забуваємо про якийсь неясний тип рядка.

З іншого боку, це також працює для похідних класів рядків.

class MyString(str):
    pass

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

Крім того, ви можете поглянути на це попереднє запитання .

Ура.


NB: поведінка змінена в Python 3 як StringTypesі basestringбільше не визначається. Залежно від ваших потреб, ви можете замінити їх isinstanceна str, або підмножину (str, bytes, unicode), наприклад, для користувачів Cython. Як згадував @Theron Luhn , ви також можете використовувати six.


Приємно, scvalex. Я зараз видаляю -1 і роблю +1 :-).
Том

2
Я думаю, що ідея поганої практики полягає в принципі друкування качок . Бути членом певного класу не означає ні того, що це єдиний об'єкт, який можна використовувати, ні того, що очікувані методи доступні. Але я думаю, що іноді ви просто не можете зробити висновок про те, що робить метод, навіть якщо він присутній, тому це isinstanceможе бути єдиним способом.
estani

2
Примітка: types.StringTypes недоступний у Python 3. Оскільки в py3k є лише один тип рядка, я вважаю, що це безпечно do isinstance(arg, str). Для версії, сумісної із зворотною стороною
Theron

Я строго використовую Python3 і помітив, що types.StringTypesвін недоступний у Python3. Яке значення в Python2?
kevinarpe

2
2017 : Ця відповідь вже недійсна, див. Stackoverflow.com/a/44328500/99834 для тієї, яка працює з усіма версіями Python.
sorin

26

Станом на 2017 рік, ось портативне рішення, яке працює з усіма версіями 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

Існують деякі незначні невідповідності між 2/3 і bytestring, але якщо ви використовуєте рідний "рядок", тоді вони обоє хибні
Нік Т

16

Оскільки Python 2.6, із введенням абстрактних базових класів isinstance(використовуваних на ABC, а не конкретних класах), зараз вважається цілком прийнятним. Зокрема:

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

Це точна копія (зміна лише назви класу), Iterableяк визначено в _abcoll.py(деталі реалізації collections.py) ... причина, по якій це працює, як ви хочете, хоча collections.Iterableі ні, полягає в тому, що остання проходить додаткову милю, щоб забезпечити вважається ітеративним, зателефонувавшиIterable.register(str) явно безпосередньо після цієї classзаяви.

Звичайно, це легко збільшити __subclasshook__, повернувшись Falseдоany викликом для інших класів, які ви хочете спеціально виключити зі свого визначення.

У будь-якому випадку, після того, як ви імпортували цей новий модуль як myiter, isinstance('ciao', myiter.NonStringIterable)буде False, так і isinstance([1,2,3], myiter.NonStringIterable)буде True, як ви запитуєте - а в Python 2.6 і пізніших версіях це вважається правильним способом втілення таких перевірок ... визначте абстрактний базовий клас і перевірити isinstanceце.


У Python 3 isinstance('spam', NonStringIterable)повертається True.
Nick T

1
(...), а в Python 2.6 та пізніших версіях це вважається належним способом втілення таких перевірок (...) Наскільки я можу зловживати добре відомим поняттям абстрактного класу, коли-небудь вважати його належним чином , поза моїм розумінням. Правильним способом було б ввести замість цього якийсь схожий оператор.
Piotr Dobrogost

Алекс, ти можеш звернутися до твердження Ніка про те, що це не працює в Python 3? Мені подобається відповідь, але я хотів би переконатись, що пишу код, захищений від майбутнього.
Мерлін Морган-Грем

@ MerlynMorgan-Graham, це правильно, тому що __iter__ в даний час здійснюється в рядках в Python 3. Так що мій «легко доповнити» пункт стає придатним і , наприклад , if issublass(cls, str): return Falseпотреби , які будуть додані на початку __subclasshook__(а також будь-які інші класи , які визначають , __iter__але в вашому мислення не слід приймати як "нестрункові ітерабелі").
Алекс Мартеллі,

@AlexMartelli Для Python 3, ви не маєте на увазі, що його if issublass(C, str): return Falseслід додати?
Роб Смальлшир

4

Я усвідомлюю, що це стара публікація, але вважав, що варто додати мій підхід до Інтернету. Наведена нижче функція, здається, працює для мене за більшості обставин як з Python 2, так і з 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

Це перевіряє нестроковий ітерабельний (mis) за допомогою вбудованого, hasattrякий піднімає a, TypeErrorколи другий аргумент не є рядком або рядком Unicode.


3

Поєднуючи попередні відповіді, я використовую:

import types
import collections

#[...]

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

#[Do stuff...]

Не 100% дурний доказ, але якщо об’єкт не є ітерируемым, ви все одно можете пропустити його і повернутися до набору качки.


Змінити: Python3

types.StringTypes == (str, unicode). Еквівалентом Phython3 є:

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

У вашій заяві про імпорт має бути "type", а не "type"
PaulR,

3

2.x

Я б запропонував:

hasattr(x, '__iter__')

або з огляду на коментар Девіда Чарльза, який налаштовує це для Python3, а як щодо:

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

3.x

вбудований basestringабстрактний тип було видалено . Використовуйте strзамість цього. В strі bytesтипах не мають функціональність досить загальний , щоб гарантувати загальний базовий клас.


3
Можливо, тому __iter__, що в Python 3 є рядки?
davidrmcharles

@DavidCharles О, справді? Моє ліжко. Я користувач Jython, і Jython наразі не має версії 3.
mike rodent

Це насправді не відповідь, більше коментар / запитання, і це неправильно для 3.x. Не могли б ви прибрати? Чи можете ви додати обґрунтування твердження "Типи 'str' та 'bytes' не мають достатньо спільних функціональних можливостей, щоб гарантувати спільний базовий клас." Одним із ключових моментів 3.x було зробити байти Unicode першокласним громадянином.
smci

Я не маю уявлення, чому я написав щось із зазначеного. Я пропоную видалити весь текст у "3.x" ... хоча ви вже редагували мою відповідь. Відредагуйте його більше, якщо хочете.
гризун Майк

0

Як ви правильно зазначили, один рядок - це послідовність символів.

Отже, що ви дійсно хочете зробити, це з’ясувати, яка це послідовність arg, використовуючи isinstance або type (a) == str.

Якщо ви хочете реалізувати функцію, яка приймає змінну кількість параметрів, вам слід зробити це так:

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

function ("ff") і function ("ff", "ff") працюватимуть.

Я не бачу сценарію, коли потрібна функція isiterable (), подібна до вашої. Не isinstance () є поганим стилем, а ситуації, коли вам потрібно використовувати isinstance ().


4
Використання type(a) == strслід уникати. Це погана практика, оскільки вона не враховує подібні типи чи типи, похідні від str. typeне піднімається по ієрархії типів, тоді як піднімається isinstance, тому краще використовувати isinstance.
AkiRoss,

0

Щоб явно розширити чудовий злом Алекса Мартеллі collections.pyта вирішити деякі запитання навколо нього: Поточне робоче рішення в 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

і продемонстрували

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

Наприклад, якщо ви хочете додати iter('')до виключень, змініть рядок

            if issubclass(c, str):
                return False

бути

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

отримати

[False, False, True, True, True, True]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.