Python:鲜为人知的功能特性(下)

栏目: Python · 发布时间: 6年前

内容简介:GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理! 原版地址:上一篇原本每个的标题都是原版中的英文,有些取名比较奇怪,不直观,我换成了可以描述主题的中文形式,有些是自己想的,不足之处请指正。另外一些

GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理! 原版地址: github.com/satwikkansa… 。 最近,一位名为“暮晨”的贡献者将其翻译成了中文。 中文版地址: github.com/leisurelich…

上一篇 Python:鲜为人知的功能特性(上)

原本每个的标题都是原版中的英文,有些取名比较奇怪,不直观,我换成了可以描述主题的中文形式,有些是自己想的,不足之处请指正。另外一些 Python 中的彩蛋被我去掉了。

我将所有代码都亲自试过了,加入了一些自己的理解和例子,所以会和原文稍有不同。

21. 子类关系

>>> from collections import Hashable
>>> issubclass(list, object)
True
>>> issubclass(object, Hashable)
True
>>> issubclass(list, Hashable)
False
复制代码

子类关系应该是可传递的,对吧?即,如果 AB 的子类, BC 的子类,那么 A 应该 是 C 的子类。 说明:

  • Python 中的子类关系并不必须是传递的,任何人都可以在元类中随意定义 __subclasscheck__
  • issubclass(cls, Hashable) 被调用时,它只是在 cls 中寻找 __hash__() 方法或继承自 __hash__() 的方法。
  • 由于 object 是可散列的(hashable),而 list 是不可散列的,所以它打破了这种传递关系。

22. 神秘的键型转换

class SomeClass(str):
    pass

some_dict = {'s': 42}
复制代码

Output:

>>> type(list(some_dict.keys())[0])
<class 'str'>
>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict # 预期: 两个不同的键值对
{'s': 40}
>>> type(list(some_dict.keys())[0])
<class 'str'>
复制代码

说明:

  • 由于 SomeClass 会从 str 自动继承 __hash__() 方法,所以 s 对象和 's' 字符串的哈希值是相同的。
  • SomeClass('s') == 's'True 是因为 SomeClass 也继承了 str__eq__() 方法。
  • 由于两者的哈希值相同且相等,所以它们在字典中表示相同的键。

如果想要实现期望的功能, 我们可以重定义 SomeClass__eq__() 方法.

class SomeClass(str):
  def __eq__(self, other):
      return (
          type(self) is SomeClass
          and type(other) is SomeClass
          and super().__eq__(other)
      )

  # 当我们自定义 __eq__() 方法时, Python 不会再自动继承 __hash__() 方法
  # 所以我们也需要定义它
  __hash__ = str.__hash__

some_dict = {'s':42}
复制代码

Output:

>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict
{'s': 40, 's': 42}
>>> keys = list(some_dict.keys())
>>> type(keys[0]), type(keys[1])
<class 'str'> <class '__main__.SomeClass'>
复制代码

23. 链式赋值表达式

>>> a, b = a[b] = {}, 5
>>> a
{5: ({...}, 5)}
复制代码

说明:根据Python 语言参考,赋值语句的形式如下:

(target_list "=")+ (expression_list | yield_expression)
复制代码

赋值语句计算表达式列表( expression list )(请记住,这可以是单个表达式或以逗号分隔的列表,后者返回元组)并将单个结果对象从左到右分配给目标列表中的每一项。

(target_list "=")+ 中的 + 意味着可以有一个或多个目标列表。在这个例子中,目标列表是 a, ba[b] 。表达式列表只能有一个,是 {}, 5

这话看着非常的晦涩,我们来看一个简单的例子:

a, b = b, c = 1, 2
print(a, b, c)
复制代码

Output:

1 1 2
复制代码

在这个简单的例子中,目标列表是 a, bb, c ,表达式是 1, 2 。将表达式从左到右赋给目标列表,上述例子就可以拆分成:

a, b = 1, 2
b, c = 1, 2
复制代码

所以结果就是 1 1 2

