流畅的Python第二版(GPT重译)(二)绝不原创的飞龙

Python基本上是用大量语法糖包装的字典。

LaloMartins,早期数字游牧民和Pythonista

我们在所有的Python程序中都使用字典。即使不是直接在我们的代码中,也是间接的,因为dict类型是Python实现的基本部分。类和实例属性、模块命名空间和函数关键字参数是内存中由字典表示的核心Python构造。__builtins__.__dict__存储所有内置类型、对象和函数。

由于其关键作用,Python字典经过高度优化,并持续改进。哈希表是Python高性能字典背后的引擎。

以下是本章的简要概述:

dict和set的基础实现仍然依赖于哈希表,但dict代码有两个重要的优化,可以节省内存并保留键在dict中的插入顺序。“dict工作原理的实际后果”和“集合工作原理的实际后果”总结了您需要了解的内容,以便很好地使用它们。

接下来的部分描述了用于构建、解包和处理映射的高级语法特性。其中一些特性在语言中并不新鲜,但对您可能是新的。其他需要Python3.9(如|运算符)或Python3.10(如match/case)的特性。让我们从其中一个最好且最古老的特性开始。

自Python2.7起,列表推导和生成器表达式的语法已经适应了dict推导(以及我们即将讨论的set推导)。dictcomp(dict推导)通过从任何可迭代对象中获取key:value对来构建一个dict实例。示例3-1展示了使用dict推导从相同的元组列表构建两个字典的用法。

>>>dial_codes=![1...(880,'Bangladesh'),...(55,'Brazil'),...(86,'China'),...(91,'India'),...(62,'Indonesia'),...(81,'Japan'),...(234,'Nigeria'),...(92,'Pakistan'),...(7,'Russia'),...(1,'UnitedStates'),...]>>>country_dial={country:codeforcode,countryindial_codes}#②>>>country_dial{'Bangladesh':880,'Brazil':55,'China':86,'India':91,'Indonesia':62,'Japan':81,'Nigeria':234,'Pakistan':92,'Russia':7,'UnitedStates':1}>>>{code:country.upper()#③...forcountry,codeinsorted(country_dial.items())...ifcode<70}{55:'BRAZIL',62:'INDONESIA',7:'RUSSIA',1:'UNITEDSTATES'}①

可以直接将类似dial_codes的键值对可迭代对象传递给dict构造函数,但是…

…在这里我们交换了键值对:country是键,code是值。

按名称对country_dial进行排序,再次反转键值对,将值大写,并使用code<70过滤项。

如果你习惯于列表推导,那么字典推导是一个自然的下一步。如果你不熟悉,那么理解推导语法的传播意味着现在比以往任何时候都更有利可图。

首先,我们可以在函数调用中对多个参数应用**。当键都是字符串且在所有参数中唯一时,这将起作用(因为禁止重复关键字参数):

>>>defdump(**kwargs):...returnkwargs...>>>dump(**{'x':1},y=2,**{'z':3}){'x':1,'y':2,'z':3}第二,**可以在dict字面量内使用——也可以多次使用:

>>>{'a':0,**{'x':1},'y':2,**{'z':3,'x':4}}{'a':0,'x':4,'y':2,'z':3}在这种情况下,允许重复的键。后续出现的键会覆盖先前的键—请参见示例中映射到x的值。

这种语法也可以用于合并映射,但还有其他方法。请继续阅读。

Python3.9支持使用|和|=来合并映射。这是有道理的,因为这些也是集合的并运算符。

|运算符创建一个新的映射:

>>>d1={'a':1,'b':3}>>>d2={'a':2,'b':4,'c':6}>>>d1|d2{'a':2,'b':4,'c':6}通常,新映射的类型将与左操作数的类型相同—在示例中是d1,但如果涉及用户定义的类型,则可以是第二个操作数的类型,根据我们在第十六章中探讨的运算符重载规则。

要就地更新现有映射,请使用|=。继续前面的例子,d1没有改变,但现在它被改变了:

现在让我们看看模式匹配如何应用于映射。

match/case语句支持作为映射对象的主题。映射的模式看起来像dict字面量,但它们可以匹配collections.abc.Mapping的任何实际或虚拟子类的实例。1

defget_creators(record:dict)->list:matchrecord:case{'type':'book','api':2,'authors':[*names]}:#①returnnamescase{'type':'book','api':1,'author':name}:#②return[name]case{'type':'book'}:#③raiseValueError(f"Invalid'book'record:{record!r}")case{'type':'movie','director':name}:#④return[name]case_:#⑤raiseValueError(f'Invalidrecord:{record!r}')①

匹配任何具有'type':'book','api':2的映射,并且一个'authors'键映射到一个序列。将序列中的项作为新的list返回。

匹配任何具有'type':'book','api':1的映射,并且一个'author'键映射到任何对象。将对象放入一个list中返回。

具有'type':'book'的任何其他映射都是无效的,引发ValueError。

匹配任何具有'type':'movie'和将'director'键映射到单个对象的映射。返回list中的对象。

任何其他主题都是无效的,引发ValueError。

示例3-2展示了处理半结构化数据(如JSON记录)的一些有用实践:

现在让我们看看get_creators如何处理一些具体的doctests:

>>>b1=dict(api=1,author='DouglasHofstadter',...type='book',title='Gdel,Escher,Bach')>>>get_creators(b1)['DouglasHofstadter']>>>fromcollectionsimportOrderedDict>>>b2=OrderedDict(api=2,type='book',...title='PythoninaNutshell',...authors='MartelliRavenscroftHolden'.split())>>>get_creators(b2)['Martelli','Ravenscroft','Holden']>>>get_creators({'type':'book','pages':770})Traceback(mostrecentcalllast):...ValueError:Invalid'book'record:{'type':'book','pages':770}>>>get_creators('Spam,spam,spam')Traceback(mostrecentcalllast):...ValueError:Invalidrecord:'Spam,spam,spam'注意,模式中键的顺序无关紧要,即使主题是OrderedDict,如b2。

与序列模式相比,映射模式在部分匹配上成功。在doctests中,b1和b2主题包括一个在任何'book'模式中都不出现的'title'键,但它们匹配。

不需要使用**extra来匹配额外的键值对,但如果要将它们捕获为dict,可以使用**前缀一个变量。它必须是模式中的最后一个,并且**_是被禁止的,因为它是多余的。一个简单的例子:

>>>food=dict(category='icecream',flavor='vanilla',cost=199)>>>matchfood:...case{'category':'icecream',**details}:...print(f'Icecreamdetails:{details}')...Icecreamdetails:{'flavor':'vanilla','cost':199}在“缺失键的自动处理”中,我们将研究defaultdict和其他映射,其中通过__getitem__(即,d[key])进行键查找成功,因为缺失项会动态创建。在模式匹配的上下文中,只有在主题已经具有match语句顶部所需键时,匹配才成功。

不会触发缺失键的自动处理,因为模式匹配总是使用d.get(key,sentinel)方法——其中默认的sentinel是一个特殊的标记值,不能出现在用户数据中。

从语法和结构转向,让我们研究映射的API。

collections.abc模块提供了描述dict和类似类型接口的Mapping和MutableMappingABCs。参见图3-1。

ABCs的主要价值在于记录和规范映射的标准接口,并作为需要支持广义映射的代码中isinstance测试的标准:

>>>my_dict={}>>>isinstance(my_dict,abc.Mapping)True>>>isinstance(my_dict,abc.MutableMapping)True提示使用ABC进行isinstance通常比检查函数参数是否为具体dict类型更好,因为这样可以使用替代映射类型。我们将在第十三章中详细讨论这个问题。

要实现自定义映射,最好扩展collections.UserDict,或通过组合包装dict,而不是继承这些ABCs。collections.UserDict类和标准库中的所有具体映射类在其实现中封装了基本的dict,而dict又建立在哈希表上。因此,它们都共享一个限制,即键必须是可哈希的(值不需要是可哈希的,只有键需要是可哈希的)。如果需要复习,下一节会解释。

如果对象具有永远不会在其生命周期内更改的哈希码(它需要一个__hash__()方法),并且可以与其他对象进行比较(它需要一个__eq__()方法),则该对象是可哈希的。比较相等的可哈希对象必须具有相同的哈希码。2

数值类型和扁平不可变类型str和bytes都是可哈希的。如果容器类型是不可变的,并且所有包含的对象也是可哈希的,则它们是可哈希的。frozenset始终是可哈希的,因为它包含的每个元素必须根据定义是可哈希的。仅当元组的所有项都是可哈希的时,元组才是可哈希的。参见元组tt、tl和tf:

>>>tt=(1,2,(30,40))>>>hash(tt)8027212646858338501>>>tl=(1,2,[30,40])>>>hash(tl)Traceback(mostrecentcalllast):File"",line1,inTypeError:unhashabletype:'list'>>>tf=(1,2,frozenset([30,40]))>>>hash(tf)-4118419923444501110对象的哈希码可能因Python版本、机器架构以及出于安全原因添加到哈希计算中的盐而有所不同。3正确实现的对象的哈希码仅在一个Python进程中保证是恒定的。

默认情况下,用户定义的类型是可哈希的,因为它们的哈希码是它们的id(),并且从object类继承的__eq__()方法只是简单地比较对象ID。如果一个对象实现了一个考虑其内部状态的自定义__eq__(),那么只有当其__hash__()始终返回相同的哈希码时,它才是可哈希的。实际上,这要求__eq__()和__hash__()只考虑在对象生命周期中永远不会改变的实例属性。

现在让我们回顾Python中最常用的映射类型dict、defaultdict和OrderedDict的API。

映射的基本API非常丰富。表3-1显示了dict和两个流行变体:defaultdict和OrderedDict的方法,它们都定义在collections模块中。

表3-1.映射类型dict、collections.defaultdict和collections.OrderedDict的方法(为简洁起见省略了常见对象方法);可选参数用[…]括起来

d.update(m)处理其第一个参数m的方式是鸭子类型的一个典型例子:它首先检查m是否有一个keys方法,如果有,就假定它是一个映射。否则,update()会回退到迭代m,假设其项是(key,value)对。大多数Python映射的构造函数在内部使用update()的逻辑,这意味着它们可以从其他映射或从产生(key,value)对的任何可迭代对象初始化。

一种微妙的映射方法是setdefault()。当我们需要就地更新项目的值时,它避免了冗余的键查找。下一节将展示如何使用它。

符合Python的失败快速哲学,使用d[k]访问dict时,当k不是现有键时会引发错误。Python程序员知道,当默认值比处理KeyError更方便时,d.get(k,default)是d[k]的替代方案。然而,当您检索可变值并希望更新它时,有一种更好的方法。

考虑编写一个脚本来索引文本,生成一个映射,其中每个键是一个单词,值是该单词出现的位置列表,如示例3-3所示。

$python3index0.pyzen.txta[(19,48),(20,53)]Although[(11,1),(16,1),(18,1)]ambiguity[(14,16)]and[(15,23)]are[(21,12)]aren[(10,15)]at[(16,38)]bad[(19,50)]be[(15,14),(16,27),(20,50)]beats[(11,23)]Beautiful[(3,1)]better[(3,14),(4,13),(5,11),(6,12),(7,9),(8,11),(17,8),(18,25)]...示例3-4是一个次优脚本,用于展示dict.get不是处理缺失键的最佳方式的一个案例。我从亚历克斯·马特利的一个示例中进行了改编。

"""Buildanindexmappingword->listofoccurrences"""importreimportsysWORD_RE=re.compile(r'\w+')index={}withopen(sys.argv[1],encoding='utf-8')asfp:forline_no,lineinenumerate(fp,1):formatchinWORD_RE.finditer(line):word=match.group()column_no=match.start()+1location=(line_no,column_no)#thisisugly;codedlikethistomakeapointoccurrences=index.get(word,[])#①occurrences.append(location)#②index[word]=occurrences#③#displayinalphabeticalorderforwordinsorted(index,key=str.upper):#④print(word,index[word])①

