在上一篇《Python避坑指南》中,我重点给大家讲了Python可变容器数据类型中的坑。除了这些,Python还有其他一些细小方面的坑,本章为大家讲解Python中这些大家可能会忽视的细节。
lambda的坑
看下面的代码,思考一下会输出什么?
def m():
return [lambda x:x*i for i in range(4)]
a = m()
for a in m():
print(a(1))
你可能以为会输出:
0
1
2
3
正确答案是:
3
3
3
3
这是因为在 [lambda x:x*i for i in range(4)]
列表解析式中,i
在lambda声明之外,也就是说i
相对lambda来说是个外部变量。当列表解析式运行完后,i
的值就定格为3。
我们可以通过简单的方法来验证:
for a in m():
print(a.__code__.co_code)
print(a(1))
从上面的代码输出你会发现,对a
的内部代码输出都是一样的,这说明列表中的lambda的逻辑都是一样的。所以输出结果也是一样的。
链式or
的坑
Python支持简化链式逻辑判断。比如:
if a > 1 and a < 3:
可以简化成:
if 1 < a < 3:
当多个相等判断连接时,比如
if a == 3 or b == 3 or c == 3:
可能有人会简化成这样:
if a or b or c == 3: # !这是错的!
注意:上面的代码是错的!因为or
比==
的优先级要低。上面表达式的执行顺序是 if (a) or (b) or (c == 3):
替代链式or
的更好的方法是用系统内置的any()
方法。
if any([a == 3, b == 3, c == 3]): # 正确
如果嫌上面代码太重复,编码不够高效,可以优化成下面这种写法:
if any(x == 3 for x in (a, b, c)): # 正确
如果比较的值都是相同的,还可以进一步简化为:
if 3 in (a, b, c): # 正确
这里我们用in
判断要比较的值是否在待比较的变量构成的元组中。
同理,下面这种写法也是不对的:
if a == 1 or 2 or 3:
应该这样写:
if a in (1, 2, 3):
访问字面量属性的坑
Python中一切皆对象,即便是字面量也是对象。例如7,在Python中也是对象。这也就意味着7也有属性和方法。例如bit_length()
这个方法,它会返回表示这个值所需的二进制位数。
x = 7
x.bit_length()
# Out: 3
上面的代码是可以正确输出的。应为7的二进制是111
,需要3位二进制来表示,所以bit_length()
会返回3。你可能直观地感觉7.bit_length()
也一样会返回3。但不幸的是你会得到SyntaxError
。为什么会这样?这是因为Python中.
有两重含义,即可以是访问对象的属性,也可以是表示浮点数。Python解析器需要区分到底是哪一种含义。7.bit_length()
和 7.2
解析器无法区分,因此会报语法错误。
有两种办法可以直接访问字面量的属性:
(7).bit_length() # 用括号将字面量括起来,告诉解析器这里7不是个浮点数
7 .bit_length() # 7后面加个空格,告诉解析器这里7不是个浮点数
注意:这里加两个点7..bit_length
是不对的。这样写第一个点会被理解为浮点数,第二个点会访问对象的属性。虽然语法上没有歧义,但是浮点数是没有bit_length()
方法的。这里如果访问的是浮点数对象有的属性或方法,程序是可以正常运行的。
7..as_integer_ratio()
# Out: (7, 1)
is
的坑
编程过程中,整型和字符串是使用最多的数据类型。为了减少整型和字符串频繁创建带来的内存开销,Python会用内部缓存一定范围的整数和字符串。当我们用is
判断两个对象是否是同一个对象时,这里的内部缓存机制可能会带来让人迷惑的结果。比如:
>>> -8 is (-7 - 1)
False
>>> -3 is (-2 - 1)
True
再举一个例子:
>>> (255 + 1) is (255 + 1)
True
>>> (256 + 1) is (256 + 1)
False
这里的输出结果着实让人疑惑。-3, 255
就返回True,-8, 256
就是False。
更具体地说,在[-5, 255]区间内地整型在Python解析器启动时会放入内部缓存。因此用is判断这个区间内的整型是否是同一对象时会返回True。不在这个区间内地整型会在使用时创建,所以即便值相同,但在内存中不是同一对象,因此会返回False。
⚠注意,可能编译器版本不同,内部缓存地范围可能不同。
解决这个问题的方法就是永远用==
判断值是否相等,不要用is
。
⚠注意,在Python交互式运行环境下,用
is
判断值相等会受到一条警告:
SyntaxWarning: "is" with a literal. Did you mean "=="?
字符串也是同样道理,永远用==
判断值相等!
GIL全局锁的坑
GIL全局锁大家可能比较陌生,它跟多线程有关。在处理多线程时,全局锁有时可能会产生疑惑。请看下面这个例子:
import math
from threading import Thread
def calc_fact(num):
math.factorial(num)
num = 600000
t = Thread(target=calc_fact, daemon=True, args=[num])
print("About to calculate: {}!".format(num))
t.start()
print("Calculating...")
t.join()
print("Calculated")
你可能以为Calculating...
会在线程启动后立即打印出来,毕竟我们将calc_fact()
这个比较耗时的运算放到了线程中执行。但实际上他会在计算完成后才打印。这是因为math.factorial()
背后是C语言实现,线程在执行C语言实现函数时会锁住GIL直到运行结束。
有多种方法可以绕开这个问题。
第一种方法,你可以用纯Python来实现factorial
的功能。
def calc_fact(num):
""" 纯Python实现阶乘 """
res = 1
while num >= 1:
res = res * num
num -= 1
return res
这样做的弊端就是运行速度变慢,因为我们不再使用C语言实现的阶乘函数。
第二种方法,你可以在调用C函数前休眠一下。
def calc_fact(num):
sleep(0.001)
math.factorial(num)
注意:这里的休眠不会影响C函数的执行,只是让主线程有机会向下执行。
多数据返回的坑
Python允许函数返回多个数据,比如下面的函数xyz
就返回了2个值:
def xyz():
return a, b
Python的这个特性很方便,当我们使用时可以用两个变量承接返回值
a, b = xyz()
但是如果用一个变量来承接多返回值,
t = xyz()
python也是允许的,只不过t
的数据类型是个元组(a, b)
,不是函数返回的第一个值。这里大家要格外注意。
JSON中的坑
JSON是我们日常开发中用到最多的数据类型,也是前后端传输数据最常用的数据类型。但是Python对json的处理跟javascript不同,这会让很多前端转型Python开发的同学不适应。我们看下面这个例子:
my_var = 'bla'
my_key = 'key'
params = {
"language": "en", my_var: my_key}
上面的代码如果在javascript中,params
的内容为:
{
"language": "en",
"my_var": "key"
}
而在python中,param
的内容为:
{
"language": "en",
"bla": "key"
}
在Python中,字典中的my_var
和 api_key
会被当做变量来求值。