那么,原例子就不难理解了,拆解开来就是:

a, b = {}, 5
a[b] = a, b
复制代码

这里不能写作 a[b] = {}, 5 ,因为这样第一句中的 {} 和第二句中的 {} 其实就是不同的对象了,而实际他们是同一个对象。这就形成了循环引用,输出中的 {...} 指与 a 引用了相同的对象。

我们来验证一下:

>>> a[b][0] is a
True
复制代码

可见确实是同一个对象。

以下是一个简单的循环引用的例子:

>>> some_list = some_list[0] = [0]
>>> some_list
[[...]]
>>> some_list[0]
[[...]]
>>> some_list is some_list[0]
True
>>> some_list[0][0][0][0][0][0] == some_list
True
复制代码

24. 空间移动

import numpy as np

def energy_send(x):
    # 初始化一个 numpy 数组
    np.array([float(x)])

def energy_receive():
    # 返回一个空的 numpy 数组
    return np.empty((), dtype=np.float).tolist()
复制代码

Output:

>>> energy_send(123.456)
>>> energy_receive()
123.456
复制代码

说明: energy_send() 函数中创建的 numpy 数组并没有返回,因此内存空间被释放并可以被重新分配。 numpy.empty() 直接返回下一段空闲内存,而不重新初始化。而这个内存点恰好就是刚刚释放的那个(通常情况下,并不绝对)。

25. 不要混用制表符(tab)和空格(space)

tab 是 8 个空格,而用空格表示则一个缩进是 4 个空格,混用就会出错。 python3 里直接不允许这种行为了,会报错:

TabError: inconsistent use of tabs and spaces in indentation

很多编辑器,例如 pycharm ,可以直接设置 tab 表示 4 个空格。

Python:鲜为人知的功能特性(下)

26. 迭代字典时的修改

x = {0: None}

for i in x:
    del x[i]
    x[i+1] = None
    print(i)
复制代码

Output(Python 2.7- Python 3.5):

0
1
2
3
4
5
6
7
复制代码

说明: Python 不支持 对字典进行迭代的同时修改它,它之所以运行 8 次,是因为字典会自动扩容以容纳更多键值(译: 应该是因为字典的初始最小值是8,扩容会导致散列表地址发生变化而中断循环)。 在不同的 Python 实现中删除键的处理方式以及调整大小的时间可能会有所不同, python3.6 开始,到 5 就会扩容。

而在 list 中,这种情况是允许的, listdict 的实现方式是不一样的, list 虽然也有扩容,但 list 的扩容是整体搬迁,并且顺序不变。

list = [1]
j = 0
for i in list:
    print(i)
    list.append(i + 1)
复制代码

这个代码可以一直运行下去直到 int 越界。但一般不建议在迭代的同时修改 list

27. _ del _

class SomeClass:
    def __del__(self):
        print("Deleted!")
复制代码

Output:

>>> x = SomeClass()
>>> y = x
>>> del x  # 这里应该会输出 "Deleted!"
>>> del y
Deleted!
复制代码

说明: del x 并不会立刻调用 x.__del__() ,每当遇到 del xPython 会将 x 的引用数减 1,当 x 的引用数减到 0 时就会调用 x.__del__()

我们再加一点变化:

>>> x = SomeClass()
>>> y = x
>>> del x
>>> y  # 检查一下y是否存在
<__main__.SomeClass instance at 0x7f98a1a67fc8>
>>> del y # 像之前一样,这里应该会输出 "Deleted!"
>>> globals() # 好吧, 并没有。让我们看一下所有的全局变量
Deleted!
{'__builtins__': <module '__builtin__' (built-in)>, 'SomeClass': <class __main__.SomeClass at 0x7f98a1a5f668>, '__package__': None, '__name__': '__main__', '__doc__': None}
复制代码

y.__del__() 之所以未被调用,是因为前一条语句( >>> y )对同一对象创建了另一个引用,从而防止在执行 del y 后对象的引用数变为 0。(这其实是 Python 交互解释器的特性,它会自动让 _ 保存上一个表达式输出的值。) 调用 globals() 导致引用被销毁,因此我们可以看到 Deleted! 终于被输出了。

