Quelle est la bonne façon de déterminer si un objet est un objet de type octets en Python?

90

J'ai du code qui attend strmais qui traitera le cas d'être passé bytesde la manière suivante:

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

Malheureusement, cela ne fonctionne pas dans le cas de bytearray. Existe-t-il un moyen plus générique de tester si un objet est l'un bytesou l' autre bytearrayou devrais-je simplement vérifier les deux? Est-ce hasattr('decode')aussi mauvais que je pense que ce serait?

A. Wilcox
la source
6
Personnellement, j'adore la frappe de canard de python autant que le prochain. Mais si vous avez besoin de faire des vérifications sur vos arguments d'entrée et de forcer à différents types, alors vous n'êtes plus duck taper - Vous rendez simplement votre code plus difficile à lire et à maintenir. Ma suggestion ici (et d'autres peuvent ne pas être d'accord) serait de créer plusieurs fonctions (qui gèrent la coercition de type et délèguent à une implémentation de base).
mgilson
(1) Sauf si vous en avez besoin pour la compatibilité avec le code Python 2 hérité; évitez d'accepter simultanément du texte et des données binaires. Si votre fonction fonctionne avec du texte, elle doit accepter uniquement str. Un autre code doit convertir des octets en Unicode dès que possible en entrée. (2) "bytes-like" a une signification particulière en Python (objets qui prennent en charge le protocole de tampon (C uniquement))
jfs
Le problème principal est que cette fonction ne fonctionne pas dans Python 2, où une simple chaîne ASCII passe le test de <octets>!
Apostolos

Réponses:

73

Vous pouvez utiliser ici quelques approches.

Typage de canard

Puisque Python est typé canard , vous pouvez simplement faire ce qui suit (ce qui semble être la manière généralement suggérée):

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

Vous pouvez cependant utiliser hasattrcomme vous le décrivez, et ce serait probablement bien. Ceci, bien sûr, en supposant que la .decode()méthode de l'objet donné renvoie une chaîne et n'a aucun effet secondaire désagréable.

Je recommande personnellement l'exception ou la hasattrméthode, mais tout ce que vous utilisez dépend de vous.

Utilisez str ()

Cette approche est rare, mais est possible:

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

D'autres encodages sont autorisés, tout comme avec le protocole tampon .decode(). Vous pouvez également passer un troisième paramètre pour spécifier la gestion des erreurs.

Fonctions génériques à répartition unique (Python 3.4+)

Python 3.4 et les versions ultérieures incluent une fonctionnalité intéressante appelée fonctions génériques à distribution unique, via functools.singledispatch . C'est un peu plus détaillé, mais c'est aussi plus explicite:

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

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

Vous pouvez également créer des gestionnaires spéciaux pour les objets bytearrayet bytessi vous le souhaitez.

Attention : les fonctions d'envoi unique ne fonctionnent que sur le premier argument! C'est une caractéristique intentionnelle, voir PEP 433 .

Elizafox
la source
+1 pour la mention des génériques à envoi unique, dont j'ai complètement oublié la bibliothèque standard fournie.
A. Wilcox le
Puisque appeler str on str ne fait rien, et me paraissait le plus clair, j'y suis allé.
A. Wilcox le
dans l'ensemble, j'aime hasattrplus que le try / sauf pour vous empêcher d'avaler accidentellement un bogue dans la fonction de décodage, mais +1.
keredson
37

Vous pouvez utiliser:

isinstance(data, (bytes, bytearray))

En raison de la classe de base différente est utilisée ici.

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

Vérifier bytes

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

cependant,

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

Les codes ci-dessus sont testés sous python 2.7

Malheureusement, sous python 3.4, ce sont les mêmes ...

>>> bytes.__base__
<class 'object'>
>>> bytearray.__base__
<class 'object'>
zangw
la source
1
six.string_types doit être compatible 2/3.
Joshua Olson
Ce type de vérification ne fonctionne pas dans Python 2, où une simple chaîne ASCII passe le test de <octets>!
Apostolos
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
la source
Notez que ce n'est pas un test fiable en Python 2 , où un objet chaîne passe également sous forme d'octets! Autrement dit, basé sur le code ci-dessus, type(text) is bytessera vrai!
Apostolos le
11

