Jaki jest właściwy sposób określenia, czy obiekt jest obiektem typu bajtowego w Pythonie?

90

Mam kod, który oczekuje, strale zajmie się przypadkiem przekazania bytesw następujący sposób:

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

Niestety to nie działa w przypadku bytearray. Czy istnieje bardziej ogólny sposób sprawdzenia, czy obiekt jest albo bytesalbo bytearray, czy powinienem po prostu sprawdzić oba? Czy jest hasattr('decode')tak źle, jak się czuję?

A. Wilcox
źródło
6
Osobiście uwielbiam pisać kaczkę Pythona tak samo jak następny facet. Ale jeśli musisz sprawdzać swoje argumenty wejściowe i wymuszać na różnych typach, to nie musisz już kaczkować - po prostu utrudniasz odczytanie kodu. Moją sugestią tutaj (a inni mogą się z tym nie zgodzić) byłoby utworzenie wielu funkcji (które obsługują wymuszanie typu i delegują do podstawowej implementacji).
mgilson
(1) Chyba że jest to potrzebne do zgodności ze starszym kodem Python 2; unikaj jednoczesnego akceptowania danych tekstowych i binarnych. Jeśli twoja funkcja działa z tekstem, powinna akceptować tylko str. Inny kod powinien konwertować z bajtów do Unicode na wejściu tak szybko, jak to możliwe. (2) „podobne do bajtów” ma specjalne znaczenie w Pythonie (obiekty obsługujące protokół bufora (tylko C))
jfs
Głównym problemem jest to, że ta funkcja nie działa w Pythonie 2, gdzie prosty łańcuch ASCII przechodzi test <bajtów>!
Apostolos

Odpowiedzi:

73

Jest kilka podejść, których możesz tutaj użyć.

Pisanie kaczki

Ponieważ Python jest pisany kaczką , możesz po prostu wykonać następujące czynności (co wydaje się być sposobem zwykle sugerowanym):

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

Możesz jednak użyć hasattrtego, co opisujesz i prawdopodobnie będzie dobrze. Jest to oczywiście założenie, że .decode()metoda dla danego obiektu zwraca ciąg znaków i nie ma żadnych nieprzyjemnych skutków ubocznych.

Osobiście polecam albo wyjątek, albo hasattrmetodę, ale cokolwiek użyjesz, zależy od ciebie.

Użyj str ()

To podejście jest rzadkie, ale jest możliwe:

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

Inne kodowania są dozwolone, podobnie jak w przypadku protokołu bufora .decode(). Możesz również przekazać trzeci parametr, aby określić obsługę błędów.

Funkcje ogólne z pojedynczą wysyłką (Python 3.4+)

Python 3.4 i nowsze wersje zawierają fajną funkcję zwaną funkcjami ogólnymi o pojedynczej wysyłce, za pośrednictwem functools.singledispatch . Jest to trochę bardziej szczegółowe, ale jest też bardziej wyraźne:

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

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

Jeśli chcesz, możesz również tworzyć specjalne programy obsługi bytearrayi bytesobiekty.

Uwaga : funkcje pojedynczego wysyłania działają tylko na pierwszym argumencie! Jest to celowa funkcja, zobacz PEP 433 .

Elizafox
źródło
+1 za wzmiankę o typach generycznych z pojedynczą wysyłką, o których zupełnie zapomniałem dostarczonej biblioteki standardowej.
A. Wilcox
Ponieważ wywołanie str na str nic nie daje i wydawało mi się to najbardziej oczywiste, poszedłem z tym.
A. Wilcox
ogólnie lubię hasattrbardziej niż try /, z wyjątkiem tego, aby zapobiec przypadkowemu połknięciu jakiegoś błędu w funkcji dekodowania, ale +1.
keredson
37

Możesz użyć:

isinstance(data, (bytes, bytearray))

Ze względu na inną klasę bazową zastosowano tutaj.

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

Sprawdzić bytes

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

Jednak,

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

Powyższe kody są testowane w Pythonie 2.7

Niestety w Pythonie 3.4 są takie same ...

>>> bytes.__base__
<class 'object'>
>>> bytearray.__base__
<class 'object'>
zangw
źródło
1
six.string_types powinno być zgodne w 2/3.
Joshua Olson,
Ten rodzaj sprawdzania nie działa w Pythonie 2, gdzie prosty łańcuch ASCII przechodzi test <bajtów>!
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
źródło
Zauważ, że nie jest to niezawodny test w Pythonie 2 , w którym obiekt łańcuchowy jest przekazywany również jako bajty! Oznacza to, że na podstawie powyższego kodu type(text) is bytesbędzie True!
Apostolos
11