28. 迭代列表时删除元素

在前面我附加了一个迭代列表时添加元素的例子,现在来看看迭代列表时删除元素。

list_1 = [1, 2, 3, 4]
list_2 = [1, 2, 3, 4]
list_3 = [1, 2, 3, 4]
list_4 = [1, 2, 3, 4]

for idx, item in enumerate(list_1):
    del item

for idx, item in enumerate(list_2):
    list_2.remove(item)

for idx, item in enumerate(list_3[:]):
    list_3.remove(item)

for idx, item in enumerate(list_4):
    list_4.pop(idx)
复制代码

Output:

>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]
复制代码

说明:在迭代时修改对象是一个很愚蠢的主意,正确的做法是迭代对象的副本, list_3[:] 就是这么做的。

del、remove、pop 的不同:

  • del var_name 只是从本地或全局命名空间中删除了 var_name(这就是为什么 list_1 没有受到影响)。
  • remove 会删除第一个匹配到的指定值,而不是特定的索引,如果找不到值则抛出 ValueError 异常。
  • pop 则会删除指定索引处的元素并返回它,如果指定了无效的索引则抛出 IndexError 异常。

为什么输出是 [2, 4]?列表迭代是按索引进行的,所以当我们从 list_2list_4 中删除 1 时,列表的内容就变成了 [2, 3, 4] 。剩余元素会依次位移,也就是说, 2 的索引会变为 0, 3 会变为 1。由于下一次迭代将获取索引为 1 的元素(即 3 ), 因此 2 将被彻底的跳过。类似的情况会交替发生在列表中的每个元素上。

29. 循环变量泄漏!

for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')
复制代码

Output:

6 : for x inside loop
6 : x in global
复制代码

# 这次我们先初始化x
x = -1
for x in range(7):
    if x == 6:
        print(x, ': for x inside loop')
print(x, ': x in global')
复制代码

Output:

6 : for x inside loop
6 : x in global
复制代码

x = 1
print([x for x in range(5)])
print(x, ': x in global')
复制代码

Output(Python 2):

[0, 1, 2, 3, 4]
(4, ': x in global')
复制代码

Output(Python 3):

[0, 1, 2, 3, 4]
1 : x in global
复制代码

说明:在 Python 中, for 循环使用所在作用域并在结束后保留定义的循环变量。如果我们曾在全局命名空间中定义过循环变量,它会重新绑定现有变量。 Python 2.xPython 3.x 解释器在列表推导式示例中的输出差异,在文档 What’s New In Python 3.0 中可以找到相关的解释:

"列表推导不再支持句法形式 [... for var in item1, item2, ...] 。使用 [... for var in (item1, item2, ...)] 代替。另外注意,列表推导具有不同的语义:它们更接近于 list() 构造函数中生成器表达式的语法糖,特别是循环控制变量不再泄漏到周围的作用域中。"

简单来说,就是 python2 中,列表推导式依然存在循环控制变量泄露,而 python3 中不存在。

30. 当心默认的可变参数!

def some_func(default_arg=[]):
    default_arg.append("some_string")
    return default_arg
复制代码

Output:

>>> some_func()
['some_string']
>>> some_func()
['some_string', 'some_string']
>>> some_func([])
['some_string']
>>> some_func()
['some_string', 'some_string', 'some_string']
复制代码

说明: Python 中函数的默认可变参数并不是每次调用该函数时都会被初始化。相反,它们会使用最近分配的值作为默认值。当我们明确的将 [] 作为参数传递给 some_func 的时候,就不会使用 default_arg 的默认值, 所以函数会返回我们所期望的结果。

>>> some_func.__defaults__ # 这里会显示函数的默认参数的值
([],)
>>> some_func()
>>> some_func.__defaults__
(['some_string'],)
>>> some_func()
>>> some_func.__defaults__
(['some_string', 'some_string'],)
>>> some_func([])
>>> some_func.__defaults__
(['some_string', 'some_string'],)
复制代码

