热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

GitHub上有一个名为《Whatthef*ckPython!》的项目,这个有趣的项目意在收集Python中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背

GitHub 上有一个名为《What the f*ck Python!》的项目,这个有趣的项目意在收集 Python 中那些难以理解和反人类直觉的例子以及鲜为人知的功能特性,并尝试讨论这些现象背后真正的原理!
原版地址:https://github.com/satwikkansal/wtfpython

最近,一位名为“暮晨”的贡献者将其翻译成了中文。
中文版地址:https://github.com/leisurelicht/wtfpython-cn

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

1. 字符串驻留

>>> a = '!'
>>> b = '!'
>>> a is b
True

>>> a = 'some_string'
>>> id(a)
140420665652016
>>> id('some' + '_' + 'string') # 注意两个的id值是相同的.
140420665652016

>>> a = 'wtf'
>>> b = 'wtf'
>>> a is b
True

>>> a = 'wtf!'
>>> b = 'wtf!'
>>> a is b
False

>>> a, b = 'wtf!', 'wtf!'
>>> a is b
True

>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False

说明:
这些行为是由于 CPython 在编译优化时,某些情况下会尝试使用已经存在的不可变对象而不是每次都创建一个新对象。这种行为被称作字符串的驻留 string interning。发生驻留之后, 许多变量可能指向内存中的相同字符串对象从而节省内存。