获取word的出现列表,如果找不到则为[]。

将新位置附加到occurrences。

将更改后的occurrences放入index字典中;这需要通过index进行第二次搜索。

在sorted的key=参数中,我没有调用str.upper,只是传递了对该方法的引用,以便sorted函数可以使用它来对单词进行规范化排序。

示例3-4中处理occurrences的三行可以用dict.setdefault替换为一行。示例3-5更接近亚历克斯·马特利的代码。

"""Buildanindexmappingword->listofoccurrences"""importreimportsysWORD_RE=re.compile(r'\w+')index={}withopen(sys.argv[1],encoding='utf-8')asfp:forline_no,lineinenumerate(fp,1):formatchinWORD_RE.finditer(line):word=match.group()column_no=match.start()+1location=(line_no,column_no)index.setdefault(word,[]).append(location)#①#displayinalphabeticalorderforwordinsorted(index,key=str.upper):print(word,index[word])①

获取word的出现列表,如果找不到则将其设置为[];setdefault返回值,因此可以在不需要第二次搜索的情况下进行更新。

换句话说,这行的最终结果是…

my_dict.setdefault(key,[]).append(new_value)…等同于运行…

ifkeynotinmy_dict:my_dict[key]=[]my_dict[key].append(new_value)…除了后者的代码至少执行两次对key的搜索—如果找不到,则执行三次—而setdefault只需一次查找就可以完成所有操作。

有时,当搜索缺失的键时返回一些虚构的值是很方便的。有两种主要方法:一种是使用defaultdict而不是普通的dict。另一种是子类化dict或任何其他映射类型,并添加一个__missing__方法。接下来将介绍这两种解决方案。

一个collections.defaultdict实例在使用d[k]语法搜索缺失键时按需创建具有默认值的项目。示例3-6使用defaultdict提供了另一个优雅的解决方案来完成来自示例3-5的单词索引任务。

它的工作原理是:在实例化defaultdict时,你提供一个可调用对象,每当__getitem__传递一个不存在的键参数时产生一个默认值。

例如,给定一个创建为dd=defaultdict(list)的defaultdict,如果'new-key'不在dd中,表达式dd['new-key']会执行以下步骤:

产生默认值的可调用对象保存在名为default_factory的实例属性中。

"""Buildanindexmappingword->listofoccurrences"""importcollectionsimportreimportsysWORD_RE=re.compile(r'\w+')index=collections.defaultdict(list)#①withopen(sys.argv[1],encoding='utf-8')asfp:forline_no,lineinenumerate(fp,1):formatchinWORD_RE.finditer(line):word=match.group()column_no=match.start()+1location=(line_no,column_no)index[word].append(location)#②#displayinalphabeticalorderforwordinsorted(index,key=str.upper):print(word,index[word])①

使用list构造函数创建一个defaultdict作为default_factory。

如果word最初不在index中,则调用default_factory来生成缺失值,这种情况下是一个空的list,然后将其分配给index[word]并返回,因此.append(location)操作总是成功的。

如果没有提供default_factory,则对于缺失的键会引发通常的KeyError。

defaultdict的default_factory仅在为__getitem__调用提供默认值时才会被调用,而不会为其他方法调用。例如,如果dd是一个defaultdict,k是一个缺失的键,dd[k]将调用default_factory来创建一个默认值,但dd.get(k)仍然返回None,kindd为False。

使defaultdict工作的机制是调用default_factory的__missing__特殊方法,这是我们接下来要讨论的一个特性。

映射处理缺失键的基础是名为__missing__的方法。这个方法在基本的dict类中没有定义,但dict知道它:如果你子类化dict并提供一个__missing__方法,标准的dict.__getitem__将在找不到键时调用它,而不是引发KeyError。

假设你想要一个映射,其中键在查找时被转换为str。一个具体的用例是物联网设备库,其中一个具有通用I/O引脚(例如树莓派或Arduino)的可编程板被表示为一个Board类,具有一个my_board.pins属性,它是物理引脚标识符到引脚软件对象的映射。物理引脚标识符可能只是一个数字或一个字符串,如"A0"或"P9_12"。为了一致性,希望board.pins中的所有键都是字符串,但也方便通过数字查找引脚,例如my_arduino.pin[13],这样初学者在想要闪烁他们的Arduino上的13号引脚时不会出错。示例3-7展示了这样一个映射如何工作。

Testsforitemretrievalusing`d[key]`notation::>>>d=StrKeyDict0([('2','two'),('4','four')])>>>d['2']'two'>>>d[4]'four'>>>d[1]Traceback(mostrecentcalllast):...KeyError:'1'Testsforitemretrievalusing`d.get(key)`notation::>>>d.get('2')'two'>>>d.get(4)'four'>>>d.get(1,'N/A')'N/A'Testsforthe`in`operator::>>>2indTrue>>>1indFalse示例3-8实现了一个通过前面的doctests的StrKeyDict0类。

创建用户定义的映射类型的更好方法是子类化collections.UserDict而不是dict(正如我们将在示例3-9中所做的那样)。这里我们子类化dict只是为了展示内置的dict.__getitem__方法支持__missing__。

classStrKeyDict0(dict):#①def__missing__(self,key):ifisinstance(key,str):#②raiseKeyError(key)returnself[str(key)]#③defget(self,key,default=None):try:returnself[key]#④exceptKeyError:returndefault#⑤def__contains__(self,key):returnkeyinself.keys()orstr(key)inself.keys()#⑥①

StrKeyDict0继承自dict。

检查key是否已经是str。如果是,并且它丢失了,那么引发KeyError。

从key构建str并查找它。

get方法通过使用self[key]符号委托给__getitem__;这给了我们的__missing__发挥作用的机会。

如果引发KeyError,则__missing__已经失败,因此我们返回default。

搜索未修改的键(实例可能包含非str键),然后搜索从键构建的str。

没有这个测试,我们的__missing__方法对于任何键k——str或非str——都能正常工作,只要str(k)产生一个现有的键。但是如果str(k)不是一个现有的键,我们将会有一个无限递归。在__missing__的最后一行,self[str(key)]会调用__getitem__,传递那个str键,然后会再次调用__missing__。

在这个例子中,__contains__方法也是必需的,因为操作kind会调用它,但从dict继承的方法不会回退到调用__missing__。在我们的__contains__实现中有一个微妙的细节:我们不是用通常的Python方式检查键——kinmy_dict——因为str(key)inself会递归调用__contains__。我们通过在self.keys()中明确查找键来避免这种情况。

在Python3中,像kinmy_dict.keys()这样的搜索对于非常大的映射也是高效的,因为dict.keys()返回一个视图,类似于集合,正如我们将在“dict视图上的集合操作”中看到的。然而,请记住,kinmy_dict也能完成同样的工作,并且更快,因为它避免了查找属性以找到.keys方法。

我在示例3-8中的__contains__方法中有一个特定的原因使用self.keys()。检查未修改的键——keyinself.keys()——对于正确性是必要的,因为StrKeyDict0不强制字典中的所有键都必须是str类型。我们这个简单示例的唯一目标是使搜索“更友好”,而不是强制类型。

派生自标准库映射的用户定义类可能会或可能不会在它们的__getitem__、get或__contains__实现中使用__missing__作为回退,如下一节所述。

考虑以下情况,以及缺失键查找是如何受影响的:

dict子类

一个只实现__missing__而没有其他方法的dict子类。在这种情况下,__missing__只能在d[k]上调用,这将使用从dict继承的__getitem__。

collections.UserDict子类

同样,一个只实现__missing__而没有其他方法的UserDict子类。从UserDict继承的get方法调用__getitem__。这意味着__missing__可能被调用来处理d[k]和d.get(k)的查找。

具有最简单可能的__getitem__的abc.Mapping子类

一个实现了__missing__和所需抽象方法的最小的abc.Mapping子类,包括一个不调用__missing__的__getitem__实现。在这个类中,__missing__方法永远不会被触发。

具有调用__missing__的__getitem__的abc.Mapping子类

一个最小的abc.Mapping子类实现了__missing__和所需的抽象方法,包括调用__missing__的__getitem__的实现。在这个类中,对使用d[k]、d.get(k)和kind进行的缺失键查找会触发__missing__方法。

刚才描述的四种情况假设最小实现。如果你的子类实现了__getitem__、get和__contains__,那么你可以根据需要让这些方法使用__missing__或不使用。本节的重点是要表明,在子类化标准库映射时要小心使用__missing__,因为基类默认支持不同的行为。

不要忘记,setdefault和update的行为也受键查找影响。最后,根据你的__missing__的逻辑,你可能需要在__setitem__中实现特殊逻辑,以避免不一致或令人惊讶的行为。我们将在“SubclassingUserDictInsteadofdict”中看到一个例子。

到目前为止,我们已经介绍了dict和defaultdict这两种映射类型,但标准库中还有其他映射实现,接下来我们将讨论它们。

本节概述了标准库中包含的映射类型,除了已在“defaultdict:AnotherTakeonMissingKeys”中介绍的defaultdict。

自从Python3.6开始,内置的dict也保持了键的有序性,使用OrderedDict的最常见原因是编写与早期Python版本向后兼容的代码。话虽如此,Python的文档列出了dict和OrderedDict之间的一些剩余差异,我在这里引用一下——只重新排列项目以便日常使用:

ChainMap实例保存了一个可以作为一个整体搜索的映射列表。查找是按照构造函数调用中出现的顺序在每个输入映射上执行的,并且一旦在这些映射中的一个中找到键,查找就成功了。例如:

>>>d1=dict(a=1,b=3)>>>d2=dict(a=2,b=4,c=6)>>>fromcollectionsimportChainMap>>>chain=ChainMap(d1,d2)>>>chain['a']1>>>chain['c']6ChainMap实例不会复制输入映射,而是保留对它们的引用。对ChainMap的更新或插入只会影响第一个输入映射。继续上一个例子:

importbuiltinspylookup=ChainMap(locals(),globals(),vars(builtins))示例18-14展示了一个用于实现Scheme编程语言子集解释器的ChainMap子类。

>>>ct=collections.Counter('abracadabra')>>>ctCounter({'a':5,'b':2,'r':2,'c':1,'d':1})>>>ct.update('aaaaazzz')>>>ctCounter({'a':10,'z':3,'b':2,'r':2,'c':1,'d':1})>>>ct.most_common(3)[('a',10),('z',3),('b',2)]请注意,'b'和'r'键并列第三,但ct.most_common(3)只显示了三个计数。

要将collections.Counter用作多重集,假装每个键是集合中的一个元素,计数是该元素在集合中出现的次数。

标准库中的shelve模块为字符串键到以pickle二进制格式序列化的Python对象的映射提供了持久存储。当你意识到pickle罐子存放在架子上时,shelve这个奇怪的名字就有了意义。

shelve.open模块级函数返回一个shelve.Shelf实例——一个简单的键-值DBM数据库,由dbm模块支持,具有以下特点:

OrderedDict、ChainMap、Counter和Shelf都可以直接使用,但也可以通过子类化进行自定义。相比之下,UserDict只是作为一个可扩展的基类。

最好通过扩展collections.UserDict来创建新的映射类型,而不是dict。当我们尝试扩展我们的StrKeyDict0(来自示例3-8)以确保将任何添加到映射中的键存储为str时,我们意识到这一点。

更好地通过子类化UserDict而不是dict的主要原因是,内置类型有一些实现快捷方式,最终迫使我们覆盖我们可以从UserDict继承而不会出现问题的方法。

请注意,UserDict不继承自dict,而是使用组合:它有一个内部的dict实例,称为data,用于保存实际的项目。这避免了在编写特殊方法如__setitem__时出现不必要的递归,并简化了__contains__的编写,与示例3-8相比更加简单。

