Как правильно определить, является ли объект байтовым объектом в Python?

90

У меня есть код, который ожидает, strно будет обрабатывать случай передачи bytesследующим образом:

if isinstance(data, bytes):
    data = data.decode()

К сожалению, это не работает в случае bytearray. Есть ли более общий способ проверить, является ли объект одним bytesили bytearrayдвумя, или я должен просто проверить оба? Так hasattr('decode')плохо, как я думаю?

А. Уилкокс
источник
6
Лично я люблю печатать уткой питона так же сильно, как и другие парни. Но если вам нужно выполнять проверки ваших входных аргументов и принуждение к различным типам, тогда вы больше не будете унывать от набора текста - вы просто усложняете чтение кода и сопровождение. Мое предложение здесь (и другие могут не согласиться) заключалось бы в создании нескольких функций (которые обрабатывают приведение типа и делегируют базовую реализацию).
mgilson
(1) Если это не требуется для совместимости с устаревшим кодом Python 2; Избегайте одновременного приема текстовых и двоичных данных. Если ваша функция работает с текстом, она должна принимать только str. Другой код должен как можно скорее преобразовывать байты в Unicode при вводе. (2) «подобный байтам» имеет особое значение в Python (объекты, поддерживающие протокол буфера (только C))
jfs
Основная проблема в том, что эта функция не работает в Python 2, где простая строка ASCII проходит проверку <байтов>!
Апостолос

Ответы:

73

Здесь вы можете использовать несколько подходов.

Утка печатает

Поскольку Python типизирован как утка , вы можете просто сделать следующее (кажется, обычно предлагается):

try:
    data = data.decode()
except (UnicodeDecodeError, AttributeError):
    pass

Однако вы можете использовать, hasattrкак вы описываете, и, вероятно, все будет хорошо. Это, конечно, предполагает, что .decode()метод для данного объекта возвращает строку и не имеет неприятных побочных эффектов.

Я лично рекомендую либо исключение, либо hasattrметод, но что бы вы ни использовали, зависит от вас.

Используйте str ()

Такой подход необычный, но возможен:

data = str(data, "utf-8")

Допускаются другие кодировки, как и в протоколе буфера .decode(). Вы также можете передать третий параметр, чтобы указать обработку ошибок.

Универсальные функции с однократной отправкой (Python 3.4+)

Python 3.4 и выше включают изящную функцию, называемую универсальными функциями с однократной отправкой, через functools.singledispatch . Это немного более подробно, но также более ясно:

def func(data):
    # This is the generic implementation
    data = data.decode()
    ...

@func.register(str)
def _(data):
    # data will already be a string
    ...

Вы можете также сделать специальные обработчик bytearrayи bytesобъекты , если вы так выбрали.

Осторожно : функции однократной отправки работают только с первым аргументом! Это намеренная функция, см. PEP 433 .

Elizafox
источник
+1 за упоминание дженериков с однократной отправкой, которые я полностью забыл о стандартной библиотеке.
A. Wilcox
Поскольку вызов str на str ничего не делает и казался мне наиболее понятным, я пошел с этим.
A. Wilcox
в целом мне нравится hasattrбольше, чем попытка / за исключением того, что вы случайно не проглотили какую-то ошибку в функции декодирования, но +1.
keredson
37

Ты можешь использовать:

isinstance(data, (bytes, bytearray))

Здесь используется другой базовый класс.

>>> bytes.__base__
<type 'basestring'>
>>> bytearray.__base__
<type 'object'>

Проверить bytes

>>> by = bytes()
>>> isinstance(by, basestring)
True

Однако,

>>> buf = bytearray()
>>> isinstance(buf, basestring)
False

Приведенные выше коды тестируются под Python 2.7.

К сожалению, в Python 3.4 они такие же ....

>>> bytes.__base__
<class 'object'>
>>> bytearray.__base__
<class 'object'>
Zangw
источник
1
six.string_types должен быть 2/3 совместимым.
Джошуа Олсон
Этот вид проверки не работает в Python 2, где простая строка ASCII проходит проверку <байтов>!
Апостолос
12
>>> content = b"hello"
>>> text = "hello"
>>> type(content)
<class 'bytes'>
>>> type(text)
<class 'str'>
>>> type(text) is str
True
>>> type(content) is bytes
True
ZeroErr0r
источник
Обратите внимание, что это ненадежный тест в Python 2 , где строковый объект передается также как байты! То есть, исходя из приведенного выше кода, type(text) is bytesбудет True!
Апостолос
11

Этот код неверен, если вы не знаете чего-то, чего мы не знаем:

if isinstance(data, bytes):
    data = data.decode()

