bagaimana cara membedakan variabel iterable tapi bukan string

92

Saya memiliki fungsi yang mengambil argumen yang bisa berupa item tunggal atau item ganda:

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

yang seperti itu:

>>> iterable (("f", "f"))
Iya

>>> iterable (["f", "f"])
Iya

>>> iterable ("ff")
tidak

Masalahnya adalah string itu secara teknis dapat diulang, jadi saya tidak bisa begitu saja menangkap ValueError saat mencoba arg[1]. Saya tidak ingin menggunakan isinstance (), karena itu bukan praktik yang baik (atau begitulah yang saya katakan).

pendeta
sumber
1
Versi Python yang mana? Saya yakin jawabannya berbeda antara 2. * dan 3
Kathy Van Stone
4
Anda diberitahu secara tidak benar, isinstance bukanlah praktik yang buruk.
Lennart Regebro
3
Oh, tunggu, mungkin dia mengacu pada prinsip bahwa itu buruk untuk memeriksa tipe objek, dan ini merupakan indikasi program rusak? Ini benar pada prinsipnya (tetapi tidak selalu dalam praktik). Ini mungkin atau mungkin bukan kasus seperti itu. Tapi bukan fungsinya adalah instans yang menjadi masalah, itu kebiasaan memeriksa tipe.
Lennart Regebro
@Lennart: canonical.org/~kragen/isinstance ini mungkin sudah ketinggalan zaman
pastorc
@up Ini tidak menyebutkan kelebihan beban fungsi berbasis tipe, dan isinstancemerupakan cara untuk melakukannya dalam bahasa yang diketik secara dinamis. Sesuatu yang tidak untuk digunakan setiap hari, tapi OK dalam kasus yang dibenarkan.
Kos

Jawaban:

50

Gunakan isinstance (Saya tidak mengerti mengapa itu praktik yang buruk)

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

Perhatikan penggunaan StringTypes. Ini memastikan bahwa kita tidak melupakan beberapa jenis string yang tidak jelas.

Sisi baiknya, ini juga berfungsi untuk kelas string turunan.

class MyString(str):
    pass

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

Selain itu, Anda mungkin ingin melihat pertanyaan sebelumnya ini .

Bersulang.


NB: perilaku diubah dalam Python 3 StringTypesdan basestringtidak lagi ditentukan. Tergantung pada kebutuhan Anda, Anda dapat menggantinya isinstancedengan str, atau subset tuple (str, bytes, unicode), misalnya untuk pengguna Cython. Seperti yang disebutkan @Theron Luhn , Anda juga dapat menggunakan six.

scvalex
sumber
Bagus, scvalex. Saya menghapus -1 saya sekarang dan menjadikannya +1 :-).
Tom
2
Saya pikir ide latihan yang buruk adalah karena prinsip mengetik bebek . Menjadi anggota kelas tertentu tidak berarti bahwa itu satu - satunya objek yang dapat digunakan atau bahwa metode yang diharapkan tersedia. Tapi saya pikir terkadang Anda tidak bisa menyimpulkan apa yang dilakukan metode ini meskipun ada, jadi isinstancemungkin satu-satunya cara.
estani
2
Catatan: types.StringTypes tidak tersedia di Python 3. Karena hanya ada satu tipe string di py3k, menurut saya aman untuk do isinstance(arg, str). Untuk versi yang kompatibel dengan mundur, pertimbangkan untuk menggunakan pythonhosted.org/six/#six.string_types
Theron Luhn
Saya benar-benar menggunakan Python3 dan perhatikan types.StringTypestidak tersedia di Python3. Berapakah nilai di Python2?
kevinarpe
2
2017 : Jawaban ini tidak valid lagi, lihat stackoverflow.com/a/44328500/99834 untuk jawaban yang berfungsi dengan semua versi Python.
sorin
26

Pada 2017, berikut adalah solusi portabel yang bekerja dengan semua versi 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
sumber
Ada beberapa ketidakkonsistenan kecil antara 2/3 dengan bytestring, tetapi jika Anda menggunakan "string" asli maka keduanya salah
Nick T
16

Sejak Python 2.6, dengan pengenalan kelas dasar abstrak, isinstance(digunakan pada ABC, bukan kelas konkret) sekarang dianggap dapat diterima. Secara khusus:

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

Ini adalah salinan persis (hanya mengubah nama kelas) Iterableseperti yang didefinisikan dalam _abcoll.py(detail implementasi collections.py) ... alasan ini berfungsi seperti yang Anda inginkan, sementara collections.Iterabletidak, adalah bahwa yang terakhir bekerja ekstra untuk memastikan string dianggap iterable, dengan memanggil Iterable.register(str)secara eksplisit tepat setelah classpernyataan ini .

Tentu saja mudah untuk menambah __subclasshook__dengan kembali Falsesebelum anypanggilan untuk kelas lain yang ingin Anda kecualikan secara khusus dari definisi Anda.

Bagaimanapun, setelah Anda mengimpor modul baru ini sebagai myiter, isinstance('ciao', myiter.NonStringIterable)akan False, dan isinstance([1,2,3], myiter.NonStringIterable)akan True, seperti yang Anda minta - dan dalam Python 2.6 dan yang lebih baru ini dianggap cara yang tepat untuk mewujudkan pemeriksaan tersebut ... definisikan kelas dasar abstrak dan periksa isinstance.

