常见的迁移问题
如果你按照该建议来确保你的代码使用Python 2.7 - 3来运行没有警告,一些现在会遇到的简单错误都是可以避免的,比如使用了Python 3的关键字当变量名及其他一些容易修复的。你也会避免整数相除会得到浮点数这个微妙的错误。这有一个你会遇到的觉错误。一些是容易修复的,而另一些则少一点。
如果你需要同时支持Python 2和Python 3,你很可能需要根据Python版本使用条件语句。在这本书里,我一贯使用sys.version_info元组和(3,)元组比较。但是还有一些其他的方式来做相同的测试,像sys.version_info[0] !=3或者使用sys.version字符串。使用哪 一个主要是个人爱好。如果你最终做了很多测试,设定一个常量是个好主意:
>>> import sys>>> PY3 = sys.version_info > (3,)
然后你可以在软件的剩余部分仅使用PY3常量。
现在进入更多的常见问题。
不正确的引入(import)
有时候你会遇到2to3似乎忘记修改一个引入(import)的情况。这通常是因为你从其他的模块引入了一个函数或者类,而不是它被定义的地方。
例如,url2pathname在Python 2是在urllib里定义的,但它被urllib2使用和引入。经常可以看到从urllib2导入url2pathname,以避免单独导入urllib。然而当你这样做时导入将没办法正确地修改成新库的位置,因为2to3不知道这个小技巧,所以你需要在运行2to3前把你的代码成从正确的地方导入。
相对引入问题
Python 3修改了在一个包里面引用的语法,规定使用相对引入语法,就是说使用from . import mymodule来代替import mymodule。对于大多数部分2to3会为你处理这个,但是有两个情况2to3会做错事。
import 固定器(fixer)会在你的引入里查看并且查看你的本地模块和包,如果引入是本地的它就会修改成新的语法。然而,当本地的模块是扩展的模块,那么模块通常不会被2to3构建。这意味着固定器(fixer)不会找到本地模块并且导入不会被个性修改。
反之,如果你从标准库引入一个模块并且你有一个和它相同名字的文件夹,import 固定器(fixer)会假设成它是一个本地的包,并且把导入修改成本地导入。甚至文件夹不是一个包它也会这样做。这个是一个import 固定器(fixer)的bug,所以你需要避免它。
为Python 2.5及后面版本解决这些问题的办法是修改导入成相对导入并且添加__future__导入来允许使用Python 3的绝对/相对导入语法。
from __future__ import absolute_importfrom . import mymodule
因为模块使用了新的导入行为,所以import 固定器(fixer)不会修改成相对导入,从而避免了这些问题。
如果你需要支持Python 2.4或者更早的版本你可以通过完全不用相对导入来避免这些问题,除了在运行2to3时import 固定器(fixer)。
Unorderable类型、__cmp__和cmp
在Python 2下,最常见的类型排序的方式是实现一个依次使用内置cmp()函数的__cmp__()方法,像这个类可以按照姓来排序:
>>> class Orderable(object):...... def __init__(self, firstname, lastname):... self.first = firstname... self.last = lastname...... def __cmp__(self, other):... return cmp("%s, %s" % (self.last, self.first),... "%s, %s" % (other.last, other.first))...... def __repr__(self):... return "%s %s" % (self.first, self.last)...>>> sorted([Orderable('Donald', 'Duck'),... Orderable('Paul', 'Anka')])[Paul Anka, Donald Duck]
因为同时拥有__cmp__()和丰富比较方法违反了只能明显地做一件事的原则,Python 3乎略了__cmp__()方法。除此之外cmp()已经取消了!这通常会导致你转换后的代码引起一个“TypeError:unorderable types error”错误。所以你需要用丰富的比较方法来替代__cmp__()方法。为了实现排序你只需要实现被“小于”操作符<使用的__lt__()。
>>> class Orderable(object):...... def __init__(self, firstname, lastname):... self.first = firstname... self.last = lastname...... def __lt__(self, other):... return ("%s, %s" % (self.last, self.first) <... "%s, %s" % (other.last, other.first))...... def __repr__(self):... return "%s %s" % (self.first, self.last)...>>> sorted([Orderable('Donald', 'Duck'),... Orderable('Paul', 'Anka')])[Paul Anka, Donald Duck]
为了支持其他的比较操作,你需要分别实现他们。在上有如何实现他们的例子。(在湖闻樟注:更准确地应该是在
排序
与同时cmp()函数和__cmp__()方法移除的同时,list.sort()和sorted()的参数也在Python3取消了。这个导致下而这些错误中的一个:
TypeError: 'cmp' is an invalid keyword argument for this functionTypeError: must use keyword argument for key function
你需要使用在Python 2.4引进的key参数来取代cmp参数。在上可以找到更多这方面的信息。
排序Unicode
因为在Python 3中cmp=参数已经被移除,使用locale.strcoll来排序Unicode能工作的时间不长了。在Python 3中可以使用locale.strxfrm来替代。
>>> import locale>>> locale.setlocale(locale.LC_ALL, 'sv_SE.UTF-8')'sv_SE.UTF-8'>>> corpus = ["art", "Älg", "ærgeligt", "Aardvark"]>>> sorted(corpus, key=locale.strxfrm)['Aardvark', 'art', 'Älg', 'ærgeligt']
这不能在Python 2下工作,在locale.strxfrm的地方会得到一个非unicode(non-unicode)字符串使用本地编码来编码。如果你只是支持Python 2.7和Python 3.2及之后的版本,幸亏在functools中有一个转换函数,你可以继续使用locale.strcoll。
>>> from functools import cmp_to_key>>> import locale>>> locale.setlocale(locale.LC_ALL, 'sv_SE.UTF-8')'sv_SE.UTF-8'>>> corpus = ["art", "Älg", "ærgeligt", "Aardvark"]>>> sorted(corpus, key=cmp_to_key(locale.strcoll))['Aardvark', 'art', 'Älg', 'ærgeligt']
然而这会慢很多并且在Python 2.6或者Python 3.1中无法工作。
字节、字符串及Unicode
最大的问题是你可能遇到涉及到在Python 3中最重要的变化之一;字符串现在总是Unicode。这对那些在非英语国家使用任何时候都需要使用Unicode的应用会是一种简化。
当然,因为现在字符串总是Unicode,所以对字节数据我们需要另一种类型。Python 3有两个新的二进制类型,bytes和 bytearrays。bytes类型类似于字符串类型,但取代字符字符串,它是整数字符串。Bytearrays更像是列表,但是这个列表只能保存0到255的整数。bytearrayis是可变并使用于你需要处理二进制数据时。因为它是一个新的类型,虽然它在Python 2.6中就已经存在了,但我在这本书中总是忽略它并专注于使用其他方式来处理二进制数据。
字节文字
你遇到的第一个问题是如何把二进制数据放进Python代码中。在Python 2我们使用准备的字符串,因此是标准的字符串文字。检查文件实际是不是一个GIF文件,我们可以查看头6个字节,GIF文件应该是以GIF89a (或者 GIF87a,但是现在让我们魅力它吧)开头的:
>>> file = open('maybe_a.gif', 'rb')>>> file.read(6) == 'GIF89a'True
在Python 3这个测试将总是抵账,因为你需要以字节对象的比较来替代。如果你不需要Python 2支持,你可以简单地通过添加前缀b把保存二进制数据的字符串文字改成字节文字。
>>> file = open('maybe_a.gif', 'rb')>>> file.read(6) == b'GIF89a'True
还有另外两个其他的情况需要修改。在Python 2中的字符串类型是一个8位的字符列表,但是在Python 3中的字节是一个8位的整数列表。所以获取字符串(string)的一个字符会返回一个字符长的字符串,但是获取字节(bytes)的一个字节会返回一个整数!因此你必须要把字节级别的操作改成整数的。另一个是大量ord()和chr()移除带来的调用问题,因为在字节级别的操作会导致第一个位置是整数而不是字符串。
>>> 'GIF89a'[2]'F'>>> b'GIF89a'[2]70
在Python 2和Python 3中的二进制数据
如果你想继续支持Python 2的话,这些变化会产生一些问题。2to3通常会假定你在Python 2中用的是字符串并且也是你在Python 3中想用的,在大数情况下这是正解的。所以当这不是正确的时候,你需要标记数据是二进制,以便它在Python 3下也是可以继续工作。
在Python 2.6和即将到来的Python 2.7都有可以用来指定数据是二进制的字节文字及字节类型。在Python 2下的字节文字和字节类型仅仅是str的重命名,所以这些对象的行为不完全像Python 3的字节。最重要的是它会是一个字符字符串而不一个字节字符串,所以 b'GIF89a'[2]在Python 2下不会返回整数70而是字符串'F'。
幸运的是这不是一个很常见的问题,在大多数处理二进制数据的情况下是一组组地处理数据并且不你需要查看或者修改独立的字节。如果你需要这些,有一个在Python 2和Python 3下都能起作用的小技巧,那就是产生一个单字符长度的切片。
>>> b'GIF89a'[2:3]b'F'
虽然你在Python 2得到一个字符的字符字符串而在Python 3得到的是一个字符的字节字符串,但这在Python 2和Python 3能同样好地工作,
然后,在Python 2.5或更早的版本,b'GIF89a'语法根本不能工作,所以它只是一个你不需要支持2.5之前的Python版本的解决办法。你可以产生一个字符串并编码它得到二进制数据来确保数据是二进制的。这代码在所有版本的Python 2和Python 3都能很好地工作。
>>> file = open('maybe_a.gif', 'rb')>>> file.read(6) == u'GIF89a'.encode('ISO-8859-1')True
当然u'GIF89a'在Python 3不是合法的语法,因为单独的Unicode文字已经取消了,但是2to3会处理它并去除u前缀。
更好的解决办法
如果你认为这全是一堆丑陋的修改,你是对的。让我们写一个特殊的函数来改进这件事吧,在Python 3下将会传递一个字符串并从它那产生一个二进制数据而Python 2就简单地返回字符串。在Python 2下是去掉了编码的步骤,这样就稍微不丑陋了一些。你可以在做你想做的任何事时调用这个函数,包括“ArthurBelling”,但是一些早期的Python 3采用者写了一个这函数的变体,他们把它叫着”b“,它很棒、很短并且看起来和二进制文字很像。我们可以像这样定义它:
import sysif sys.version_info < (3,): def b(x): return xelse: import codecs def b(x): return codecs.latin_1_encode(x)[0]
在Python 2下这会返回你传进的字符串,为作为一个二进制数据使用作准备:
>>> from makebytes import b>>> b('GIF89a')'GIF89a'
但是在Python 3下它会使用编码字符串并返回一个字节对象。
>>> from makebytes import b>>> b('GIF89a')b'GIF89a'
这个方法是用ISO-8859-1编码,就也是为人所知的Latin-1,因为它是唯一的256个字符完全相同于Unicode的256个开始字符的编码。这个例子将会使用ASCII编码很好地工作,但是如果你有一个字符的值超过了127那么你需要使用ISO-8859-1和它对一对一的映像。
这个b()函数的实现使用了直接从codecs导入的编码函数,因为这是略微快一些的,但是它在实践中不是显而易见的。在你调用b()数百万次它才会有所不同,而从你把当成字节文字的替代品起这就不会发生。如果你更喜欢的话,可以自由地使用x.encode('ISO-8859-1')来代替。
操作二进制数据
b()函数使从文字创建二进制数据成了可能,所以它解决了同时支持Python 2和Python 3时使用二进制数据的主要问题。它不没解决你需要一个个地检查或者修改字节这些不常见的情况,因为在二进制数据上索引或迭代在Python 2会返回一个字符的字符串而在Python 3是整数。
如果你只需要支持Python 2.6和Python 2.7,你可以使用新的bytearray来做这个。它是拥有一个实用的融合了列表和字符串可变的类型大多数常用操作的类型,例如它同时拥有.append()和.find(),还有一些它自己的方法,像方便的.fromhex()。
>>> data = bytearray(b'Monty Python')>>> data[5] = 33>>> databytearray(b'Monty!Python')
如果你需要支持Python 2.5或者更早的版本,你可以能过引入迭代或者得到一个分别索引出指定的一个字符串或者字节并且在每一种情况都返回整数的帮助函数来解决这个问题:
>>> import sys>>> if sys.version_info < (3,):... def byteindex(data, index):... return ord(data[index])...... def iterbytes(data):... return (ord (char) for char in data)...... else:... byteindex = lambda x, i: x[i]... iterbytes = lambda x: iter(x)...>>> from makebytes import b>>> byteindex(b('Test'), 2)115>>> print([x for x in iterbytes(b('Test'))])[84, 101, 115, 116]
前面这个iterbytes例子使用了一个构造器语句,这需要Python 2.4。支持更早的Python你可以使用列表来替代,这仅仅会使用更多的内存。
如果你不喜欢那些帮助函数,你可以想要引进一个能同样在Python 2和Python 3下工作的特殊二进制类型。然而,标准库在Python 2下会假设你传进的是字符串并且在Python 3下假设是字节,所以它在Python 2下必须是str的子类而在Python 3下是bytes的子类。这个解决方式引入了一个有在所有Python版本行为一样的额外函数的新类型,但是这会单独存于标准函数存在:
import sysif sys.version_info < (3,): class Bites(str): def __new__(cls, value): if isinstance(value[0], int): # It's a list of integers value = ''.join([chr(x) for x in value]) return super(Bites, cls).__new__(cls, value) def itemint(self, index): return ord(self[index]) def iterint(self): for x in self: yield ord(x)else: class Bites(bytes): def __new__(cls, value): if isinstance(value, str): # It's a unicode string: value = value.encode('ISO-8859-1') return super(Bites, cls).__new__(cls, value) def itemint(self, x): return self[x] def iterint(self): for x in self: yield x
这个被我称作Bites同样也可以被叫成其他名字(包括类似于前面方法的b这个简单的名字)的新类型在构建时同时接受字符串和整数列表。因为你看到它在Python 2是str的子类而在Python 3是字节的并且在两个版本都是二进制类型的扩展,所以它可以直接传递给需要二进制数据的标准库函数。
你可以像这样使用它:
>>> from bites import Bites>>> Bites([71, 73, 70, 56, 57, 97]).itemint(2)70>>> binary_data = Bites(open('maybe_a.gif', 'rb').read())>>> binary_data.itemint(2)70>>> print([x for x in Bites('GIF89a').iterint()])[71, 73, 70, 56, 57, 97]
你也可以很容易地添加在类外做切片并总是得到一个整数列表的支持,或者其他一些你需要在Python 2和Python 3同样工作的方法。
如果你认为这整个像这样的类过于复杂了,那么你是对的。除了处理二进制数据,相当于你永远不会需要它。大多数需要处理二进制数据时,你是通过从一个流或者调用函数读或者写来做到的,并且在这些情况下你是把数据当成块来处理而不是看成单独的字节。所以差不多对所有的二进制数据处理来说Bites类是过度行为。
从文件中读取
其他的问题源于在你从文件或者其他流中读写二进制和Unicode数据并处理他们。一个常见的问题是文件以错误的模式打开。确保使用‘t’标志打开文本文件及使用‘b’标志打开二进制文件就可以解决很多问题。
使用‘t’标志打开文件在Python 3下会返回一个unicode对象并且它是使用在不同系统中不同的系统默认编码解码的。如果它使用其它的编码,你需要把他个编码作为一个参数传入open()函数。open()函数在Python 2不能带编码参数并且返回的是str对象。只要你的文件只包含ASCII字符这就不是问题,但是当这时你需要做一些修改。
使用二进制打开文件并在之后解码数据是一个选项,例如使用codecs.open()。但发生在Windows下的行尾转换在有些情况下不会起作用:
>>> import codecs>>> infile = codecs.open('UTF-8.txt', 'r', encoding='UTF-8')>>> print(infile.read())It wörks
Python 3的处理及打开方法包含在新的io模块里。这个模块被移植到Python 2.6和Python 2.7,所以如果你不需要支持Python 2.5及更早的版本,你可以把所有open()的调用替换成io.open()以达到Python 3的兼容。它不会遇到codecs.open()那样在Windows下的行结束符问题并且明确地使用newline=''参数指明没有换行符它实际在所有平台的文本模式下都转换了换行符。
>>> import io>>> infile = io.open('UTF-8.txt', 'rt', encoding='UTF-8')>>> print(infile.read())It wörks
但要注意io模块在Python 2.6和Python 3.0下是相当慢的,所以如果你要支持Python 2.6的处理加载数据,这对你来说或许不是一个正确的解决方式。
其他你需要使用二进制模式打开一个文件的情况是在你打开一部分之前不知道这个文件是文本还是二进制文件或者文件同时包含二进制和文本。那么你需要在二进制模式加打文件并且有条件地解码它,这在Python 2下你可能经常在二进制模式下打开并且跳过了解码。
另外,如果你要在文件里做大量的搜索,如果你你用文件模式打开文件会变得非常慢,因为搜索需要解码数据。在这种情况下你需要在二进制模式下打开文件,并且在读取文件之后再解码。
取代UserDict
当你想要写一个行为像字典却又不是的类时,UserDict模块是一个很流行的解决方案,因为你没必要自己实现所有的字典方法。然而UserDict模块在Python 3取消了,被合并到了collections模块里。因为字典在Python 3中如何工作的变化,items(),、keys() 和 values()返回视图取代列表并且在排序和比较中的变化也已经完善了,这些替换并不完全兼容。因为没有固定器(fixer)做这些修改,你必须要手动做这些。
在多数情况下它只是一个替换基本类的问题。UserDict.IterableUserDict被collections.UserDict替换并且UserDict.DictMixin是现在的collections.MutableMapping,UserDict.UserDict取消了,但是collections.UserDict将作为一个大多数时的解决方案而工作。
一个常见的问题是collections.MutableMapping需要你实现__len__ 和 __iter__而DictMixin不需要。不管怎么样,为了能在Python 3下工作而实现他们不会在Python 2下破坏任何事。
如果你需要同时支持Python 3和Python 2.6之后的版本,你还必须要有条件地导入:
>>> try:... from UserDict import UserDict... from UserDict import DictMixin... except ImportError:... from collections import UserDict... from collections import MutableMapping as DictMixin
CSV API 的变化
在Python 2,csv模块需要你在二进制模式下打开文件。这是由于这个模块需要允许控制换行符,因为典型的CSV文件使用的是DOS的换行符,并且Python 2下的文本模式在一些平台会改变换行符。csv模块也会返回预期的字节字符串。
在Python 3 csv模块取代的是需要你打开文件使用带newline=''参数的文本模式,并且它返回预期的Unicode字符串。
如果你需要同时支持Python 2和Python 3,并且你需要支持Unicode,我找到的最好的解决办法是使用“封装”类。下面的类可以在Python 2.6及之后的版本工作:
import sys, csv, codecsPY3 = sys.version_info > (3,)class UnicodeReader: def __init__(self, filename, dialect=csv.excel, encoding="utf-8", **kw): self.filename = filename self.dialect = dialect self.encoding = encoding self.kw = kw def __enter__(self): if PY3: self.f = open(self.filename, 'rt', encoding=self.encoding, newline='') else: self.f = open(self.filename, 'rb') self.reader = csv.reader(self.f, dialect=self.dialect, **self.kw) return self def __exit__(self, type, value, traceback): self.f.close() def next(self): row = next(self.reader) if PY3: return row return [s.decode("utf-8") for s in row] __next__ = next def __iter__(self): return selfclass UnicodeWriter: def __init__(self, filename, dialect=csv.excel, encoding="utf-8", **kw): self.filename = filename self.dialect = dialect self.encoding = encoding self.kw = kw def __enter__(self): if PY3: self.f = open(self.filename, 'wt', encoding=self.encoding, newline='') else: self.f = open(self.filename, 'wb') self.writer = csv.writer(self.f, dialect=self.dialect, **self.kw) return self def __exit__(self, type, value, traceback): self.f.close() def writerow(self, row): if not PY3: row = [s.encode(self.encoding) for s in row] self.writer.writerow(row) def writerows(self, rows): for row in rows: self.writerow(row)
DictReader和DictWriter可以很容易通过同时编码和解码键和值的方式来扩展。
执行文档测试
更多你可能遇到的持久恼人问题中的一个是文档测试。我个人认为文档测试对测试文档是美妙的,但是建议在一些回路里尽可能多地做文档测试。这在Python 3成了一个问题,因为文档测试依赖于对代码输出的比较。那意味着他们对格式化中的变化是敏感的并且Python 3有好几处是这样的。这意味着如果你做文档测试将会得到很多很多的故障。不要失去信心!他们中的大多数不是实际故障,除了对格式化输出的变化。2to3会处理代码文档测试的变化,但不会处理输出中的。
如果你只需要支持Python 3,解决办法是简单并乏味的。执行文档测试并查看每个故障看看是实际的故障还是格式化的变化。这有时候是令人沮丧的,因为你要坐着并注视着在预期和实际输出上有什么实际的不同。另一方面,这是即使你不使用Python 3进行文档测试的正常情况,这当然也是一个他们不适合成为项目测试主要构成的一个原因。
如果你需要继续支持Python 2它将会变得更加棘手,因为你需要写能同时在各版本工作的输出而且那会变得很因难并且在一个情况不可能写出异常测试的例子,见下文。
write()返回一个值
一个常见的Python 3下文档测试失败原因发生在写一个文件时。write()方法现在返回写入的字节数。Python 2的Doctests不会期望任何的返回值,所以他们将会中断。对这个的变通方案是很容易的并且在Python 2下能同样很好地起作用。只要给结果分配一个虚拟的值:
>>> ignore = open('/tmp/file.out', 'wt').write('Some text')
类形(Types)现在是类(classes)
还有,__repr__()输出的很多类型都变了。在Python 2内置的类把他们自己描述成类型。
>>> type([])
在Python 3,他们是类,就像其他的一样。
>>> type([])
如果你想要同时支持Python 2和Python 3的话这有两个选择。第一个是用来isinstance替代:
>>> isinstance([], list)True
另一个是允许文档测试的ELLIPSIS标志并使用三个点来替代变化的输出部分。
>>> type([])<... 'list'>
处理预期的异常
当你需要支持Python 2和Python 3时ELLIPSIS标志可以用于大多数你发现不同之处,但只能是一个异常,也就是异常。错误信息和回跟踪的输出现在包含异常的模块名。在Python 2查检一个异常将会像这样:
>>> import socket>>> socket.gethostbyname(" Traceback (most recent call last):gaierror: [Errno -2] Name or service not known
然而,在Python 3错误信息和回跟踪包括模块名,所有你可以看到它像这样:
>>> import socket>>> socket.gethostbyname(" Traceback (most recent call last):socket.gaierror: [Errno -2] Name or service not known
除此这外,一些异常(Exceptions)因为标准库总的整改而被移除。你不能使用ELLIPSIS标志并且把一个省略号放进异常定义的开头,因为如果你添加的文档测试不久会把输出识别成一个异常,它在所有版本的Python都会停止工作!这个是原因是软中断异常:
>>> import socket>>> try:... socket.gethostbyname(" ... raise AssertionError("gaierror exception was not raised")... except socket.gaierror:... pass
它不是一个完美的解决方案,但这是现在唯一能用的一个。幸运的是大多数常见的内置模块的异常没有改变他们的描述,所以它们可以继续工作。你只需要用来自标准模块I或者第三方模块的异常来修改这些。Python 2.7的IGNORE_EXCEPTION_DETAIL标志已经被扩展,因此它可以处理在异常格式里的不同。然而它不是不能在Python 2.6下工作,所以如果你需要支持PYthon 2.6或者更早的版本,你需要重写测试来限制异常。
如果我做带有很多异常测试的文档测试,我经常会停止使用帮助函数 ,类似于标准单元测试里的assertRaises:
import sysdef shouldRaise(eclass, method, *args, **kw): try: method(*args, **kw) except: e = sys.exc_info()[1] if not isinstance(e, eclass): raise return raise Exception("Expected exception %s not raised" % str(eclass))
这个的用法就像这样:
>>> import socket>>> shouldRaise(socket.gaierror,... socket.gethostbyname, "www.python.rog")
字符表达
从像读取网站这样的返回二进制数据的函数的输出在Python 3会返回字节,这有一个不同的表达。
解决这个问题,我使用一个确保在打印前输出是一个字符串的帮助函数:
>>> def bprint(data):... if not isinstance(data, str):... data = data.decode()... print(data.strip())
它也为了适度移除了前导和后续的空格,因此你没必须在代码写很多<BLANKLINE>声明。
dict 和set的顺序
在Python 3.3 因为安全的原因一个随机种子值已经被加进了哈希函数。这意味着当你尝试在Python 3.3做的任何测试字典或者集合输出的文档测试都会失败,因为每一次运行时顺序都会改变。
Failed example: {x for x in department}Expected: {'a', ' ', 'i', 'k', 'l', 'S', 'W', 'y'}Got: {'y', 'S', 'W', 'i', 'k', 'l', 'a', ' '}
这在会不幸地使失败提供更少信息的相等性测试时必须要修改。
Failed example: {x for x in department} == \ {'a', ' ', 'i', 'e', 'l', 'S', 'W', 'y'}Expected: TrueGot: False
在湖闻樟注:
原文
引导页
目录