Overview
笔试是昨天进行的,距现在已经过去了接近 24 h,而且昨晚为了抚平我劳累的大脑,还看了两部电影。因此这里给出的题目和原本可能略不完全一样。
注:本文未完全完成;看出来我在本文介绍的 python 一些语法特性的话,想要了解,请参阅本文给出的“引用”来源。
背景知识
有一题笔试题,我在做的时候脑子里在想什么,这里展示给读者看:
上面截图是我大约在3,4 个月前左右看到的1。所以我对于元组这一奇怪的现象印象深刻。
只不过我昨天忘记了上面这个例子在
+=
的情况中才会发生。
看下面例子:>>> l = [2, 3, 4] >>> l_1 = l + [9, 5] # l_1 == [2, 3, 4 , 9, 5] >>> l_2 = l; l_2.extend([9, 5]) # l_2 == [2, 3, 4, 9, 5]
注意上面的例子。
我这里可以问一个问题,但是我还是省略过去,直接说明我要说的重点:
l == l_1 == l_2
是True
l is l_1
是False
l is l_2
是True
也就是说l = l + [9, 5]
执行完,
l
就已经不是原来的l
了。
所以说,在写代码的时候,可以使用_list.append()
和_list.extend()
的时候就不要使用+=
2。
- 在列表中,
l += [...]
和l = l + [...]
不是完全等价的。
1 对象,引用和运算
1.1 考你一道笔试
问题:
l = [3, 4]
ll = [l] * 4
print(ll) # (1)
t = tuple(ll)
print(t) # (2)
t[0][0] = -8
print(t) # (3)
以上 (1), (2), (3) 分别输出什么?
经过思考,你可能已经有了你自己的答案。
下面我提供一份我的答案,然后剖析一下它哪里错误,为什么错了,以及一些我们一直在使用,但是却鲜为人知的 Python 秘密。
1.2 我的答案和剖析
这里是我的答案:
(1) [[3, 4], [3, 4], [3, 4], [3, 4]]
(2) ([3, 4], [3, 4], [3, 4], [3, 4])
抛出异常
(3) ([-8, 4], [3, 4], [3, 4], [3, 4])
再看一眼题目,和我的答案对比一下:
第一个答案(回答)应该来说得出一个“正确的结果”不难。
实际上我还考虑了一下下,因为 l
本来就是数组了,你还给它包一层数组 [l]
是要搞什么鬼,那我最后再 *
一下的结果是要有几层数组(这里数组说的是列表)。
但是我想 *
不要想的太复杂,它其实就是 +
嘛。
所以:
ll = [[3, 4]] + [[3, 4]] + [[3, 4]] + [[3, 4]]
后面你会看到,这种
*
到+
的等效切换方式是后面灾难的源头。
然后第二个答案(回答),列表转换为元组,我想这一点几乎没有问题。
最后第三个回答…咦,等等,回答里 抛出错误
是什么鬼?
我想有认真看上面"背景知识"的读者应该猜到我这么给答案的原因是什么了。
在我对“背景知识”里面的元组的“特殊表现”印象深刻之后,我忘了上面说的是在 +=
的条件下发生的。
还记得上面我又给出的 l += [83, -9]
和 l.extend([83, -9])
的例子吗?
在例子中我对 l_1 = l + [83, -9]
和 l_2 = l; l_2.extend(83, -9)
用 is
做了判断。==
比较的实例(对象)的“值”,而 is
比较的是“对象”是否是同一个。
用 C/C++ 语言的说法,对象可以看成指针,而“值”就是指针指向的地址上的值。
所以==
在 Python 中的运算,用 C/C++ 表示是*p1 == *p2
。
而is
在 Python 中的运算,用 C/C++ 表示是p1 == p2
。
如果你不了解 C/C++,或者对 C/C++ 的指针行为还没有理解透彻,那么按上面比较 “对象的值”相等 和比较 “对象” 是否是同一个 来理解即可。
所以,对于元组来说,“不可变”其实是针对它的“引用”而言的3
写好一份博客有点费心力,目前脑子不太精气神,后续再更新。
注:
- 回答修改成(把“抛出异常删除”)
(3) ([-8, 4], [3, 4], [3, 4], [3, 4])
还是错误。
原因:上面说到的ll = [[3, 4]] + [[3, 4]]
与l = [3, 4]; ll = [l] * 2
的不同!
实际应用:构造二维数组(列表)时需要使用深拷贝,对象的默认浅复制行为4t[0] = t[0] + [8, -3]
和t[0] += [8, -3]
的相同和不同之处(运算符号运行时背后发生的事情)。
2 类的数据属性 - 共享特性
注:
遇到这一笔试题的时候,我个人想到的是编写“单例”的时候使用的“懒汉单例”,而且我使用了@finally
修饰单例,避开了继承,所以实际编程中,我是特意避开这种编程方式的。
2.1 闷头一锤
给你一题“类的数据属性”以及对其的继承,然后问修改这个变量,结果如何。
我个人在实际编程中特意避开使用“数据属性”,而是对所有需要的类属性在 __init__
中定义,既然是避开这种编程方式,其实就是没有特定去花时间了解清楚,所以遇到这个问题我真的是吃了一惊。
class Parent(object):
x = 0
class Child1(Parent):
pass
class Child2(Parent):
pass
要点:实例修改 x,类修改 x,子类修改x。
2.2 Answer
共享特性 - 解密一切的一句话:由所有实例共享5
写好一份博客有点费心力,目前脑子不太精气神,后续再更新。
看引用 ‘[^5]’ 书籍中的解释。
注意父类,子类的“命名空间”也是回答正确的/正确思路的一种方式。
个人提醒:共享特性和一直还没去看的 slot 有关系吗?
虽然知道共享特性是这么表现的,不过它的原理什么?是属于“不要问为什么,它就是这么定义的”,还是属于基本语法的衍生表现?
2.3 拓展 - 待续
- ORM 编程中的应用(实例)。
- 对于使用需要使用
super().__init__(...)
初始化参数的子类,如果仅仅只是为了调用父类来初始化固定参数,那么可以考虑使用类的实例共享属性这一特性代替调用super().__init__(...)
3 编译(模块载入)和运行两个阶段
实话说这次笔试出的 Python 题目还真的不简单,都是比较中级偏上的题目。
而且在提问方式上,如果一不小心“忘了xxx”(就是标题说的,一时没想到 xxx),那么必错无疑。
比如上面 ll = [l] * 4
的提问方式换成如何从已有的 l = [2, 3]
创建一个“二维数组”(当然,这种描述方式不够清楚,也比较别扭)来看,更像在问实际应用中会不会犯浅复制的错误。
之所以没有说高级,是因为在我心目中,高级是元编程这一主题,从在
def
函数中返回带有变量属性和方法属性的类实例到类级装饰器再到继承类型检查和创建。这些是我了解过还没有时间和应用场景机会研读的地方。(高级往上可能是 DSL,不过个人目前对此应该无学习和应用的需求)
3.1 似曾相识
问题:
from functools import wraps
def outer(func):
print("into")
@wraps(func)
def inner(**kwargs):
kwargs.update(a='b')
result = func(**kwargs)
return result
print('outer')
return inner
@outer
def func(**kwargs):
return kwargs
print(func(a='I'))
print(func(a='e'))
所以,运行上面代码(可以保存到一个 *.py 文件中,然后运行)得到的输出是什么?
3.2 答案与分析 - 待续
函数级装饰器和闭包之间的关系
写好一份博客有点费心力,目前脑子不太精气神,后续再更新。
要点:
函数装饰器原理:
@outer def func(...): ...
等价于
def func(...): ... func = outer(func)
需要说明清楚一下闭包吗?
本题应用场景:缓存(非
timeit
)
模块只导入一次 > 1. 共享实例设计 => 模块内全局变量(实例) ;可参考 《python 学习手册》(Learning Python) CH22。
4 字符串行为
4.1 自己加戏的笔试题
今天(2019/Jun/21)有位群友提问了 ?
这种“结果”是什么原因?
a = "a aa"; b = "a aa"; a is b => False
这种现象确实比较意外,我之前没有见过,而我原本认为相同的字符会是同一块空间(就像 C 语言中的 hard-code 的字符串会分配“常量静态存储区”一样),但是实际上并非如此。
既然之前没有学过,那么刚好在这篇博客内记录一下。
4.2 intern 机制
Reference:
1. stackoverflow.com: Is there a difference between “==” and “is”? ?
? 这篇讲到了 x = "a"; "aa" is intern(x*2) => True
这一行为。
2. cnblogs.com: Python中字符串的intern机制 ?