由于UserDict的存在,StrKeyDict(示例3-9)比StrKeyDict0(示例3-8)更简洁,但它做得更多:它将所有键都存储为str,避免了如果实例被构建或更新时包含非字符串键时可能出现的令人不快的情况。

importcollectionsclassStrKeyDict(collections.UserDict):#①def__missing__(self,key):#②ifisinstance(key,str):raiseKeyError(key)returnself[str(key)]def__contains__(self,key):returnstr(key)inself.data#③def__setitem__(self,key,item):self.data[str(key)]=item#④①

StrKeyDict扩展了UserDict。

__missing__与示例3-8中的一样。

__contains__更简单:我们可以假定所有存储的键都是str,并且可以在self.data上进行检查,而不是像在StrKeyDict0中那样调用self.keys()。

__setitem__将任何key转换为str。当我们可以委托给self.data属性时,这种方法更容易被覆盖。

因为UserDict扩展了abc.MutableMapping,使得使StrKeyDict成为一个完整的映射的剩余方法都是从UserDict、MutableMapping或Mapping继承的。尽管后者是抽象基类(ABC),但它们有几个有用的具体方法。以下方法值得注意:

MutableMapping.update

这种强大的方法可以直接调用,但也被__init__用于从其他映射、从(key,value)对的可迭代对象和关键字参数加载实例。因为它使用self[key]=value来添加项目,所以最终会调用我们的__setitem__实现。

Mapping.get

我们知道有不可变的序列类型,但不可变的映射呢?在标准库中确实没有真正的不可变映射,但有一个替代品可用。接下来是。

标准库提供的映射类型都是可变的,但您可能需要防止用户意外更改映射。再次在硬件编程库中找到一个具体的用例,比如Pingo,在“缺失方法”中提到:board.pins映射表示设备上的物理GPIO引脚。因此,防止意外更新board.pins是有用的,因为硬件不能通过软件更改,所以映射的任何更改都会使其与设备的物理现实不一致。

types模块提供了一个名为MappingProxyType的包装类,给定一个映射,它返回一个mappingproxy实例,这是原始映射的只读但动态代理。这意味着可以在mappingproxy中看到对原始映射的更新,但不能通过它进行更改。参见示例3-10进行简要演示。

>>>fromtypesimportMappingProxyType>>>d={1:'A'}>>>d_proxy=MappingProxyType(d)>>>d_proxymappingproxy({1:'A'})>>>d_proxy[1]#①'A'>>>d_proxy[2]='x'#②Traceback(mostrecentcalllast):File"",line1,inTypeError:'mappingproxy'objectdoesnotsupportitemassignment>>>d[2]='B'>>>d_proxy#③mappingproxy({1:'A',2:'B'})>>>d_proxy[2]'B'>>>①

d中的项目可以通过d_proxy看到。

不能通过d_proxy进行更改。

d_proxy是动态的:d中的任何更改都会反映出来。

在硬件编程场景中,这个方法在实践中可以这样使用:具体的Board子类中的构造函数会用pin对象填充一个私有映射,并通过一个实现为mappingproxy的公共.pins属性将其暴露给API的客户端。这样,客户端就无法意外地添加、删除或更改pin。

接下来,我们将介绍视图—它允许在dict上进行高性能操作,而无需不必要地复制数据。

dict实例方法.keys()、.values()和.items()返回类dict_keys、dict_values和dict_items的实例,分别。这些字典视图是dict实现中使用的内部数据结构的只读投影。它们避免了等效Python2方法的内存开销,这些方法返回了重复数据的列表,这些数据已经在目标dict中,它们还替换了返回迭代器的旧方法。

示例3-11展示了所有字典视图支持的一些基本操作。

>>>d=dict(a=10,b=20,c=30)>>>values=d.values()>>>valuesdict_values([10,20,30])#①>>>len(values)#②3>>>list(values)#③[10,20,30]>>>reversed(values)#④>>>values[0]#⑤Traceback(mostrecentcalllast):File"",line1,inTypeError:'dict_values'objectisnotsubscriptable①

视图对象的repr显示其内容。

我们可以查询视图的len。

视图是可迭代的,因此很容易从中创建列表。

视图实现了__reversed__,返回一个自定义迭代器。

我们不能使用[]从视图中获取单个项目。

视图对象是动态代理。如果源dict被更新,您可以立即通过现有视图看到更改。继续自示例3-11:

>>>d['z']=99>>>d{'a':10,'b':20,'c':30,'z':99}>>>valuesdict_values([10,20,30,99])类dict_keys、dict_values和dict_items是内部的:它们不通过__builtins__或任何标准库模块可用,即使你获得了其中一个的引用,也不能在Python代码中从头开始创建视图:

>>>values_class=type({}.values())>>>v=values_class()Traceback(mostrecentcalllast):File"",line1,inTypeError:cannotcreate'dict_values'instancesdict_values类是最简单的字典视图——它只实现了__len__、__iter__和__reversed__特殊方法。除了这些方法,dict_keys和dict_items实现了几个集合方法,几乎和frozenset类一样多。在我们讨论集合之后,我们将在“字典视图上的集合操作”中更多地谈到dict_keys和dict_items。

现在让我们看一些由dict在幕后实现的规则和提示。

Python的dict的哈希表实现非常高效,但重要的是要了解这种设计的实际影响:

现在让我们深入研究集合。

在Python中,集合并不新鲜,但仍然有些被低估。set类型及其不可变的姊妹frozenset首次出现在Python2.3标准库中作为模块,并在Python2.6中被提升为内置类型。

在本书中,我使用“集合”一词来指代set和frozenset。当专门讨论set类型,我使用等宽字体:set。

集合是一组唯一对象。一个基本用例是去除重复项:

>>>l=['spam','spam','eggs','spam','bacon','eggs']>>>set(l){'eggs','spam','bacon'}>>>list(set(l))['eggs','spam','bacon']提示如果你想去除重复项但又保留每个项目的第一次出现的顺序,你现在可以使用一个普通的dict来实现,就像这样:

>>>dict.fromkeys(l).keys()dict_keys(['spam','eggs','bacon'])>>>list(dict.fromkeys(l).keys())['spam','eggs','bacon']集合元素必须是可散列的。set类型不可散列,因此你不能用嵌套的set实例构建一个set。但是frozenset是可散列的,所以你可以在set中包含frozenset元素。

例如,想象一下你有一个大型的电子邮件地址集合(haystack)和一个较小的地址集合(needles),你需要计算needles在haystack中出现的次数。由于集合交集(&运算符),你可以用一行代码实现这个功能(参见示例3-12)。

found=len(needles&haystack)没有交集运算符,你将不得不编写示例3-13来完成与示例3-12相同的任务。

found=0forninneedles:ifninhaystack:found+=1示例3-12比示例3-13运行速度稍快。另一方面,示例3-13适用于任何可迭代对象needles和haystack,而示例3-12要求两者都是集合。但是,如果你手头没有集合,你可以随时动态构建它们,就像示例3-14中所示。

found=len(set(needles)&set(haystack))#anotherway:found=len(set(needles).intersection(haystack))当然,在构建示例3-14中的集合时会有额外的成本,但如果needles或haystack中的一个已经是一个集合,那么示例3-14中的替代方案可能比示例3-13更便宜。

任何前述示例中的一个都能在haystack中搜索1,000个元素,其中包含10,000,000个项目,大约需要0.3毫秒,即每个元素接近0.3微秒。

除了极快的成员测试(由底层哈希表支持),set和frozenset内置类型提供了丰富的API来创建新集合或在set的情况下更改现有集合。我们将很快讨论这些操作,但首先让我们谈谈语法。

set字面量的语法—{1},{1,2}等—看起来与数学符号一样,但有一个重要的例外:没有空set的字面表示,因此我们必须记得写set()。

不要忘记,要创建一个空的set,应该使用没有参数的构造函数:set()。如果写{},你将创建一个空的dict—在Python3中这一点没有改变。

在Python3中,集合的标准字符串表示总是使用{…}符号,除了空集:

>>>s={1}>>>type(s)>>>s{1}>>>s.pop()1>>>sset()字面set语法如{1,2,3}比调用构造函数(例如,set([1,2,3]))更快且更易读。后一种形式较慢,因为要评估它,Python必须查找set名称以获取构造函数,然后构建一个列表,最后将其传递给构造函数。相比之下,要处理像{1,2,3}这样的字面量,Python运行一个专门的BUILD_SET字节码。1

没有特殊的语法来表示frozenset字面量—它们必须通过调用构造函数创建。在Python3中的标准字符串表示看起来像一个frozenset构造函数调用。请注意控制台会话中的输出:

>>>frozenset(range(10))frozenset({0,1,2,3,4,5,6,7,8,9})谈到语法,列表推导的想法也被用来构建集合。

集合推导式(setcomps)在Python2.7中添加,与我们在“dict推导式”中看到的dictcomps一起。示例3-15展示了如何。

>>>fromunicodedataimportname#①>>>{chr(i)foriinrange(32,256)if'SIGN'inname(chr(i),'')}#②{'§','=','¢','#','¤','<','¥','μ','×','$','','£','','°','+','÷','±','>','','','%'}①

从unicodedata导入name函数以获取字符名称。

构建字符集,其中字符代码从32到255,名称中包含'SIGN'一词。

输出的顺序会因为“什么是可哈希的”中提到的盐哈希而对每个Python进程进行更改。

语法问题在一边,现在让我们考虑集合的行为。

set和frozenset类型都是使用哈希表实现的。这会产生以下影响:

现在让我们来看看集合提供的丰富操作。

|||s.difference(it,…)|s和从可迭代对象it构建的所有集合的差集|

|数学符号|Python运算符|方法|描述|

提示

|||||

|表3-3列出了集合谓词:返回True或False的运算符和方法。|

除了从数学集合理论中派生的运算符和方法外,集合类型还实现了其他实用的方法,总结在表3-4中。

表3-4.额外的集合方法

这完成了我们对集合特性的概述。如“字典视图”中承诺的,我们现在将看到两种字典视图类型的行为非常类似于frozenset。

表3-5显示了由dict方法.keys()和.items()返回的视图对象与frozenset非常相似。

表3-5.frozenset、dict_keys和dict_items实现的方法

特别地,dict_keys和dict_items实现了支持强大的集合运算符&(交集)、|(并集)、-(差集)和^(对称差集)的特殊方法。

例如,使用&很容易获得出现在两个字典中的键:

>>>d1=dict(a=1,b=2,c=3,d=4)>>>d2=dict(b=20,d=40,e=50)>>>d1.keys()&d2.keys(){'b','d'}请注意&的返回值是一个set。更好的是:字典视图中的集合运算符与set实例兼容。看看这个:

>>>s={'a','e','i'}>>>d1.keys()&s{'a'}>>>d1.keys()|s{'a','c','b','d','i','e'}警告一个dict_items视图仅在字典中的所有值都是可哈希的情况下才能作为集合使用。尝试在具有不可哈希值的dict_items视图上进行集合操作会引发TypeError:unhashabletype'T',其中T是有问题值的类型。

另一方面,dict_keys视图始终可以用作集合,因为每个键都是可哈希的—按定义。

使用视图和集合运算符将节省大量循环和条件语句,当检查代码中字典内容时,让Python在C中高效实现为您工作!

就这样,我们可以结束这一章了。

字典是Python的基石。多年来,熟悉的{k1:v1,k2:v2}文字语法得到了增强,支持使用**、模式匹配以及dict推导式。

除了基本的dict,标准库还提供了方便、即用即用的专用映射,如defaultdict、ChainMap和Counter,都定义在collections模块中。随着新的dict实现,OrderedDict不再像以前那样有用,但应该保留在标准库中以保持向后兼容性,并具有dict没有的特定特性,例如在==比较中考虑键的顺序。collections模块中还有UserDict,一个易于使用的基类,用于创建自定义映射。