有一些方法可以用来猜测字符串是否会被驻留:

  • 所有长度为 0 和长度为 1 的字符串都被驻留(①中字符串被驻留)
  • 字符串在编译时被实现('wtf' 将被驻留,但是 ''.join(['w', 't', 'f'] 将不会被驻留)
  • 字符串中只包含字母、数字或下划线时将会驻留,所以 'wtf!' 由于包含 '!' 而未被驻留
  • 当在同一行将 ab 的值设置为 'wtf!' 的时候,Python 解释器会创建一个新对象,然后两个变量同时指向这个对象。如果你在不同的行上进行赋值操作,它就不会“知道”已经有一个 'wtf!' 对象(因为 'wtf!' 不是按照上面提到的方式被隐式驻留的)。
  • 常量折叠(constant folding)是 Python 中的一种窥孔优化(peephole optimization)技术。这意味着在编译时表达式 'a' * 20 会被替换为 'aaaaaaaaaaaaaaaaaaaa' 以减少运行时的时钟周期。只有长度小于 20 的字符串才会发生常量折叠。(为啥?想象一下由于表达式 'a' * 10 ** 10 而生成的 .pyc 文件的大小)。

如果你在 .py 文件中尝试这个例子,则不会看到相同的行为,因为文件是一次性编译的。

2. 字典的键
>>> some_dict = {}
>>> some_dict[5.5] = "Ruby"
>>> some_dict[5.0] = "Javascript"
>>> some_dict[5] = "Python"

>>> some_dict[5.5]
"Ruby"
>>> some_dict[5.0]
"Python"
>>> some_dict[5]
"Python"

说明:
Python 字典检查键值是否相等是通过比较哈希值是否相等来确定的。如果两个对象在比较的时候是相等的,那它们的散列值必须相等,否则散列表就不能正常运行了。例如,如果 1 == 1.0 为真,那么 hash(1) == hash(1.0) 必须也为真,但其实两个数字(整数和浮点数)的内部结构是完全不一样的。

3. finally 子句中的 return
def some_func():
    try:
        return 'from_try'
    finally:
        return 'from_finally'

Output:

>>> some_func()
'from_finally'

说明:
函数的返回值由最后执行的 return 语句决定。由于 finally 子句一定会执行,所以 finally 子句中的 return 将始终是最后执行的语句。

4. 同一个对象
class WTF:
    pass

Output:

>>> WTF() == WTF() # 两个不同的对象应该不相等
False
>>> WTF() is WTF() # 也不相同
False
>>> hash(WTF()) == hash(WTF()) # 哈希值也应该不同
True
>>> id(WTF()) == id(WTF())
True

说明:
当调用 id() 函数时,Python 创建了一个 WTF 类的对象并传给 id() 函数,然后 id() 函数获取其 id 值(也就是内存地址),然后丢弃该对象,该对象就被销毁了。

当我们连续两次进行这个操作时,Python会将相同的内存地址分配给第二个对象,因为在 CPythonid() 函数使用对象的内存地址作为对象的 id 值,所以两个对象的 id 值是相同的。

综上,对象的 id 值仅仅在对象的生命周期内唯一,在对象被销毁之后或被创建之前,其他对象可以具有相同的 id 值。

class WTF(object):
  def __init__(self): print("I")
  def __del__(self): print("D")

Output:

>>> WTF() is WTF()
I
I
D
D
False
>>> id(WTF()) == id(WTF())
I
D
I
D
True

正如你所看到的,对象销毁的顺序是造成所有不同之处的原因。

5. for 循环分配目标赋值
>>> some_string = "wtf"
>>> some_dict = {}
>>> for i, some_dict[i] in enumerate(some_string): pass
>>> some_dict
{0: 'w', 1: 't', 2: 'f'}

说明:
这一条仔细看一下很好理解,for 循环每次迭代都会给分配目标赋值,some_dict[i] = value 就相当于给字典添加键值对了。
有趣的是下面这个例子,你可曾觉得这个循环只会运行一次?

for i in range(4):
    print(i)
    i = 10

6. 执行时机差异

>>> array = [1, 8, 15]
>>> g = (x for x in array if array.count(x) > 0)
>>> array = [2, 8, 22]
>>> list(g)
[8]

>>> array_1 = [1, 2, 3, 4]
>>> g1 = (x for x in array_1)
>>> array_1 = [1, 2, 3, 4, 5]

>>> array_2 = [1, 2, 3, 4]
>>> g2 = (x for x in array_2)
>>> array_2[:] = [1, 2, 3, 4, 5]

>>> list(g1)
[1, 2, 3, 4]

>>> list(g2)
[1, 2, 3, 4, 5]

说明:
在生成器表达式中 in 子句在声明时执行,而条件子句则是在运行时执行。
①中,在运行前 array 已经被重新赋值为 [2, 8, 22],因此对于之前的 1, 8, 15,只有 count(8) 的结果是大于 0 ,所以生成器只会生成 8。
②中,g1g2 的输出差异则是由于变量 array_1array_2 被重新赋值的方式导致的。

  • 在第一种情况下,array_1 被绑定到新对象 [1, 2, 3, 4, 5],因为 in 子句是在声明时被执行的,所以它仍然引用旧对象 [1, 2, 3, 4](并没有被销毁)。
  • 在第二种情况下,对 array_2 的切片赋值将相同的旧对象 [1, 2, 3, 4] 原地更新为 [1, 2, 3, 4, 5]。因此 g2 和 array_2 仍然引用同一个对象[1, 2, 3, 4, 5]

7. 整数的预分配
>>> a = 256
>>> b = 256
>>> a is b
True

>>> a = 257
>>> b = 257
>>> a is b
False

>>> a = 257; b = 257
>>> a is b
True

is 和 == 的区别

  • is 运算符检查两个运算对象是否引用自同一对象
  • == 运算符比较两个运算对象的值是否相等

因此 is 代表引用相同,== 代表值相等。下面的例子可以很好的说明这点:

>>> [] == []
True
>>> [] is []  # 这两个空列表位于不同的内存地址
False

256 是一个已经存在的对象,而 257 不是

当启动 Python 的时候,-5 到 256 的数值就已经被分配好了。这些数字因为经常使用所以适合被提前准备好。

当前的实现为 -5 到 256 之间的所有整数保留一个整数对象数组,当你创建了一个该范围内的整数时,你只需要返回现有对象的引用。所以改变 1 的值是有可能的。

但是,当 ab 在同一行中使用相同的值初始化时,会指向同一个对象。

>>> id(256)
10922528
>>> a = 256
>>> b = 256
>>> id(a)
10922528
>>> id(b)
10922528
>>> id(257)
140084850247312

>>> x = 257
>>> y = 257
>>> id(x)
140084850247440
>>> id(y)
140084850247344

>>> a, b = 257, 257
>>> id(a)
140640774013296
>>> id(b)
140640774013296

这是一种特别为交互式环境做的编译器优化,当你在实时解释器中输入两行的时候,他们会单独编译,因此也会单独进行优化, 如果你在 .py 文件中尝试这个例子,则不会看到相同的行为,因为文件是一次性编译的。

8. 容易疏忽的引用类型赋值
>>> row = [''] * 3
>>> board = [row] * 3
>>> board
[['', '', ''], ['', '', ''], ['', '', '']]
>>> board[0]
['', '', '']
>>> board[0][0]
''
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['X', '', ''], ['X', '', '']]

说明:
我们来输出 id 看下:

>>> id(row[0])
7536232
>>> id(row[1])
5143216
>>> id(row[2])
5143216
>>> id(board[0])
7416840
>>> id(board[1])
7416840
>>> id(board[2])
7416840

