Совместное использование в коде строк и байтообразных объектов для запуска в Python 2 и 3

Я пытаюсь изменить код, показанный далеко ниже, который работает в Python 2.7.x, поэтому он также будет работать без изменений в Python 3.x. Однако я сталкиваюсь со следующей проблемой, которую не могу решить в первой функции: bin_to_float() как показано в результате ниже:

float_to_bin(0.000000): '0'
Traceback (most recent call last):
  File "binary-to-a-float-number.py", line 36, in <module>
    float = bin_to_float(binary)
  File "binary-to-a-float-number.py", line 9, in bin_to_float
    return struct.unpack('>d', bf)[0]
TypeError: a bytes-like object is required, not 'str'

Я пытался это исправить, добавив bf = bytes(bf) прямо перед звонком struct.unpack(), но это произвело свое собственное TypeError:

TypeError: string argument without an encoding

Итак, мои вопросы: можно ли исправить эту проблему и достичь моей цели? И если да, то как? Желательно, чтобы это работало в обеих версиях Python.

Вот код, который работает в Python 2:

import struct

def bin_to_float(b):
    """ Convert binary string to a float. """
    bf = int_to_bytes(int(b, 2), 8)  # 8 bytes needed for IEEE 754 binary64
    return struct.unpack('>d', bf)[0]

def int_to_bytes(n, minlen=0):  # helper function
    """ Int/long to byte string. """
    nbits = n.bit_length() + (1 if n < 0 else 0)  # plus one for any sign bit
    nbytes = (nbits+7) // 8  # number of whole bytes
    bytes = []
    for _ in range(nbytes):
        bytes.append(chr(n & 0xff))
        n >>= 8
    if minlen > 0 and len(bytes) < minlen:  # zero pad?
        bytes.extend((minlen-len(bytes)) * '0')
    return ''.join(reversed(bytes))  # high bytes at beginning

# tests

def float_to_bin(f):
    """ Convert a float into a binary string. """
    ba = struct.pack('>d', f)
    ba = bytearray(ba)
    s = ''.join('{:08b}'.format(b) for b in ba)
    s = s.lstrip('0')  # strip leading zeros
    return s if s else '0'  # but leave at least one

for f in 0.0, 1.0, -14.0, 12.546, 3.141593:
    binary = float_to_bin(f)
    print('float_to_bin(%f): %r' % (f, binary))
    float = bin_to_float(binary)
    print('bin_to_float(%r): %f' % (binary, float))
    print('')

3 ответа

Решение

У меня был другой подход от ответа @metatoaster. Я только что модифицировал int_to_bytes использовать и вернуть bytearray:

def int_to_bytes(n, minlen=0):  # helper function
    """ Int/long to byte string. """
    nbits = n.bit_length() + (1 if n < 0 else 0)  # plus one for any sign bit
    nbytes = (nbits+7) // 8  # number of whole bytes
    b = bytearray()
    for _ in range(nbytes):
        b.append(n & 0xff)
        n >>= 8
    if minlen > 0 and len(b) < minlen:  # zero pad?
        b.extend([0] * (minlen-len(b)))
    return bytearray(reversed(b))  # high bytes at beginning

Похоже, что это работает без каких-либо других модификаций под Python 2.7.11 и Python 3.5.1.

Обратите внимание, что я обнуляю 0 вместо '0', Я не делал много испытаний, но, конечно, это то, что вы имели в виду?

Чтобы создать переносимый код, который работает с байтами как в Python 2, так и в 3, с использованием библиотек, которые буквально используют разные типы данных между ними, вам нужно явно объявить их, используя соответствующую буквенную метку для каждой строки (или добавить from __future__ import unicode_literals к вершине каждого модуля, делающего это). Этот шаг должен гарантировать, что ваши типы данных внутренне правильны в вашем коде.

Во-вторых, примите решение о поддержке Python 3 в будущем, с откатами, специфичными для Python 2. Это означает переопределение str с unicode и выяснить методы / функции, которые не возвращают одинаковые типы в обеих версиях Python, следует изменить и заменить, чтобы они возвращали правильный тип (являющийся версией Python 3). Обратите внимание, что bytes тоже зарезервированное слово, так что не используйте его.

Соединив это, ваш код будет выглядеть примерно так:

import struct
import sys

if sys.version_info < (3, 0):
    str = unicode
    chr = unichr