大多数映射中可用的两个强大方法是setdefault和update。setdefault方法可以更新持有可变值的项目,例如在list值的dict中,避免为相同键进行第二次搜索。update方法允许从任何其他映射、提供(key,value)对的可迭代对象以及关键字参数进行批量插入或覆盖项目。映射构造函数也在内部使用update,允许实例从映射、可迭代对象或关键字参数初始化。自Python3.9起,我们还可以使用|=运算符更新映射,使用|运算符从两个映射的并集创建一个新映射。

映射API中一个巧妙的钩子是__missing__方法,它允许你自定义当使用d[k]语法(调用__getitem__)时找不到键时发生的情况。

collections.abc模块提供了Mapping和MutableMapping抽象基类作为标准接口,对于运行时类型检查非常有用。types模块中的MappingProxyType创建了一个不可变的外观,用于保护不希望意外更改的映射。还有用于Set和MutableSet的抽象基类。

字典视图是Python3中的一个重要补充,消除了Python2中.keys()、.values()和.items()方法造成的内存开销,这些方法构建了重复数据的列表,复制了目标dict实例中的数据。此外,dict_keys和dict_items类支持frozenset的最有用的运算符和方法。

这是将方法作为一等函数使用的示例,是第七章的主题。

关于子类化dict和其他内置类型的确切问题在“子类化内置类型是棘手的”中有所涵盖。

这就是元组的存储方式。

除非类有一个__slots__属性,如“使用slots节省内存”中所解释的那样。

1这可能很有趣,但并不是非常重要。加速只会在评估集合字面值时发生,而这最多只会发生一次Python进程—当模块最初编译时。如果你好奇,可以从dis模块中导入dis函数,并使用它来反汇编set字面值的字节码—例如,dis('{1}')—和set调用—dis('set([1])')。

人类使用文本。计算机使用字节。

EstherNam和TravisFischer,“Python中的字符编码和Unicode”1

Python3引入了人类文本字符串和原始字节序列之间的明显区别。将字节序列隐式转换为Unicode文本已经成为过去。本章涉及Unicode字符串、二进制序列以及用于在它们之间转换的编码。

根据您在Python中的工作类型,您可能认为理解Unicode并不重要。这不太可能,但无论如何,无法避免str与byte之间的分歧。作为奖励,您会发现专门的二进制序列类型提供了Python2通用str类型没有的功能。

在本章中,我们将讨论以下主题:

Python3中对Unicode的支持是全面且稳定的,因此最值得注意的新增内容是“按名称查找字符”,描述了一种用于搜索Unicode数据库的实用程序——这是从命令行查找带圈数字和微笑猫的好方法。

值得一提的一项较小更改是关于Windows上的Unicode支持,自Python3.6以来更好且更简单,我们将在“注意编码默认值”中看到。

让我们从不那么新颖但基础的概念开始,即字符、代码点和字节。

“字符串”的概念足够简单:字符串是字符序列。问题在于“字符”的定义。

在2021年,我们对“字符”的最佳定义是Unicode字符。因此,我们从Python3的str中获取的项目是Unicode字符,就像在Python2中的unicode对象中获取的项目一样——而不是从Python2的str中获取的原始字节。

Unicode标准明确将字符的身份与特定字节表示分开:

从代码点转换为字节是编码;从字节转换为代码点是解码。参见示例4-1。

>>>s='café'>>>len(s)#①4>>>b=s.encode('utf8')#②>>>bb'caf\xc3\xa9'#③>>>len(b)#④5>>>b.decode('utf8')#⑤'café'①

str'café'有四个Unicode字符。

使用UTF-8编码将str编码为bytes。

bytes字面量有一个b前缀。

bytesb有五个字节(“é”的代码点在UTF-8中编码为两个字节)。

使用UTF-8编码将bytes解码为str。

如果你需要一个记忆辅助来帮助区分.decode()和.encode(),说服自己字节序列可以是晦涩的机器核心转储,而Unicodestr对象是“人类”文本。因此,将bytes解码为str以获取可读文本是有意义的,而将str编码为bytes用于存储或传输也是有意义的。

新的二进制序列类型在许多方面与Python2的str不同。首先要知道的是,有两种基本的内置二进制序列类型:Python3中引入的不可变bytes类型和早在Python2.6中添加的可变bytearray。2Python文档有时使用通用术语“字节字符串”来指代bytes和bytearray。我避免使用这个令人困惑的术语。

bytes或bytearray中的每个项都是从0到255的整数,而不是像Python2的str中的单个字符字符串。然而,二进制序列的切片始终产生相同类型的二进制序列,包括长度为1的切片。参见示例4-2。

>>>cafe=bytes('café',encoding='utf_8')#①>>>cafeb'caf\xc3\xa9'>>>cafe[0]#②99>>>cafe[:1]#③b'c'>>>cafe_arr=bytearray(cafe)>>>cafe_arr#④bytearray(b'caf\xc3\xa9')>>>cafe_arr[-1:]#⑤bytearray(b'\xa9')①

可以从str构建bytes,并给定一个编码。

每个项都是range(256)中的整数。

bytes的切片也是bytes——即使是单个字节的切片。

bytearray没有字面量语法:它们显示为带有bytes字面量作为参数的bytearray()。

bytearray的切片也是bytearray。

my_bytes[0]检索一个int,但my_bytes[:1]返回长度为1的bytes序列,这只是因为我们习惯于Python的str类型,其中s[0]==s[:1]。对于Python中的所有其他序列类型,1项不等于长度为1的切片。

尽管二进制序列实际上是整数序列,但它们的字面值表示反映了ASCII文本经常嵌入其中的事实。因此,根据每个字节值的不同,使用四种不同的显示方式:

这就是为什么在示例4-2中你会看到b'caf\xc3\xa9':前三个字节b'caf'在可打印的ASCII范围内,而最后两个不在范围内。

bytes和bytearray都支持除了依赖于Unicode数据的格式化方法(format,format_map)和那些依赖于Unicode数据的方法(包括casefold,isdecimal,isidentifier,isnumeric,isprintable和encode)之外的所有str方法。这意味着您可以使用熟悉的字符串方法,如endswith,replace,strip,translate,upper等,与二进制序列一起使用——只使用bytes而不是str参数。此外,如果正则表达式是从二进制序列而不是str编译而成,则re模块中的正则表达式函数也适用于二进制序列。自Python3.5以来,%运算符再次适用于二进制序列。

二进制序列有一个str没有的类方法,称为fromhex,它通过解析以空格分隔的十六进制数字对构建二进制序列:

>>>bytes.fromhex('314BCEA9')b'1K\xce\xa9'构建bytes或bytearray实例的其他方法是使用它们的构造函数,并提供:

从类似缓冲区的对象构建二进制序列是一个涉及类型转换的低级操作。在示例4-3中看到演示。

>>>importarray>>>numbers=array.array('h',[-2,-1,0,1,2])#①>>>octets=bytes(numbers)#②>>>octetsb'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00'#③①

类型码'h'创建一个短整数(16位)的array。

octets保存构成numbers的字节的副本。

这是代表5个短整数的10个字节。

从任何类似缓冲区的源创建bytes或bytearray对象将始终复制字节。相反,memoryview对象允许您在二进制数据结构之间共享内存,正如我们在“内存视图”中看到的那样。

在这对Python中二进制序列类型的基本探索之后,让我们看看它们如何转换为/从字符串。

Python发行版捆绑了100多个编解码器(编码器/解码器),用于文本到字节的转换以及反之。每个编解码器都有一个名称,如'utf_8',通常还有别名,如'utf8','utf-8'和'U8',您可以将其用作函数中的encoding参数,如open(),str.encode(),bytes.decode()等。示例4-4展示了相同文本编码为三种不同的字节序列。

>>>forcodecin['latin_1','utf_8','utf_16']:...print(codec,'ElNio'.encode(codec),sep='\t')...latin_1b'ElNi\xf1o'utf_8b'ElNi\xc3\xb1o'utf_16b'\xff\xfeE\x00l\x00\x00N\x00i\x00\xf1\x00o\x00'图4-1展示了各种编解码器从字符(如字母“A”到G大调音符)生成字节的情况。请注意,最后三种编码是可变长度的多字节编码。

图4-1中所有那些星号清楚地表明,一些编码,如ASCII甚至多字节GB2312,无法表示每个Unicode字符。然而,UTF编码被设计用于处理每个Unicode代码点。

在图4-1中显示的编码被选为代表性样本:

latin1又称iso8859_1

重要,因为它是其他编码的基础,例如cp1252和Unicode本身(注意latin1字节值如何出现在cp1252字节和代码点中)。

cp1252

由Microsoft创建的有用的latin1超集,添加了诸如弯引号和€(欧元)等有用符号;一些 Windows应用程序称其为“ANSI”,但它从未是真正的ANSI标准。

cp437

IBMPC的原始字符集,带有绘制框线字符。与latin1不兼容,后者出现得更晚。

gb2312

用于编码中国大陆使用的简体中文汉字的传统标准;亚洲语言的几种广泛部署的多字节编码之一。

utf-8

utf-16le

UTF16位编码方案的一种形式;所有UTF-16编码通过称为“代理对”的转义序列支持U+FFFF之上的代码点。

UTF-16在1996年取代了原始的16位Unicode1.0编码——UCS-2。尽管UCS-2自上个世纪以来已被弃用,但仍在许多系统中使用,因为它仅支持到U+FFFF的代码点。截至2021年,超过57%的分配代码点在U+FFFF以上,包括所有重要的表情符号。

现在完成了对常见编码的概述,我们将转向处理编码和解码操作中的问题。

尽管存在一个通用的UnicodeError异常,Python报告的错误通常更具体:要么是UnicodeEncodeError(将str转换为二进制序列时),要么是UnicodeDecodeError(将二进制序列读入str时)。加载Python模块时,如果源编码意外,则还可能引发SyntaxError。我们将在接下来的部分展示如何处理所有这些错误。

当遇到Unicode错误时,首先要注意异常的确切类型。它是UnicodeEncodeError、UnicodeDecodeError,还是提到编码问题的其他错误(例如SyntaxError)?要解决问题,首先必须理解它。

大多数非UTF编解码器仅处理Unicode字符的一小部分。将文本转换为字节时,如果目标编码中未定义字符,则会引发UnicodeEncodeError,除非通过向编码方法或函数传递errors参数提供了特殊处理。错误处理程序的行为显示在示例4-5中。

>>>city='SoPaulo'>>>city.encode('utf_8')#①b'S\xc3\xa3oPaulo'>>>city.encode('utf_16')b'\xff\xfeS\x00\xe3\x00o\x00\x00P\x00a\x00u\x00l\x00o\x00'>>>city.encode('iso8859_1')#②b'S\xe3oPaulo'>>>city.encode('cp437')#③Traceback(mostrecentcalllast):File"",line1,inFile"/.../lib/python3.4/encodings/cp437.py",line12,inencodereturncodecs.charmap_encode(input,errors,encoding_map)UnicodeEncodeError:'charmap'codeccan'tencodecharacter'\xe3'inposition1:charactermapsto>>>city.encode('cp437',errors='ignore')#④b'SoPaulo'>>>city.encode('cp437',errors='replace')#⑤b'SoPaulo'>>>city.encode('cp437',errors='xmlcharrefreplace')#⑥b'SãoPaulo'①

UTF编码处理任何str。

iso8859_1也适用于'SoPaulo'字符串。

cp437无法编码''(带有波浪符号的“a”)。默认错误处理程序'strict'会引发UnicodeEncodeError。

error='ignore'处理程序跳过无法编码的字符;这通常是一个非常糟糕的主意,会导致数据悄悄丢失。

在编码时,error='replace'用''替换无法编码的字符;数据也会丢失,但用户会得到提示有问题的线索。

'xmlcharrefreplace'用XML实体替换无法编码的字符。如果不能使用UTF,也不能承受数据丢失,这是唯一的选择。

