文档开发者中心
我的

收藏 | 27个Python设计和历史问题(下篇)

为什么字典key必须是不可变的?字典的哈希表实现使用从键值计算的哈希值来查找键。如果键是可变对象,则其值可能会发生变化,因此其哈希值也会发生变化。但是,由于无论谁更改键对象都无法判断它是否被用作字典键值,因此无法在字典中修改条目。然后,当你尝试在字典中查找相同的对象时,将无法找到它,因为其哈希值不同。如果你尝试查找旧值,也不会找到它,因为在该哈希表中找到的对象的值会有所不同。如果你想要一个用列表索引的字典,只需先将列表转换为元组;用函数 tuple(L) 创建一个元组,其条目与列表 L相同。元组是不可变的,因此可以用作字典键。已经提出的一些不可接受的解决方案:哈希按其地址(对象ID)列出。这不起作用,因为如果你构造一个具有相同值的新列表,它将无法找到;例如:
mydict = {[1, 2]: '12'}
print(mydict[[1, 2]])
会引发一个 KeyError 异常,因为第二行中使用的 [1, 2] 的 id 与第一行中的 id 不同。换句话说,应该使用 == 来比较字典键,而不是使用 is 。使用列表作为键时进行复制。这没有用的,因为作为可变对象的列表可以包含对自身的引用,然后复制代码将进入无限循环。允许列表作为键,但告诉用户不要修改它们。当你意外忘记或修改列表时,这将产生程序中的一类难以跟踪的错误。它还使一个重要的字典不变量无效:d.keys() 中的每个值都可用作字典的键。将列表用作字典键后,应标记为其只读。问题是,它不仅仅是可以改变其值的顶级对象;你可以使用包含列表作为键的元组。将任何内容作为键关联到字典中都需要将从那里可到达的所有对象标记为只读 —— 并且自引用对象可能会导致无限循环。如果需要,可以使用以下方法来解决这个问题,但使用它需要你自担风险:你可以将一个可变结构包装在一个类实例中,该实例同时具有 eq__() 和 __hash() 方法。然后,你必须确保驻留在字典(或其他基于 hash 的结构)中的所有此类包装器对象的哈希值在对象位于字典(或其他结构)中时保持固定。
class ListWrapper:
   def __init__(self, the_list):
       self.the_list = the_list

   def __eq__(self, other):
       return self.the_list == other.the_list

   def __hash__(self):
       l = self.the_list
       result = 98767 - len(l)*555
       for i, el in enumerate(l):
           try:
               result = result + (hash(el) % 9999999) * 1001 + i
           except Exception:
               result = (result % 7777777) + i * 333
       return result
注意,哈希计算由于列表的某些成员可能不可用以及算术溢出的可能性而变得复杂。此外,必须始终如此,如果 o1 == o2 (即 o1.eq__(o2) is True )则 hash(o1) == hash(o2)(即o1._hash_() == o2.__hash() ),无论对象是否在字典中。如果你不能满足这些限制,字典和其他基于 hash 的结构将会出错。对于 ListWrapper ,只要包装器对象在字典中,包装列表就不能更改以避免异常。除非你准备好认真考虑需求以及不正确地满足这些需求的后果,否则不要这样做。请留意。为什么 list.sort() 没有返回排序列表?在性能很重要的情况下,仅仅为了排序而复制一份列表将是一种浪费。因此, list.sort() 对列表进行了适当的排序。为了提醒您这一事实,它不会返回已排序的列表。这样,当您需要排序的副本,但也需要保留未排序的版本时,就不会意外地覆盖列表。如果要返回新列表,请使用内置 sorted() 函数。此函数从提供的可迭代列表中创建新列表,对其进行排序并返回。例如,下面是如何迭代遍历字典并按keys排序:
for key in sorted(mydict):
   ...  # do whatever with mydict[key]...
如何在Python中指定和实施接口规范?由C++和Java等语言提供的模块接口规范描述了模块的方法和函数的原型。许多人认为接口规范的编译时强制执行有助于构建大型程序。Python 2.6添加了一个 abc 模块,允许定义抽象基类 (ABCs)。然后可以使用 isinstance() 和 issubclass() 来检查实例或类是否实现了特定的ABC。collections.abc 模块定义了一组有用的ABCs 例如 Iterable , Container , 和 MutableMapping对于Python,通过对组件进行适当的测试规程,可以获得接口规范的许多好处。还有一个工具PyChecker,可用于查找由于子类化引起的问题。一个好的模块测试套件既可以提供回归测试,也可以作为模块接口规范和一组示例。许多Python模块可以作为脚本运行,以提供简单的“自我测试”。即使是使用复杂外部接口的模块,也常常可以使用外部接口的简单“桩代码(stub)”模拟进行隔离测试。可以使用 doctest 和 unittest 模块或第三方测试框架来构造详尽的测试套件,以运行模块中的每一行代码。适当的测试规程可以帮助在Python中构建大型的、复杂的应用程序以及接口规范。事实上,它可能会更好,因为接口规范不能测试程序的某些属性。例如, append() 方法将向一些内部列表的末尾添加新元素;接口规范不能测试您的 append() 实现是否能够正确执行此操作,但是在测试套件中检查这个属性是很简单的。编写测试套件非常有用,您可能希望设计代码时着眼于使其易于测试。一种日益流行的技术是面向测试的开发,它要求在编写任何实际代码之前,首先编写测试套件的各个部分。当然,Python允许您草率行事,根本不编写测试用例。为什么没有goto?可以使用异常捕获来提供 “goto结构” ,甚至可以跨函数调用工作的 。许多人认为异常捕获可以方便地模拟C,Fortran和其他语言的 'go' 或 'goto' 结构的所有合理用法。例如:
class label(Exception): pass  # declare a label

