大纲
- 面向对象
- 类
- 继承
- 多态
- 静态
- 单元测试
- 练习
面向对象
面向对象编程是一种程序设计思想。
我们到目前为止所编写的程序,都是基于面向过程
的。
面向过程就是我们在写程序过程中,将需求拆成多个流程
,每个封装一个方法
,逐个调用。
比如买菜、洗菜、炒菜、装盘这四个步骤,我们对于的方法就是buy
、wash
、cook
、dish
。
如果需求有变化,如我还要买米、做饭,用铁锅炒菜,用微波炉做菜等等。这时候用面向过程就非常麻烦且会越来越乱。
面向对象则是从另一个角度来思考,它用对象
来取代方法作为程序的基础
。
对象是对事物的抽象
,如人可以抽象成名字、年龄以及能做的事情,这就是类
。
我们在程序设计时,就只要创建这个对象,让它来做它能做的事情
。
还是买菜、洗菜、炒菜、装盘这个四个步骤,现在我们就可以有超市
、厨师
两个对象。超市负责买菜,厨师负责洗菜、炒菜、装盘。
进一步厨师用什么洗菜,用什么做菜怎么做、怎么装盘又是一系列的对象来处理。
超市这个对象提供买米、买菜、买肉等等功能,必要时还可以继续添加
。
除了基本的功能外,沃尔玛和永辉卖的东西就有差别,即使同样是卖米也会有差异。
这就是继承,沃尔玛和永辉都是超市的继承,但并不是相同的。
类
类【class
】就是对对象的描述
。
我们要把人的名字和年龄以及做的事情用python的类写出来。
创建一个类:class
。
我们创建一个user包,在包里创建一个Person模块。在模块中编写Person类。
class Person:
def __init__(self):
pass
class后面就是类名Person,其下的代码块就是类的定义。类名使用驼峰写法
。一个文件一个类
,大部分情况我们会这样做。Python中并不反对一个文件多个类
。
第一个在类中要定义的方法就是构造方法
,方法名为__init__
。这是类默认存在
的方法。
构造方法里的第一个参数是self
,代表对象自己
。默认
类的方法第一个参数
都是self
。
实例化:将类创建出来进行使用。
我们创建一个main.py来实例化这个类。
from user import Person
p1 = Person.Person()
from user.Person import Person
p1 = Person()
上面两种方式是在包下的类实例化方式,与方法的调用时一样的。
对于类,由于一文件一类的原则,我们更希望能简单一点调用到包里的类。如下:
from user import Person
p1 = Person()
p2 = Person()
那么我们就要在包的__init__.py
文件里做文章了,写入
from .Person import Person
然后我们就可以直接从包导入Person类了。
构造方法:用于初始化
,每个类被实例化时会默认调用的方法。
def __init__(self, name, age):
self.name = name # 名字
self.age = age # 年龄
我们加入Person类有的数据,名字和年龄。self
代表自己,它点
出来的变量
称为成员变量
。self.name
和self.age
分别被赋值给构造方法传入的参数name和age,用于初始化
。
实例化并获取成员变量:
p1 = Person('Green', 12)
print(p1.name, p1.age)
使用构造方法进行创建,然后直接用创建好的变量,点出成员变量即可。
成员变量原则上
只能在构造方法
中声明
。
成员方法:构造方法其实是一个特殊的成员方法。
def sleep(self, t):
print('{} sleeps for {} seconds'.format(self.name, t))
定义了一个sleep方法,同样的第一个参数
是self
,代表该方法是成员方法。
在成员方法内部,我们就可以用self
来获得成员变量
。调用
:同样使用点
的方式调用该方法
。
p2 = Person('Lucy', 14)
p2.sleep(10)
这时如果我们需要在sleep前添加一步到床上的步骤,我们可以这样做,
添加一个go2bed成员方法,并在sleep方法中用self
调用这个成员方法
。
class Person:
def __init__(self, name, age):
self.name = name # 名字
self.age = age # 年龄
def sleep(self, t):
self.go2bed()
print('{} sleeps for {} seconds.'.format(self.name, t))
def go2bed(self):
print(self.name + ' go to bed.')
访问限制:对成员变量和成员方法进行限制,不让使用者可以直接使用。成员变量
作为一个类的数据,不应该可以直接被使用者修改,保证类被正确使用。
变量名用 两个下划线
【__
】开头的就是被限制访问
的私有变量
,只可内部访问
。
def __init__(self, name, age):
self.__name = name # 名字
self.__age = age # 年龄
调用时报错
如果需要获取这两个变量的值,或进行修改,我们定义get
,set
方法进行。
def get_age(self):
return self.__age
def set_age(self, age):
self.__age = age
def get_name(self):
return self.__name
我们希望age可以进行设置,而name没有set方法。这样成员变量的任何修改都在我们的控制范围内。
成员方法也使用两个下划线
来做限制。__go2bed
就是只能内部访问
的方法了。
class Person:
def __init__(self, name, age):
self.__name = name # 名字
self.__age = age # 年龄
def sleep(self, t):
self.__go2bed()
print('{} sleeps for {} seconds.'.format(self.__name, t))
def __go2bed(self):
print(self.__name + ' go to bed.')
def get_name(self):
return self.__name
def get_age(self):
return self.__age
主要目的是:
1、不希望使用者调用到。
2、对用户提供的方法也可以叫做接口
。接口就是使用者可以调用到的所有方法了。这样对于使用者来说也是最好的。
继承
继承是在已有类【父类
】的基础上,再编写一个子类
,其拥有父类的所有内容。
from user import Person
class Student(Person):
pass
在类名Student后面用括号抱起来的Person就是父类。
此时这个子类没有任何的代码,但是它已经继承了父类所有的内容,使用方法一样。
from user import Student
student1 = Student('Green', 12)
print(student1.get_name())
所有方法变量自动继承。
object类:默认的Python继承类。
没有做继承的类,其实都默认继承
了python的object类
【对象类】。
class Person(object):
object可以不写。
添加成员变量和方法:子类一般拥有比父类更强大的功能。
在Students类中,我们添加一个grade变量来标识年级,并添加一个新方法。
class Student(Person):
def __init__(self, name, age, grade=1):
Person.__init__(self, name, age) # 父类构造方法
self.__grade = grade # 年级
def get_grade(self):
return self.__grade
代码中的Person.__init__
表示了我们在新的构造方法中先调用了Person父类
的构造方法
。
这样我们就减少了重复的代码。
复写成员方法:修改
父类的方法
内容。
方法的复写必须保证方法名
和参数
与父类一致
。
def sleep(self, t):
print('student ' + self.get_name())
Person.sleep(self, t) # 父类方法
通过调用父类的方法来扩充该方法。或者直接不调用父类方法全部重写也是可以的。
def sleep(self, t):
print('student {} sleeps for {} seconds.'.format(self.__name, t))
子类
的方法覆盖
了父类
的方法,在被调用时就会运行子类方法。
复写实现了面向对象中多态
的概念。
多态
判断变量类型:type
、isinstance
。
p1 = Person('Green', 12)
s1 = Student('Green', 12)
print(type(s1))
print(type(p1))
输出:type
可以获得
该变量的类型
。
print(isinstance(p1, Person))
print(isinstance(p1, Student))
输出:isinstance
可以直接判断
变量是不是某个类型
。
对于父类
变量结果与下面的代码相同:
print(type(p1) == Person)
print(type(p1) == Student)
但对于子类
,情况并不相同
。
子类的类型判断:
type(s1) == Person # False
type(s1) == Student # True
isinstance(s1, Person) # True
isinstance(s1, Student) # True
我们发现子类变量在type的相等判断中,并不等于父类。
但在isinstance
判断中,子类被判断为与父类一样,这就是多态
。
一个类可以是自己,也可以是其父类,逻辑上也是通的,学生也是一个人。
反之则不对,一个人未必是学生。
多态的用处:
def poly_test(person):
if not isinstance(person, Person):
raise Exception('数据类型传入错误')
return person.sleep(10)
这个方法里,我们对传入的类型进行了限定。
这样我们就可以直接传入Student、Person或者其他子类都可以。
sleep也是父类就有方法,直接调用不会有问题。
当我们新增一个子类,如Programmer类,我们就不需要对这个方法进行修改,直接使用。
根据对象的真实类型
来运行
,就是多态的用处。
静态
除了成员变量和成员方法,类中还可以定义静态属性
:类变量
和类方法
。静态
的意思是不需要实例化
就可以使用。
类变量的数据属于所有实例共享
一个。
直接创建在类的定义中与方法同级。调用时使用类名 . 变量名
。
class Programmer(Person):
programmers = {} # 静态变量
def __init__(self, name, age):
Person.__init__(self, name, age)
if name not in Programmer.programmers:
Programmer.programmers[name] = 0
Programmer.programmers[name] += 1
这里做了一个统计,统计同一姓名的人有几个。每创建一个实例就统计一次。
from user import Programmer
p = Programmer('Green', 18)
print(Programmer.programmers)
类方法是直接属于类的方法。
定义时,与成员方法不同的是第一个参数不是self而是cls
【这个类】。并需要在方法上添加一个装饰器
。
@classmethod
def count_by_name(cls, name):
return cls.programmers[name]
以@开头
并写在def上面一行的就是装饰器
,具体内容以后的高阶语法再介绍。@classmethod
就定义了下面
的方法是一个类方法
,且没有self参数,因为没有实例化。
第一参数是cls
,代表了当前类
,可以通过cls调用类变量,相当于Programmer.programmers
。
使用时直接通过类名 . 方法
调用,无需实例化
。
count = Programmer.count_by_name('Green')
静态方法与类方法类似,但更为独立
,仅仅表现为放在类里面托管的方法。
定义时,没有self参数
,也没有cls参数
。下面是上个方法的静态方法版本。
@staticmethod
def count_by_name(name):
return Programmer.programmers[name]
@staticmethod
就定义了下面的方法是一个静态方法
。其他都与类方法一样。
使用上,静态方法一般与类没有什么关系,不会涉及类的操作,是一个比较独立的方法。
继承问题:类变量与子类共享,父类没有该类变量。
类方法同样可以继承和复写,有复写时要调用这个类的类方法。
静态方法也一样。
单元测试
单元测试是一个测试框架
。
我们直接来看一个简单的示例代码。
import unittest # 单元测试框架
from user import Student # 要测试的类
class TestStudentMethods(unittest.TestCase):
def test_student(self): # 测试用例
s = Student('Green', 12)
self.assertEqual(s.get_age(), 12)
self.assertEqual(s.get_name(), 'Green')
if __name__ == '__main__': # 当作为启动文件时
unittest.main()
unittest
是要引入的包。创建一个类继承unittest.TestCase
类。然后其他方法就不需要了。
我们直接开始写测试用例
。以test开头
的成员方法就是一个用例
。
定义完类后,我们启动该测试unittest.main()
。
前面的if语句判断了该文件是否启动文件
,也是一种常用的方法。
启动后,单元测试会自动执行所有用例
,并给出结果。
测试用例:
def test_student(self): # 测试用例
s = Student('Green', 12)
self.assertEqual(s.get_age(), 12)
self.assertEqual(s.get_name(), 'Green')
先进行一些操作代码,就是我们的用例过程,这里创建了一个Student对象。
然后就要使用单元测试类给我们提供的assert系列方法
来判断结果是否正确。assertEqual
是最常用的方法之一了,参数1写入获得的数据,
参数2写入期望值
。
在运行过程中就会进行对比,如果两者相等则正确,不等则错误。
常用的assert方法还有:assertNotEqual
、assertTrue
、assertFalse
、assertIsNone
、assertIsNotNone
、assertIn
等。
一个单元测试可以包含很多测试用例,一次进行测试。
测试结果:所有assert都正确的情况下,即测试通过。
有没有通过的用例时,会告诉你哪个用例的哪个assert有问题,与期望值不符。
练习
1、模拟一个小游戏,编写一个Sprite类【拥有血量和攻击力两个属性,并进行攻击】。
然后继承出Monster类和Hero类,让两者每回合互相攻击一次,直到有一方死亡。
每次攻击扣除对方的血量是攻击力加减N的随机值。
2、编写一个进行接口请求的单元测试。
github: https://github.com/lvancer/course_python