并非每个字节都包含有效的ASCII字符,并非每个字节序列都是有效的UTF-8或UTF-16;因此,当您在将二进制序列转换为文本时假定其中一个编码时,如果发现意外字节,则会收到UnicodeDecodeError。

另一方面,许多传统的8位编码,如'cp1252'、'iso8859_1'和'koi8_r',能够解码任何字节流,包括随机噪音,而不报告错误。因此,如果您的程序假定了错误的8位编码,它将悄悄地解码垃圾数据。

乱码字符被称为gremlins或mojibake(文字化け—日语中的“转换文本”)。

Example4-6说明了使用错误的编解码器可能会产生乱码或UnicodeDecodeError。

>>>octets=b'Montr\xe9al'#①>>>octets.decode('cp1252')#②'Montréal'>>>octets.decode('iso8859_7')#③'Montrιal'>>>octets.decode('koi8_r')#④'MontrИal'>>>octets.decode('utf_8')#⑤Traceback(mostrecentcalllast):File"",line1,inUnicodeDecodeError:'utf-8'codeccan'tdecodebyte0xe9inposition5:invalidcontinuationbyte>>>octets.decode('utf_8',errors='replace')#⑥'Montral'①

编码为latin1的单词“Montréal”;'\xe9'是“é”的字节。

使用Windows1252解码有效,因为它是latin1的超集。

ISO-8859-7用于希腊语,因此'\xe9'字节被错误解释,不会发出错误。

KOI8-R用于俄语。现在'\xe9'代表西里尔字母“И”。

'utf_8'编解码器检测到octets不是有效的UTF-8,并引发UnicodeDecodeError。

使用'replace'错误处理,\xe9会被“”(代码点U+FFFD)替换,这是官方的UnicodeREPLACEMENTCHARACTER,用于表示未知字符。

要解决这个问题,在文件顶部添加一个魔术coding注释,如Example4-7所示。

#coding:cp1252print('Olá,Mundo!')提示现在Python3源代码不再限于ASCII,并且默认使用优秀的UTF-8编码,因此对于像'cp1252'这样的遗留编码的源代码,最好的“修复”方法是将它们转换为UTF-8,并且不再使用coding注释。如果您的编辑器不支持UTF-8,那么是时候换一个了。

假设您有一个文本文件,无论是源代码还是诗歌,但您不知道其编码。如何检测实际的编码?答案在下一节中。

如何找到字节序列的编码?简短回答:你无法。你必须被告知。

一些通信协议和文件格式,比如HTTP和XML,包含明确告诉我们内容如何编码的头部。你可以确定一些字节流不是ASCII,因为它们包含超过127的字节值,而UTF-8和UTF-16的构建方式也限制了可能的字节序列。

然而,考虑到人类语言也有其规则和限制,一旦假定一系列字节是人类纯文本,可能可以通过启发式和统计方法来嗅探其编码。例如,如果b'\x00'字节很常见,那么它可能是16位或32位编码,而不是8位方案,因为纯文本中的空字符是错误的。当字节序列b'\x20\x00'经常出现时,更可能是UTF-16LE编码中的空格字符(U+0020),而不是晦涩的U+2000ENQUAD字符—不管那是什么。

$chardetect04-text-byte.asciidoc04-text-byte.asciidoc:utf-8withconfidence0.99尽管编码文本的二进制序列通常不包含其编码的明确提示,但UTF格式可能在文本内容前面添加字节顺序标记。接下来将对此进行解释。

在示例4-4中,你可能已经注意到UTF-16编码序列开头有一对额外的字节。这里再次展示:

>>>u16='ElNio'.encode('utf_16')>>>u16b'\xff\xfeE\x00l\x00\x00N\x00i\x00\xf1\x00o\x00'这些字节是b'\xff\xfe'。这是一个BOM—字节顺序标记—表示进行编码的IntelCPU的“小端”字节顺序。

在小端机器上,对于每个代码点,最低有效字节先出现:字母'E',代码点U+0045(十进制69),在字节偏移2和3中编码为69和0:

>>>list(u16)[255,254,69,0,108,0,32,0,78,0,105,0,241,0,111,0]在大端CPU上,编码会被颠倒;'E'会被编码为0和69。

为了避免混淆,UTF-16编码在要编码的文本前面加上特殊的不可见字符零宽不换行空格(U+FEFF)。在小端系统上,它被编码为b'\xff\xfe'(十进制255,254)。因为按设计,Unicode中没有U+FFFE字符,字节序列b'\xff\xfe'必须表示小端编码中的零宽不换行空格,所以编解码器知道要使用哪种字节顺序。

有一种UTF-16的变体——UTF-16LE,明确是小端的,另一种是明确是大端的,UTF-16BE。如果使用它们,就不会生成BOM:

>>>u16le='ElNio'.encode('utf_16le')>>>list(u16le)[69,0,108,0,32,0,78,0,105,0,241,0,111,0]>>>u16be='ElNio'.encode('utf_16be')>>>list(u16be)[0,69,0,108,0,32,0,78,0,105,0,241,0,111]如果存在BOM,应该由UTF-16编解码器过滤,这样你只会得到文件的实际文本内容,而不包括前导的零宽不换行空格。Unicode标准规定,如果一个文件是UTF-16且没有BOM,应该假定为UTF-16BE(大端)。然而,Intelx86架构是小端的,因此在实际中有很多没有BOM的小端UTF-16。

这整个字节序问题只影响使用多字节的编码,比如UTF-16和UTF-32。UTF-8的一个重要优势是,无论机器的字节序如何,它都会产生相同的字节序列,因此不需要BOM。然而,一些Windows应用程序(特别是记事本)仍然会向UTF-8文件添加BOM—Excel依赖BOM来检测UTF-8文件,否则它会假定内容是用Windows代码页编码的。Python的编解码器注册表中称带有BOM的UTF-8编码为UTF-8-SIG。UTF-8-SIG中编码的字符U+FEFF是三字节序列b'\xef\xbb\xbf'。因此,如果一个文件以这三个字节开头,很可能是带有BOM的UTF-8文件。

现在我们转向在Python3中处理文本文件。

处理文本I/O的最佳实践是“Unicode三明治”(图4-2)。这意味着bytes应尽早解码为str(例如,在打开文件进行读取时)。三明治的“馅料”是程序的业务逻辑,在这里文本处理完全在str对象上进行。您永远不应该在其他处理过程中进行编码或解码。在输出时,str应尽可能晚地编码为bytes。大多数Web框架都是这样工作的,当使用它们时我们很少接触bytes。例如,在Django中,您的视图应输出Unicodestr;Django本身负责将响应编码为bytes,默认使用UTF-8。

Python3更容易遵循Unicode三明治的建议,因为内置的open()在读取和写入文本模式文件时进行必要的解码和编码,因此从my_file.read()获取的内容并传递给my_file.write(text)的都是str对象。

因此,使用文本文件似乎很简单。但是,如果依赖默认编码,您将受到影响。

考虑示例4-8中的控制台会话。您能发现bug吗?

>>>open('cafe.txt','w',encoding='utf_8').write('café')4>>>open('cafe.txt').read()'caf'Bug:我在写入文件时指定了UTF-8编码,但在读取文件时未这样做,因此Python假定Windows默认文件编码为代码页1252,并且文件中的尾随字节被解码为字符''而不是'é'。

我在Windows10(版本18363)上运行了Python3.8.164位上的示例4-8。在最近的GNU/Linux或macOS上运行相同的语句完全正常,因为它们的默认编码是UTF-8,给人一种一切正常的假象。如果在打开文件进行写入时省略了编码参数,将使用区域设置的默认编码,我们将使用相同的编码正确读取文件。但是,这个脚本将根据平台或甚至相同平台中的区域设置生成具有不同字节内容的文件,从而创建兼容性问题。

必须在多台机器上运行或在多个场合上运行的代码绝不能依赖于编码默认值。在打开文本文件时始终传递显式的encoding=参数,因为默认值可能会从一台机器变为另一台机器,或者从一天变为另一天。

示例4-8中一个有趣的细节是,第一条语句中的write函数报告写入了四个字符,但在下一行读取了五个字符。示例4-9是示例4-8的扩展版本,解释了这个问题和其他细节。

>>>fp=open('cafe.txt','w',encoding='utf_8')>>>fp#①<_io.TextIOWrappername='cafe.txt'mode='w'encoding='utf_8'>>>>fp.write('café')#②4>>>fp.close()>>>importos>>>os.stat('cafe.txt').st_size#③5>>>fp2=open('cafe.txt')>>>fp2#④<_io.TextIOWrappername='cafe.txt'mode='r'encoding='cp1252'>>>>fp2.encoding#⑤'cp1252'>>>fp2.read()#⑥'caf'>>>fp3=open('cafe.txt',encoding='utf_8')#⑦>>>fp3<_io.TextIOWrappername='cafe.txt'mode='r'encoding='utf_8'>>>>fp3.read()#⑧'café'>>>fp4=open('cafe.txt','rb')#⑨>>>fp4#⑩<_io.BufferedReadername='cafe.txt'>>>>fp4.read()b'caf\xc3\xa9'①

默认情况下,open使用文本模式并返回一个具有特定编码的TextIOWrapper对象。

TextIOWrapper上的write方法返回写入的Unicode字符数。

os.stat显示文件有5个字节;UTF-8将'é'编码为2个字节,0xc3和0xa9。

打开一个没有明确编码的文本文件会返回一个TextIOWrapper,其编码设置为来自区域设置的默认值。

TextIOWrapper对象有一个编码属性,可以进行检查:在这种情况下是cp1252。

使用正确的编码打开相同的文件。

预期结果:对于'café'相同的四个Unicode字符。

'rb'标志以二进制模式打开文件进行读取。

返回的对象是BufferedReader而不是TextIOWrapper。

读取返回的是字节,符合预期。

除非需要分析文件内容以确定编码,否则不要以二进制模式打开文本文件——即使这样,你应该使用Chardet而不是重复造轮子(参见“如何发现字节序列的编码”)。普通代码应该只使用二进制模式打开二进制文件,如光栅图像。

几个设置影响Python中I/O的编码默认值。查看Example4-10中的default_encodings.py脚本。

importlocaleimportsysexpressions="""locale.getpreferredencoding()type(my_file)my_file.encodingsys.stdout.isatty()sys.stdout.encodingsys.stdin.isatty()sys.stdin.encodingsys.stderr.isatty()sys.stderr.encodingsys.getdefaultencoding()sys.getfilesystemencoding()"""my_file=open('dummy','w')forexpressioninexpressions.split():value=eval(expression)print(f'{expression:>30}->{value!r}')Example4-10在GNU/Linux(Ubuntu14.04至19.10)和macOS(10.9至10.14)上的输出是相同的,显示UTF-8在这些系统中随处可用:

$python3default_encodings.pylocale.getpreferredencoding()->'UTF-8'type(my_file)->my_file.encoding->'UTF-8'sys.stdout.isatty()->Truesys.stdout.encoding->'utf-8'sys.stdin.isatty()->Truesys.stdin.encoding->'utf-8'sys.stderr.isatty()->Truesys.stderr.encoding->'utf-8'sys.getdefaultencoding()->'utf-8'sys.getfilesystemencoding()->'utf-8'然而,在Windows上,输出是Example4-11。

>chcp#①Activecodepage:437>pythondefault_encodings.py#②locale.getpreferredencoding()->'cp1252'#③type(my_file)->my_file.encoding->'cp1252'#④sys.stdout.isatty()->True#⑤sys.stdout.encoding->'utf-8'#⑥sys.stdin.isatty()->Truesys.stdin.encoding->'utf-8'sys.stderr.isatty()->Truesys.stderr.encoding->'utf-8'sys.getdefaultencoding()->'utf-8'sys.getfilesystemencoding()->'utf-8'①

chcp显示控制台的活动代码页为437。

运行default_encodings.py并输出到控制台。

locale.getpreferredencoding()是最重要的设置。

