学编程究竟学的是什么呢?在写文章的这几天也一直在思考这个问题——恐怕这也是接下来的几年一直会去思考的问题。这个问题的答案也会指导我的方法论,所以索性整顿一下。
现阶段我的回答是,发现需求,然后解决。
最大的需求无非是完成一个项目,为了做到这一点,还有很多需求是完成模块化的功能,再细分下去则是要实现每一个具体的函数。这些都做到了,可能整个功能就跑通了。但是,需求其实没有止步:代码效率能更高么?
鲁棒性能优化么?
可读性能改善么?
整体功能的架构完善么?
甚至工程细节,debug效率能更高么?
logger输出能更合理么?
这些需求则是关系到积累,可不只是一次项目就能简单回答的,需要更深的思考和总结。
所以,在学习中怎么贯彻这一点呢?将学到的知识和项目尽量建立联系,以后的知识点的例子我也会尽量从这方面去构建。
查看python源码。很多需求提不出来的原因是见得不够多,但是源码里,很多经验丰富的前辈不仅预见了这些需求,还封装了这个需求的解决办法。所以不仅对现阶段有帮助,也能是下一阶段——如何把问题解决的更高效的预热。
下面,我们开始讨论面向过程抽象的最基本也是最重要的单位——函数。
环境与作用域
在SICP(第三章)中对环境有着如下定义:一个环境是框架(frame)的一个序列,每个框架是包含着一些约束的一个表格(可能为空),这些约束将一些变量的名字关联于对应的值(在一个框架里,任何变量至多只能有一个约束)。每一个框架还包含了一个指针,指向这一框架的外部环境......
当时走马观花的看到了这个定义,但是在python中debug的时候才发现理解了环境是多么的好用:我们可以通过选取不同的frame,观看到不同的变量在各个环境中是如何变化的。
但是被动的使用肯定是不足够的,我们还是要主动的洞察变量定义和frame的关系,更好的预见我们写出的代码真实的效果是什么。(SICP大法好。。自学者还是要夯实基础。虽然第一次看的时候似懂非懂,不过这就是积累哦,第二次看到的时候就有机会融会贯通了)
变量名解析原则LEGB
其实上述一大段话,说的是这个意思:如果当前frame里有这个变量,直接引用,否则去上层frame中查找是否有同名变量,直到找到最高层;若都没有就报错咯~
看个例子
x = 1
def fun():
print(x)
fun()
# 1
引用好说,但当牵扯到赋值的时候,就不那么显然了
x = 1
def fun():
x = 1
print(x)
fun()
# 2
这个还好,比较符合大家直观感受,但是下面就不一样了
x = 1
def fun():
x += 1
print(x)
fun()
# UnboundLocalError: local variable 'x' referenced before assignment
既然引用没错,这咋不能引用加赋值呢??原来,在函数内部,一旦牵扯到赋值语句,变量就会变成局部变量,像第二个例子一样屏蔽掉全局变量x(x=1)。如果想改变全局变量,那么就在函数内部事先声明
def fun():
global x
...
这样,我们就有一个比较直观的感觉:牵扯到在函数内部赋值时,如果是内部变量没什么关系,但如果改变外部变量的话,一定要像一个办法将其引入内部空间(一般不是global);相反,如果仅仅是普通引用的话则十分方便,无需过度担心。
但是如果函数嵌套的话会发生什么情况呢?显见,最内层函数的外一层就不再是global环境。具体的层次就是所谓的LEGB。Scope Resolution in Python | LEGB Rule - GeeksforGeekswww.geeksforgeeks.org
文中小图清晰的展现了frame的嵌套关系。留一个问题:如果import了其他py文件,frame结构又是什么样子的呢?
frame的存在时间
最后举一个特别精巧的例子,在这个例子中,我们把函数作为另一个函数的返回值。熟悉数学的朋友们知道这个在数学里叫做泛函,是一种强有力的抽象手段。如果有机会会在SICP中好好讨论一下这种所谓的过程的抽象。
def counter():
c = [0]
def inc():
c[0] += 1
return c[0]
return inc
f = counter()
f()
# 1
f()
# 2
f()
# 3
从这个例子不难看出,局部变量c一直存在在f所代表的frame当中。如果g=counter(),则g的计数与f毫无关联。
为什么可以c[0] += 1?这是因为,我们赋值的是变量c所代表的列表中的元素,即,本质上,我们对c是引用,所以在local frame中找不到c时,我们能从enclosed frame中找到c拿来引用。
(这个例子给了我们做计数器的巨大的启发。
默认值的本质
def fun(x=[1]):
x.append([2])
print(x)
# 比较下面两个输出
for _ in range(2):
fun()
# [1, 2]
# [1, 2, 2]
for _ in range(2):
fun([1])
# [1, 2]
# [1, 2]
原来,函数中有这两个属性收集默认值:fun.__defaults__ 收集默认位置参数
fun.__kwdefaults_ 收集默认关键字参数
如果我们不明确的赋值,则调用默认值。但是!!因为我们这里的默认值可变(虽然列表的内存地址没有改变,但是列表的内容变了),所以我们的行为也许会修改默认值!
只要函数不被销毁,作为属性的默认值就会一直记录所有的改变。
首先,我们要意识到,这种做法有时有利,有时有害,不可一概而论;下面我们展示两种方法:如果一旦我们不想让默认值改变,该采取什么做法。
def fun(x=[]):
x = x[:] # shadow copy
....
第一方面,如果传入了x,立即对xshadow copy,没问题;如果没传入,我们操作的是默认值x的shadow copy,而不是x本身,所以。。。?
这里比较讨巧,用了一个空列表,如果一个默认值很复杂(其实不推荐复杂的默认值吧。。),那么shadow copy也是会有shadow copy的问题的对吧?
所以这种做法须谨慎。
def fun(x=None):
if x is None:
x = []
....
哇!这个方法还是厉害的咧。也是推荐大家使用的。每次调用,对x重新赋值,肯定能避免这次的改变泄漏到下次操作中。
我们也可以这么理解:
函数的属性,和函数定义本身绑定在一起,存在于global frame(假设是最外层函数);而每次调用函数,就会自动生成一个新的local frame。所以,方法二中的赋值方法是没有问题的。