今年的校招来得格外早。上一级的师兄师姐说十月份才是面试高峰。然而我有一种八月份就要面完的感觉。。。
知乎是我最早去面的一家。四月份向认识的工程师问知乎的招聘流程,说并没有固定的时间点,于是六月份就去面试了。一共有两轮,各种知识都有涉及,然而一问到操作系统我基本就懵了,因为没有怎么看。面试之前我临时又看了一遍之前写的 Tornado 的一些总结,多少还是有点用。第二面要我写一个 class decorator,结果我错把应该在 __init__
传入的函数放到 __call__
里传入了。最后问问题,了解到知乎用 Go 写了很多基础架构的东西,还是比较出乎意料的。
面试完之后和 hr 聊得挺开心,她问我怎么才能招到技术牛人,我说你们不是都从豆瓣招么,她说也不能全部从豆瓣招吧。于是我就推荐她去加 CPyUG。她还想从技术博客发掘牛人,我说这种找法比较困难,博客都太分散了。
然后面了美团的云计算部门。之前师兄说美团算法会面很难,比国内其它公司难,要我好好准备。于是面试前一周我一直疯狂看算法+刷题。搞笑的是,面了三轮技术面,就考了一道算法题,还是反转链表。竟然也并没有考设计题。基!本!都!在!问!项!目!我简历上的每个项目至少讲了两遍。当然也问了网络和操作系统。问得挺深,于是就又一堆不会,比如 fork
之后发生了什么,socket
是怎么创建的(((゚Д゚;)))。有意思的是一个面试官居然纠正了我在《还在疑惑并发和并行》一文中的观点,他用“指令集并行”的例子反驳了并行是属于并发的说法,并且指出“并发”是逻辑上的,“并行”是物理上的,两者并没有包含关系。我之前都不知道指令集并行这件事。还有就是问问题环节,一个面试官提到了“程序员如何面对重复性的日常工作的态度”,确实是一个值得思考的问题。
过了几天阿里的一个工程师给我打电话说要电面。我内推的部门是蚂蚁金服,然后面试官告诉我,蚂蚁金服全部在杭州,如果你不想来杭州,我们也就没必要面了。WHAT THE FUCK??于是就没有面。现在内推时间已过,我感觉被阿里坑了_(:3」∠)_
今天昨天上午是 Google 的电话面试。为了方便美国面试官,所有中国的 candidate 都不得不在早上七点参加面试。不知道是因为紧张还是怕睡过,我一晚上都没睡好,一点半,两点半,三点半各醒了一次,后来还醒了一次但没看是几点。于是六点半起床随便吃了个面包就开始面试了。面试官直接从美国一个越洋电话打过来,是个中国人。题说实话不难,但是描述得非常含糊,第一题我 clarify 了五分钟,第二题 clarify 了超过十分钟。7:20 的时候我还在就第二题到底是什么意思反复和面试官讨论,这可是限定在 45 分钟内完成的面试啊!我急了,面试官也急了。我能感觉到她非常不理解我为什么不能理解题意,但是我 TM 就是不能理解啊!没办法了,我说,我给这样的一个输入,你能不能告诉我应该输出什么?最后终于终于通过举一个例子弄清了题意,我发现我之前想得太复杂了。做完这个又加了一问,结果这一问是我之前以为第二问要求的东西。。。
没想到很快就接到电话说电面过了,只能说十分幸运,当时一直没弄清题意的时候真的以为要挂了。
其实能进现场面就已经达到我的目标了,因为我知道自己不可能拿到 Offer。为什么呢,因为面得太早。一个去了 Google 的师兄叮嘱我,“一定要 10、11 月再面试,8、9 月好多大牛都挂了。拿到 Offer 的基本都是 10、11 月面的。”但是今年 Google 听说是缩招了,要求内推的人必须 8 月份面试完。听到这个消息的时候,我就直接把目标设定成了去感受下现场面试的气氛,因为挂了才是正常的。
今天在知乎回答了一个问题,居然一个赞都没有,也是神奇,毕竟这算是我非常认真答的题之一。既然如此就贴过来好了,有些内容之后再补充。
原问题
Python中既然可以直接通过父类名调用父类方法为什么还会存在super函数?
比如
class Child(Parent):
def __init(self):
Parent.__init(self)
这种方式与super(Child, self).init()有区别么?
回答
针对你的问题,答案是可以,并没有区别。但是这题下的回答我感觉都不够好。
要谈论 super
,首先我们应该无视 "super" 这个名字带给我们的干扰。
不要一说到 super 就想到父类!super 指的是 MRO 中的下一个类!
不要一说到 super 就想到父类!super 指的是 MRO 中的下一个类!
不要一说到 super 就想到父类!super 指的是 MRO 中的下一个类!
一说到 super 就想到父类这是初学者很容易犯的一个错误,也是我当年犯的错误。
忘记了这件事之后,再去看这篇文章:Python’s super() considered super!
这是 Raymond Hettinger 写的一篇文章,也是全世界公认的对 super
讲解最透彻的一篇文章,凡是讨论 super 都一定会提到它(当然还有一篇 Python's Super Considered Harmful)。
如果不想看长篇大论就去看这个答案,super
其实干的是这件事:
def super(cls, inst):
mro = inst.__class__.mro()
return mro[mro.index(cls) + 1]
两个参数 cls 和 inst 分别做了两件事:
1. inst 负责生成 MRO 的 list
2. 通过 cls 定位当前 MRO 中的 index, 并返回 mro[index + 1]
这两件事才是 super 的实质,一定要记住!
MRO 全称 Method Resolution Order,它代表了类继承的顺序。后面详细说。
举个例子
class Root(object):
def __init__(self):
print("this is Root")
class B(Root):
def __init__(self):
print("enter B")
# print(self) # this will print <__main__.D object at 0x...>
super(B, self).__init__()
print("leave B")
class C(Root):
def __init__(self):
print("enter C")
super(C, self).__init__()
print("leave C")
class D(B, C):
pass
d = D()
print(d.__class__.__mro__)
输出
enter B
enter C
this is Root
leave C
leave B
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.Root'>, <type 'object'>)
知道了 super
和父类其实没有实质关联之后,我们就不难理解为什么 enter B 下一句是 enter C 而不是 this is Root(如果认为 super 代表“调用父类的方法”,会想当然的认为下一句应该是this is Root)。流程如下,在 B 的 __init__
函数中:
super(B, self).__init__()
首先,我们获取 self.__class__.__mro__
,注意这里的 self 是 D 的 instance 而不是 B 的
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.Root'>, <type 'object'>)
然后,通过 B 来定位 MRO 中的 index,并找到下一个。显然 B 的下一个是 C。于是,我们调用 C 的 __init__
,打出 enter C。
顺便说一句为什么 B 的 __init__
会被调用:因为 D 没有定义 __init__
,所以会在 MRO 中找下一个类,去查看它有没有定义 __init__
,也就是去调用 B 的 __init__
。
其实这一切逻辑还是很清晰的,关键是理解 super
到底做了什么。
于是,MRO 中类的顺序到底是怎么排的呢?Python’s super() considered super!中已经有很好的解释,我翻译一下:
在 MRO 中,基类永远出现在派生类后面,如果有多个基类,基类的相对顺序保持不变。
关于 MRO 的官方文档参见:The Python 2.3 Method Resolution Order,有一些关于 MRO 顺序的理论上的解释。
最后的最后,提醒大家.
什么 super 啊,MRO 啊,都是针对 new-style class。如果不是 new-style class,就老老实实用父类的类名去调用函数吧。
昨天晚上师兄在 qq 上和我诉苦,说我们的代码测试起来太不方便了。问题大概出在这段代码:
# motorclient.py
import motor
from settings import mongo_machines, REPLICASET_NAME
from pymongo import ReadPreference
_motorclient = motor.MotorReplicaSetClient(
','.join(mongo_machines),
replicaSet=REPLICASET_NAME,
readPreference=ReadPreference.NEAREST)
fbt = _motorclient.fbt
reward = _motorclient.fbt_reward
fbt_log = _motorclient.fbt_log
这是我之前写的对 motor 的简单封装,无关代码已经去掉。目的很简单,他们每次要访问数据库只要先 import motorclient
,然后用 motorclient.dbname
操作各个数据库就行了。问题在哪里呢?
发现 motorclient 好蛋疼,只要一 import 就必须连接数据库,本地测试每次都要打 mock
这是他的原话,他想在没有配置副本集的本机进行测试,然而 motorclient 只要一 import 就会初始化一个 MotorReplicaSetClient
,于是只能 Mock。于是现在有如下需求:
- 希望保证现有接口不变
- 不要一 import 就初始化
- 能非常容易地变成只连接本地数据库而不是副本集
怎么做呢?
我突然想到,David Beazley 的演讲里好像提到了这个概念(关于他的演讲请参考 PyCon2015 笔记)。在 slide 的 150-152 页,他当时想实现的是,import 某个 package 的时候,不直接把 submodules/subpackage 都给 import 进来(因为很耗时间,相当于把所有文件执行一次),而是按需 import,他把这个技巧叫 "Lazy Module Assembly"。我面临的需求和他类似,也要用 "lazy" 的方式加载,只不过针对的是一个 module 里的变量。
上网搜了搜,参考了 SO 上的某答案,完成了 lazy 版,我把它叫做 lazy module attribute。
# motorclient.py
mode = None
def set_mode(m):
global mode
mode = m
class Wrapper:
localhost = '127.0.0.1'
port = 27017
dbs = {
'fbt': None,
'reward': None,
'fbt_log': None
}
def __init__(self, module):
self.module = module
self._motorclient = None
def __getattr__(self, item):
if item in self.dbs and self._motorclient is None:
if mode == 'test':
self._motorclient = motor.MotorClient(host=self.localhost,
port=self.port)
else:
self._motorclient = motor.MotorReplicaSetClient(
','.join(mongo_machines),
replicaSet=REPLICASET_NAME,
readPreference=ReadPreference.NEAREST)
self.dbs['fbt'] = self._motorclient.fbt
self.dbs['reward'] = self._motorclient.fbt_reward
self.dbs['fbt_log'] = self._motorclient.fbt_log
self.module.__dict__.update(self.dbs)
return getattr(self.module, item)
sys.modules[__name__] = Wrapper(sys.modules[__name__])
它的工作流程是这样:
1. 在 import motorclient
,会创建一个 Wrapper
类的实例替换掉这个 module 本身,并且把原来的 module object 赋给 self.module
,别的什么都不做。
2. 然后我们在别的文件中访问 motorclient.fbt
,进入 Wrapper
实例的 __getattr__
方法,item='fbt'
。因为是初次访问,self._motorclient is None
的条件满足,这时开始初始化变量。
3. 根据全局变量 mode
的值,我们会创建 MotorClient
或是 MotorReplicaSetClient
,然后把那几个数据库变量也赋值,并且更新 self.module.__dict__.update(self.dbs)
。这个效果就和我们的老版本初始化完全一样了,相当于直接把变量定义写在文件里。
4. 然后调用 getattr(self.module, item)
,因为我们已经更新过 self.module
的 __dict__
,所以能够正常返回属性值。第一次访问至此结束
5. OK,下一次再访问 motorclient.fbt
,因为 self._motorclient
已经有值了,所以我们就不再初始化,直接把活交给 self.module
就好了。
下面的内容比较 internal,看不下去的同学就不要看了。。。
本来说到这里就差不多了,不过对 Python import 机制比较了解的同学可能会看出代码中的一个潜在问题。就是这句话:
python
sys.modules[__name__] = Wrapper(sys.modules[__name__])
为什么 sys.modules
的 key 一定就是 __name__
呢?下面将追根溯源,证明这一点。
据 Brett Cannon 在《How Import Works》演讲的 slide 第 27 页对 load_module
函数的描述,sys.modules
所用的 key 是 fullname
:
如果标准库中有类似 module.__name__ = fullname
这种东西,那么我们可以断定 __name__
就是 fullname
。于是苦逼地翻了半天源码,好在终于找到了,有这么一句话:
python
module.__name__ = spec.name
那么这个 Spec 又是什么呢?它实际上是 Python3.4 里才引入的一个类,官方的描述是 "A specification for a module's import-system-related state"。好,就差最后一步了!我又找到了一句代码:
spec = spec_from_loader(fullname, self)
没错,Spec 初始化的第一个参数,传入的是 fullname,而它恰恰被赋给了 spec.name
。至此,我们的推理终于完成,现在可以肯定地说,sys.modules
的 key 就是这个 module 的 __name__
。(实际上这个证明针对的是 Python3.4,不过这种接口肯定是向前兼容的,对于所有版本都成立)
Q.E.D.
当初写 ezcf 的时候其实就遇到过这个问题,只不过没深究。我的代码中有这么一段:
class BaseLoader(_BaseClass):
def __init__(self, *args, **kwargs):
pass
def load_module(self, fullname):
if fullname in sys.modules:
mod = sys.modules[fullname]
else:
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
mod.__file__ = self.cfg_file
mod.__name__ = fullname # notice this !!
mod.__loader__ = self
mod.__package__ = '.'.join(fullname.split('.')[:-1])
return mod
当时看到别人都写 self.__name__ = fullname
,于是就跟着这么写了,并不明白其中原理。现在终于弄清了,这是 convention,必须得这么写。
不是后记:
感觉好久都没有写文章了啊。。空余时间基本都去刷题了啊。。。跪求 Offer
以及如果读者中有能内推的请联系我(然而并没有什么读者(╥﹏╥)