数据人:不懂业务,分析就仅仅只是提数

数据人:不懂业务,分析就仅仅只是提数

01. 为什么使用缩进来分组语句?

Guido van Rossum 认为使用缩进进行分组非常优雅,并且大大提高了普通 Python 程序的清晰度。大多数人在一段时间后就学会并喜欢上这个功能。

由于没有开始/结束括号,因此解析器感知的分组与人类读者之间不会存在分歧。偶尔 C 程序员会遇到像这样的代码片段:

if (x <= y) x++; y--;z++;

如果条件为真,则只执行 x++语句,但缩进会使你认为情况并非如此。即使是经验丰富的 C 程序员有时会长时间盯着它,想知道为什么即使x > y , y 也在减少。

因为没有开始/结束括号,所以 Python 不太容易发生编码式冲突。在 C 中,括号可以放到许多不同的位置。如果您习惯于阅读和编写使用一种风格的代码,那么在阅读(或被要求编写)另一种风格时,您至少会感到有些不安。

许多编码风格将开始/结束括号单独放在一行上。这使得程序相当长,浪费了宝贵的屏幕空间,使得更难以对程序进行全面的了解。理想情况下,函数应该适合一个屏幕(例如,20--30 行)。20 行 Python 可以完成比 20 行 C 更多的工作。这不仅仅是由于缺少开始/结束括号 -- 缺少声明和高级数据类型也是其中的原因 -- 但缩进基于语法肯定有帮助

02. 为什么简单的算术运算得到奇怪的结果?

请看下一个问题。

03. 为什么浮点计算不准确?

用户经常对这样的结果感到惊讶:

>>> 1.2 - 1.00.19999999999999996

并且认为这是 Python 中的一个 bug。其实不是这样。这与 Python 关系不大,而与底层平台如何处理浮点数字关系更大。

CPython 中的 float类型使用 C 语言的double 类型进行存储。float 对象的值是以固定的精度(通常为 53 位)存储的二进制浮点数,由于 Python 使用 C 操作,而后者依赖于处理器中的硬件实现来执行浮点运算。这意味着就浮点运算而言,Python 的行为类似于许多流行的语言,包括 C 和 Java。

许多可以轻松地用十进制表示的数字不能用二进制浮点表示。例如,在输入以下语句后:

>>> x = 1.2

x

存储的值是与十进制的值 1.2 (非常接近) 的近似值,但不完全等于它。在典型的机器上,实际存储的值是:

1.0011001100110011001100110011001100110011001100110011 (binary)

它对应于十进制数值:

1.1999999999999999555910790149937383830547332763671875 (decimal)

典型的 53 位精度为 Python 浮点数提供了 15-16 位小数的精度。

04. 为什么 Python 字符串是不可变的?

有几个优点。

一个是性能:知道字符串是不可变的,意味着我们可以在创建时为它分配空间,并且存储需求是固定不变的。这也是元组和列表之间区别的原因之一。

另一个优点是,Python 中的字符串被视为与数字一样“基本”。任何动作都不会将值 8 更改为其他值,在 Python 中,任何动作都不会将字符串 "8" 更改为其他值。

05. 异常有多快?

如果没有引发异常,则 try/except 块的效率极高。实际上捕获异常是昂贵的。在 2.0 之前的 Python 版本中,通常使用这个习惯用法:

try:value = mydict[key]except KeyError:mydict[key] = getvalue(key)value = mydict[key]

只有当你期望 dict 在任何时候都有 key 时,这才有意义。如果不是这样的话,你就是应该这样编码:

if key in mydict:value = mydict[key]else:value = mydict[key] = getvalue(key)

对于这种特定的情况,您还可以使用 value = dict.setdefault(key, getvalue(key)),但前提是调用getvalue()足够便宜,因为在所有情况下都会对其进行评估。

06. 为什么 Python 中没有 switch 或 case 语句?

你可以通过一系列if... elif... elif... else.轻松完成这项工作。对于 switch 语句语法已经有了一些建议,但尚未就是否以及如何进行范围测试达成共识。

对于需要从大量可能性中进行选择的情况,可以创建一个字典,将 case 值映射到要调用的函数。例如:

def function_1(...):

functions = {'a': function_1,

'b': function_2,

'c': self.method_1, ...}

func = functions[value]

func()

对于对象调用方法,可以通过使用getattr() 内置检索具有特定名称的方法来进一步简化:

def visit_a(self, ...):

def dispatch(self, value):

method_name = 'visit_' + str(value)

method = getattr(self, method_name)

method()

建议对方法名使用前缀,例如本例中的visit_ 。如果没有这样的前缀,如果值来自不受信任的源,攻击者将能够调用对象上的任何方法。

07. 难道不能在解释器中模拟线程,而非得依赖特定于操作系统的线程实现吗?

答案 1:不幸的是,解释器为每个 Python 堆栈帧推送至少一个 C 堆栈帧。此外,扩展可以随时回调 Python。因此,一个完整的线程实现需要对 C 的线程支持。

答案 2:幸运的是, Stackless Python 有一个完全重新设计的解释器循环,可以避免 C 堆栈。

08. 为什么 lambda 表达式不包含语句?

Python 的 lambda 表达式不能包含语句,因为 Python 的语法框架不能处理嵌套在表达式内部的语句。然而,在 Python 中,这并不是一个严重的问题。与其他语言中添加功能的 lambda 表单不同,Python 的 lambdas 只是一种速记符号,如果您懒得定义函数的话。