Alex Martelli
sumber
Dalam Python 3 isinstance('spam', NonStringIterable)kembali True.
Nick T
1
(...) dan di Python 2.6 dan yang lebih baru ini dianggap sebagai cara yang tepat untuk mewujudkan pemeriksaan tersebut (...) Bagaimana menyalahgunakan konsep kelas abstrak yang terkenal sedemikian rupa dapat dianggap cara yang tepat berada di luar pemahaman saya. Cara yang tepat adalah dengan memperkenalkan beberapa operator yang mirip .
Piotr Dobrogost
Alex, dapatkah Anda mengatasi pernyataan Nick bahwa ini tidak berfungsi di Python 3? Saya suka jawabannya, tetapi ingin memastikan bahwa saya menulis kode bukti masa depan.
Merlyn Morgan-Graham
@ MerlynMorgan-Graham, itu benar, karena __iter__ yang sekarang diterapkan di string di Python 3. Jadi saya "mudah untuk menambah" ayat menjadi berlaku dan misalnya if issublass(cls, str): return Falseperlu ditambahkan pada awal __subclasshook__(serta setiap kelas lain yang menentukan __iter__tetapi dalam Anda pola pikir tidak boleh diterima sebagai "non-string iterables").
Alex Martelli
@AlexMartelli Untuk Python 3, bukankah maksud Anda itu if issublass(C, str): return Falseharus ditambahkan?
Rob Smallshire
4

Saya menyadari ini adalah posting lama tetapi saya pikir itu layak menambahkan pendekatan saya untuk keturunan Internet. Fungsi di bawah ini tampaknya berfungsi untuk saya dalam banyak situasi dengan Python 2 dan 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

Ini memeriksa non-string yang dapat diulang oleh (mis) menggunakan built-in hasattryang akan memunculkan TypeErrorketika argumen keduanya bukan string atau string unicode.

Nigel Small
sumber
3

Dengan menggabungkan balasan sebelumnya, saya menggunakan:

import types
import collections

#[...]

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

#[Do stuff...]

Bukan 100% bukti bodoh, tetapi jika sebuah objek bukan merupakan iterable Anda masih bisa membiarkannya lewat dan kembali mengetik.


Edit: Python3

types.StringTypes == (str, unicode). Setara Phython3 adalah:

if isinstance(var, str ) \
    or not isinstance(var, collections.Iterable):
xvan
sumber
Pernyataan impor Anda harus 'jenis' bukan 'jenis'
PaulR
3

2.x

Saya akan menyarankan:

hasattr(x, '__iter__')

atau mengingat komentar David Charles yang mengubah ini untuk Python3, bagaimana dengan:

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

3.x

basestringtipe abstrak bawaan telah dihapus . Gunakan strsebagai gantinya. Jenis strdan bytestidak memiliki fungsi yang cukup umum untuk menjamin kelas dasar bersama.

mike rodent
sumber
3
Mungkin karena __iter__ada string di Python 3?
davidrmcharles
@DavidCharles Oh, benarkah? Salahku. Saya pengguna Jython dan Jython saat ini tidak memiliki versi 3.
mike rodent
Ini sebenarnya bukan jawaban, lebih banyak komentar / pertanyaan, dan itu salah untuk 3.x. Bisakah Anda membersihkannya? Dapatkah Anda menambahkan justifikasi untuk mengklaim "Jenis 'str' dan 'bytes' tidak memiliki fungsi yang cukup umum untuk menjamin kelas basis bersama." Salah satu poin penting dari 3.x adalah membuat byte Unicode menjadi warga negara kelas satu.
smci
Saya tidak tahu mengapa saya menulis semua hal di atas. Saya mengusulkan untuk menghapus semua teks di bawah "3.x" ... meskipun Anda sudah mengedit jawaban saya. Edit lebih banyak jika Anda suka.
mike rodent
0

Seperti yang Anda tunjukkan dengan benar, satu string adalah urutan karakter.

Jadi hal yang benar-benar ingin Anda lakukan adalah mencari tahu seperti apa urutan argitu dengan menggunakan isinstance atau type (a) == str.

Jika Anda ingin mewujudkan fungsi yang mengambil sejumlah parameter variabel, Anda harus melakukannya seperti ini:

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

function ("ff") dan function ("ff", "ff") akan bekerja.

Saya tidak dapat melihat skenario di mana fungsi isiterable () seperti milik Anda diperlukan. Ini bukan isinstance () yang merupakan gaya yang buruk tetapi situasi di mana Anda perlu menggunakan isinstance ().

Otto Allmendinger
sumber
4
Penggunaan type(a) == strharus dihindari. Ini adalah praktik yang buruk karena tidak memperhitungkan jenis atau jenis yang serupa str. typetidak menaiki hierarki tipe, sedangkan isinstancetidak, oleh karena itu lebih baik digunakan isinstance.
AkiRoss
0

Untuk secara eksplisit memperluas peretasan Alex Martelli yang sangat baik collections.pydan menjawab beberapa pertanyaan di sekitarnya: Solusi yang berfungsi saat ini di python 3.6+ adalah

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

dan didemonstrasikan

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

Jika Anda ingin menambahkan iter('')pengecualian, misalnya, ubah baris

            if issubclass(c, str):
                return False

menjadi

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

mendapatkan

[False, False, True, True, True, True]
Alexander McFarlane
sumber