row 是一个 list,其中三个元素都指向地址 5143216,当对 board[0][0] 进行赋值以后,row 的第一个元素指向 7536232。而 board 中的三个元素都指向 rowrow 的地址并没有改变。

我们可以通过不使用变量 row 生成 board 来避免这种情况。

>>> board = [[''] * 3 for _ in range(3)]
>>> board[0][0] = "X"
>>> board
[['X', '', ''], ['', '', ''], ['', '', '']]

这里用了推导式,每次迭代都会生成一个新的 _ ,所以 board 中三个元素指向的是不同的变量。

9. 闭包函数
funcs = []
results = []
for x in range(7):
    def some_func():
        return x
    funcs.append(some_func)
    results.append(some_func())

funcs_results = [func() for func in funcs]

Output:

>>> results
[0, 1, 2, 3, 4, 5, 6]
>>> funcs_results
[6, 6, 6, 6, 6, 6, 6]

说明:
当在循环内部定义一个函数时,如果该函数在其主体中使用了循环变量,则闭包函数将与循环变量绑定,而不是它的值。因此,所有的函数都是使用最后分配给变量的值来进行计算的。

可以通过将循环变量作为命名变量传递给函数来获得预期的结果。为什么这样可行?因为这会在函数内再次定义一个局部变量。

funcs = []
for x in range(7):
    def some_func(x=x):
        return x
    funcs.append(some_func)

Output:

>>> funcs_results = [func() for func in funcs]
>>> funcs_results
[0, 1, 2, 3, 4, 5, 6]