函数已经是 Python 中的第一类对象,可以在本地范围内声明。因此,使用 lambda 而不是本地定义的函数的唯一优点是你不需要为函数创建一个名称 -- 这只是一个分配了函数对象(与 lambda 表达式生成的对象类型完全相同)的局部变量!

09. 可以将 Python 编译为机器代码,C 或其他语言吗?

Cython 将带有可选注释的 Python 修改版本编译到 C 扩展中。Nuitka 是一个将 Python 编译成 C++ 代码的新兴编译器,旨在支持完整的 Python 语言。要编译成 Java,可以考虑 VOC 。

10. Python 如何管理内存?

Python 内存管理的细节取决于实现。Python 的标准实现 CPython 使用引用计数来检测不可访问的对象,并使用另一种机制来收集引用循环,定期执行循环检测算法来查找不可访问的循环并删除所涉及的对象。gc 模块提供了执行垃圾回收、获取调试统计信息和优化收集器参数的函数。

但是,其他实现(如 Jython 或 PyPy ),)可以依赖不同的机制,如完全的垃圾回收器 。如果你的 Python 代码依赖于引用计数实现的行为,则这种差异可能会导致一些微妙的移植问题。

在一些 Python 实现中,以下代码(在 CPython 中工作的很好)可能会耗尽文件描述符:

for file in very_long_list_of_files: f = open(file)c = f.read(1)

实际上,使用 CPython 的引用计数和析构函数方案, 每个新赋值的f 都会关闭前一个文件。然而,对于传统的 GC,这些文件对象只能以不同的时间间隔(可能很长的时间间隔)被收集(和关闭)。

如果要编写可用于任何 python 实现的代码,则应显式关闭该文件或使用with 语句;无论内存管理方案如何,这都有效:

for file in very_long_list_of_files: with open(file) as f:c = f.read(1)

11. 为什么 CPython 不使用更传统的垃圾回收方案?

首先,这不是 C 标准特性,因此不能移植。(是的,我们知道 Boehm GC 库。它包含了大多数 常见平台(但不是所有平台)的汇编代码,尽管它基本上是透明的,但也不是完全透明的; 要让 Python 使用它,需要使用补丁。)

当 Python 嵌入到其他应用程序中时,传统的 GC 也成为一个问题。在独立的 Python 中,可以用 GC 库提供的版本替换标准的 malloc()和 free(),嵌入 Python 的应用程序可能希望用 它自己替代 malloc()和 free(),而可能不需要 Python 的。现在,CPython 可以正确地实现 malloc()和 free()。

12. CPython 退出时为什么不释放所有内存?

当 Python 退出时,从全局命名空间或 Python 模块引用的对象并不总是被释放。如果存在循环引用,则可能发生这种情况 C 库分配的某些内存也是不可能释放的(例如像 Purify 这样的工具会抱怨这些内容)。但是,Python 在退出时清理内存并尝试销毁每个对象。

如果要强制 Python 在释放时删除某些内容,请使用 atexit模块运行一个函数,强制删除这些内容。

13. 为什么有单独的元组和列表数据类型?

虽然列表和元组在许多方面是相似的,但它们的使用方式通常是完全不同的。可以认为元组类似于 Pascal 记录或 C 结构;它们是相关数据的小集合,可以是不同类型的数据,可以作为一个组进行操作。例如,笛卡尔坐标适当地表示为两个或三个数字的元组。

另一方面,列表更像其他语言中的数组。它们倾向于持有不同数量的对象,所有对象都具有相同的类型,并且逐个操作。例如,os.listdir('.') 返回表示当前目录中的文件的字符串列表。如果向目录中添加了一两个文件,对此输出进行操作的函数通常不会中断。

元组是不可变的,这意味着一旦创建了元组,就不能用新值替换它的任何元素。列表是可变的,这意味着您始终可以更改列表的元素。只有不变元素可以用作字典的 key,因此只能将元组和非列表用作 key。

14. 列表如何在 CPython 中实现?

CPython 的列表实际上是可变长度的数组,而不是 lisp 风格的链表。该实现使用对其他对象的引用的连续数组,并在列表头结构中保留指向该数组和数组长度的指针。

这使得索引列表a[i]的操作成本与列表的大小或索引的值无关。

当添加或插入项时,将调整引用数组的大小。并采用了一些巧妙的方法来提高重复添加项的性能; 当数组必须增长时,会分配一些额外的空间,以便在接下来的几次中不需要实际调整大小。

15. 字典如何在 CPython 中实现?

CPython 的字典实现为可调整大小的哈希表。与 B-树相比,这在大多数情况下为查找(目前最常见的操作)提供了更好的性能,并且实现更简单。

字典的工作方式是使用hash() 内置函数计算字典中存储的每个键的 hash 代码。hash 代码根据键和每个进程的种子而变化很大;例如,"Python" 的 hash 值为-539294296,而"python"(一个按位不同的字符串)的 hash 值为 1142331976。然后,hash 代码用于计算内部数组中将存储该值的位置。假设您存储的键都具有不同的 hash 值,这意味着字典需要恒定的时间 -- O(1),用 Big-O 表示法 -- 来检索一个键。


分享到:


相關文章: