引子
假设我们要在我们的程序里表示狗,狗有如下属性:名字、品种、颜色。那么可以先定义一个模板,然后调用这个模板生成各种狗。
def dog(name,d_type,color): data = { 'name':name, 'd_type':d_type, 'color':color} return datad1 = dog('小七','拉布拉多','黄')d2 = dog('旺财','中华田野犬','黄')
上面用函数作为模板来定义数据确实很方便。然后我们要让狗叫,那么还需要在写一个程序,简单点,比如是这样的
def dog(name,d_type,color): data = { 'name':name, 'd_type':d_type, 'color':color} return datad1 = dog('小七','拉布拉多','黄')d2 = dog('旺财','中华田野犬','黄')def bark(d): print("dog %s:Wang~Wang~Wang~~~"%d['name'])bark(d2)
貌似也很完美,但是如果这是bark不小心引用了别的参数,比如一只猫,那么问题就来了。
def dog(name,d_type,color): data = { 'name':name, 'd_type':d_type, 'color':color} return datad1 = dog('小七','拉布拉多','黄')d2 = dog('旺财','中华田野犬','黄')def cat(name,c_type,color): data = { 'name':name, 'c_type':c_type, 'color':color} return datac1 = cat('比格沃斯','波斯猫','白') # 在定义一个猫def bark(d): print("dog %s:Wang~Wang~Wang~~~"%d['name'])def miaow(c): print("cat %s:Miao~Miao~Miaow~~~"%c['name'])bark(c1) # 这里是不是乱套了miaow(d1)
所以这里的问题就是,我们希望bark只能调用狗,cat只能调用猫。当然可以加上if判断,但是很low。于是可以改成这样:
def dog(name,d_type,color): def bark(): print("dog %s:Wang~Wang~Wang~~~"%name) data = { 'name':name, 'd_type':d_type, 'color':color, 'action':bark} # 把动作的函数写到狗的函数里,把函数名作为返回字典里的一个元素 return datad1 = dog('小七','拉布拉多','黄')d2 = dog('旺财','中华田野犬','黄')def cat(name,c_type,color): def miaow(): print("cat %s:Miao~Miao~Miaow~~~"%name) data = { 'name':name, 'c_type':c_type, 'color':color, 'action':miaow} return datac1 = cat('比格沃斯','波斯猫','白')d1['action']() # 这样调用狗模板里的狗叫函数c1['action']()
把狗叫的函数也写到狗的模板里去,然后就实现了只能由狗调用狗叫的需求了。
引子讲完了,上面并不是面向对象。上面的问题也不是很复杂,但是如果问题再复杂可能就解决不下去了,我们需要面向对象来解决这类问题。
面向过程 VS 面向对象
编程范式
编程是程序员用特定的语法+数据结构+算法组成的代码来告诉计算机如何执行任务的过程,实现一个任务的方式有很多种不同的方式,对这些不同的编程方式的特点进行归纳总结得出来的编程方式类别,即为编程范式。两种最重要的编程范式分别是面向过程编程和面向对象编程,然后还有一个函数式编程。
面向过程编程(Procedural Programming)
面向过程编程,就是程序从上到下一步步执行,一步步从上到下,从头到尾的解决问题。基本设计思路就是程序一开始是要着手解决一个大的问题,然后把一个大问题分解成很多个小问题或子过程,这些子过程再执行的过程再继续分解直到小问题足够简单到可以在一个小步骤范围内解决。
这样做的问题也是显而易见的,就是如果你要对程序进行修改,对你修改的那部分有依赖的各个部分你都也要跟着修改,随着程序越来越大,这种编程方式的维护难度会越来越高。
但是面向过程依然是有可取之处的,如果你只是写一些简单的脚本,去做一些一次性任务,用面向过程的方式是极好的,但如果你要处理的任务是复杂的,且需要不断迭代和维护的,那还是用面向对象最方便了。
面向对象编程(Object-Oriented Programming)
OOP编程是利用“类”和“对象”来创建各种模型来实现对真实世界的描述,使用面向对象编程的原因一方面是因为它可以使程序的维护和扩展变得更简单,并且可以大大提高程序开发效率。另外,基于面向对象的程序可以使它人更加容易理解你的代码逻辑,从而使团队开发变得更从容。
面向对象编程介绍
无论用什么形式来编程,我们都要明确记住以下原则:
写重复代码是非常不好的低级行为
你写的代码需要经常变更
面向对象的核心特性
Class 类
Object 对象
Encapsulation 封装
Inheritance 继承
Polymorphism 多态
Class 类 和 Object 对象
类:一个类即是对一类拥有相同属性的对象的抽象、蓝图、原型。在类中定义了这些对象的都具备的属性(variables(data))、共同的方法。
对象:一个对象即是一个类的实例化后实例,一个类必须经过实例化后方可在程序中调用,一个类可以实例化多个对象,每个对象亦可以有不同的属性。
首先要定义类,然后将类实例化,最后通过这个实例来调用类中的功能
这里类名要用大驼峰规范来命名
# class是定义类,Dog是类名,括号中的object暂时知道是必填就好了class Dog(object): print("Hello , I am a dog.")# 上面已经定义好了类,下面来将类实例化d = Dog() # 但是这里会把print打印出来
我们不希望在实例化的时候就把print打印出来,而是要在调用类中的功能的时候再打印,那么上面的类还得修改一下。
class Dog(object): # 定义一个函数,把print写到函数里,避免被直接调用执行 def sayhi(self): print("Hello , I am a dog.")d = Dog() # 这是这里就不会直接打印了d.sayhi() # 这样来调用执行类里的sayhi
d = Dog()
这步叫实例化。先去实例化,然后 d.sayhi()
再去调用它的功能
上面的例子我们没有传入参数,现在把引子里的例子改成类,传入属性:
class Dog(object): # 参数要写在__init__这个函数里,这个叫构造函数或构造方法 def __init__(self,name,d_type,color): self.name = name # 将传入的传输传给self self.type = d_type self.color = color # 下面的函数叫做类的方法 def bark(self): print("dog %s: Wang~Wang~Wang~~~"%self.name) # 这里就可以调用self里的值了d1 = Dog('旺财','中华田野犬','黄') # 先实例化d1.bark() # 然后调用功能
self,就是实例本身。你实例化时python会自动把这个实例本身通过self参数传进去。上面例子中的self就是d1。再通俗一点,self就是调用当前方法的对象。
实例化后产生的对象,就叫实例,是这个类的实例。d1 = Dog('旺财','中华田野犬','黄')
这个就是进行实例化,产生了Dog这个类的一个实例d1,而self就是这个实例本身
上面的__init__
叫做构造函数,或者叫构造方法,也就是初始化的方法
上面的bark
函数叫做类的方法,我们可以根据需要写多个方法
我们写一个给狗吃东西的函数food,把食物通过参数传入
再写一个给狗改名字的函数,这里牵涉到修改对象的属性值,也就是初始化的内容可以后期修改
再写一个自报名字的函数,看看改名的效果:
class Dog(object): def __init__(self,name,d_type,color): self.name = name self.type = d_type self.color = color def bark(self): print("dog %s: Wang~Wang~Wang~~~"%self.name) def eat(self,food): print("%s 正在吃 %s"%(self.name,food)) def rename(self,new_name): "给狗改名" print("%s 改名为 %s"%(self.name,new_name)) self.name = new_name # 这里改变了对象的属性 def say_name(self): "报名字" print("我的名字是:%s"%self.name)d1 = Dog('旺财','中华田野犬','黄')d1.bark()d1.eat("骨头") # 把骨头传给了foodd1.say_name() # 现在的名字d1.rename("小黄") # 改名d1.say_name() # 再看看名字变了没
总结:
类 ==》 实例化 ==》 实例(对象):类经过实例化后变成了实例也就是对象__init__
:构造函数self.name = name
:属性,或者叫成员变量、字段def bark(self)
:方法,或者叫动态属性
Encapsulation 封装
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
私有属性
之前使用的属性并不是公有属性,而是叫成员属性,这些属性是只属于它的对象的。公有属性最后讲。
私有属性在类的外部不不可访问的。我们来增加一个私有属性hp,然后写一个函数来操作hp这个私有属性。定义私有属性使用self.__属性名
class Cat(object): def __init__(self,name,c_type,color,hp=100): self.name = name self.c_type = c_type self.color = color self.__hp = hp # 定义为私有属性 def be_hit(self,damage): "造成伤害,扣除hp值" print("%s 受到了 %d点 伤害"%(self.name,damage)) print("当前hp:%d,伤害:%d,剩余hp:%d" %(self.__hp,damage,self.__hp-damage)) self.__hp -= damagec1 = Cat('比格沃斯','波斯猫','白')print("name:",c1.name) # 公有属性可以正常获取到#print(c1.__hp) # 这句会报错,类的外部是获取不到私有属性的c1.be_hit(10) # 这里是通过类内部的方法打印的私有属性的数值c1.be_hit(5)
既然可以通过内部方法访问私有属性,我们可以将私有属性写到函数里return,提供一个给外部访问的方法,但是不能修改。另外其实也是有方法可以强制访问的。
class Cat(object): def __init__(self,name,c_type,color,hp=100): self.name = name self.c_type = c_type self.color = color self.__hp = hp def be_hit(self,damage): print("%s 受到了 %d点 伤害"%(self.name,damage)) print("当前hp:%d,伤害:%d,剩余hp:%d" %(self.__hp,damage,self.__hp-damage)) self.__hp -= damage def get_hp(self): "提供方法,返回私有属性hp的值" return self.__hpc1 = Cat('比格沃斯','波斯猫','白')print(c1.get_hp()) # 通过内部提供的方法来访问私有属性print(c1._Cat__hp) # 其实也可以强制访问到私有属性,并这个是可以修改私有属性值的
对象名._类名__私有属性名
: 强制访问私有属性,可读写
公有属性
公有属性,所有属于这个类的对象都可以访问的属性,才叫公有属性。
之前的例子中,类Dog定义了2个对象d1 和 d2 ,但是d1里的属性是只属于d1的,无法通过d2来访问,这些都是叫成员属性
在类里直接定义的属性,既公有属性。
class Dog(object): called = "狗" # 在类里定义公有属性 def __init__(self,name,d_type,color): self.name = name self.type = d_type self.color = colord1 = Dog('旺财','中华田野犬','黄')d2 = Dog('小七','拉布拉多','黄')print(d1.called)print(d2.called)Dog.called = "犬" # 通过类更改类公有属性print(d1.called) # 通过类修改,所有对象都会变print(d2.called)d1.called = "看门狗" # 通过对象更改,其实这句是新建了d1里的成员属性# 同时有成员属性和公有属性,则使用成员属性print(d1.called) # 这里打印的是刚才新建的成员属性print(d2.called) # 这里还是原来的公有属性Dog.called = "dog" # 再通过类更改公有属性print(d1.called) # 这里没有变化,因为成员属性还在,并且没变print(d2.called) # 这里变化了,因为这里只有公有属性del d1.called # 把成员属性从内存中清楚print(d1.called) # 现在全部是公有属性值了print(d2.called)d1.called = "看门狗"d2.called = "导盲犬" # 现在全部都有成员属性了print(d1.called)print(d2.called)print(Dog.called) # 直接通过类而不是对象获取的一定是公有属性
这里公有属性和成员属性的情况和之前学的全局变量和局部变量是一样的效果。
展开讲一下函数的情况,类中def定义的所有函数我们也可以理解为是公有的。那么也可以通过定义成员的方法来替换原来的公有的方法:
class Dog(object): called = "狗" def __init__(self,name,d_type,color): self.name = name self.type = d_type self.color = color def bark(self): print("dog %s: Wang~Wang~Wang~~~" %self.name)'''在类外面再定义一个函数,这里无法使用self传入变量了如果要传入参数,那么下面调用的时候也得带参数传入,这样调用方法也变了'''def bark(): "自己再定义一个bark" print("TEST")d1 = Dog('旺财','中华田野犬','黄')d2 = Dog('小七','拉布拉多','黄')d1.bark() # 正常调用类中的方法d2.bark()d1.bark = bark # 通过这个方法来实现给d1一个自己的成员方法d1.bark() # 现在调用的不是公有方法,而是d1自己的成员方法d2.bark()del d1.bark # 清除d1的bark方法后,d1有可以正常调用类的bark方法了d1.bark()d2.bark()
析构方法 和 构造方法
方法和函数:在类里面定义的函数就是这个类的方法,所以方法和函数这两个词有时候会混用,主要看你是在描述什么东西
构造方法:之前在为类传入参数的时候用到了构造函数,构造函数其实就是在生成1个实例的时候自动运行的函数,所以通过构造函数我们可以实现在生成实例的时候自动把参数传递给self
析构方法:和构造方法差不多,就是在一个实例被销毁的时候自动运行的函数
class test(object): def __init__(self): # 构造函数 print("init in the test") def __del__(self): # 析构函数 print("del in the test")input("准备实例化对象")obj = test()input("准备销毁对象")del objinput("执行完毕")
把一些收尾的工作写在析构函数里,在你销毁这个对象的时候就自动执行,比如关闭所有的客户连接、关闭所有打开的文件等等。具体怎么用得到了以后用的时候才知道了。
Inheritance 继承
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
通过继承创建的新类称为“子类”或“派生类”。
被继承的类称为“基类”、“父类”或“超类”。
继承的过程,就是从一般到特殊的过程。
要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。
要同时继承多个类有2种方法:多重继承和多级继承
继承概念的实现方式主要有2种:实现继承、接口继承。
实现继承是指使用基类的属性和方法而无需额外编码的能力
接口继承是指仅使用属性和方法的名称,但是子类必须提供实现的能力(子类重构父类方法)
抽象类:仅定义将由子类创建的一般属性和方法。
OOP开发范式大致为:划分对象→抽象类→将类组织成为层次化结构(继承和合成) →用类与实例进行设计和实现几个阶段。
简单的继承例子:
class Human(object): def talk(self): print("Hello World")class Chinese(Human): # 这样就继承了Human这个类 pass # 什么也不写,但是我们有继承啊h1 = Chinese() # 实例化1个chinese的对象h1.talk() # 虽然Chinaes没有talk方法,但是继承了Human的talk方法
子类也可以有自己的方法,还可能重构父类的方法:
class Human(object): def talk(self): print("Hello World")class Chinese(Human): # 这样就基础了Human这个类 "这次我们来写几个方法" def greatWall(self): # 定义一个新的方法 print("长城长啊长") def talk(self): # 重构父类的方法 print("你好,世界")h1 = Chinese() # 实例化1个chinese的对象h1.talk() # 调用的是重构后的新方法h1.greatWall() # 在子类中定义的新方法
再加上类的参数:
先只写上父类的构造函数,子类不写。子类就是完全继承父类构造函数。
class Human(object): def __init__(self,name,age): # 父类里有构造函数 self.name = name self.age = age def talk(self): print("Hello World")class Chinese(Human): pass # 子类没有构造函数,就直接继承父类的构造函数#h1 = Chinese() # 没有参数会报错,因为构造函数需要传入2个参数h1 = Chinese("张三",33) # 实例化的时候,需要根据构造函数的要求传入参数h1.talk()
再来看子类的属性,上面是完全继承父类的属性,那么就不用写构造函数。
也可以完全无视父类的属性,那么直接重构自己的构造函数就好了。
复杂一点情况,继承父类的属性,但是还要有自己的额外属性。
class Human(object): def __init__(self,name,age): # 父类的构造函数 self.name = name self.age = age def talk(self): print("Hello World")class Chinese(Human): def __init__(self,name,age,kungfu): # 先继承,再重构 #Human.__init__(self,name,age) # 调用父类的构造函数,实现继承 super(Chinese,self).__init__(name,age) # 和上面那句效果一样,用这种写法 self.kungfu = kungfu # 再增加自己重构的属性 def talk(self): # 这里顺便把函数也重构了,但是我们要保留父类里的部分 Human.talk(self) # 方法也可以和属性一样,实现继承和重构 print("你好,世界")h1 = Chinese("张三",33,"少林") # 实例化的时候,需要根据构造函数的要求传入参数h1.talk()print(h1.__dict__) # 顺便来看一下这个的作用
子类要继承父类属性并且有自己的特有属性,需要先继承,再重构。通过父类的名字调用执行父类的构造函数,实现继承,然后可以在后面写上自己需要重构的代码。
函数也是可以用这个方法来调用父类的方法,添加自己的代码,实现在父类的基础上重构自己特有的部分__dict__
属性:可以查看对象所有的属性和值,以字典的形式。
多继承
class Chinese(Human):
这个是继承的语法,括号中的变量可以传入多个类,用逗号隔开就实现了多继承。比如:class Chinese(Human,Person):
上课说用的不多,也没展开。所以暂时就知道这个语法就好了。
下面也是在多继承的时候才会有区别的内容
新式类 和 经典类
python3里已经没有这个问题了,并且现在都是用的新式类的写法。不过还是举个例子说明一下继承的顺序,这个是多继承的情况下会遇到的问题。
定义一个基类A,然后是A的两个子类B和C。最后来个孙子类D,D要继承B和C。每个类里都定义一个属性n,写在构造方法里
class A(object): pass def __init__(self): self.n = "A"class B(A): pass def __init__(self): self.n = "B"class C(A): pass def __init__(self): self.n = "C"class D(B,C): pass #def __init__(self): #self.n = "D"d1 = D()print(d1.n)
如果D有构造方法,那么结果一定是D。然后依次将上面的构造方法也注释掉,看看D的继承顺序。结果是B-C-A。这个叫广度查找。
经典类就不试了,要在python2里才会有深度查找的效果。在python3里还是广度查找。
新式类,广度查找 B-C-A
经典类,深度查找 B-A-C
语法上的区别,都用新式类就好了,经典类知道一下,看到的时候别不认识。
定义类的语法:class A:
# 经典类写法,python2里和下面的写法有区别。python3里不必显示声明,这么写也是新式类了。class A(object):
# 新式类写法,咱就这么写
调用父类方法的语法:Human.__init__(self,name,age)
# 经典类写法,这个是子类构造函数里实现先继承的那句代码super(Chinese,self).__init__(name,age)
# 新式类写法,咱就这么写
不单是构造函数,其他函数也一样,尽量都super,不是多继承的话两个都一样。但是绝对不要混用。
Polymorphism 多态
多态,简单点说:"一个接口,多种实现",指一个基类中派生出了不同的子类,且每个子类在继承了同样的方法名的同时又对父类的方法做了不同的实现,这就是同一种事物表现出的多种形态。
多态允许将子类的对象当作父类的对象使用,某父类型的引用指向其子类型的对象,调用的方法是该子类型的方法。这里引用和调用方法的代码编译前就已经决定了,而引用所指向的对象可以在运行期间动态绑定
封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了,代码重用。而多态则是为了实现另一个目的,接口重用。多态的作用,就是为了类在继承和派生的时候,保证使用“家谱”中任一类的实例的某一属性时的正确调用。
举例:Pyhon 很多语法都是支持多态的,比如 len(),sorted(), 你给len传字符串就返回字符串的长度,传列表就返回列表长度。
讲了那么多,到底是要通过多态做什么?就是要,通过父类调子类,python不直接支持,但是可以间接支持。
class People(object): # 先定义一个基类 def talk(self): # 基类的talk方法,我们不希望被调用,写一个会抛出错误的代码 "如果基类的这个方法被调用,就抛出一个错误" raise NotImplementedError("Subclass must implement abstract method")class Chinese(People): # 这个是子类 def talk(self): # 重构talk方法 print("你好,世界")class American(People): def talk(self): print("Hello World")# 如果调用了基类的方法,会根据raise里定义的,抛出一个错误。去掉下面的注释测试一下#p1 = People() # 实例化一个基类#p1.talk() # 调用基类的talk方法# 实例化2个对象c1 = Chinese()a1 = American()# 通过子类调用自己的方法当然没问题。要用多态就是要使用统一的接口来实现这2条命令c1.talk()a1.talk()# 多态是要用父类调用子类#People.talk(c1) # 这样是最好的,真正的直接实现多态的方法,但是Python不支持#People.talk(a1)# 间接支持多态的方法,定义一个函数作为统一的接口def People_talk(obj): obj.talk()# 用新定义的接口,调用不同的子类,每次用的都是这个子类里重构的那个方法People_talk(c1) # 传入一个c1对象,实际就是执行c1.talk()People_talk(a1) # 传入一个a1对象,实际就是执行a1.talk()
间接支持多态,新定义一个函数,用参数传入一个对象。然后再函数中调用这个对象的一个方法。
主动抛出一个错误,上面在基类里使用了一个raise命令,可以实现主动触发一个错误,可以定义这个错误的类型和消息。这里的作用就是验证这个方法没有被调用到,貌似一般都是用一句print来验证的,这个也很高级。
补充
面向对象的应用场景
学了那么多面向对象,但是什么时候用呢?毕竟在python里我们使用面向过程的方法也是一样可以实现的。课上总结了3种场景,推荐使用面向对象的方法来实现,并且确实更好。
根据一个模板来创建某些东西的时候
纵向扩展
横向扩容
第一条从前面引子开始就在举例子了。后面两个名词用在这是我自己总结的。
纵向扩展,对一个对象有多个不同的操作,比如连接一个服务器、执行一条命令、上传一个文件、断开与服务器的连接。把这种对同一个对象执行的不同的操作写在一个类里,每一种操作就是类里的一个函数
横向扩展,原本有很多个函数都需要传公共的参数的时候,可以都写到一个类里。比如有很多个操作都需要和服务器交互,那么就都会需要地址、端口、密码这些参数,然后不同的方法又需要不同的其他参数。每次定义函数以及之后调用函数都会重复的引用这几个重复的参数。
# 面向过程定义3个函数,其中都会用到3个一样的参数def f1(host,port,pwd,arg1): passdef f2(host,port,pwd,arg1,arg2): passdef f3(host,port,pwd,arg1,arg2,arg3): pass# 调用的时候也要反复的来引用这些参数f1(1,2,3,4)f2(1,2,3,4,5)f3(1,2,3,4,5,6)# 面向对象来做同样的事情,重复的参数写到构造方法里class Foo(object): def __init__(self,host,port,pwd): self.host = host self.port = port self.pwd = pwd def f1(arg1): pass def f2(arg1,arg2): pass def f3(arg1,arg2,arg3): pass# 调用的时候先把重复的参数写在一个对象里,然后可以分别调用这个对象的不同的方法obj = Foo(1,2,3)obj.f1(4)obj.f2(4,5)obj.f3(4,5,6)
类中的其他方法
类中的函数我们叫方法,默认在类中定义的函数都是保存在类中,要调用这个方法需要通过对象。叫做实例方法,就是必须是实例化之后才能使用的方法,之前都是这种实例方法。
静态方法,就是一种普通函数,保存在类中,可以通过类来调用。使用装饰器@staticmethod定义静态方法。
class Foo(object): def f1(self): # 这里的self参数是必须的 print("test in f1") @staticmethod # 这个是静态方法 def f2(): # 这里的参数不是必须的了 print("test in f2")Foo.f2() # 并没有创建对象,直接通过类调用了静态方法,类似函数了obj.f2() # 当然由于有继承,通过对象也能够调用类中的静态方法obj = Foo()obj.f1()
上面的例子中,f1是实例方法,f2就是静态方法。
这里的f1不要这么用,因为这个方法里并没有用到对象里面的任何东西。但是这么写,要调用f1必须得先实例化一个对象,但是其实并不需这个对象,而且还浪费空间。所以这种情况下,按照f2那样定义成一个静态方法会更好。
在别的纯面向对象的语言里,也是提供静态方法的。通过这种静态方法,可以让我们直接就能执行这个方法了。
另外除了静态方法和实例方法,还有一个类方法,这个貌似和静态方法在python里差不多,下面直接看看区别。
静态方法 和 类方法的区别
类方法可以将类作为参数传入,在继承的时候,子类会优先调用子类的属性和方法。
静态方法,无法传入类和对象,所以无论在哪个级别,永远都是调用基类的属性和方法
class Father(object): test = 'This is in Father' @classmethod def test_classmethod(cls): print(cls.test) # 类方法,可以将类作为参数 @staticmethod def test_staticmethod(): print(Father.test) # 静态方法,没有参数传入,只能用自己的类名,调用自己这个类的属性class Son(Father): test = "This is in Son"Father.test_classmethod()Father.test_staticmethod()Son.test_classmethod() # 继承后的子类的类方法的结果不同了Son.test_staticmethod()
类方法和静态方法的作用就是可以通过类来调用类中的属性和方法,而不需要先进行实例化。在还没有实例化生成对象的时候,只能调用类中的这两种方法。
类的组合关系
类与类之间除了可以是继承关系,还可以有组合关系。比如有两个类,学生和老师,那么可以在这之上在定义一个学校成员类,把学生和老师公有的属性和方法放在学校成员类里,作为父类。而学生和老师就作为两个子类继承父类。这是是之前将的继承,类于类之间是一种属于的关系。
现在又有一个类,课程类。课程不属于任何的人,但是老师有教授的课程,学生也有学习的课程。我们希望可以通过老师来获得他所教授的课程,也就是获得课程类的属性和方法。所以课程和老师之间又需要有这某种关系,这是就需要用到组合关系。
假设一个老师只教一门课,先搞清楚组合关系,不考虑教多门课。
如果没有组合关系,我们大概可以把课程的属性作为老师的属性,如下:
class Teacher(object): def __init__(self,name,age,course_name,period,title): "名字、年龄、课程名、课时、职称" self.name = name self.age = age self.course_name = course_name self.period = period self.title = title
当然我们还会有学生类,这会用到这些属性,除了职称。通过继承关系我们可以把名字和年龄作为一个父类,让老师和学生继承,但是课程和课时就无法利用继承关系实现代码的重复使用了。
class Course(object): def __init__(self,name,period): "课程名、课时" self.name = name self.period = periodclass People(object): def __init__(self,name,age): "名字、年龄" self.name = name self.age = ageclass Teacher(People): def __init__(self,name,age,title): "名字、年龄、职称" super(Teacher,self).__init__(name,age) self.title = titleclass Student(People): def __init__(self,name,age): super(Student,self).__init__(name,age)t1 = Teacher("Bob",32,"教授")print(t1.name,t1.age,t1.title)
上面只是把继承关系写好了,单独把课程定义为了一个类,但是并没有把课程组合和别的类组合起来。
方法一:先把课程实例化,将实例化后的课程作为People类的一个属性传入
class Course(object): def __init__(self,name,period): "课程名、课时" self.name = name self.period = periodclass People(object): def __init__(self,name,age,course): "名字、年龄、课程" self.name = name self.age = age self.course = courseclass Teacher(People): def __init__(self,name,age,course,title): super(Teacher,self).__init__(name,age,course) self.title = title # 教师比学生多一个职称属性class Student(People): def __init__(self,name,age,course): super(Student,self).__init__(name,age,course)c1 = Course("python",360) # 先实例化一个课程t1 = Teacher("Bob",32,c1,"教授") # 课程是教师的一个属性print(t1.course.name,t1.course.period) # 通过教师实例来调用课程类的属性
方法二:不用实例化课程,而是在People类的构造方法里完成课程类的实例化。这样需要在实例化教师的时候,将课程类的属性一起传入。
class Course(object): def __init__(self,name,period): "课程名、课时" self.name = name self.period = periodclass People(object): def __init__(self,name,age,course_name,period): self.name = name self.age = age self.course = Course(course_name,period) # 在构造函数里完成实例化class Teacher(People): def __init__(self,name,age,course_name,period,title): super(Teacher,self).__init__(name,age,course_name,period) self.title = titleclass Student(People): def __init__(self,name,age,course_name,period): super(Student,self).__init__(name,age,course)t1 = Teacher("Bob",32,"python",360,"教授") # 需要将课程类的属性在实例化的时候一起传入print(t1.course.name,t1.course.period) # 调用方法都是一样的
组合同样也是为了减少重复代码。把另一个类的实例作为自己的属性完成实例化(方法一),或者在实例化的时候同时完成了另一个类的实例化来用作自己的属性(方法二)。之后就可以通过一个类来调用被组合的类的属性和方法了。
上面只举例了调用一个被组合的类的属性,要使用方法也是一样的。另外例子中老师和学生都是会上多门课的,这里只要引入数组的概念。这是之前学过的概念,关键是要把所有的知识点融会贯通。
作业:选课系统
角色:学校、讲师、学员、课程
要求:
创建北京、上海 2 所学校
创建linux , python , go 3个课程 , linux\py 在北京开, go 在上海开
课程包含,周期,价格,通过学校创建课程
通过学校创建班级, 班级关联课程、讲师
创建学员时,选择学校,关联班级
创建讲师角色时要关联学校,
提供至少两个角色接口
7.1 学员视图, 可以注册, 交学费, 选择班级,
7.2 讲师视图, 讲师可管理自己的班级, 上课时选择班级, 查看班级学员列表 , 修改所管理的学员的成绩
7.3 管理视图,创建讲师, 创建班级,创建课程上面的操作产生的数据都通过pickle序列化保存到文件里