避免可变参数导致的错误的常见做法是将 None 指定为参数的默认值,然后检查是否有值传给对应的参数。例:

def some_func(default_arg=None):
    if not default_arg:
        default_arg = []
    default_arg.append("some_string")
    return default_arg
复制代码

31. 捕获异常

这里讲的是 python2

some_list = [1, 2, 3]
try:
    # 这里会抛出异常 ``IndexError``
    print(some_list[4])
except IndexError, ValueError:
    print("Caught!")

try:
    # 这里会抛出异常 ``ValueError``
    some_list.remove(4)
except IndexError, ValueError:
    print("Caught again!")
复制代码

Output:

Caught!

ValueError: list.remove(x): x not in list
复制代码

说明:如果你想要同时捕获多个不同类型的异常时,你需要将它们用括号包成一个元组作为第一个参数传递。第二个参数是可选名称,如果你提供,它将与被捕获的异常实例绑定。 也就是说,代码原意是捕获 IndexError, ValueError 两种异常,但在 python2 中,必须写成 (IndexError, ValueError) ,示例中的写法解析器会将 ValueError 理解成绑定的异常实例名。 在 python3 中,不会有这种误解,因为必须使用 as 关键字。

32. +=就地修改

a = [1, 2, 3, 4]
b = a
a = a + [5, 6, 7, 8]
复制代码

Output:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4]
复制代码

a = [1, 2, 3, 4]
b = a
a += [5, 6, 7, 8]
复制代码

Output:

>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]
复制代码

说明: a += b 并不总是与 a = a + b 表现相同。 表达式 a = a + [5,6,7,8] 会生成一个新列表,并让 a 引用这个新列表,同时保持 b 不变。 表达式 a += [5, 6, 7, 8] 实际上是使用的是 extend() 函数,就地修改列表,所以 ab 仍然指向已被修改的同一列表。

33. 外部作用域变量

a = 1
def some_func():
    return a

def another_func():
    a += 1
    return a
复制代码

Output:

>>> some_func()
1
>>> another_func()
UnboundLocalError: local variable 'a' referenced before assignment
复制代码

说明:当在函数中引用外部作用域的变量时,如果不对这个变量进行修改,则可以直接引用,如果要对其进行修改,则必须使用 global 关键字,否则解析器将认为这个变量是局部变量,而做修改之前并没有定义它,所以会报错。

def another_func()
    global a
    a += 1
    return a
复制代码

Output:

>>> another_func()
2
复制代码

34. 小心链式操作

>>> (False == False) in [False] # 可以理解
False
>>> False == (False in [False]) # 可以理解
False
>>> False == False in [False] # 为毛?
True

>>> True is False == False
False
>>> False is False is False
True

>>> 1 > 0 < 1
True
>>> (1 > 0) < 1
False
>>> 1 > (0 < 1)
False
复制代码

根据 docs.python.org/2/reference…

形式上,如果 a, b, c, ..., y, z 是表达式,而 op1, op2, ..., opN 是比较运算符,那么 a op1 b op2 c ... y opN z 就等于 a op1 b and b op2 c and ... y opN z,除了每个表达式最多被评估一次。

  • False == False in [False] 就相当于 False == False and False in [False]
  • 1 > 0 < 1 就相当于 1 > 0 and 0 < 1

虽然上面的例子似乎很愚蠢,但是像 a == b == c0 <= x <= 100 就很棒了。

35. 忽略类作用域的名称解析

① 生成器表达式

x = 5
class SomeClass:
    x = 17
    y = (x for i in range(10))
复制代码

Output:

>>> list(SomeClass.y)
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
复制代码

② 列表推导式

x = 5
class SomeClass:
    x = 17
    y = [x for i in range(10)]
复制代码

Output(Python 2):

>>> SomeClass.y
[17, 17, 17, 17, 17, 17, 17, 17, 17, 17]
复制代码

Output(Python 3):