Вы (кажется) не знаете кодировку data. Вы предполагаете, что это UTF-8 , но это вполне может быть ошибкой. Поскольку вы не знаете кодировку, у вас нет текста . У вас есть байты, которые могут иметь любое значение под солнцем.

Хорошая новость заключается в том, что большинство случайных последовательностей байтов недействительны для UTF-8, поэтому, когда он прерывается, он будет громко ломаться ( errors='strict'по умолчанию) вместо того, чтобы молча делать неправильные вещи. Еще лучшая новость заключается в том, что большинство тех случайных последовательностей, которые оказываются действительными в кодировке UTF-8, также являются действительными ASCII, которые ( почти ) все согласны с тем, как их анализировать.

Плохая новость в том, что нет разумного способа исправить это. Существует стандартный способ предоставления информации о кодировке: использовать strвместо bytes. Если какой-то сторонний код передал вам объект bytesили bytearrayбез какого-либо дополнительного контекста или информации, единственное правильное действие - сбой.


Теперь, если вы знаете кодировку, вы можете использовать functools.singledispatchздесь:

@functools.singledispatch
def foo(data, other_arguments, ...):
    raise TypeError('Unknown type: '+repr(type(data)))

@foo.register(str)
def _(data, other_arguments, ...):
    # data is a str

@foo.register(bytes)
@foo.register(bytearray)
def _(data, other_arguments, ...):
    data = data.decode('encoding')
    # explicit is better than implicit; don't leave the encoding out for UTF-8
    return foo(data, other_arguments, ...)

Это не работает с методами и dataдолжно быть первым аргументом. Если эти ограничения не работают для вас, используйте вместо этого один из других ответов.

Кевин
источник
В библиотеке, которую я пишу, для этого конкретного метода я точно знаю, что байты и / или массив байтов, которые я получаю, закодированы в UTF-8.
A. Wilcox
1
@AndrewWilcox: Достаточно честно, но я оставляю эту информацию для будущего трафика Google.
Кевин
4

Это зависит от того, что вы хотите решить. Если вы хотите иметь один и тот же код, который преобразует оба случая в строку, вы можете просто bytesсначала преобразовать тип , а затем декодировать. Таким образом, это однострочный:

#!python3

b1 = b'123456'
b2 = bytearray(b'123456')

print(type(b1))
print(type(b2))

s1 = bytes(b1).decode('utf-8')
s2 = bytes(b2).decode('utf-8')

print(s1)
print(s2)

Таким образом, ответ для вас может быть таким:

data = bytes(data).decode()

В любом случае, я предлагаю писать 'utf-8'явно для декодирования, если вы не хотите сэкономить несколько байтов. Причина в том, что в следующий раз, когда вы или кто-то другой прочтете исходный код, ситуация станет более очевидной.

перец
источник
3

Здесь есть два вопроса, и ответы на них разные.

Первый вопрос, название этого поста: как правильно определить, является ли объект байтовым объектом в Python? Это включает в себя ряд встроенных типов ( bytes, bytearray, array.array, memoryview, другие?) И , возможно , также определяемые пользователем типы. Лучший способ проверить их - это попытаться создать memoryviewиз них:

>>> memoryview(b"foo")
<memory at 0x7f7c43a70888>
>>> memoryview(u"foo")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: memoryview: a bytes-like object is required, not 'str'

Однако в тексте исходного сообщения вопрос звучит так, будто вместо этого возникает вопрос: как мне проверить, поддерживает ли объект decode ()? Ответ @ elizabeth-myers на этот вопрос великолепен. Обратите внимание, что не все байтовые объекты поддерживают decode ().

Джек О'Коннор
источник
1
Обратите внимание: если вы это сделаете, вы должны вызвать .release()или использовать версию диспетчера контекста.
o11c
Я думаю, что в CPython временное memoryviewбудет немедленно освобождено и .release()будет вызываться неявно. Но я согласен, что лучше не полагаться на это, поскольку не все реализации Python учитываются по ссылкам.
Джек О'Коннор,
0

Тест if isinstance(data, bytes)or if type(data) == bytesи т. Д. Не работает в Python 2, где простая строка ASCII проходит проверку! Поскольку я использую как Python 2, так и Python 3, для решения этой проблемы я выполняю следующую проверку:

if str(type(data)).find("bytes") != -1: print("It's <bytes>")

Это немного некрасиво, но выполняет работу, которую задает вопрос, и всегда работает самым простым способом.

Апостолос
источник
Python2 strобъекты являются bytes хотя: str is bytes-> Trueв python2
snakecharmerb
Очевидно, отсюда и проблема обнаружения! :)
Апостолос