def bin_to_float(b):
    """ Convert binary string to a float. """
    bf = int_to_bytes(int(b, 2), 8)  # 8 bytes needed for IEEE 754 binary64
    return struct.unpack(b'>d', bf)[0]

def int_to_bytes(n, minlen=0):  # helper function
    """ Int/long to byte string. """
    nbits = n.bit_length() + (1 if n < 0 else 0)  # plus one for any sign bit
    nbytes = (nbits+7) // 8  # number of whole bytes
    ba = bytearray(b'')
    for _ in range(nbytes):
        ba.append(n & 0xff)
        n >>= 8
    if minlen > 0 and len(ba) < minlen:  # zero pad?
        ba.extend((minlen-len(ba)) * b'0')
    return u''.join(str(chr(b)) for b in reversed(ba)).encode('latin1')  # high bytes at beginning

# tests

def float_to_bin(f):
    """ Convert a float into a binary string. """
    ba = struct.pack(b'>d', f)
    ba = bytearray(ba)
    s = u''.join(u'{:08b}'.format(b) for b in ba)
    s = s.lstrip(u'0')  # strip leading zeros
    return (s if s else u'0').encode('latin1')  # but leave at least one

for f in 0.0, 1.0, -14.0, 12.546, 3.141593:
    binary = float_to_bin(f)
    print(u'float_to_bin(%f): %r' % (f, binary))
    float = bin_to_float(binary)
    print(u'bin_to_float(%r): %f' % (binary, float))
    print(u'')

Я использовал latin1 кодек просто потому, что это то, что байтовые сопоставления изначально определены, и, кажется, работает

$ python2 foo.py 
float_to_bin(0.000000): '0'
bin_to_float('0'): 0.000000

float_to_bin(1.000000): '11111111110000000000000000000000000000000000000000000000000000'
bin_to_float('11111111110000000000000000000000000000000000000000000000000000'): 1.000000

float_to_bin(-14.000000): '1100000000101100000000000000000000000000000000000000000000000000'
bin_to_float('1100000000101100000000000000000000000000000000000000000000000000'): -14.000000

float_to_bin(12.546000): '100000000101001000101111000110101001111110111110011101101100100'
bin_to_float('100000000101001000101111000110101001111110111110011101101100100'): 12.546000

float_to_bin(3.141593): '100000000001001001000011111101110000010110000101011110101111111'
bin_to_float('100000000001001001000011111101110000010110000101011110101111111'): 3.141593

Опять же, но на этот раз под Python 3.5)

$ python3 foo.py 
float_to_bin(0.000000): b'0'
bin_to_float(b'0'): 0.000000

float_to_bin(1.000000): b'11111111110000000000000000000000000000000000000000000000000000'
bin_to_float(b'11111111110000000000000000000000000000000000000000000000000000'): 1.000000

float_to_bin(-14.000000): b'1100000000101100000000000000000000000000000000000000000000000000'
bin_to_float(b'1100000000101100000000000000000000000000000000000000000000000000'): -14.000000

float_to_bin(12.546000): b'100000000101001000101111000110101001111110111110011101101100100'
bin_to_float(b'100000000101001000101111000110101001111110111110011101101100100'): 12.546000

float_to_bin(3.141593): b'100000000001001001000011111101110000010110000101011110101111111'
bin_to_float(b'100000000001001001000011111101110000010110000101011110101111111'): 3.141593

Это намного больше работы, но в Python3 вы можете более четко видеть, что типы выполняются как правильные байты. Я также изменил ваш bytes = [] к байту, чтобы более четко выразить то, что вы пытались сделать.

В Python 3 целые числа имеют to_bytes() метод, который может выполнить преобразование в одном вызове. Однако, поскольку вы запросили решение, которое работает на Python 2 и 3 без изменений, вот альтернативный подход.

Если вы берете объезд через шестнадцатеричное представление, функция int_to_bytes() становится очень просто:

import codecs

def int_to_bytes(n, minlen=0):
    hex_str = format(n, "0{}x".format(2 * minlen))
    return codecs.decode(hex_str, "hex")

Вам может потребоваться некоторая особая обработка случая, чтобы иметь дело со случаем, когда шестнадцатеричная строка получает нечетное количество символов.

Обратите внимание, что я не уверен, что это работает со всеми версиями Python 3. Я помню, что псевдокодировки не поддерживались в некоторых версиях 3.x, но я не помню деталей. Я тестировал код с Python 3.5.

Другие вопросы по тегам