Qual é a maneira adequada de determinar se um objeto é um objeto semelhante a bytes em Python?

93

Eu tenho um código que espera, strmas vai lidar com o caso de ser passado bytesda seguinte maneira:

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

Infelizmente, isso não funciona no caso de bytearray. Existe uma maneira mais genérica de testar se um objeto é bytesou bytearray, ou devo apenas verificar os dois? É hasattr('decode')tão ruim quanto eu sinto que seria?

A. Wilcox
fonte
6
Pessoalmente, adoro a digitação de pato do python tanto quanto qualquer outro cara. Mas se você precisa fazer verificações em seus argumentos de entrada e coagir para diferentes tipos, então você não está mais se esquivando de digitar - você está apenas tornando seu código mais difícil de ler e manter. Minha sugestão aqui (e outros podem discordar) seria fazer várias funções (que lidam com a coerção de tipo e delegam para uma implementação básica).
mgilson
(1) A menos que você precise para compatibilidade com o código Python 2 legado; evite aceitar texto e dados binários simultaneamente. Se sua função trabalhar com texto, ela deve aceitar apenas str. Algum outro código deve ser convertido de bytes para Unicode na entrada o mais rápido possível. (2) "semelhante a bytes" tem um significado especial em Python (objetos que suportam o protocolo de buffer (apenas C))
jfs
O principal problema é que esta função não funciona no Python 2, onde uma string ASCII simples passa no teste de <bytes>!
Apostolos

Respostas:

76

Existem algumas abordagens que você pode usar aqui.

Digitação de pato

Como o Python é digitado em pato , você poderia simplesmente fazer o seguinte (que parece ser a forma geralmente sugerida):

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

Você poderia usar hasattrcomo descreve, no entanto, e provavelmente seria bom. Isso, é claro, assumindo que o .decode()método para o objeto fornecido retorna uma string e não tem efeitos colaterais desagradáveis.

Eu pessoalmente recomendo a exceção ou o hasattrmétodo, mas o que quer que você use é com você.

Use str ()

Essa abordagem é incomum, mas é possível:

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

Outras codificações são permitidas, assim como com o protocolo de buffer .decode(). Você também pode passar um terceiro parâmetro para especificar o tratamento de erros.

Funções genéricas de despacho único (Python 3.4+)

Python 3.4 e superior incluem um recurso bacana chamado funções genéricas de despacho único, via functools.singledispatch . Isso é um pouco mais detalhado, mas também é mais explícito:

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

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

Você também pode fazer manipuladores especiais para objetos bytearraye bytes, se assim desejar.

Cuidado : as funções de despacho único funcionam apenas no primeiro argumento! Este é um recurso intencional, consulte PEP 433 .

Elizafox
fonte
+1 para a menção de genéricos de despacho único, que esqueci completamente da biblioteca padrão fornecida.
A. Wilcox
Como chamar str on str não adianta nada, e me pareceu o mais claro, aceitei.
A. Wilcox
no geral eu gosto hasattrmais do que tentar / exceto para evitar que você engula acidentalmente algum bug na função de decodificação, mas +1.
keredson
39

Você pode usar:

isinstance(data, (bytes, bytearray))

Devido à classe base diferente é usada aqui.

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

Checar bytes

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

Contudo,

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

Os códigos acima são testados em python 2.7

Infelizmente, no python 3.4, eles são os mesmos ....

>>> bytes.__base__
<class 'object'>
>>> bytearray.__base__
<class 'object'>
zangw
fonte
1
six.string_types deve ser compatível com 2/3.
Joshua Olson
Esse tipo de verificação não funciona no Python 2, onde uma string ASCII simples passa no teste de <bytes>!
Apostolos
13
>>> content = b"hello"
>>> text = "hello"
>>> type(content)
<class 'bytes'>
>>> type(text)
<class 'str'>
>>> type(text) is str
True
>>> type(content) is bytes
True
ZeroErr0r
fonte
Observe que este não é um teste confiável no Python 2 , onde um objeto string passa também como bytes! Ou seja, com base no código acima, type(text) is bytesserá True!
Apostolos
11

