前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >细节见真知

细节见真知

作者头像
somenzz
发布2020-11-30 09:42:43
4440
发布2020-11-30 09:42:43
举报
文章被收录于专栏:Python七号

Python 虽然简单易学,但要真正掌握和精通也不是件容易的事情,比如本文将要分享的这些有趣的特性,如果你一眼就看穿了问题的本质,说明你已经非常精通了。如果没有那就多看几次,细节见真知,敲敲代码验证下,对于提升 Python 编程技能,非常有效。

1、小心链式操作

一开始我看到有人问为什么 Python 语句中True is False is False的结果是 False 时,我自己也产生了疑问?

代码语言:javascript
复制
>>> True is False is False
False
>>> (True is False) is False
True
>>> True is (False is False)
True
>>>

于是就搜索了下 stackoverflow,然后索引到官方文档[1]对比较操作的说明,一次子就长知识了,发现 Python 中的比较运算与 C 语言不同,这些比较操作具有相同的优先级,该优先级低于任何算术,移位或按位运算。

这些比较操作in, not in, is, is not, <, <=, >, >=, !=, == 操作符,会产生 True 或 False 的结果,这些比较操作符号可以任意的链式比较,比如:x < y <= zx < yy <= z 具有相同的优先级,不存在先计算 x < y ,得到结果后再与 <=z 进行比较的情况,因此x < y <= zx < y and y <= z是等价的。

x < y and y <= z 中,如果 x < y 的结果是 False,那么 y <= z 根本不会被计算。

也就是说a op1 b op2 c ... y opN z 等价于 a op1 b and b op2 c and ... y opN z,每一个表达式最多被执行一次。

注意,a op1 b op2 c 并不代表 a 和 c 有必然的关系,比如这样写x < y > z 也是合法的,虽然并不好看。

那么开始的问题就变得简单了:

代码语言:javascript
复制
True is False is False

相当于

代码语言:javascript
复制
(True is False) and (False is False)

结果自然就是 False。

类似的还有:

代码语言:javascript
复制
>>> 1 in [0,1] == True
False
>>> not True in [True,False]
False

2、析构函数__del__的执行时机

先看一段代码现象:

代码语言:javascript
复制
>>> class SomeClass:
...     def __del__(self):
...         print("Deleted!")
...
>>> x = SomeClass()
>>> y = x
>>> del x # 这里应该会输出 "Deleted!"
>>> del y
Deleted!
>>>

上述代码中有注释的部分,即 del x 的操作本应该会执行析构函数 __del__ 的,为什么没有被执行,直到 del y 时才被执行?

或者下面的代码,为什么调用了 globals() 后,才执行?

代码语言:javascript
复制
>>> x = SomeClass()
>>> y = x
>>> del x
>>> y
<__main__.SomeClass object at 0x7fa8e1cb94c0>
>>> del y
>>> globals()
Deleted!
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'some_func': <function some_func at 0x7fa8dedacd30>...

原因是:首先 del x 并不会立刻调用 x.__del__(),而是每当遇到 del x, Python 会将 x 的引用数减 1, 当 x 的引用数减到 0 时就会调用 x.__del__()。因此第一个示例代码中 del x 前 x 的引用技术为 2,执行后变为 1,并不执行 x.__del__()

在第二个例子中, y.__del__() 之所以未被调用, 是因为前一条语句 (>>> y) 对同一对象创建了另一个引用, 从而防止在执行 del y 后对象的引用数变为 0。

调用 globals 导致引用被销毁, 因此我们可以看到 "Deleted!" 终于被输出了,这其实是 Python 交互解释器的特性, 它会自动让 _ 保存上一个表达式输出的值。

3、不要迭代列表自身时删除

代码语言:javascript
复制
>>> 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)
...
1
3
>>> list_1
[1, 2, 3, 4]
>>> list_2
[2, 4]
>>> list_3
[]
>>> list_4
[2, 4]
>>>

为什么 list_1 没有被删除,为什么 list_2 和 list_4 还会有元素 [2,4]?

list_1 这个很好理解,item 只是 for 循环内部的一个临时变量,删除这个根本不影响原始列表。

在迭代时修改对象是一个很愚蠢的主意,正确的做法是迭代对象的副本, list_3[:] 就相当于完整的复制了 list_3,因此可以全部删除。

那么为什么输出是 [2, 4]?

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

4、当心默认的可变参数

看下面的代码,你会觉得困惑吗?

代码语言:javascript
复制
>>> def some_func(default_arg=[]):
...     default_arg.append("some_string")
...     return default_arg
...
>>> 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 的默认值, 所以函数会返回我们所期望的结果,可以运行以下代码进行验证。

代码语言:javascript
复制
>>> 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 指定为参数的默认值,然后检查是否有值传给对应的参数:

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

5、+=有什么不同

代码 a :

代码语言:javascript
复制
>>> a = [1, 2, 3, 4]
>>> b = a
>>> a = a + [5, 6, 7, 8]
>>>
>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4]

代码 b:

代码语言:javascript
复制
>>> a = [1, 2, 3, 4]
>>> b = a
>>> a += [5, 6, 7, 8]
>>> a
[1, 2, 3, 4, 5, 6, 7, 8]
>>> b
[1, 2, 3, 4, 5, 6, 7, 8]

两者的区别仅仅在于 a = a + [5,6,7,8] 和 a += [5,6,7,8],结果却完全不同,这是为什么呢?

因为:a += b 并不总是与 a = a + b 表现相同,类实现 op= 运算符的方式也许是不同的,列表就是这样做的:表达式 a = a + [5,6,7,8] 会生成一个新列表,并让 a 引用这个新列表,同时保持 b 不变。表达式 a += [5,6,7,8] 实际上是使用的是 "extend" 函数,所以 a 和 b 仍然指向已被修改的同一列表。

6、类的作用域

代码语言:javascript
复制
>>> x = 5
>>> class SomeClass:
...     x = 17
...     y = (x for i in range(10))
...
>>>
>>> list(SomeClass.y)[0]
5

原因是:类定义中嵌套的作用域会忽略类内的名称绑定,生成器表达式有它自己的作用域,因此生成器表达式忽略了类内部定义的 17 而使用全局变量 5,从 Python 3.X 开始, 列表推导式也有自己的作用域,因此 () 换成 [] 在 Python 3.X 的结果也是 5,Python 2.X 则是 17。

7、Python 为什么没有 goto

也许你会问这个问题,之前我在学习 C 语言的时候就非常好奇,为什么要提供 goto,让程序跳转呢,用个函数调用不就行了,是的,Python 语言就回答了这个问题,完全没必要用 goto,它让程序严重的结构化,且难以理解。比如:

代码语言:javascript
复制
void somefunc(int a)
{
    if (a == 1)
        goto label1;
    if (a == 2)
        goto label2;

    label1:
        ...
    label2:
        ...
}

完全可以用

代码语言:javascript
复制
def func1():
    ...

def func2():
    ...

funcmap = {1 : func1, 2 : func2}

def somefunc(a):
    funcmap[a]()  #Ugly!  But it works.

替代。

编程细节藏着魔鬼,搞懂了就豁然开朗,希望这些知识对你有用。

参考资料

[1]

官方文档: https://docs.python.org/3/reference/expressions.html#comparisons

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python七号 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、小心链式操作
  • 2、析构函数__del__的执行时机
  • 3、不要迭代列表自身时删除
  • 4、当心默认的可变参数
  • 5、+=有什么不同
  • 6、类的作用域
  • 7、Python 为什么没有 goto
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档