#python #python-3.x #indentation #literals #python-unicode
#python #python-3.x #отступ #литералы #python-unicode
Вопрос:
Когда я использую многострочную строку в тройных кавычках в Python, я обычно использую textwrap.dedent, чтобы код оставался читаемым с хорошим отступом:
some_string = textwrap.dedent("""
First line
Second line
...
""").strip()
Однако в Python 3.x textwrap.dedent, похоже, не работает с байтовыми строками. Я столкнулся с этим при написании модульного теста для метода, который возвращал длинную многострочную байтовую строку, например:
# The function to be tested
def some_function():
return b'Lorem ipsum dolor sit ametn consectetuer adipiscing elit'
# Unit test
import unittest
import textwrap
class SomeTest(unittest.TestCase):
def test_some_function(self):
self.assertEqual(some_function(), textwrap.dedent(b"""
Lorem ipsum dolor sit amet
consectetuer adipiscing elit
""").strip())
if __name__ == '__main__':
unittest.main()
В Python 2.7.10 приведенный выше код работает нормально, но в Python 3.4.3 он терпит неудачу:
E
======================================================================
ERROR: test_some_function (__main__.SomeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 16, in test_some_function
""").strip())
File "/usr/lib64/python3.4/textwrap.py", line 416, in dedent
text = _whitespace_only_re.sub('', text)
TypeError: can't use a string pattern on a bytes-like object
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
Итак: есть ли альтернатива textwrap.dedent, которая работает с байтовыми строками?
- Я мог бы написать такую функцию сам, но если есть существующая функция, я бы предпочел использовать ее.
- Я мог бы преобразовать в юникод, использовать textwrap.dedent и преобразовать обратно в байты. Но это возможно только в том случае, если строка байтов соответствует некоторой кодировке Unicode.
Ответ №1:
Ответ 2: textwrap
в первую очередь касается Textwrap
класса и функций. dedent
находится в разделе
# -- Loosely related functionality --------------------
Насколько я могу судить, единственное, что делает его специфичным для текста (unicode str
), — это литералы re . Я поставил перед всеми 6 префиксами b
и вуаля! (Я больше ничего не редактировал, но строка документации функции должна быть скорректирована.)
import re
_whitespace_only_re = re.compile(b'^[ t] $', re.MULTILINE)
_leading_whitespace_re = re.compile(b'(^[ t]*)(?:[^ tn])', re.MULTILINE)
def dedent_bytes(text):
"""Remove any common leading whitespace from every line in `text`.
This can be used to make triple-quoted strings line up with the left
edge of the display, while still presenting them in the source code
in indented form.
Note that tabs and spaces are both treated as whitespace, but they
are not equal: the lines " hello" and "\thello" are
considered to have no common leading whitespace. (This behaviour is
new in Python 2.5; older versions of this module incorrectly
expanded tabs before searching for common leading whitespace.)
"""
# Look for the longest leading string of spaces and tabs common to
# all lines.
margin = None
text = _whitespace_only_re.sub(b'', text)
indents = _leading_whitespace_re.findall(text)
for indent in indents:
if margin is None:
margin = indent
# Current line more deeply indented than previous winner:
# no change (previous winner is still on top).
elif indent.startswith(margin):
pass
# Current line consistent with and no deeper than previous winner:
# it's the new winner.
elif margin.startswith(indent):
margin = indent
# Find the largest common whitespace between current line
# and previous winner.
else:
for i, (x, y) in enumerate(zip(margin, indent)):
if x != y:
margin = margin[:i]
break
else:
margin = margin[:len(indent)]
# sanity check (testing/debugging only)
if 0 and margin:
for line in text.split(b"n"):
assert not line or line.startswith(margin),
"line = %r, margin = %r" % (line, margin)
if margin:
text = re.sub(rb'(?m)^' margin, b'', text)
return text
print(dedent_bytes(b"""
Lorem ipsum dolor sit amet
consectetuer adipiscing elit
""")
)
# prints
b'nLorem ipsum dolor sit ametn consectetuer adipiscing elitn'
Ответ №2:
К сожалению, похоже, что dedent
он не поддерживает байтовые строки. Однако, если вам нужен кросс-совместимый код, я рекомендую вам воспользоваться six
библиотекой:
import sys, unittest
from textwrap import dedent
import six
def some_function():
return b'Lorem ipsum dolor sit ametn consectetuer adipiscing elit'
class SomeTest(unittest.TestCase):
def test_some_function(self):
actual = some_function()
expected = six.b(dedent("""
Lorem ipsum dolor sit amet
consectetuer adipiscing elit
""")).strip()
self.assertEqual(actual, expected)
if __name__ == '__main__':
unittest.main()
Это похоже на ваше предложение в вопросе
Я мог бы преобразовать в юникод, использовать textwrap.dedent и преобразовать обратно в байты. Но это возможно только в том случае, если строка байтов соответствует некоторой кодировке Unicode.
Но вы что-то недопонимаете в кодировках здесь — если вы можете написать строковый литерал в своем тесте таким образом в первую очередь и успешно проанализировать файл с помощью python (т. Е. Правильное объявление coding находится в модуле), то здесь нет шага «преобразовать в юникод». Файл анализируется в указанной кодировке (или sys.defaultencoding
, если вы не указали), а затем, когда строка является переменной python, она уже декодируется.
Комментарии:
1. Хорошая идея использовать six.b вне dedent. Я уже использую six в своем проекте, поэтому использование six.b не добавляет дополнительной зависимости. Мои проблемы с кодированием были связаны не столько с символами, отличными от ascii, в исходном файле, сколько с шестнадцатеричными escape-последовательностями, такими как » xff». Но я протестировал его сейчас, и он работает для всех таких последовательностей (six.b (s) на Python 3 эквивалентно s.encode(«latin-1»)). Я приму этот ответ.
Ответ №3:
Ответ 1: Многострочные строки в тройных кавычках (и dedent) — это удобство (иногда), а не необходимость. Вместо этого вы можете написать отдельный байтовый литерал, заканчивающийся на b’ n’ для каждой строки, и позволить анализатору объединить их. Пример:
>>> b = (
b'Lorem ipsum dolor sit ametn' # first line
b'consectetuer adipiscing elitn' # 2nd line
)
>>> b
b'Lorem ipsum dolor sit ametnconsectetuer adipiscing elitn'
Я намеренно добавил пробелы и комментарии к коду, которые не требуются в результирующих байтах, чтобы они не были включены. Иногда я делаю эквивалент вышеупомянутого с текстовыми строками.
Ответ 2: Преобразовать textwrap.dedent в байт обработки (см. Отдельный ответ)
Ответ 3: Опустите b
префикс и добавьте .encode()
до или после .strip()
.
print(textwrap.dedent("""
Lorem ipsum dolor sit amet
consectetuer adipiscing elit
""").encode())
# prints (same as Answer 2).
b'nLorem ipsum dolor sit ametn consectetuer adipiscing elitn'
Комментарии:
1. Хороший ответ. Я уже использовал этот подход для небольшого количества строк; думаю, мне не стоит бояться использовать его и для более длинных текстов. Тем не менее, я думаю, что для очень длинных текстов или для текстов, которые регулярно будут обновляться с помощью копирования вставки (что иногда случается в моих модульных тестах), тройные кавычки dedent были бы лучше и имели меньше визуального беспорядка.
2. Ответ 3 не совсем работает во всех случаях, например, textwrap.dedent(«»» xff»»»).encode() — это b’xc3 xbf’, а не b’ xff’ (я хочу последнее). В Python 2 это вызывает ошибку UnicodeDecodeError. Использование six.b() вместо .encode() устраняет эти проблемы, см. Ответ wim.
3.
'xff'.encode('latin1') == b'xff'
Вы можете получить любые байты с latin-1 . Предположение textwrap и любого его моделирования заключается в том, что текст / байты имеют случайные разрывы строк.