文本文件默认使用locale.getpreferredencoding()。

输出将发送到控制台,因此sys.stdout.isatty()为True。

现在,sys.stdout.encoding与chcp报告的控制台代码页不同!

然而,如果示例4-10的输出被重定向到文件,就像这样:

Z:\>pythondefault_encodings.py>encodings.log然后,sys.stdout.isatty()的值变为False,sys.stdout.encoding由locale.getpreferredencoding()设置,在该机器上为'cp1252'—但sys.stdin.encoding和sys.stderr.encoding仍然为utf-8。

在示例4-12中,我使用'\N{}'转义来表示Unicode文字,其中我们在\N{}内写入字符的官方名称。这样做相当冗长,但明确且安全:如果名称不存在,Python会引发SyntaxError——比起写一个可能错误的十六进制数,这样做要好得多,但你只能在很久以后才会发现。你可能想要写一个解释字符代码的注释,所以\N{}的冗长是容易接受的。

这意味着像示例4-12这样的脚本在打印到控制台时可以正常工作,但在输出被重定向到文件时可能会出现问题。

importsysfromunicodedataimportnameprint(sys.version)print()print('sys.stdout.isatty():',sys.stdout.isatty())print('sys.stdout.encoding:',sys.stdout.encoding)print()test_chars=['\N{HORIZONTALELLIPSIS}',#existsincp1252,notincp437'\N{INFINITY}',#existsincp437,notincp1252'\N{CIRCLEDNUMBERFORTYTWO}',#notincp437orincp1252]forcharintest_chars:print(f'Tryingtooutput{name(char)}:')print(char)示例4-12显示了sys.stdout.isatty()的结果,sys.stdout.encoding的值,以及这三个字符:

当我在PowerShell或cmd.exe上运行stdout_check.py时,它的运行情况如图4-3所示。

尽管chcp报告活动代码为437,但sys.stdout.encoding为UTF-8,因此HORIZONTALELLIPSIS和INFINITY都能正确输出。CIRCLEDNUMBERFORTYTWO被一个矩形替换,但不会引发错误。可能它被识别为有效字符,但控制台字体没有显示它的字形。

然而,当我将stdout_check.py的输出重定向到文件时,我得到了图4-4。

图4-4展示的第一个问题是UnicodeEncodeError,提到字符'\u221e',因为sys.stdout.encoding是'cp1252'—一个不包含INFINITY字符的代码页。

使用type命令读取out.txt,或者使用Windows编辑器如VSCode或SublimeText,显示的不是水平省略号,而是'à'(带重音的拉丁小写字母A)。事实证明,在CP1252中,字节值0x85表示'…',但在CP437中,相同的字节值代表'à'。因此,似乎活动代码页确实很重要,但并不是以明智或有用的方式,而是作为糟糕的Unicode经历的部分解释。

我使用配置为美国市场的笔记本电脑,运行Windows10OEM来运行这些实验。为其他国家本地化的Windows版本可能具有不同的编码配置。例如,在巴西,Windows控制台默认使用代码页850,而不是437。

为了总结这个令人疯狂的默认编码问题,让我们最后看一下示例4-11中的不同编码:

在GNU/Linux和macOS上,默认情况下,所有这些编码都设置为UTF-8,已经有好几年了,因此I/O处理所有Unicode字符。在Windows上,不仅在同一系统中使用不同的编码,而且通常是像'cp850'或'cp1252'这样只支持ASCII的代码页,还有127个额外字符,这些字符在不同编码之间并不相同。因此,Windows用户更有可能遇到编码错误,除非他们特别小心。

locale.getpreferredencoding(do_setlocale=True)

根据用户偏好返回用于文本数据的编码。用户偏好在不同系统上表达方式不同,有些系统可能无法以编程方式获取,因此此函数只返回一个猜测。[…]

因此,关于编码默认值的最佳建议是:不要依赖于它们。

如果您遵循Unicode三明治的建议并始终明确指定程序中的编码,您将避免很多痛苦。不幸的是,即使您将您的bytes正确转换为str,Unicode也是令人头痛的。接下来的两节涵盖了在ASCII领域简单的主题,在Unicode行星上变得非常复杂的文本规范化(即将文本转换为用于比较的统一表示)和排序。

字符串比较变得复杂的原因在于Unicode具有组合字符:附加到前一个字符的变音符号和其他标记,在打印时会显示为一个字符。

例如,单词“café”可以用四个或五个代码点组成,但结果看起来完全相同:

>>>s1='café'>>>s2='cafe\N{COMBININGACUTEACCENT}'>>>s1,s2('café','café')>>>len(s1),len(s2)(4,5)>>>s1==s2False在“e”后面放置COMBININGACUTEACCENT(U+0301)会呈现“é”。在Unicode标准中,像'é'和'e\u0301'这样的序列被称为“规范等价物”,应用程序应将它们视为相同。但是Python看到两个不同的代码点序列,并认为它们不相等。

解决方案是unicodedata.normalize()。该函数的第一个参数是四个字符串之一:'NFC','NFD','NFKC'和'NFKD'。让我们从前两个开始。

规范化形式C(NFC)将代码点组合以生成最短等效字符串,而NFD将分解,将组合字符扩展为基本字符和单独的组合字符。这两种规范化使比较按预期工作,如下一个示例所示:

一些单个字符被NFC规范化为另一个单个字符。电阻单位欧姆(Ω)的符号被规范化为希腊大写omega。它们在视觉上是相同的,但它们比较不相等,因此规范化是必不可少的,以避免意外:

>>>fromunicodedataimportnormalize,name>>>ohm='\u2126'>>>name(ohm)'OHMSIGN'>>>ohm_c=normalize('NFC',ohm)>>>name(ohm_c)'GREEKCAPITALLETTEROMEGA'>>>ohm==ohm_cFalse>>>normalize('NFC',ohm)==normalize('NFC',ohm_c)True另外两种规范化形式是NFKC和NFKD,其中字母K代表“兼容性”。这些是更强的规范化形式,影响所谓的“兼容性字符”。尽管Unicode的一个目标是为每个字符有一个单一的“规范”代码点,但一些字符出现多次是为了与现有标准兼容。例如,MICROSIGN,μ(U+00B5),被添加到Unicode以支持与包括它在内的latin1的往返转换,即使相同的字符是希腊字母表的一部分,具有代码点U+03BC(GREEKSMALLLETTERMU)。因此,微符号被视为“兼容性字符”。

在NFKC和NFKD形式中,每个兼容字符都被一个或多个字符的“兼容分解”替换,这些字符被认为是“首选”表示,即使存在一些格式损失——理想情况下,格式应该由外部标记负责,而不是Unicode的一部分。举例来说,一个半分数''(U+00BD)的兼容分解是三个字符的序列'1/2',而微符号'μ'(U+00B5)的兼容分解是小写的希腊字母mu'μ'(U+03BC)。

下面是NFKC在实践中的工作方式:

>>>fromunicodedataimportnormalize,name>>>half='\N{VULGARFRACTIONONEHALF}'>>>print(half)>>>normalize('NFKC',half)'12'>>>forcharinnormalize('NFKC',half):...print(char,name(char),sep='\t')...1 DIGITONE FRACTIONSLASH2 DIGITTWO>>>four_squared='42'>>>normalize('NFKC',four_squared)'42'>>>micro='μ'>>>micro_kc=normalize('NFKC',micro)>>>micro,micro_kc('μ','μ')>>>ord(micro),ord(micro_kc)(181,956)>>>name(micro),name(micro_kc)('MICROSIGN','GREEKSMALLLETTERMU')尽管'12'是''的一个合理替代品,而微符号实际上是一个小写希腊字母mu,但将'42'转换为'42'会改变含义。一个应用程序可以将'42'存储为'42',但normalize函数对格式一无所知。因此,NFKC或NFKD可能会丢失或扭曲信息,但它们可以生成方便的中间表示形式用于搜索和索引。

不幸的是,对于Unicode来说,一切总是比起初看起来更加复杂。对于VULGARFRACTIONONEHALF,NFKC规范化产生了用FRACTIONSLASH连接的1和2,而不是SOLIDUS,即“斜杠”—ASCII代码十进制47的熟悉字符。因此,搜索三字符ASCII序列'1/2'将找不到规范化的Unicode序列。

NFKC和NFKD规范会导致数据丢失,应仅在特殊情况下如搜索和索引中应用,而不是用于文本的永久存储。

当准备文本进行搜索或索引时,另一个有用的操作是大小写折叠,我们的下一个主题。

大小写折叠基本上是将所有文本转换为小写,还有一些额外的转换。它由str.casefold()方法支持。

对于只包含latin1字符的任何字符串s,s.casefold()产生与s.lower()相同的结果,只有两个例外——微符号'μ'被更改为希腊小写mu(在大多数字体中看起来相同),德语Eszett或“sharps”()变为“ss”:

>>>micro='μ'>>>name(micro)'MICROSIGN'>>>micro_cf=micro.casefold()>>>name(micro_cf)'GREEKSMALLLETTERMU'>>>micro,micro_cf('μ','μ')>>>eszett=''>>>name(eszett)'LATINSMALLLETTERSHARPS'>>>eszett_cf=eszett.casefold()>>>eszett,eszett_cf('','ss')有将近300个代码点,str.casefold()和str.lower()返回不同的结果。

在接下来的几节中,我们将利用我们的规范化知识开发实用函数。

正如我们所见,NFC和NFD是安全的,并允许在Unicode字符串之间进行明智的比较。对于大多数应用程序,NFC是最佳的规范化形式。str.casefold()是进行不区分大小写比较的方法。

如果您使用多种语言的文本,像示例4-13中的nfc_equal和fold_equal这样的一对函数对您的工具箱是有用的补充。

"""UtilityfunctionsfornormalizedUnicodestringcomparison.UsingNormalFormC,casesensitive:>>>s1='café'>>>s2='cafe\u0301'>>>s1==s2False>>>nfc_equal(s1,s2)True>>>nfc_equal('A','a')FalseUsingNormalFormCwithcasefolding:>>>s3='Strae'>>>s4='strasse'>>>s3==s4False>>>nfc_equal(s3,s4)False>>>fold_equal(s3,s4)True>>>fold_equal(s1,s2)True>>>fold_equal('A','a')True"""fromunicodedataimportnormalizedefnfc_equal(str1,str2):returnnormalize('NFC',str1)==normalize('NFC',str2)deffold_equal(str1,str2):return(normalize('NFC',str1).casefold()==normalize('NFC',str2).casefold())超出Unicode标准中的规范化和大小写折叠之外,有时候进行更深层次的转换是有意义的,比如将'café'改为'cafe'。我们将在下一节看到何时以及如何进行。

importunicodedataimportstringdefshave_marks(txt):"""Removealldiacriticmarks"""norm_txt=unicodedata.normalize('NFD',txt)#①shaved=''.join(cforcinnorm_txtifnotunicodedata.combining(c))#②returnunicodedata.normalize('NFC',shaved)#③①

将所有字符分解为基本字符和组合标记。

过滤掉所有组合标记。

重新组合所有字符。

示例4-15展示了几种使用shave_marks的方法。

>>>order='“HerrVo:cupoftkercaffèlattebowlofaaí.”'>>>shave_marks(order)'“HerrVo:cupoftkercaffelattebowlofacai.”'#①>>>Greek='Ζφυρο,Zéfiro'>>>shave_marks(Greek)'Ζεφυρο,Zefiro'#②①

仅字母“è”、“”和“í”被替换。

“”和“é”都被替换了。

来自示例4-14的函数shave_marks运行良好,但也许它做得太过了。通常移除变音符号的原因是将拉丁文本更改为纯ASCII,但shave_marks也会改变非拉丁字符,比如希腊字母,这些字母仅仅通过失去重音就不会变成ASCII。因此,有必要分析每个基本字符,并仅在基本字符是拉丁字母时才移除附加标记。这就是示例4-16的作用。

defshave_marks_latin(txt):"""RemovealldiacriticmarksfromLatinbasecharacters"""norm_txt=unicodedata.normalize('NFD',txt)#①latin_base=Falsepreserve=[]forcinnorm_txt:ifunicodedata.combining(c)andlatin_base:#②continue#ignorediacriticonLatinbasecharpreserve.append(c)#③#ifitisn'tacombiningchar,it'sanewbasecharifnotunicodedata.combining(c):#④latin_base=cinstring.ascii_lettersshaved=''.join(preserve)returnunicodedata.normalize('NFC',shaved)#⑤①

当基本字符为拉丁字符时,跳过组合标记。

否则,保留当前字符。

检测新的基本字符,并确定它是否为拉丁字符。

更激进的一步是将西方文本中的常见符号(例如,卷曲引号、破折号、项目符号等)替换为ASCII等效符号。这就是示例4-17中的asciize函数所做的。

single_map=str.maketrans("""‘’“”–—""",#①"""'f"^<''""---~>""")multi_map=str.maketrans({#②'€':'EUR','…':'...','':'AE','':'ae','':'OE','':'oe','':'(TM)','‰':'','':'**','':'***',})multi_map.update(single_map)#③defdewinize(txt):"""ReplaceWin1252symbolswithASCIIcharsorsequences"""returntxt.translate(multi_map)#④defasciize(txt):no_marks=shave_marks_latin(dewinize(txt))#⑤no_marks=no_marks.replace('','ss')#⑥returnunicodedata.normalize('NFKC',no_marks)#⑦①

为字符替换构建映射表。

为字符到字符串替换构建映射表。

合并映射表。

dewinize不影响ASCII或latin1文本,只影响cp1252中的Microsoft附加内容。

应用dewinize并移除变音符号。

用“ss”替换Eszett(我们这里不使用大小写折叠,因为我们想保留大小写)。

对具有其兼容性代码点的字符进行NFKC规范化以组合字符。

示例4-18展示了asciize的使用。

>>>order='“HerrVo:cupoftkercaffèlattebowlofaaí.”'>>>dewinize(order)'"HerrVo:-cupofOEtker(TM)caffèlatte-bowlofaaí."'#①>>>asciize(order)'"HerrVoss:-12cupofOEtker(TM)caffelatte-bowlofacai."'#②①

dewinize替换卷曲引号、项目符号和(商标符号)。

asciize应用dewinize,删除变音符号,并替换''。

不同语言有自己的去除变音符号的规则。例如,德语将'ü'改为'ue'。我们的asciize函数不够精细,因此可能不适合您的语言。但对葡萄牙语来说,它的效果还可以接受。

总结一下,在simplify.py中的函数远远超出了标准规范化,并对文本进行了深度处理,有可能改变其含义。只有您可以决定是否走得这么远,了解目标语言、您的用户以及转换后的文本将如何使用。

这就结束了我们对规范化Unicode文本的讨论。

现在让我们来解决Unicode排序问题。

Python通过逐个比较每个序列中的项目来对任何类型的序列进行排序。对于字符串,这意味着比较代码点。不幸的是,这对于使用非ASCII字符的人来说产生了无法接受的结果。

考虑对在巴西种植的水果列表进行排序:

>>>fruits=['caju','atemoia','cajá','aaí','acerola']>>>sorted(fruits)['acerola','atemoia','aaí','caju','cajá']不同区域设置的排序规则不同,但在葡萄牙语和许多使用拉丁字母表的语言中,重音符号和塞迪利亚很少在排序时产生差异。因此,“cajá”被排序为“caja”,并且必须位于“caju”之前。

排序后的fruits列表应为:

要启用locale.strxfrm,您必须首先为您的应用程序设置一个合适的区域设置,并祈祷操作系统支持它。示例4-19中的命令序列可能适用于您。

importlocalemy_locale=locale.setlocale(locale.LC_COLLATE,'pt_BR.UTF-8')print(my_locale)fruits=['caju','atemoia','cajá','aaí','acerola']sorted_fruits=sorted(fruits,key=locale.strxfrm)print(sorted_fruits)在GNU/Linux(Ubuntu19.10)上运行示例4-19,安装了pt_BR.UTF-8区域设置,我得到了正确的结果:

'pt_BR.UTF-8'['aaí','acerola','atemoia','cajá','caju']因此,在排序时需要在使用locale.strxfrm作为键之前调用setlocale(LC_COLLATE,your_locale)。

不过,还有一些注意事项:

因此,标准库提供的国际化排序解决方案有效,但似乎只在GNU/Linux上得到很好的支持(也许在Windows上也是如此,如果您是专家的话)。即使在那里,它也依赖于区域设置,会带来部署上的麻烦。

幸运的是,有一个更简单的解决方案:pyuca库,可以在PyPI上找到。

>>>importpyuca>>>coll=pyuca.Collator()>>>fruits=['caju','atemoia','cajá','aaí','acerola']>>>sorted_fruits=sorted(fruits,key=coll.sort_key)>>>sorted_fruits['aaí','acerola','atemoia','cajá','caju']这个方法简单易行,在GNU/Linux、macOS和Windows上都可以运行,至少在我的小样本中是这样的。

(技术审阅员Miroslavedivy是一位多语言使用者,也是Unicode方面的专家。这是他对pyuca的评价。)

顺便说一句,那个排序表是组成Unicode数据库的许多数据文件之一,我们下一个主题。

unicodedata模块包括检索字符元数据的函数,包括unicodedata.name(),它返回标准中字符的官方名称。图4-5展示了该函数的使用。1

您可以使用name()函数构建应用程序,让用户可以按名称搜索字符。图4-6展示了cf.py命令行脚本,它接受一个或多个单词作为参数,并列出具有这些单词在官方Unicode名称中的字符。cf.py的完整源代码在示例4-21中。

在示例4-21中,请注意find函数中的if语句,使用.issubset()方法快速测试query集合中的所有单词是否出现在从字符名称构建的单词列表中。由于Python丰富的集合API,我们不需要嵌套的for循环和另一个if来实现此检查。

#!/usr/bin/envpython3importsysimportunicodedataSTART,END=ord(''),sys.maxunicode+1#①deffind(*query_words,start=START,end=END):#②query={w.upper()forwinquery_words}#③forcodeinrange(start,end):char=chr(code)#④name=unicodedata.name(char,None)#⑤ifnameandquery.issubset(name.split()):#⑥print(f'U+{code:04X}\t{char}\t{name}')#⑦defmain(words):ifwords:find(*words)else:print('Pleaseprovidewordstofind.')if__name__=='__main__':main(sys.argv[1:])①

设置搜索的代码点范围的默认值。

find接受query_words和可选的关键字参数来限制搜索范围,以便进行测试。

将query_words转换为大写字符串集合。

获取code的Unicode字符。

获取字符的名称,如果代码点未分配,则返回None。

如果有名称,将其拆分为单词列表,然后检查query集合是否是该列表的子集。

打印出以U+9999格式的代码点、字符和其名称的行。

unicodedata模块包括函数,用于检查Unicode字符是否表示数字,如果是,则返回其人类的数值,而不是其代码点数。示例4-22展示了unicodedata.name()和unicodedata.numeric()的使用,以及str的.isdecimal()和.isnumeric()方法。

importunicodedataimportrere_digit=re.compile(r'\d')sample='1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'forcharinsample:print(f'U+{ord(char):04x}',#①char.center(6),#②'re_dig'ifre_digit.match(char)else'-',#③'isdig'ifchar.isdigit()else'-',#④'isnum'ifchar.isnumeric()else'-',#⑤f'{unicodedata.numeric(char):5.2f}',#⑥unicodedata.name(char),#⑦sep='\t')①

以U+0000格式的代码点。

字符在长度为6的str中居中。

如果字符匹配r'\d'正则表达式,则显示re_dig。

如果char.isdigit()为True,则显示isdig。

如果char.isnumeric()为True,则显示isnum。

数值格式化为宽度为5和2位小数。

Unicode字符名称。

运行示例4-22会给你图4-7,如果你的终端字体有所有这些字形。

图4-7的第六列是在字符上调用unicodedata.numeric(char)的结果。它显示Unicode知道代表数字的符号的数值。因此,如果你想创建支持泰米尔数字或罗马数字的电子表格应用程序,就去做吧!

图4-7显示正则表达式r'\d'匹配数字“1”和梵文数字3,但不匹配一些其他被isdigit函数视为数字的字符。re模块对Unicode的了解不如它本应该的那样深入。PyPI上提供的新regex模块旨在最终取代re,并提供更好的Unicode支持。11我们将在下一节回到re模块。

接下来我们将快速查看双模式API,提供接受str或bytes参数的函数,并根据类型进行特殊处理。

Python标准库有接受str或bytes参数并根据类型表现不同的函数。一些示例可以在re和os模块中找到。

如果用bytes构建正则表达式,模式如\d和\w只匹配ASCII字符;相反,如果这些模式给定为str,它们将匹配ASCII之外的Unicode数字或字母。示例4-23和图4-8比较了str和bytes模式如何匹配字母、ASCII数字、上标和泰米尔数字。

importrere_numbers_str=re.compile(r'\d+')#①re_words_str=re.compile(r'\w+')re_numbers_bytes=re.compile(rb'\d+')#②re_words_bytes=re.compile(rb'\w+')text_str=("Ramanujansaw\u0be7\u0bed\u0be8\u0bef"#③"as1729=13+123=93+103.")#④text_bytes=text_str.encode('utf_8')#⑤print(f'Text\n{text_str!r}')print('Numbers')print('str:',re_numbers_str.findall(text_str))#⑥print('bytes:',re_numbers_bytes.findall(text_bytes))#⑦print('Words')print('str:',re_words_str.findall(text_str))#⑧print('bytes:',re_words_bytes.findall(text_bytes))#⑨①

前两个正则表达式是str类型。

最后两个是bytes类型。

Unicode文本搜索,包含泰米尔数字1729(逻辑行一直延续到右括号标记)。

需要使用bytes正则表达式来搜索bytes字符串。

⑥](#co_unicode_text_versus_bytes_CO15-6)

str模式r'\d+'匹配泰米尔和ASCII数字。

bytes模式rb'\d+'仅匹配数字的ASCII字节。

str模式r'\w+'匹配字母、上标、泰米尔语和ASCII数字。

bytes模式rb'\w+'仅匹配字母和数字的ASCII字节。

示例4-23是一个简单的例子,用来说明一个观点:你可以在str和bytes上使用正则表达式,但在第二种情况下,ASCII范围之外的字节被视为非数字和非单词字符。

另一个重要的双模块是os。

GNU/Linux内核不支持Unicode,因此在现实世界中,您可能会发现由字节序列组成的文件名,这些文件名在任何明智的编码方案中都无效,并且无法解码为str。使用各种操作系统的客户端的文件服务器特别容易出现这个问题。

为了解决这个问题,所有接受文件名或路径名的os模块函数都以str或bytes形式接受参数。如果调用这样的函数时使用str参数,参数将自动使用sys.getfilesystemencoding()命名的编解码器进行转换,并且OS响应将使用相同的编解码器进行解码。这几乎总是您想要的,符合Unicode三明治最佳实践。

但是,如果您必须处理(或者可能修复)无法以这种方式处理的文件名,您可以将bytes参数传递给os函数以获得bytes返回值。这个功能让您可以处理任何文件或路径名,无论您可能遇到多少小精灵。请参阅示例4-24。

>>>os.listdir('.')#①['abc.txt','digits-of-π.txt']>>>os.listdir(b'.')#②[b'abc.txt',b'digits-of-\xcf\x80.txt']①

第二个文件名是“digits-of-π.txt”(带有希腊字母π)。

给定一个byte参数,listdir以字节形式返回文件名:b'\xcf\x80'是希腊字母π的UTF-8编码。

为了帮助处理作为文件名或路径名的str或bytes序列,os模块提供了特殊的编码和解码函数os.fsencode(name_or_path)和os.fsdecode(name_or_path)。自Python3.6起,这两个函数都接受str、bytes或实现os.PathLike接口的对象作为参数。

Unicode是一个深奥的领域。是时候结束我们对str和bytes的探索了。

我们在本章开始时否定了1个字符==1个字节的概念。随着世界采用Unicode,我们需要将文本字符串的概念与文件中表示它们的二进制序列分开,而Python3强制执行这种分离。

在简要概述二进制序列数据类型——bytes、bytearray和memoryview后,我们开始了编码和解码,列举了一些重要的编解码器,然后介绍了如何防止或处理由Python源文件中错误编码引起的臭名昭著的UnicodeEncodeError、UnicodeDecodeError和SyntaxError。

在没有元数据的情况下考虑编码检测的理论和实践:理论上是不可能的,但实际上Chardet软件包对一些流行的编码做得相当不错。然后介绍了字节顺序标记作为UTF-16和UTF-32文件中唯一常见的编码提示,有时也会在UTF-8文件中找到。

在下一节中,我们演示了如何打开文本文件,这是一个简单的任务,除了一个陷阱:当你打开文本文件时,encoding=关键字参数不是强制的,但应该是。如果你未指定编码,你最终会得到一个在不同平台上不兼容的“纯文本”生成程序,这是由于冲突的默认编码。然后我们揭示了Python使用的不同编码设置作为默认值以及如何检测它们。对于Windows用户来说,一个令人沮丧的认识是这些设置在同一台机器内往往具有不同的值,并且这些值是相互不兼容的;相比之下,GNU/Linux和macOS用户生活在一个更幸福的地方,UTF-8几乎是默认编码。

Unicode提供了多种表示某些字符的方式,因此规范化是文本匹配的先决条件。除了解释规范化和大小写折叠外,我们还提供了一些实用函数,您可以根据自己的需求进行调整,包括像删除所有重音这样的彻底转换。然后我们看到如何通过利用标准的locale模块正确对Unicode文本进行排序——带有一些注意事项——以及一个不依赖于棘手的locale配置的替代方案:外部的pyuca包。

我们利用Unicode数据库编写了一个命令行实用程序,通过名称搜索字符——感谢Python的强大功能,只需28行代码。我们还简要介绍了其他Unicode元数据,并对一些双模式API进行了概述,其中一些函数可以使用str或bytes参数调用,产生不同的结果。

2Python2.6和2.7也有bytes,但它只是str类型的别名。

3小知识:Python默认使用的ASCII“单引号”字符实际上在Unicode标准中被命名为APOSTROPHE。真正的单引号是不对称的:左边是U+2018,右边是U+2019。

有趣的是,微符号被认为是一个“兼容字符”,但欧姆符号不是。最终结果是NFC不会触及微符号,但会将欧姆符号更改为大写希腊字母omega,而NFKC和NFKD会将欧姆符号和微符号都更改为希腊字符。

重音符号只在两个单词之间唯一的区别是它们时才会影响排序—在这种情况下,带有重音符号的单词会在普通单词之后排序。

再次,我找不到解决方案,但发现其他人报告了相同的问题。其中一位技术审阅者AlexMartelli在他的Macintosh上使用setlocale和locale.strxfrm没有问题,他的macOS版本是10.9。总结:结果可能有所不同。

THE END
1.从素食到海鲜探索世界美食十大菜谱选强玛丽亚·塞拉(Marcella Hazan)的这本书深入浅出地介绍了意大利传统厨房中的秘密。这本书让读者能轻松理解并制作出正宗意式佳肴,无论是家常面条还是复杂蛋糕。 3. 新世纪美食启蒙:《The New Best Recipe》 由美国公共电视台(PBS)出品,美国家庭科学协会出版,这本 cookbook 是对众多传统美式主厨手艺进行重新审视后的成https://www.hgddesks.cn/te-chan/576685.html
2.十大菜谱书籍探秘厨房宝库《小さなスイッチの魔法》:这本日本原版的 cookbook 非常受欢迎,特别是在全球范围内引起了人们对于日式料理的大好奇心。这本书以其独特而有趣的配方和图解为特色,每个步骤都经过精心设计,使初学者也能轻松掌握。 《世界美食大全》:这是一本汇集全球多种美味佳肴的巨著,它涵盖了从意大利面条到印度咖喱,从墨西哥玉https://www.yehsjuxj.cn/zi-xun/557156.html
3.儿童读物原版书DK出品厨师 想要超过40道美味食谱英文原版 Chefs Wanted More Than 40 Delicious Recipes for Curious Cooks 精装进口英语书籍20 元优惠券 使用期限: 2024-12-04 去天猫领券声明:此商品数据来源由淘宝官方接口提供,所有交易过程在淘宝或天猫与第三方卖家进行,本网站不参与交易,如有交易产生的疑问请联系天猫卖家【优https://m.ftxia.com/item.htm?id=6yan0yrIypPZr376sz
4.对于素食主义者的日常饮食有没有特别值得推荐的一本高质量素食《完全植物蛋白》由詹妮弗·科尔曼编著,这本书以其详尽而全面的信息闻名,它展示了如何通过精心挑选不同的豆类、谷物和其他植物性食品组合出营养丰富、蛋白质含量全面的大餐。它不仅适合初学者,还能给经验丰富的厨师带来新的灵感。 《绿色蔬菜大百科》 这本由玛丽莎·梅勒编著的小册子专门聚焦于蔬菜世界,它包括了从简单https://www.dhrmkewmy.cn/zi-xun/366653.html
5.英文原版CookbookBook食谱之书百年来最优秀的食谱合集精装《英文原版 Cookbook Book 食谱之书 百年来最优秀的食谱合集 精装 英文版 进口英语原版书籍》,作者:英文原版 Cookbook Book 食谱之书 百年来最优秀的食谱合集 精装 英文版 进口英语原版书籍Kamali 著,出版社:Phaidon Press,ISBN:9780714867502。http://product.m.dangdang.com/11454003870.html
6.食谱美食营养降烹饪菜肴美味新鲜唤醒早餐生活类原版书食谱美食营养健康烹饪菜肴美味新鲜唤醒早餐生活类原版书 【现货】英文原版 Breakfast Recipes to Wake Up for早餐食谱唤醒 新鲜美味菜肴烹饪健康营养美食食谱书籍 风格: 食谱 美食 营养 健康 烹饪 菜肴 美味 新鲜 唤醒 早餐 图文详情https://www.zhe2.com/note/625924397208
7.TowardsDataScience博客中文翻译2020(九百八十)人工智能是如何解决 50 年来蛋白质折叠和建模的大挑战的 towardsdatascience.com](/the-cornerstone-to一份食谱或操作指南 关心如何 要发送电子邮件>首先前往 gmail.com >然后选择撰写>键入收件人>编写电子英文 话 “狗猫人”(?) |“狗抱人”() “我累了”(?)——有意义,但语义错误(语法) https://blog.csdn.net/wizardforcel/article/details/142709697
8.原神/成就名称内容折叠备注 成就·殊技 既然掌握了这些技术,或许不需要战斗也能很好地活下去吧。如果没有使命在身的话。 名片纹饰,达成下述所有成就后获得 人铸赋形 锻造一把四星武器 生存专家 学习10/20/40个料理食谱。 星级大厨 10/20/40个料理食谱达到熟练。 累加条件为该食谱的熟练进度已攒满,完美烹饪一次累加一次https://mzh.moegirl.org.cn/%E5%8E%9F%E7%A5%9E/%E6%88%90%E5%B0%B1
9.东京迷城在店内贩卖记载着杜宫市新闻的“杜宫日报”等,各个角色所读的“推荐书”也可以买到。 蓬莱町 地处杜宫站东侧的杜宫最大的娱乐街区。酒馆和被称为“手套?酒吧”的店铺林立,特别在晚上顾客会更多。除了洸其中一个打工地,咖啡吧“N”之外,交换店“大黑堂”、游戏中心“绿洲”等等,尤其是游戏中心中可以游玩https://www.moegirl.org/%E4%B8%9C%E4%BA%AC%E8%BF%B7%E5%9F%8E
10.小熊隔水电炖盅食谱1.doc小熊隔水电炖盅食谱1.doc 12页内容提供方:Seiryu 大小:27.5 KB 字数:约7.2千字 发布时间:2022-09-04发布于湖北 浏览人气:36 下载次数:仅上传者可见 收藏次数:0 需要金币:*** 金币 (10金币=人民币1元)小熊隔水电炖盅食谱1.doc 关闭预览 想预览更多内容,点击免费在线预览全文 免费在线预览全文 https://m.book118.com/html/2022/0902/8000105032004134.shtm
11.时尚内含1050张照片蕾哈娜写真自传成长蜕变Icon7张折叠园艺手工DIY 书 中商原版 清仓特价 英文原版 图书捡漏 食谱 生活休闲类 影视设定集 特价 原版 旅游家居生活穿搭运动 ¥19.9月销0 Stard 一生中一定要尝试 完美意大利面可拍多册英文餐饮生活类进口原版 Official 101种鸡尾酒 星露谷物语官方食谱 痴迷沙拉 书The ¥74.25月销0 港台原版 山铎 大家出版 卡兹 预售https://www.igove.cn/m_item.php?id=3RW59oskdzkjzqmfRM
12.礼仪习俗课教案这一天还有特定的食谱:吃白面饼卷鸡蛋、内撒芝麻盐、喝麦仁稀饭。三天中最重要的也最具特色的活动是元代王祯《农书》中《荞麦》部分记载:“北方山后, 诸郡多种……磨而为面或作汤饼, 谓之河漏。”? 了解各国见面礼的礼仪规范,并且能完成这些礼仪的基本操作 ? 掌握各国的英文国名 https://www.360wenmi.com/f/filewe6c0ag5.html
13.是我吗的翻译是:Ido中文翻译英文意思,翻译英语ai am bliend folded and hit a pinata open. 我是被折叠的bliend并且击中彩饰陶罐开放。[translate] a我们应该上课认真的听讲 We should attend class agrow and harvest 10 crop of raspberries for greed's newest recipe 生长并且收获10莓庄稼为贪婪的最新的食谱[translate] http://riyu.zaixian-fanyi.com/fan_yi_1249215
14.「上帝游戏」拯救世界(下)机核GCORES这个24k 的游戏是游戏编程的教科书,它定义了“动作”、“平台”、“关卡设计”,确立了角色、游戏目的、流程分布、手感、隐藏要素、BOSS、杂兵等通用至今的概念。第一个关卡“Super Mario World 1-1”,成了设计师必读的启蒙教材,开头的那几十个音符,被IGN评为历代游戏音乐NO.1。 任天堂GameBoy崛起的最大功臣是《https://www.gcores.com/articles/107012
15.三年级食品安全教案公道养分与食品宁静讲授面貌标 1.举例说出什么是公道养分 2.存眷食品宁静讲授计划与案例3.实验运用有关公道养分的知识,计划一份养分公道的食谱, 体贴尊长的饮食。 4.认同情况掩护与食品宁静之间的同一性。 重点和难点 重点 ①存眷公道养分和食品宁静在康健生存中的意义; ②门生经过阐发,认同情况掩护与食品宁静之间的https://www.xdyy8.com/wenxue/zuowen/70593.html
16.哈利波特主题专区哈利波特魔法宝箱 英文原版 魔法道具折叠书 Harry Potter Magic系列 含旅行魔法 格兰芬多 斯莱特林 拉文克劳 赫奇帕奇学院 《哈利·波特》格兰芬多学院 ¥ 129 .00 4评价 100%好评 哈利波特斯莱特林学院魔法宝箱 魔法道具折叠书 英文原版 Harry Potter Slytherin Magic 电影周边 精装 搭旅行魔法 格兰芬多 《哈利·波https://sale.jd.com/m/h5/iukdyijxh0kd.html