说实话,最初我只是打算是分享一个关于 BackTrader 踩过的坑的
1. 代码的设计
首先,我们要问出第一个问题:我们为什么要设计?或者说,设计是解决什么问题的?这里也给出一个简单的回答:设计主要是解决代码的复用问题。
那什么是好的设计呢?好的设计就意味着以尽可能容易的方式,来帮助对方来使用我们的代码。所以做设计的三个层次是:
- 个人的代码设计——我们用自己的代码,设计模式
- 团队的代码设计——他人用我们的代码,设计模式
- 系统之间的配合设计——架构
所以我们无论是想写好自己的代码给别人用也好,去理解他人的代码也好,还是说以后自己想成为一个架构师,不断磨练自己的设计功底是必经之路
1.1. 设计模式系列计划
在这个计划里,我准备只涉及到代码设计,因为所有的技术都是为了业务服务的,刚才提的三个层次越前面就具体,他和我们在平时工作密切相关,人人都可能遇到,越到后面就越虚,当我们谈论架构时,就必不可少地要考虑业务方向,以至于组织架构,这就真的要具体问题具体分析了。
而我在写这些文章的时候,主要是面向希望深入理解 Python 这门语言的人,所以我会更多地写一些逻辑理解,而不仅仅是粘代码,希望能够以一个不同的角度来提供出来。当然我自己也在一个学习的过程中,如果发现错误还请指出。
这个系列会不断更新,所以这里先列一下能想到的计划:
-
Part1:类,对象,继承
这就是本篇的内容,类、对象、继承是所有面向对象语言里的基础,所以放在一块儿,因为并不是所有的语言都有 “多重继承” “函数式编程” 等概念的,所以 Python 的额外的一些有关设计的东西,就放到其它几个部分 -
Part2:MixIn
MixIn 是 Python 有关的多重继承的实现方案,在这里我们会涉及到 MRO 等机制,更新好了之后我会放上链接 -
Part3:函数式编程
函数式编程是和面向对象编程相似的一种设计思路,我会单独来说
系列里随后的文章都不会有上面这么啰嗦的一大段了。好了,现在我们开始吧。
2. 从零开始
假如我们现在有一个用户系统,它要提供改名的操作,在用户访问的时候还能输出一段问候语,那我们最简单清晰的代码大概会长这样:
name = None
def setName(newName):
global name
name = newName
def greeting():
print("Hi, I am "+name)
# 模拟别人在使用我们的代码
if __name__ == "__main__":
# 我叫 peter
setName("peter")
# 啊,刚才弄错了,我要改成大写字母打头的
setName("Peter")
# 好了,我来了
greeting()
因为我们要在 greeting 函数里使用之前设置好的名字,而且还不知道调用者的调用顺序,所以我们没办法把设置名字和输出问候语写在一个函数里面,因此 name 变量就需要放在全局的位置,这样才能让两个函数都能访问到这个变量,对吧?
好的,现在需求变化了:我们来了第二个用户,也需要设置名字和输出问候语。那么我们的代码可以改成这样:
name1 = None
name2 = None
def setName1(newName):
global name1
name1 = newName
def greeting1():
print("Hi, I am "+name1)
def setName2(newName):
global name2
name2 = newName
def greeting2():
print("Hi, I am "+name2)
if __name__ == "__main__":
setName1("Peter")
greeting1()
setName2("Cate")
greeting2()
很明显,这是一个没有复用的情况,相似的逻辑我们写了两次,如果来了10个用户那我们不得写10份代码了么,那我们要怎么改动呢?
假设这里我们增加一个字段 id,来区分不同的用户的话,我们就可以把上面这段代码改成复用的形式:
names = {}
def setName(id, newName):
global names
names[id] = newName
def greeting(id):
print("Hi, I am "+names[id])
if __name__ == "__main__":
setName(1, "Peter")
greeting(1)
setName(2, "Cate")
greeting(2)
我们用一个字典做存储,把所有名字都放在一块儿了,这样我们的两个函数也实现了复用,很棒!
那我们再想想,这种设计有什么问题么?
问题在于,names 这个命名太通用了,如果我写的代码里有存用户名然后就用了 names,同事的代码里存宠物名,也用了 names,那么两段代码放在一块儿的时候,就有可能“不小心”就访问到了对方的变量然后做出了不必要的修改,从而产生 bug 。单看自己的代码部分还特别难找出来!
所以我们在这时候要注意所有变量的作用域,作用域做的越小,就越不容易被其它的代码干扰。因此我们可以这样:
def createUser():
return {}
def setName(user, newName):
user['name'] = newName
def greeting(user):
print("Hi, I am "+user['name'])
if __name__ == "__main__":
user1 = createUser()
setName(user1, 'Peter')
greeting(user1)
user2 = createUser()
setName(user2, "Cate")
greeting(user2)
我们首先通过 createUser 方法创建出一个极小的“作用域”,然后随后的 setName 和 greeting 都围绕这一个小的作用域来操作。
能不能再给力点?
当然,我们刚才把变量的作用域缩小了,所以变量不会产生干扰了,但现在我们的函数名依旧是全局的,也有可能被其它人误操作,那我们就把函数也放在小的作用域里面:
def createUser():
user = {
"name": None,
}
def setName(newName):
user['name'] = newName
def greeting():
print("Hi, I am "+user['name'])
user['setName'] = setName
user['greeting'] = greeting
return user
if __name__ == "__main__":
user1 = createUser()
user1['setName']('Peter')
user1['greeting']()
user2 = createUser()
user2['setName']('Cate')
user2['greeting']()
现在我们居然只有一个函数了!别人要做宠物逻辑的就让他们叫 createPet 去吧!
其实在这个思考的过程中,我们就通过自己的方式来实现了一套迷你的面向对象的系统,而这里的 createUser 就是这个对象的构造函数,接下来我们看一下官方提供的面向对象系统吧:
3. Python 的类和对象
class User():
def __init__(self):
self.name = None
def setName(self, newName):
self.name = newName
def greeting(self):
print("Hi, I am "+self.name)
if __name__ == "__main__":
user1 = User()
user1.setName('Peter')
user1.greeting()
user2 = User()
user2.setName('Cate')
user2.greeting()
在这里 User 就是一个类,而 user1、user2 就是这个类具体实例化出来的对象,官方的面向对象系统和我们自己写的有一点点区别,但功能都是一样的,差别主要有
-
__init__ 函数
User 后面的括号在这里不是函数的作用,它是下面说继承的时候要介绍的,所以在这里提供了 __init__ 函数作为构造函数 -
self 参数
刚才我们自己的代码里都通过 user[xxx] 来直接访问作用域的数据,而这里系统会通过 self 参数来让我们定义的函数来访问“自己”这个作用域。
到这里我们可以休息一下,希望我上面的东西能够让大家了解到为什么 Python 和其它的各种语言要提供类和对象的机制,它其实都是为了更好地实现系统隔离,让我们的代码远离其它代码的干扰,从而更好地与别人的代码互相配合。
所以我们可以看到,我们在这个系统里其实是付出了一定的“复杂度”换回来“可维护性”以及代码的稳定程度,有些东西确实比平铺直叙来的要复杂一些——有什么东西要比直接写一个全局变量加两个函数要更简单呢,但复杂不应该作为唯一的看待系统的指标,我们要看到这个复杂给我们带来了什么价值,我们要去思考在同样的价值下能否有没那么复杂的方案,或者在同等的复杂度情况下能否带来更多的价值,这个在大型系统设计里也是同样的。
4. 继承
好了,接下来需求又要变化了,我们需要加入一类新的用户,VIP 用户,其它功能都和普通 User 一样,只是有一点区别是他的欢迎语是输出两句 1. “Hi, I am xxx”,2. “I am a VIP”,我们是否要写一个全新的类 VipUser 呢?
class VipUser():
def __init__(self):
self.name = None
def setName(self, newName):
self.name = newName
def greeting(self):
print("Hi, I am "+self.name)
print("I am a VIP")
这样明显我们又重复了自己的工作,把原来 User 的部分代码复制了一份,而在面向对象的语言里,解决多个“对象”的复用是通过“类”,而解决多个“类”的复用就是通过继承,在 Python 里使用继承的方式如下:
class VipUser(User):
def greeting(self):
print("Hi, I am "+self.name)
print("I am a VIP")
这里通过在 VipUser 类后面的括号里把它的父类写上就可以指明它继承的类了,是不是很方便?我们只把有差异的部分写出来就好了,其它都可以保留下来。
那我们能否更给力一点,进一步复用 User 类里的 greeting 方法,只把新增的语句写上呢?可以:
class VipUser(User):
def greeting(self):
super().greeting()
print("I am a VIP")
要注意,这个是 Python 3 里的写法,在 Python 2 里是这样 super(VipUser, self).greeting()
或者这样 super(self.__class__, self).greeting()
通过 super 我们可以拿到指代的父类,而 super() 就是父类的状态来指代的当前对象,最后就可以通过 .greeting() 来调用对应的父类上的方法了。我们其实也可以通过 Python 2 的方法看到 Python 在 super 关键字上演变的过程, super函数 其实是把当前对象 self 和当前类 VipUser 都传入进去,然后通过当前类的 MRO 链上找到父类上对应的方法,最后实现方法的调用,具体关于 MRO 的机制,我会放到下一篇里介绍。
然后这里我们还有一个办法可以调用到父类的方法:
class VipUser(User):
def greeting(self):
User.greeting(self)
print("I am a VIP")
这种方法可以实现但是它越过了 MRO 的机制,所以并不推荐,具体分析我也会放到下一篇里,同时这种写法也 hard code 了 User,如果代码特别长而哪天我们又想给 VipUser 换一个父类,那就很容易漏掉这里从而造成 bug。
所有代码片断我都放在了这里: 代码