首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python中反人类直觉的特性,你踩过坑吗?

Python中反人类直觉的特性,你踩过坑吗?

作者头像
星星在线
发布2020-05-22 15:12:12
1.1K0
发布2020-05-22 15:12:12
举报

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

奇妙的字符串

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

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

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

  • 所有长度为 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中有一些其他的调整。

字典的魔法

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

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

5 == 5.0
# 输出:True
hash(5) == hash(5.0)
# 输出:True

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

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

到处都返回

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

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

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

出人意料的is

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

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

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

  • is 运算符检查两个运算对象是否引用自同一对象 (即, 它检查两个运算对象地址是否相同)
  • ==运算符比较两个运算对象的值是否相等

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

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

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

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

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

a = 257
b = 257
id(a)
# 输出:140232163575152
id(b)
# 输出:140232163574768

test.py

a, b = 257, 257
print(id(a))
print(id(b))
# 输出:

列表复制

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

board[0][0] = 'X'
print(board)
# 输出:[['X', '', ''], ['X', '', ''], ['X', '', '']]
  • 而当通过对 row 做乘法来初始化 board 时, 内存中的情况则如下图所示 (每个元素 board[0], board[1] 和 board[2] 都和 row 一样引用了同一列表.)
  • 我们可以通过不使用变量 row 生成 board 来避免这种情况
board = [['']*3 for _ in range(3)]
board[0][0] = "X"
board
# 输出:[['X', '', ''], ['', '', ''], ['', '', '']]

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

闭包

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]

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

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]
  • 当在循环内部定义一个函数时, 如果该函数在其主体中使用了循环变量, 则闭包函数将与循环变量绑定, 而不是它的值. 因此, 所有的函数都是使用最后分配给变量的值来进行计算的
  • 可以通过将循环变量作为命名变量传递给函数来获得预期的结果. 为什么这样可行? 因为这会在函数内再次定义一个局部变量
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]

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

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

不存在的零点

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

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

类属性和实例属性

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	
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)

从有到无

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

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

子类继承关系

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

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

  • 当 issubclass(cls, Hashable) 被调用时, 它只是在 cls 中寻找 __hash__ 方法或者从继承的父类中寻找 __hash__ 方法.
  • 由于 object is 可散列的(hashable), 但是 list 是不可散列的, 所以它打破了这种传递关系

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    因为是随机取的

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

斗转星移

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

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

  • 在energy_send函数中创建的numpy数组并没有返回, 因此内存空间被释放并可以被重新分配.
  • numpy.empty()直接返回下一段空闲内存,而不重新初始化. 而这个内存点恰好就是刚刚释放的那个但是这并不是绝对的.
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 python爬虫实战之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 奇妙的字符串
  • 字典的魔法
  • 到处都返回
  • 出人意料的is
  • 列表复制
  • 闭包
  • is not ... 不是 is (not ...)
  • 不存在的零点
  • 类属性和实例属性
  • 从有到无
  • 子类继承关系
  • 斗转星移
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档