>>> SomeClass.y
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
复制代码

说明:

  • 类定义中嵌套的作用域会忽略类内的名称绑定。
  • 生成器表达式有它自己的作用域。
  • Python 3 开始,列表推导式也有自己的作用域。

36. 元组

x, y = (0, 1) if True else None, None
复制代码

Output:

>>> x, y  # 期望的结果是 (0, 1)
((0, 1), None)
复制代码

t = ('one', 'two')
for i in t:
    print(i)

t = ('one')
for i in t:
    print(i)

t = ()
print(t)
复制代码

Output:

one
two
o
n
e
tuple()
复制代码

说明:

  • 对于 1,正确的语句是 x, y = (0, 1) if True else (None, None)
  • 对于 2,正确的语句是 t = ('one',) 或者 t = 'one' , (缺少逗号) 否则解释器会认为 t 是一个字符串,并逐个字符对其进行迭代。
  • () 是一个特殊的标记,表示空元组。

37. else

① 循环末尾的 else

def does_exists_num(l, to_find):
    for num in l:
        if num == to_find:
            print("Exists!")
            break
    else:
        print("Does not exist")
复制代码

Output:

>>> some_list = [1, 2, 3, 4, 5]
>>> does_exists_num(some_list, 4)
Exists!
>>> does_exists_num(some_list, -1)
Does not exist
复制代码

② try 末尾的 else

try:
    pass
except:
    print("Exception occurred!!!")
else:
    print("Try block executed successfully...")
复制代码

Output:

Try block executed successfully...
复制代码

说明:循环后的 else 子句只会在循环执行完成(没有触发 break、return 语句)的情况下才会执行。 try 之后的 else 子句也被称为 "完成子句",因为在 try 语句中到达 else 子句意味着 try 块实际上已成功完成。

38. 名称改写

class Yo(object):
    def __init__(self):
        self.__honey = True
        self.bitch = True
复制代码

Output:

>>> Yo().bitch
True
>>> Yo().__honey
AttributeError: 'Yo' object has no attribute '__honey'
>>> Yo()._Yo__honey
True
复制代码

说明: python 中不能像 Java 那样使用 private 修饰符创建私有属性。但是,解释器会通过给类中以 __(双下划线)开头且结尾最多只有一个下划线的类成员名称加上 __类名_ 来修饰。这能避免子类意外覆盖父类的“私有”属性。

举个例子:有人编写了一个名为 Dog 的类,这个类的内部用到了 mood 实例属性,但是没有将其开放。现在,你创建了 Dog 类的子类 Beagle ,如果你在毫不知情的情况下又创建了一个 mood 实例属性,那么在继承的方法中就会把 Dog 类的 mood 属性覆盖掉。

为了避免这种情况,python 会将 __mood 变成 _Dog__mood ,而对于 Beagle 类来说,会变成 _Beagle__mood 。这个语言特性就叫名称改写(name mangling)。

39. +=更快

>>> timeit.timeit("s1 = s1 + s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.25748300552368164
# 用 "+=" 连接三个字符串:
>>> timeit.timeit("s1 += s2 + s3", setup="s1 = ' ' * 100000; s2 = ' ' * 100000; s3 = ' ' * 100000", number=100)
0.012188911437988281
复制代码

说明:连接两个以上的字符串时 +=+ 更快,因为在计算过程中第一个字符串(例如, s1 += s2 + s3 中的 s1 )不会被销毁。(就是 += 执行的是追加操作,少了一个销毁新建的动作。)

Python:鲜为人知的功能特性(下)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

RESTful Web Services Cookbook中文版

RESTful Web Services Cookbook中文版

Subbu Allamaraju / 丁雪丰、常可 / 电子工业出版社 / 2011-9 / 59.00元

RESTful Web Services Cookbook中文版:REST最佳实践手册,ISBN:9787121143908,作者:(美)Subbu Allamaraju(沙布·阿拉马拉尤)著,丁雪丰,常可 译一起来看看 《RESTful Web Services Cookbook中文版》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具