Python中有趣却鲜为人知的特性

Python是一个基于C语言实现的解释型高级语言, 提供了很多舒适的功能特性,使用起来非常方便。 但有的时候, Python的输出结果,让我们感觉一头雾水,其中原因自然是Python语言内部实现导致的,下面我们就给大家总结一些难以理解和反人类直觉的例子。

奇妙的字符串

  • 普通相同字符
<code>a = 'small_tom'
id(a)

# 输出: 140232182302576/<code>
<code>b = 'small' + '_' + 'tom'
id(b)
# 输出:140232182302576/<code>
<code>id(a) == id(b)
# 输出: True/<code>
  • 包含特殊字符
<code>a = 'tom'
b = 'tom'
a is b
# 输出:True/<code>
<code>a = 'tom!'
b = 'tom!'
a is b
# 输出:False/<code>
<code>a, b = 'tom!', 'tom!'
a is b
# 输出:False Python3.7以下为True/<code>
<code>'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
# 输出:True
'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
# 输出:True Python3.7以下为False/<code>
<code>a = 'tom'
b = ''.join(['t', 'o', 'm'])
a is b
# 输出:/<code>

为什么会出现以上的现象呢?因为编译器的优化特性(很多语言的不同编译器都有相应的优化策略),对于不可变对象,在某些情况下并不会创建新的对象,而是会尝试使用已存在的对象,从而节省内存,可以称之为**字符串驻留**。字符串的驻留是隐式的,不受我们控制,但是我们可以根据一些规律来猜测是否发生字符串驻留:

  • 所有长度为 0 和长度为 1 的字符串都被驻留
  • 字符串中只包含字母,数字或下划线时将会驻留。所以 'tom!' 由于包含 ! 而未被驻留。
  • 'tom'将被驻留,而''.join(['t', 'o', 'm'])不被驻留
  • 当在同一行将 a 和 b 的值设置为 "tom!" 的时候, Python 解释器会创建一个新对象, 然后同时引用第二个变量(译: 仅适用于3.7以下). 如果你在不同的行上进行赋值操作, 它就不会“知道”已经有一个 wtf! 对象 (因为 "wtf!" 不是按照上面提到的方式被隐式驻留的). 它是一种编译器优化, 特别适用于交互式环境
  • 当在同一行将 a 和 b 的值设置为 "tom!" 的时候, Python 解释器会创建一个新对象, 然后同时引用第二个变量(仅适用于3.7以下). 如果你在不同的行上进行赋值操作, 它就不会“知道”已经有一个 tom! 对象 (因为 "tom!" 不是按照上面提到的方式被隐式驻留的). 它是一种编译器优化, 特别适用于交互式环境.
  • 常量折叠(constant folding) 是 Python 中的一种 窥孔优化(peephole optimization) 技术. 这意味着在编译时表达式 'a'*20 会被替换为 'aaaaaaaaaaaaaaaaaaaa' 以减少运行时的时钟周期. 只有长度小于 20 的字符串才会发生常量折叠. 为什么呢?想象一下由于表达式 'a'*10**10 而生成的.pyc 文件的大小)。
  • **PS**:如果是在Python3.7中会发现部分执行结果会不一样,因为3.7版本中常量折叠已经从窥孔优化器迁移至新的AST优化器,后者可以以更高的一致性来执行优化。但是在3.8中结果又不一样了,他们都是用了AST优化器,可能是3.8中有一些其他的调整。

    字典的魔法

    <code>some_dict = {}
    some_dict[5.5] = "Ruby"
    some_dict[5.0] = "JavaScript"
    some_dict[5] = "Python"/<code>
    <code>some_dict[5.5]
    # 输出:Ruby
    some_dict[5.0]
    # 输出:Python
    some_dict[5]
    # 输出:Python/<code>
    • Python字典通过检查键值是否相等和比较哈希值来确定两个键是否相同
    • 具有相同值的不可变对象在Python中始终具有相同的哈希值

    虽然5.0和5好像是不一样,但实际上是一样的,在python中是不存在整型和浮点型的,只有一个数值型

    <code>5 == 5.0
    # 输出:True
    hash(5) == hash(5.0)
    # 输出:True/<code>

    注意: 具有不同值的对象也可能具有相同的哈希值(哈希冲突)

    • 当执行 some_dict[5] = "Python" 语句时, 因为Python将5和5.0识别为some_dict 的同一个键, 所以已有值 "JavaScript" 就被 "Python" 覆盖了.

    到处都返回

    <code>def some_func():
    try:
    return 'from_try'
    finally:
    return 'from_finally'
    some_func()
    # 始终输出:from_finally/<code>

    这是一个非常严重的问题,而且也非常常见,也很长用到,需要格外的注意。在异常捕获的时候,我们经常会用到finally来执行异常捕获后必须执行的处理。但是return在很多语言当中表示跳出当前的执行模块,但是在这里就有些颠覆我们的认知了,所以必须重点关注。

  • 当在 "try...finally" 语句的 try 中执行 return, break 或 continue 后, finally 子句依然会执行.
  • 函数的返回值由最后执行的 return 语句决定. 由于 finally 子句一定会执行, 所以 finally 子句中的 return 将始终是最后执行的语句
  • 出人意料的is

    下面是一个在网上非常有名的例子.

    <code>a = 256
    b = 256
    a is b
    # 输出:True

    a = 257
    b = 257
    a is b
    # 输出:False

    a = 257; b = 257
    a is b
    # 输出:True

    a, b = 257, 257
    a is b
    # 输出:True/<code>

    1.我们要说一下is和==的区别

    • is 运算符检查两个运算对象是否引用自同一对象 (即, 它检查两个运算对象地址是否相同)
    • ==运算符比较两个运算对象的值是否相等
    <code>a = 257
    b = 257
    a is b
    # 输出:False
    a == b
    # 输出:True/<code>

    2.为什么256和257的结果不一样?

    当你启动Python的时候, 数值为-5到256 的对象就已经被分配好了. 这些数字因为经常被使用, 所以会被提前准备好。Python通过这种创建小整数池的方式来避免小整数频繁的申请和销毁内存空间,从而造成内存泄漏和碎片。

    3.当a和b在同一行中使用相同的值初始化时,会指向同一个对象.

    <code>a, b = 257, 257
    id(a)
    # 输出:4391026960
    id(b)
    # 输出:4391026960

    a = 257
    b = 257
    id(a)
    # 输出:140232163575152
    id(b)
    # 输出:140232163574768/<code>
    • 当 a 和 b 在同一行中被设置为 257 时, Python 解释器会创建一个新对象, 然后同时引用第二个变量. 如果你在不同的行上进行, 它就不会 "知道" 已经存在一个 257 对象
    • 必须要注意的是这是一种特别为交互式环境做的编译器优化. 当你在实时解释器中输入两行的时候, 他们会单独编译, 因此也会单独进行优化. 如果你在 .py 文件中尝试这个例子, 则不会看到相同的行为, 因为文件是一次性编译的,如果是运行py文件将得到不同的结果

    test.py

    <code>a, b = 257, 257
    print(id(a))
    print(id(b))
    # 输出:
    /<code>

    列表复制

    <code>row = [""]*3
    # 并创建一个变量board
    board = [row]*3
    print(row)
    print(board)
    # 输出:['', '', '']
    # 输出:[['', '', ''], ['', '', ''], ['', '', '']]

    board[0][0] = 'X'
    print(board)
    # 输出:[['X', '', ''], ['X', '', ''], ['X', '', '']]/<code>
    • 当我们初始化 row 变量时, 下面这张图展示了内存中的情况。
    Python中有趣却鲜为人知的特性

    • 而当通过对 row 做乘法来初始化 board 时, 内存中的情况则如下图所示 (每个元素 board[0], board[1] 和 board[2] 都和 row 一样引用了同一列表.)
    Python中有趣却鲜为人知的特性

    • 我们可以通过不使用变量 row 生成 board 来避免这种情况
    <code>board = [['']*3 for _ in range(3)]
    board[0][0] = "X"
    board
    # 输出:[['X', '', ''], ['', '', ''], ['', '', '']]/<code>

    这样就会创建三个[''] * 3,而不是把[''] * 3标记三次

    闭包

    <code>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]
    print(results)
    print(funcs_results)
    # 输出:[0, 1, 2, 3, 4, 5, 6]
    # 输出:[6, 6, 6, 6, 6, 6, 6]/<code>

    即使每次在迭代中some_func中的x值都不相同,所有的函数还是都返回6.

    <code>powers_of_x = [lambda x: x**i for i in range(10)]
    [f(2) for f in powers_of_x]
    # 输出:[512, 512, 512, 512, 512, 512, 512, 512, 512, 512]/<code>
  • 当在循环内部定义一个函数时, 如果该函数在其主体中使用了循环变量, 则闭包函数将与循环变量绑定, 而不是它的值.
    因此, 所有的函数都是使用最后分配给变量的值来进行计算的
  • 可以通过将循环变量作为命名变量传递给函数来获得预期的结果. 为什么这样可行? 因为这会在函数内再次定义一个局部变量
  • <code>funcs = []
    for x in range(7):
    def some_func(x=x):
    return x
    funcs.append(some_func)
    funcs_results = [func() for func in funcs]
    print(funcs_results)
    # 输出:[0, 1, 2, 3, 4, 5, 6]/<code>

    is not ... 不是 is (not ...)

    <code>'something' is not None
    # 输出:True
    'something' is (not None)
    # 输出:False/<code>
  • is not 是个单独的二元运算符, 与分别使用 is 和 not 不同.
  • 如果操作符两侧的变量指向同一个对象, 则 is not 的结果为 False, 否则结果为 True.
  • 不存在的零点

    <code>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)
    # 输出:Time at midnight is 00:00:00
    # 输出:Time at noon is 12:00:00/<code>

    以上代码如果是在python3.5之前的版本,只会输出Time at noon is 12:00:00,在Python 3.5之前, 如果 datetime.time 对象存储的UTC的午夜时间(译: 就是 00:00), 那么它的布尔值会被认为是 False. 当使用 if obj: 语句来检查 obj 是否为 null 或者某些“空”值的时候, 很容易出错.

    类属性和实例属性

    <code>class A:
    x = 1

    class B(A):
    pass

    class C(A):
    pass
    print(A.x, B.x, C.x)
    # 输出:1 1 1

    B.x = 2
    print(A.x, B.x, C.x)
    # 输出:1 2 1

    A.x = 3
    print(A.x, B.x, C.x)
    # 输出:3 2 3

    a = A()
    print(a.x, A.x)
    # 输出:3 3

    a.x += 1
    print(a.x, A.x)
    # 输出:4 3\t/<code>
    <code>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]

    some_obj = SomeClass(420)
    print(some_obj.some_list)

    print(some_obj.another_list)
    another_obj = SomeClass(111)
    print(another_obj.some_list)
    print(another_obj.another_list)
    print(another_obj.another_list is SomeClass.another_list)
    print(another_obj.another_list is some_obj.another_list)/<code>
    • 类变量和实例变量在内部是通过类对象的字典来处理. 如果在当前类的字典中找不到的话就去它的父类中寻找
    • += 运算符会在原地修改可变对象, 而不是创建新对象. 因此, 在这种情况下, 修改一个实例的属性会影响其他实例和类属性.

    从有到无

    <code>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})
    print(some_list)
    print(some_dict)
    # 输出:None

    # 输出:None/<code>

    不知道有没有人能一眼看出问题所在,这是一个写法错误,并不是特殊用法。因为列表和字典的操作函数,比如list.append、list.extend、dict.update等都是原地修改变量,不创建也不返还新的变量

    子类继承关系

    <code>from collections import Hashable
    print(issubclass(list, object))
    print(issubclass(object, Hashable))
    print(issubclass(list, Hashable))
    # 输出:True
    # 输出:True
    # 输出:False/<code>

    子类关系是可以传递的,A是B的子类,B是C的子类,那么A应该也是C的子类,但是在python中就不一定了,因为在python中使用__subclasscheck__函数进行判断,而任何人都可以定义自己的__subclasscheck__函数

    • 当 issubclass(cls, Hashable) 被调用时, 它只是在 cls 中寻找 __hash__ 方法或者从继承的父类中寻找 __hash__ 方法.
    • 由于 object is 可散列的(hashable), 但是 list 是不可散列的, 所以它打破了这种传递关系
    <code>class MyMetaClass(type):
    def __subclasscheck__(cls, subclass):
    print("Whateva, I do what I want!")
    import random
    return random.choice([True, False])


    class MyClass(metaclass=MyMetaClass):
    pass

    print(issubclass(list, MyClass))
    # 输出:Whateva, I do what I want!
    # 输出:True 或者 False 因为是随机取的/<code>

    元类在python中是比较深入的知识点,后面我们有时间再讲

    斗转星移

    <code>import numpy as np

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

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

    energy_send(123.456)
    print(energy_receive())
    # 输出:123.456/<code>

    这到底是无中生有还是斗转星移呢?energy_receive函数我们返回了一个空的对象,但是结果是上一个数组的值,为什么呢?

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

  • 分享到:


    相關文章: