字节、字符和 Python 2

从 Python 2 迁移到 3? 这是您需要了解的关于字符串及其在升级中作用的内容。

一个老笑话问:“会说三种语言的人叫什么? 三语者。 两种语言? 双语者。 一种语言? 美国人。”

既然我已经成功激怒了我所有的美国读者,我现在可以切入正题了,正题是由于许多计算机技术是在英语国家(尤其是在美国)开发的,因此早期计算机技术常常忽略了其他语言的需求。 20世纪60年代建立的将数字翻译成字符(以及反向翻译)的标准,即ASCII(美国信息交换标准代码),考虑了使用英语所需的所有字母、数字和符号。 这就是它可以处理的全部内容,因为它是一个七字节(即128字符)编码。

如果您愿意忽略带重音的字母,ASCII也可以勉强地与其他语言一起使用,但是当您想使用另一种字符集(例如中文或希伯来语)时,您就倒霉了。 ASCII的变体,例如ISO-8859-x(“x”有多个值),在有限程度上解决了这个问题,但是该系统存在许多问题。

Unicode为全球每种语言的每个字符都赋予了一个唯一的数字。 这使您可以表示(几乎)每种语言的每个字符。 问题是如何使用字节表示这些数字。 毕竟,归根结底,字节仍然是将数据存储到文件系统和从文件系统读取数据的方式,数据在内存中的表示方式以及数据在网络上的传输方式。 在许多语言和操作系统中,使用的编码是UTF-8。 这种巧妙的系统对不同的字符使用不同数量的字节。 ASCII中出现的字符继续使用单字节。 其他一些字符集(例如,阿拉伯语、希腊语、希伯来语和俄语)每个字符使用两个字节。 还有一些其他字符集(例如中文和表情符号)每个字符使用三个字节。

在现代编程语言中,您不应该过多地担心这些东西。 如果您从文件系统、用户或网络获取输入,它应该只是以字符的形式出现。 每个字符需要多少字节是一个您可以(或应该能够)忽略的实现细节。

我为什么要提到这个? 因为越来越多的客户开始从 Python 2 升级到 Python 3。是的,Python 3 已经存在十年了,但是最近版本中的一些重大改进以及 Python 2 在弃用前只剩下 18 个月的认识,使得许多公司意识到,“哎呀,也许我们终于应该升级了。”

对他们中的许多人来说,主要的症结所在? 字节与字符的问题。

因此,在本文中,我开始研究这意味着什么以及如何处理它,首先从检查 Python 2 中的字节与字符开始。 在我的下一篇文章中,我计划研究 Python 3 以及即使您确切知道何时需要字节以及何时需要字符,升级也可能很棘手。

基本字符串

传统上,Python 字符串是由字节构建的,也就是说,您可以将 Python 字符串视为字节序列。 如果这些字节恰好与字符对齐(如在 ASCII 中),那么您就非常顺利。 但是,如果这些字节来自另一个字符集,则需要重新考虑一下。 例如


>>> s = 'hello'
>>> len(s)
5
>>> s = 'שלום'  # Hebrew
>>> len(s)
8
>>> s = '你好'  # Chinese
>>> len(s)
6

 

这里发生了什么? Python 2 允许您输入任何您想要的字符,但它不将输入视为字符。 相反,它只将它们视为字节。 这几乎就像您去看机械师并说:“我的车有问题”,而您的机械师说:“我没看到汽车。 我看到四个门、一个挡风玻璃、一个油箱、一个发动机、四个车轮和轮胎等等。” Python 正在关注各个部分,而不是关注由这些部分构建的字符。

检查字符串的长度是您看到此问题的一个地方。 另一个是当您只想打印字符串的一部分时。 例如,中文字符串中的第一个字符是什么? 它应该是字符 你 ,意思是“你”


>>> print(s[0])


 

呸! 这太不成功了,并且可能对任何用户都毫无用处。

如果您想编写一个可靠地打印 Python 2 字符串的第一个字符(而不是字节)的函数,您可以跟踪您正在使用的语言,然后查看适当的字节数。 但是这肯定会有很多问题和错误,而且也会非常复杂。

合适的解决方案是使用 Python 2 的“Unicode 字符串”。 Unicode 字符串就像常规 Python 字符串一样,只是它使用字符而不是字节。 实际上,Python 2 的 Unicode 字符串就像 Python 3 的默认字符串一样。 使用 Unicode 字符串,您可以执行以下操作


>>> s = u'hello'
>>> len(s)
5
>>> s = u'שלום'  # Hebrew
>>> len(s)
4
>>> s = u'你好'  # Chinese
>>> len(s)
2
>>> print(s[0])


 

太棒了! 这些是期望的结果。 您甚至可以使用我最喜欢的模块之一 __future__ 使其成为 Python 2 程序中的默认行为。 __future__ 模块的存在是为了让您可以利用计划在更高版本中包含的功能,即使您使用的是现有版本。 它允许 Python 缓慢地推出新功能,并让您在准备就绪时随时使用它。

__future__ 功能之一是 unicode_literal。 这会将 Python 中字符串的默认类型更改为 Unicode 字符串,从而无需前导“u”。 例如


>>> from __future__ import unicode_literals
>>> s = 'hello'
>>> len(s)
5
>>> s = 'שלום'  # Hebrew
>>> len(s)
4
>>> s = '你好'  # Chinese
>>> len(s)
2
>>> print(s[0])
 

 

现在,这并不意味着您的所有问题都已解决。 首先,此 from 语句意味着您的字符串实际上不再是字符串,而是 unicode 类型的对象


>>> type(s)


 