Este código não está correto a menos que você saiba algo que não sabemos:

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

Você não (parece) saber a codificação de data. Você está assumindo que é UTF-8 , mas isso pode muito bem estar errado. Como você não sabe a codificação, não tem texto . Você tem bytes, que podem ter algum significado sob o sol.

A boa notícia é que a maioria das sequências aleatórias de bytes não são UTF-8 válidas, portanto, quando isso for interrompido, será interrompido ruidosamente ( errors='strict'é o padrão) em vez de fazer silenciosamente a coisa errada. A notícia ainda melhor é que a maioria dessas sequências aleatórias que por acaso são UTF-8 válidas também são ASCII válidas, que ( quase ) todos concordam em como analisar de qualquer maneira.

A má notícia é que não existe uma maneira razoável de corrigir isso. Existe uma maneira padrão de fornecer informações de codificação: use em strvez de bytes. Se algum código de terceiros entregou a você um objeto bytesou bytearraysem qualquer contexto ou informação adicional, a única ação correta é falhar.


Agora, supondo que você conheça a codificação, você pode usar functools.singledispatchaqui:

@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, ...)

Isso não funciona em métodos e datadeve ser o primeiro argumento. Se essas restrições não funcionarem para você, use uma das outras respostas.

Kevin
fonte
Na biblioteca que estou escrevendo, para este método específico, eu definitivamente sei que os bytes e / ou bytearray que estou recebendo são codificados em UTF-8.
A. Wilcox
1
@AndrewWilcox: É justo, mas estou deixando essas informações para o futuro tráfego do Google.
Kevin
4

Depende do que você deseja resolver. Se você deseja que o mesmo código que converta ambos os casos em uma string, você pode simplesmente converter o tipo em bytesprimeiro e depois decodificar. Dessa forma, é uma linha:

#!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)

Dessa forma, a resposta para você pode ser:

data = bytes(data).decode()

De qualquer forma, sugiro escrever 'utf-8'explicitamente no decodificador, se você não se importar em poupar alguns bytes. O motivo é que da próxima vez que você ou outra pessoa ler o código-fonte, a situação ficará mais aparente.

pepr
fonte
3

Existem duas perguntas aqui, e as respostas a elas são diferentes.

A primeira pergunta, o título desta postagem, é: Qual é a maneira adequada de determinar se um objeto é um objeto semelhante a bytes em Python? Isto inclui uma série de built-in tipos ( bytes, bytearray, array.array, memoryview, outros?) E possivelmente também tipos definidos pelo usuário. A melhor maneira que conheço de verificar isso é tentar criar um a memoryviewpartir deles:

>>> 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'

No corpo da postagem original, porém, parece que a pergunta é: Como faço para testar se um objeto oferece suporte a decode ()? A resposta acima de @elizabeth-myers a esta pergunta é excelente. Observe que nem todos os objetos semelhantes a bytes suportam decode ().

Jack O'Connor
fonte
1
Observe que, se você fizer isso, deverá chamar .release()ou usar a versão do gerenciador de contexto.
o11c
Acho que no CPython o temporário memoryviewseria liberado imediatamente e .release()seria chamado implicitamente. Mas concordo que é melhor não confiar nisso, já que nem todas as implementações Python são contadas por referência.
Jack O'Connor
0

O teste if isinstance(data, bytes)ou if type(data) == bytesetc. não funciona no Python 2, onde uma string ASCII simples passa no teste de! Como uso Python 2 e Python 3, para resolver isso, faço a seguinte verificação:

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

É um pouco feio, mas cumpre o que a pergunta pede e sempre funciona, da maneira mais simples.

Apostolos
fonte
Os strobjetos Python2 são bytes : str is bytes-> Trueem Python2
snakecharmerb
Obviamente, daí o problema de detecção! :)
Apostolos