try:
   ...
   if condition: raise label()  # goto label
   ...
except label:  # where to goto
   pass
...
但是不允许你跳到循环的中间,这通常被认为是滥用goto。谨慎使用。为什么原始字符串(r-strings)不能以反斜杠结尾?更准确地说,它们不能以奇数个反斜杠结束:结尾处的不成对反斜杠会转义结束引号字符,留下未结束的字符串。原始字符串的设计是为了方便想要执行自己的反斜杠转义处理的处理器(主要是正则表达式引擎)创建输入。此类处理器将不匹配的尾随反斜杠视为错误,因此原始字符串不允许这样做。反过来,允许通过使用引号字符转义反斜杠转义字符串。当r-string用于它们的预期目的时,这些规则工作的很好。如果您正在尝试构建Windows路径名,请注意所有Windows系统调用都使用正斜杠:
f = open('/mydir/file.txt')  # works fine!
如果您正在尝试为DOS命令构建路径名,请尝试以下示例
dir = r'\this\is\my\dos\dir' '\\'
dir = r'\this\is\my\dos\dir\ '[:-1]
dir = '\\this\\is\\my\\dos\\dir\\'
为什么Python没有属性赋值的“with”语句?Python有一个 'with' 语句,它封装了块的执行,在块的入口和出口调用代码。有些语言的结构是这样的:
with obj:
   a = 1               # equivalent to obj.a = 1
   total = total + 1   # obj.total = obj.total + 1
在Python中,这样的结构是不明确的。其他语言,如ObjectPascal、Delphi和C++ 使用静态类型,因此可以毫不含糊地知道分配给什么成员。这是静态类型的要点 -- 编译器 总是 在编译时知道每个变量的作用域。Python使用动态类型。事先不可能知道在运行时引用哪个属性。可以动态地在对象中添加或删除成员属性。这使得无法通过简单的阅读就知道引用的是什么属性:局部属性、全局属性还是成员属性?例如,采用以下不完整的代码段:
def foo(a):
   with a:
       print(x)
该代码段假设 'a' 必须有一个名为 'x' 的成员属性。然而,Python中并没有告诉解释器这一点。假设 'a' 是整数,会发生什么?如果有一个名为 'x' 的全局变量,它是否会在with块中使用?如您所见,Python的动态特性使得这样的选择更加困难。然而,Python 可以通过赋值轻松实现 'with' 和类似语言特性(减少代码量)的主要好处。代替:
function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63
写成这样:
ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63
这也具有提高执行速度的副作用,因为Python在运行时解析名称绑定,而第二个版本只需要执行一次解析。为什么 if/while/def/class语句需要冒号?冒号主要用于增强可读性(ABC语言实验的结果之一)。考虑一下这个:
if a == b
   print(a)
与

if a == b:
   print(a)
注意第二种方法稍微容易一些。请进一步注意,在这个FAQ解答的示例中,冒号是如何设置的;这是英语中的标准用法。另一个次要原因是冒号使带有语法突出显示的编辑器更容易工作;他们可以寻找冒号来决定何时需要增加缩进,而不必对程序文本进行更精细的解析。为什么Python在列表和元组的末尾允许使用逗号?
Python 允许您在列表,元组和字典的末尾添加一个尾随逗号:
[1, 2, 3,]
('a', 'b', 'c',)
d = {
   'A': [1, 5],
   'B': [6, 7],  # last trailing comma is optional but good style
}
有几个理由允许这样做。如果列表,元组或字典的字面值分布在多行中,则更容易添加更多元素,因为不必记住在上一行中添加逗号。这些行也可以重新排序,而不会产生语法错误。不小心省略逗号会导致难以诊断的错误。例如:
x = [
 'fee',
 'fie'
 'foo',
 'fum'
]
这个列表看起来有四个元素,但实际上包含三个 : 'fee', 'fiefoo' 和 'fum' 。总是加上逗号可以避免这个错误的来源。允许尾随逗号也可以使编程代码更容易生成。


点赞
收藏
回复
分享
举报
浏览256 编辑于2020-04-26 11:28未知归属地
全部评论
最多点赞
最新发布
最早发布
暂无评论
暂无评论
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要发帖子

了解社区公约,与您携手共创和谐专业的开发者社区。