如果您有代码(您不应该有!),通过显式检查类型来检查 s 是否为字符串,则在使用 unicode_literals 后,该代码将会中断。 但是,其他事情也会中断。

从文件读取

例如,假设我想将二进制文件(例如 PDF 文档或 JPEG)读入 Python。 通常,在 Python 2 中,我可以使用字符串来执行此操作,因为字符串可以包含任何字节,以任何顺序排列。 但是,Unicode 对哪些字节表示字符非常严格,这在很大程度上是因为第八位(最高位)为激活状态的字节是较大字符的一部分,不能单独存在。

这是我编写的一个简短程序,用于读取和打印此类文件


>>> filename = 'Files/unicode.txt'
>>> from __future__ import unicode_literals
>>> for one_line in open(filename):
...  for index, one_char in one_line:
...     print("{0}: {1}".format(index, one_char))

 

当我运行它时,此程序崩溃了


Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xd7
 ↪in position 0: ordinal not in range(128)

 

问题是什么? 嗯,它仍然使用字节而不是字符来读取文件。 从文件系统读取当前行后,Python 尝试创建字符串。 但是,它无法解决它接收到的字节与它必须创建为字符串的 Unicode 之间的冲突。

换句话说,虽然它已设法使 Python 的字符串符合 Unicode 标准,但常规 Python 环境中还有许多东西不了解 Unicode 或对 Unicode 不友好。

您可以使用 codecs 模块及其提供的 open 方法来解决此问题,告诉它您在从文件读取时要使用的编码


>>> import codecs
>>> for one_line in codecs.open(filename, encoding='utf-8'):
...  for index, one_char in enumerate(one_line):
...     print("{0}: {1}".format(index, one_char))

 

总而言之,如果您使用 unicode_literals,则可以使 Python 的所有字符串都符合 Unicode 标准。 但是,一旦您这样做,您就会遇到从用户、网络或文件系统以字节形式获取数据,并且出现错误的潜在问题。 尽管这看起来像是处理整个 Unicode 问题的非常诱人的方法,但我建议您仅在拥有非常好的测试套件,并且确定您使用的所有库都知道在这种方式更改字符串时该怎么做的情况下,才使用 unicode_literals 路由。 您很可能会惊讶地发现,尽管许多事情都可以正常工作,但还有许多事情却不行。

bytes 类型

在讨论字符串和 Unicode 时,还应该提到另一种类型:“字节字符串”,也称为 bytes 类型。 在 Python 2 中,bytes 只是 str 的别名,您可以在这个导入 unicode_literals 的 Python shell 中看到


>>> s = 'abcd'
>>> type(s) == bytes
True
>>> str == bytes
True
>>> bytes(1234)
'1234'
>>> type(bytes(1234))
<type 'str'>
>>>

 

换句话说,尽管 Python 字符串通常被认为具有 str 类型,但它们同样可以被视为具有 bytes 类型。 你为什么要关心? 因为它允许您在 Python 2 中已经将使用字节的字符串与使用 Unicode 的字符串分开,并在您进入 Python 3 时继续保持这种显式差异。

我应该补充一点,我遇到(和教过)的许多使用 Python 2 的开发人员都不知道字节字符串的存在。 在 Python 3 中谈论它们更为常见,在 Python 3 中,它们充当 Unicode 感知字符串的对应物。

正如 Unicode 字符串具有“u”前缀一样,字节字符串具有“b”前缀。 例如


>>> b'abcd'
'abcd'
>>> type(b'abcd')
<type 'str'>

 

在 Python 2 中,您不需要显式地谈论字节字符串,但是通过使用它们,您可以非常清楚地表明您是在使用字节还是字符。

这就提出了一个问题,即您如何从一个世界转移到另一个世界。 假设,例如,您有中文“Hello”的 Unicode 字符串,又名 ?| 好。 您如何获得包含(六个)字节的字节字符串? 您可以使用 str.encode 方法,该方法返回一个字节字符串(也称为 Python 2 字符串),其中包含六个字节


s.encode('utf-8')

 

有点令人困惑的是(至少对我来说),您从 Unicode “编码”为字节,并且您指示字符串存储内容的编码。 无论如何,然后您得到


>>> s.encode('utf-8')
'\xe4\xbd\xa0\xe5\xa5\xbd'

 

现在,您为什么要将 Unicode 字符串转换为字节? 原因之一是您想将字节写入文件,并且您不想使用 codecs.open 来执行此操作。 (不要尝试将 Unicode 字符串写入以常用方式(使用“open”)打开的文件)。

如果您想做相反的事情,即将一堆字节转换为 Unicode 怎么办? 您可能可以猜到,相反的操作是通过 str.decode 方法执行的


>>> b.decode('utf-8')
u'\u4f60\u597d'

 

再次,您指示应使用的编码,结果是一个 Unicode 字符串,您在此处看到它在 Python 中使用特殊的 \u 语法表示。 此语法允许您通过其唯一的“代码点”指定任何 Unicode 字符。 如果您将其打印出来,您可以看到它的外观


>>> print(b.decode('utf-8'))
你好

 

总结

Python 2 将于 2020 年弃用,许多公司开始研究如何升级。 对他们来说,一个主要问题将是程序中的字符串。 本文着眼于 Python 2 中的字符串、Unicode 字符串和字节字符串,为在我的下一篇文章中介绍 Python 3 中的这些相同问题以及如何处理升级铺平了道路。

Reuven Lerner 在全球范围内向公司教授 Python、数据科学和 Git。 您可以订阅他的免费每周“更好的开发者”电子邮件列表,并从他的书籍和课程中学习,网址为 http://lerner.co.il。 Reuven 与他的妻子和孩子住在以色列的莫迪因。

加载 Disqus 评论