10. 字符串末尾的反斜杠
>>> print("\\ C:\\")
\ C:\
>>> print(r"\ C:")
\ C:
>>> print(r"\ C:\")

    File "", line 1
      print(r"\ C:\")
                     ^
SyntaxError: EOL while scanning string literal

说明:
在以 r 开头的原始字符串中,反斜杠并没有特殊含义。解释器所做的只是简单的改变了反斜杠的行为,因此会直接传递反斜杠及后一个的字符。这就是反斜杠在原始字符串末尾不起作用的原因。

11. == 和 not 运算符的优先级
>>> not x == y
True
>>> x == not y
  File "", line 1
    x == not y
           ^
SyntaxError: invalid syntax

说明:
一句话,== 运算符的优先级要高于 not 运算符。

12. 三引号
>>> print('wtfpython''')
wtfpython
>>> print("wtfpython""")
wtfpython
>>> # 下面的语句会抛出 `SyntaxError` 异常
>>> # print('''wtfpython')
>>> # print("""wtfpython")

说明:
'''"""Python 中也是字符串定界符,Python 解释器在先遇到三个引号的的时候会尝试再寻找三个终止引号作为定界符,如果不存在则会导致 SyntaxError 异常。

而 Python 提供隐式的字符串链接:

>>> print("wtf" "python")
wtfpython
>>> print("wtf""")  # 相当于 "wtf" ""
wtf

13. 消失的午夜0点
from datetime import datetime

midnight = datetime(2018, 1, 1, 0, 0)
midnight_time = midnight.time()

noon = datetime(2018, 1, 1, 12, 0)
noon_time = noon.time()

if midnight_time:
    print("Time at midnight is", midnight_time)

if noon_time:
    print("Time at noon is", noon_time)

Output:

Time at noon is 12:00:00

midnight_time 并没有被输出。

说明:
Python 3.5 之前,如果 datetime.time 对象存储的 UTC 的午夜 0 点, 那么它的布尔值会被认为是 False。
这个我特意下了个 python 3.4 验证了下,真是这样。

14. bool 值
mixed_list = [False, 1.0, "some_string", 3, True, [], False]
integers_found_so_far = 0
booleans_found_so_far = 0

for item in mixed_list:
    if isinstance(item, int):
        integers_found_so_far += 1
    elif isinstance(item, bool):
        booleans_found_so_far += 1

Output:

>>> booleans_found_so_far
0
>>> integers_found_so_far
4

说明:
布尔值是 int 的子类

>>> isinstance(True, int)
True
>>> isinstance(False, int)
True

在引入实际 bool 类型之前,0 和 1 是真值的官方表示。为了向下兼容,新的 bool 类型需要像 0 和 1 一样工作。

15. 类属性和实例属性

class A:
    x = 1

class B(A):
    pass

class C(A):
    pass

Output:

>>> A.x, B.x, C.x
(1, 1, 1)
>>> B.x = 2
>>> A.x, B.x, C.x
(1, 2, 1)
>>> A.x = 3
>>> A.x, B.x, C.x
(3, 2, 3)
>>> a = A()
>>> a.x, A.x
(3, 3)
>>> a.x += 1
>>> a.x, A.x
(4, 3)

class SomeClass:
    some_var = 15
    some_list = [5]
    another_list = [5]
    def __init__(self, x):
        self.some_var = x + 1
        self.some_list = self.some_list + [x]
        self.another_list += [x]

Output:

>>> some_obj = SomeClass(420)
>>> some_obj.some_list
[5, 420]
>>> some_obj.another_list
[5, 420]
>>> another_obj = SomeClass(111)
>>> another_obj.some_list
[5, 111]
>>> another_obj.another_list
[5, 420, 111]
>>> another_obj.another_list is SomeClass.another_list
True
>>> another_obj.another_list is some_obj.another_list
True

说明:

  • 类变量和实例变量在内部是通过类对象的字典来处理(__dict__ 属性),如果在当前类的字典中找不到的话就去它的父类中寻找。
  • += 运算符会在原地修改可变对象,而不是创建新对象。因此,修改一个实例的属性会影响其他实例和类属性。

16. yield 的 bug
some_iterable = ('a', 'b')

def some_func(val):
    return "something"

Output:

>>> [x for x in some_iterable]
['a', 'b']
>>> [(yield x) for x in some_iterable]
 at 0x7f70b0a4ad58>
>>> list([(yield x) for x in some_iterable])
['a', 'b']
>>> list((yield x) for x in some_iterable)
['a', None, 'b', None]
>>> list(some_func((yield x)) for x in some_iterable)
['a', 'something', 'b', 'something']

说明:
这是 CPython 在理解和生成器表达式中处理 yield 的一个错误,在 Python 3.8 中修复,在 Python 3.7 中有弃用警告。 请参阅 Python 错误报告和 Python 3.7Python 3.8 的新增条目。

来源和解释可以在这里找到: https://stackoverflow.com/questions/32139885/yield-in-list-comprehensions-and-generator-expressions
相关错误报告: http://bugs.python.org/issue10544

17. 元组的相对不可变性
>>> some_tuple = ("A", "tuple", "with", "values")
>>> another_tuple = ([1, 2], [3, 4], [5, 6])

>>> some_tuple[2] = "change this"
TypeError: 'tuple' object does not support item assignment
>>> another_tuple[2].append(1000) # 这里不出现错误
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000])
>>> another_tuple[2] += [99, 999]
TypeError: 'tuple' object does not support item assignment
>>> another_tuple
([1, 2], [3, 4], [5, 6, 1000, 99, 999])

说明:
元组中不可变的元素的标识(即元素的地址),如果元素是引用类型,元组的值会随着引用的可变对象的变化而变化。所以 another_tuple[2].append(1000) 是可以的。
+= 操作符在原地修改了列表。元素赋值操作并不工作,但是当异常抛出时,元素已经在原地被修改了。+= 并不是原子操作,而是 extend= 两个动作,这里 = 操作虽然会抛出异常,但 extend 操作已经修改成功了。

18. 消失的外部变量
e = 7
try:
    raise Exception()
except Exception as e:
    pass

Output: python2

>>> print(e)
# prints nothing

Output: python3

>>> print(e)
NameError: name 'e' is not defined

说明:
当使用 as 为目标分配异常的时候,将在 except 子句的末尾清除该异常。

这就好像:

except E as N:
    foo

会被翻译成:

except E as N:
    try:
        foo
    finally:
        del N

这意味着必须将异常分配给其他名称才能在 except 子句之后引用它。而异常之所以会被清除,是因为附加了回溯信息(trackback),它们与栈帧(stack frame)形成一个引用循环,使得该栈帧中的所有本地变量在下一次垃圾回收发生之前都处于活动状态(不会被回收)。

子句在 Python 中并没有独立的作用域。示例中的所有内容都处于同一作用域内,所以变量 e 会由于执行了 except 子句而被删除。而对于有独立的内部作用域的函数来说情况就不一样了。下面的例子说明了这一点:

def f(x):
    del(x)
    print(x)

x = 5
y = [5, 4, 3]

Output:

>>>f(x)
UnboundLocalError: local variable 'x' referenced before assignment
>>>f(y)
UnboundLocalError: local variable 'x' referenced before assignment
>>> x
5
>>> y
[5, 4, 3]

19. bool 类型
True = False
if True == False:
    print("I've lost faith in truth!")

Output:

I've lost faith in truth!

说明:
最初,Python 并没有 bool 型(人们用 0 表示假值, 用非零值比如 1 作为真值)。后来他们添加了 True, False, 和 bool 型,但是,为了向后兼容,他们没法把 TrueFalse 设置为常量,只是设置成了内置变量。
Python 3 由于不再需要向后兼容,终于可以修复这个问题了,所以这个例子无法在 Python 3.x 中执行。

20. append 方法陷阱
some_list = [1, 2, 3]
some_dict = {
  "key_1": 1,
  "key_2": 2,
  "key_3": 3
}

some_list = some_list.append(4)
some_dict = some_dict.update({"key_4": 4})

Output:

>>> print(some_list)
None
>>> print(some_dict)
None

说明:
大多数修改序列/映射对象的方法,比如 list.appenddict.updatelist.sort 等等,都是原地修改对象并返回 None,这样可以避免创建对象的副本来提高性能。


推荐阅读
  • 技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统
    技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统 ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 单元测试:使用mocha和should.js搭建nodejs的单元测试
    2019独角兽企业重金招聘Python工程师标准BDD测试利器:mochashould.js众所周知对于任何一个项目来说,做好单元测试都是必不可少 ... [详细]
  • 2.2 组件间父子通信机制详解
    2.2 组件间父子通信机制详解 ... [详细]
  • 属性类 `Properties` 是 `Hashtable` 类的子类,用于存储键值对形式的数据。该类在 Java 中广泛应用于配置文件的读取与写入,支持字符串类型的键和值。通过 `Properties` 类,开发者可以方便地进行配置信息的管理,确保应用程序的灵活性和可维护性。此外,`Properties` 类还提供了加载和保存属性文件的方法,使其在实际开发中具有较高的实用价值。 ... [详细]
  • 如何将TS文件转换为M3U8直播流:HLS与M3U8格式详解
    在视频传输领域,MP4虽然常见,但在直播场景中直接使用MP4格式存在诸多问题。例如,MP4文件的头部信息(如ftyp、moov)较大,导致初始加载时间较长,影响用户体验。相比之下,HLS(HTTP Live Streaming)协议及其M3U8格式更具优势。HLS通过将视频切分成多个小片段,并生成一个M3U8播放列表文件,实现低延迟和高稳定性。本文详细介绍了如何将TS文件转换为M3U8直播流,包括技术原理和具体操作步骤,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 本文详细介绍了在 CentOS 7 系统中配置 fstab 文件以实现开机自动挂载 NFS 共享目录的方法,并解决了常见的配置失败问题。 ... [详细]
  • com.sun.javadoc.PackageDoc.exceptions()方法的使用及代码示例 ... [详细]
  • 解决Only fullscreen opaque activities can request orientation错误的方法
    本文介绍了在使用PictureSelectorLight第三方框架时遇到的Only fullscreen opaque activities can request orientation错误,并提供了一种有效的解决方案。 ... [详细]
  • Linux CentOS 7 安装PostgreSQL 9.5.17 (源码编译)
    近日需要将PostgreSQL数据库从Windows中迁移到Linux中,LinuxCentOS7安装PostgreSQL9.5.17安装过程特此记录。安装环境&#x ... [详细]
  • 第二十五天接口、多态
    1.java是面向对象的语言。设计模式:接口接口类是从java里衍生出来的,不是python原生支持的主要用于继承里多继承抽象类是python原生支持的主要用于继承里的单继承但是接 ... [详细]
  • 在PHP中,为了更高效地打开和读取目录并列出其中的文件,可以使用一个自定义函数来返回查询目录下的文件和文件夹列表。该函数会将结果以数组形式返回,并明确区分每个条目是文件还是目录,从而提供更友好和实用的输出。此外,该函数还可以进一步扩展,支持递归查询子目录,以便更全面地获取目录结构信息。 ... [详细]
  • 本文深入探讨了Hibernate框架中乐观锁和悲观锁的机制及其多态特性。乐观锁假设数据在大多数情况下不会发生冲突,因此在读取数据时不加锁,而是在更新时检查版本号以确保数据未被修改。相比之下,悲观锁则认为数据在并发环境下容易产生冲突,因此在读取数据时立即加锁,以防止其他事务访问同一数据,从而避免潜在的数据不一致问题。文章还详细分析了这两种锁机制在实际应用中的优缺点,并介绍了Hibernate中的多态特性如何与锁机制结合,以实现更高效的数据管理和并发控制。 ... [详细]
author-avatar
mobiledu2502862217
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有