Ce code n'est pas correct sauf si vous savez quelque chose que nous ignorons:

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

Vous ne connaissez pas (semblez-vous) le codage de data. Vous supposez que c'est UTF-8 , mais cela pourrait très bien être faux. Puisque vous ne connaissez pas l'encodage, vous n'avez pas de texte . Vous avez des octets, qui pourraient avoir n'importe quelle signification sous le soleil.

La bonne nouvelle est que la plupart des séquences aléatoires d'octets ne sont pas UTF-8 valides, donc quand cela se brise, il se cassera bruyamment ( errors='strict'c'est la valeur par défaut) au lieu de faire la mauvaise chose en silence. La meilleure nouvelle est que la plupart de ces séquences aléatoires qui se trouvent être du UTF-8 valide sont également de l'ASCII valide, que ( presque ) tout le monde s'accorde de toute façon sur la façon d'analyser.

La mauvaise nouvelle est qu'il n'existe aucun moyen raisonnable de résoudre ce problème. Il existe une manière standard de fournir des informations d'encodage: utilisez strplutôt que bytes. Si un code tiers vous a remis un objet bytesou bytearraysans autre contexte ou information, la seule action correcte est d'échouer.


Maintenant, en supposant que vous connaissez l'encodage, vous pouvez utiliser functools.singledispatchici:

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

Cela ne fonctionne pas sur les méthodes et datadoit être le premier argument. Si ces restrictions ne fonctionnent pas pour vous, utilisez plutôt l'une des autres réponses.

Kevin
la source
Dans la bibliothèque que j'écris, pour cette méthode spécifique, je sais certainement que les octets et / ou bytearray que je reçois sont encodés en UTF-8.
A. Wilcox le
1
@AndrewWilcox: Très bien, mais je laisse cette information pour le futur trafic Google.
Kevin le
4

Cela dépend de ce que vous voulez résoudre. Si vous souhaitez avoir le même code qui convertit les deux cas en une chaîne, vous pouvez simplement convertir le type en bytespremier, puis le décoder. De cette façon, c'est un one-liner:

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

De cette façon, la réponse pour vous peut être:

data = bytes(data).decode()

Quoi qu'il en soit, je suggère d'écrire 'utf-8'explicitement dans le décodage, si vous ne voulez pas épargner quelques octets. La raison en est que la prochaine fois que vous ou quelqu'un d'autre lirez le code source, la situation sera plus apparente.

pepr
la source
3

Il y a deux questions ici, et les réponses sont différentes.

La première question, le titre de cet article, est: Quelle est la bonne façon de déterminer si un objet est un objet de type octets en Python? Cela comprend un certain nombre de types intégrés ( bytes, bytearray, array.array, memoryview, autres?) Et peut - être aussi types définis par l' utilisateur. Le meilleur moyen que je connaisse pour les vérifier est d'essayer d'en créer un 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'

Dans le corps du message original, cependant, il semble que la question soit plutôt: Comment puis-je tester si un objet prend en charge decode ()? La réponse ci-dessus de @ elizabeth-myers à cette question est excellente. Notez que tous les objets de type octets ne prennent pas en charge decode ().

Jack O'Connor
la source
1
Notez que si vous faites cela, vous devez appeler .release()ou utiliser la version du gestionnaire de contexte.
o11c du
Je pense qu'en CPython, le temporaire memoryviewserait immédiatement libéré et .release()serait appelé implicitement. Mais je conviens qu'il vaut mieux ne pas se fier à cela, car toutes les implémentations Python ne sont pas comptées par référence.
Jack O'Connor le
0

Le test if isinstance(data, bytes)or if type(data) == bytes, etc. ne fonctionne pas dans Python 2, où une simple chaîne ASCII réussit le test de! Parce que j'utilise à la fois Python 2 et Python 3, afin de surmonter cela, je fais la vérification suivante:

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

C'est un peu moche, mais ça fait le travail que la question pose et ça marche toujours, de la manière la plus simple.

Apostolos
la source
Les strobjets Python2 sont bytes cependant: str is bytes-> Trueen Python2
snakecharmerb
Evidemment, d'où le problème de détection! :)
Apostolos le