Ten kod nie jest poprawny, chyba że wiesz coś, czego nie wiemy:

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

Nie znasz (wydajesz się) znać kodowania data. Zakładasz, że to UTF-8 , ale to może być bardzo złe. Ponieważ nie znasz kodowania, nie masz tekstu . Masz bajty, które pod słońcem mogą mieć jakiekolwiek znaczenie.

Dobrą wiadomością jest to, że większość losowych sekwencji bajtów nie jest prawidłowym kodem UTF-8, więc kiedy to się zepsuje, będzie się łamać głośno ( errors='strict'jest to ustawienie domyślne) zamiast po cichu robić źle. Jeszcze lepszą wiadomością jest to, że większość tych losowych sekwencji, które okazały się być prawidłowymi kodami UTF-8, to również prawidłowe ASCII, które i tak ( prawie ) wszyscy zgadzają się co do sposobu parsowania.

Zła wiadomość jest taka, że ​​nie ma rozsądnego sposobu, aby to naprawić. Istnieje standardowy sposób dostarczania informacji o kodowaniu: użyj strzamiast bytes. Jeśli jakiś kod innej firmy przekazał ci obiekt byteslub bytearraybez dalszego kontekstu lub informacji, jedyną poprawną czynnością jest niepowodzenie.


Zakładając, że znasz kodowanie, możesz użyć functools.singledispatchtutaj:

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

To nie działa na metodach i datamusi być pierwszym argumentem. Jeśli te ograniczenia nie działają w Twoim przypadku, użyj jednej z pozostałych odpowiedzi.

Kevin
źródło
W bibliotece, którą piszę, dla tej konkretnej metody zdecydowanie wiem, że bajty i / lub bajty, które otrzymuję, są zakodowane w UTF-8.
A. Wilcox
1
@AndrewWilcox: W porządku, ale zostawiam te informacje dla przyszłego ruchu Google.
Kevin
4

To zależy od tego, co chcesz rozwiązać. Jeśli chcesz mieć ten sam kod, który konwertuje obie obserwacje na ciąg, możesz po prostu przekonwertować typ na bytespierwszy, a następnie zdekodować. W ten sposób jest to jednowierszowy:

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

W ten sposób odpowiedzią dla Ciebie może być:

data = bytes(data).decode()

W każdym razie proponuję napisać 'utf-8'wprost do dekodowania, jeśli nie zależy ci na oszczędzeniu kilku bajtów. Powodem jest to, że gdy następnym razem ty lub ktoś inny przeczytacie kod źródłowy, sytuacja będzie bardziej widoczna.

pepr
źródło
3

Są tu dwa pytania, na które odpowiedzi są różne.

Pierwsze pytanie, tytuł tego posta, brzmi: Jaki jest właściwy sposób określenia, czy obiekt jest obiektem bajtopodobnym w Pythonie? Obejmuje szereg wbudowanych typów ( bytes, bytearray, array.array, memoryview, inne?) I ewentualnie także typy zdefiniowane przez użytkownika. Najlepszym sposobem, jaki znam, aby to sprawdzić, jest próba utworzenia memoryviewz nich:

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

Jednak w treści oryginalnego posta brzmi to tak, jakby zamiast tego pytanie brzmiało: Jak sprawdzić, czy obiekt obsługuje dekodowanie ()? Powyższa odpowiedź @ elizabeth-myers na to pytanie jest świetna. Zauważ, że nie wszystkie obiekty podobne do bajtów obsługują decode ().

Jack O'Connor
źródło
1
Zauważ, że jeśli to zrobisz, musisz zadzwonić .release()lub użyć wersji menedżera kontekstu.
o11c
Myślę, że w CPythonie plik tymczasowy memoryviewzostałby natychmiast zwolniony i .release()zostałby wywołany niejawnie. Ale zgadzam się, że najlepiej na tym nie polegać, ponieważ nie wszystkie implementacje Pythona są liczone jako odwołania.
Jack O'Connor
0

Test if isinstance(data, bytes)lub if type(data) == bytesitp. Nie działa w Pythonie 2, gdzie prosty łańcuch ASCII przechodzi test! Ponieważ używam zarówno Pythona 2, jak i Pythona 3, aby temu zaradzić, wykonuję następujące sprawdzenie:

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

Jest trochę brzydki, ale spełnia to, o co chodzi w pytaniu, i zawsze działa, w najprostszy sposób.

Apostolos
źródło
Python2 strobiekty bytes jednak: str is bytes-> Truew python2
snakecharmerb
Oczywiście stąd problem z wykrywaniem! :)
Apostolos