Python面向对象初识
一 编程方式简介
1.1 面向过程
面向过程的程序设计的核心是过程(流水线式思维),根据过程(业务逻辑)从上到下写代码
优点:复杂度的问题流程化,只需要顺着执行的过程,堆叠代码即可
缺点:一套程序只能解决单一问题,修改会造成牵一发而动全身
应用场景:一旦完成基本很少改变的场景,如:Linux內核,git,Apache HTTP Server等
1.2 函数式
将完成某一功能的代码封装到函数中,以后如需使用,调用函数即可,不用重复编写
优缺点:介于面向过程与面向对象之间
应用场景:各个函数之间是独立且无共用的数据时可采用函数式编程
1.3 面向对象
对函数进行分类和封装,让开发更快更好更强,此方式的实施需要使用 “类” 和 “对象” 来实现,所以,面向对象编程其实就是对 “类” 和 “对象” 的使用
优点:解决了程序的扩展性。对某一个对象单独修改,会立刻反映到整个体系中
缺点:可控性差,无法像面向过程一样可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题
应用场景:多函数需使用共同的值;需要创建多个事物,每个事物属性个数相同,值不同而已;需求需求经常变化的软件,如:互联网应用,企业内部软件,游戏等
二 创建类与对象
类:它就是一个模板(具有相同特征的一类事物(比如:猫,狗,老虎等,它们都是动物类)),里面包含多个函数,函数里实现一些功能
对象:根据模板创建的实例对象(具体的某一事物),通过实例对象可以执行类中的函数
实例化:类到对象的过程(实例=类名(参数1,参数2))
原理展示
注:类中的函数叫方法
实例展示
# 创建类
class Foo:
def func1(self):
print('func1')
def func2(self):
print('func2')
# 根据类Foo创建对象obj
obj = Foo()
obj.func1() # 执行func1方法
obj.func2() # 执行func1方法
三 面向对象三大特性
面向对象的三大特性是指:封装、继承和多态
3.1 封装
封装,顾名思义就是将内容封装到某处,以后再去调用被封装在某处的内容。个人理解面向对象的封装体现在以下两点:
1. 封装方法和属性,将一类操作封装到一个类中
class Foo:
xxx = 'xxx'
def __init__(self, name, age): # __init__称为构造方法,根据类创建对象时自动执行
self.name = name
self.age = age
def show(self):
print(self.name, self.age)
2. 数据的封装(将数据封装到对象中)
obj = Foo('joe1991',18) # 将joe1991和18分别封装到obj中的name和age属性中
补充说明:封装分为两个层面
封装其实分为两个层面,但无论哪种层面的封装,都需要对外界提供访问封装内容的接口(接口可以理解为入口,有了这个入口,使用者无需且不能够直接访问到内部隐藏的细节,只能走接口,并且我们可以在接口的实现上附加更多的处理逻辑,从而严格控制使用者的访问)。
第一个层面的封装:创建类和对象会分别创建二者的名称空间,我们只能用类名.或者obj.(类内部通过self.)的方式去访问里面的名字,这本身就是一种封装。
class Foo:
xxx = 'xxx'
def __init__(self, name, age):
self.name = name
self.age = age
def show(self):
print(self.name, self.age)
print(Foo.xxx) # xxx,通过类名.的方式访问
print(Foo.show) # <function Foo.show at 0x000001FB870B0268>,通过类名.的方式访问
obj = Foo('joe1991',18)
print(obj.name) # 直接调用obj对象的name属性
print(obj.age) # 直接调用obj对象的age属性
obj.show() # Python默认会将obj传给self参数,即:obj.show(obj),所以,此时方法内部的self=obj,即self.name是joe1991;self.age是18
注意:对于这一层面的封装(隐藏),类名.和实例名.就是访问隐藏属性的接口
第二个层面的封装:类中把某些属性和方法隐藏起来(或者说定义成私有的),只在类的内部使用、外部无法访问,或者留下少量接口(函数)供外部访问。
在python中用双下划线的方式实现隐藏属性(设置成私有的)。类中所有双下划线开头的名称如__x都会自动变形成:_类名__x的形式:
class Foo:
xxx = 'xxx'
def __init__(self, name, age):
self.__name = name
self.__age = age
def show(self):
print(self.__name, self.__age)
print(Foo.xxx) # xxx,通过类名.的方式访问
print(Foo.show) # <function Foo.show at 0x000001FB870B0268>,通过类名.的方式访问
obj = Foo('joe1991',18)
print(obj.__name) # 报错
print(obj.__age) # 报错
obj.show() # Python默认会将obj传给self参数,即:obj.show(obj),所以,此时方法内部的self=obj,即self.__name是joe1991;self.__age是18
这种自动变形的特点:
1. 类中定义的__xx只能在内部使用,如self.__xx,引用的就是变形的结果
2. 这种变形其实正是针对外部的变形,在外部是无法通过__xx这个名字访问到
3. 在子类定义的__xx不会覆盖在父类定义的__xx,因为子类中变形成了:_子类名__xx,而父类中变形成了:_父类名__xx,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的
注意:对于这一层面的封装(隐藏),我们需要在类中定义一个函数(接口函数)在它内部访问被隐藏的属性,然后外部就可以使用,比如上面例子中的show方法
这种变形需要注意的问题是:
1. 这种机制也并没有真正意义上限制我们从外部直接访问属性,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,
2. 变形的过程只在类的定义时发生一次,在定义后的赋值操作,不会变形
3.2 继承
3.2.1 什么是继承?
继承是一种创建新类的方式,新建的类可以继承一个或多个父类(python支持多继承),父类又可称为基类或超类,新建的类称为派生类或子类,通过继承可以大大增加代码的重用性
在python中类的继承分为:单继承或多继承(Java和C#中则只能继承一个类)
class ParentClass1: # 定义父类
pass
class ParentClass2: # 定义父类
pass
class SubClass1(ParentClass1): # 单继承,基类是ParentClass1,派生类是SubClass1
pass
class SubClass2(ParentClass1,ParentClass2): # python支持多继承,用逗号分隔开多个继承的类
pass
3.2.2 多继承查找方式
如果继承的多个类每个类中都有相同的方法,那么那一个会被使用呢?这里有两种方式:分别是:深度优先和广度优先
那什么时候采用深度优先?什么时候采用广度优先?
- 当类是经典类时,按照深度优先方式查找
- 当类是新式类时,按照广度优先方式查找
何为经典类?何为新式类?
# python2中分为:经典类、新式类
# 经典类:
class A:
pass
# 新式类:
class B(object):
pass
# python3中均为新式类,默认继承object类
class A:
pass
class D: def func(self): print 'D.func' class C(D): def func(self): print 'C.func' class B(D): def func(self): print 'B.func' class A(B, C): def func(self): print 'A.func' a = A() a.func() # 执行func方法时:查找顺序:A --> B --> D --> C
class D(object): def func(self): print 'D.func' class C(D): def func(self): print 'C.func' class B(D): def func(self): print 'B.func' class A(B, C): def func(self): print 'A.func' a = A() a.func() # 执行func方法时:查找顺序:A --> B --> C –> D
注意:在上述查找过程中,一旦找到,则寻找过程立即中断,便不会再继续找了;若一直未找到,则报错
3.2.3 继承原理
python到底是如何实现继承的,对于你定义的每一个类,python会计算出一个方法解析顺序(MRO)列表,这个MRO列表就是一个简单的所有基类的线性顺序列表,例如:
在上面的例子中,我们可以通过mro()方法查询执行顺序,等同于__mro__,注意只有新式才有这个属性
print(A.mro()) # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <type 'object'>]
3.2.4 子类中调用父类的方法
方式一:父类名.父类方法()
class Animal: # 定义动物类 def __init__(self, name, age,): self.name = name self.age = age def eat(self): print('吃饭了...') class Cat(Animal): # 猫 def __init__(self, name, age, species): Animal.__init__(self, name, age ) self.species = species def eat(self): print('%s品种的猫' %self.species, end='') Animal.eat(self) garfield = Cat('傻猫', '3', '加菲') garfield.eat()
方式二:super()
class Animal: # 定义动物类 def __init__(self, name, age,): self.name = name self.age = age def eat(self): print('吃饭了...') class Cat(Animal): # 猫 def __init__(self, name, age, species): # super(Cat, self).__init__(name, age) # 在python3中super()等同于super(Cat,self) super().__init__(name, age) self.species = species def eat(self): print('%s品种的猫' %self.species, end='') # super(Cat, self).eat() super().eat() garfield = Cat('傻猫', '3', '加菲') garfield.eat()
补充:在子类外使用父类方法:super(子类名,对象名))方法名()
当我们使用super()函数时,Python会在MRO列表上继续搜索下一个类。只要每个重定义的方法统一使用super()并只调用它一次,那么控制流最终会遍历完整个MRO列表,每个方法也只会被调用一次
注意:使用super调用的所有属性,都是从MRO列表当前的位置往后找,千万不要通过看代码去找继承关系,一定要看MRO列表
3.2.5 其他说明
1. 父类里没有的方法,在子类中有,这样的方法就叫做派生方法
2. 父类里有,子类也有的方法,就叫做方法的重写(就是把父类里的方法重写了)
3. 查看所有继承的父类
- 类名.__base __:查看从左到右继承的第一个子类
- 类名.__bases__:查看所有继承的父类
如果没有指定父类,python会默认继承object类,object是所有python的父类。
3.2.5 isinstance 和 issubclass
isinstance(obj,cls):判断obj是不是cls的对象(传两个参数,一个是对象,一个是类)
issubclass(sub,super):判断sub是不是super的子类(传两个参数,一个是子类,一个是父类)
class Parent:
pass
class Child(Parent):
pass
p = Parent()
c = Child()
print(isinstance(p,Parent)) # True
print(isinstance(c,Child)) # True
print(isinstance(c,Parent)) # True,子类产生的对象也为Ture
print(issubclass(Child,Parent)) # True
# 我们也可以使用type判断
print(type(c) is Parent) # False
print(type(c) is Child) # True
3.3 多态
在说明多态之前我们先看一个例子:
class Animal:
def walk(self):
print('Animal is walking.')
class Dog(Animal):
def walk(self):
print('Dog is walking.')
class Pig(Animal):
def walk(self):
print('Pig is walking.')
class Cat(Animal):
def walk(self):
print('Cat is walking.')
dog.walk() # Dog is walking.
pig.walk() # Pig is walking.
cat.walk() # Cat is walking.
从上面的例子可以看出,当子类和父类都存在相同的walk()方法时,子类的walk()覆盖了父类的walk(),在代码运行的时候,总是会调用子类的walk()。这样,我们就获得了继承的另一个好处:多态。
现在可以说明:多态就是同一类事物的多种形态
接下来我们将上面的例子改写一下:定义一个函数,它可以接受一个Animal类型的变量
class Animal:
def walk(self):
print('Animal is walking.')
class Dog(Animal):
def walk(self):
print('Dog is walking.')
class Pig(Animal):
def walk(self):
print('Pig is walking.')
class Cat(Animal):
def walk(self):
print('Cat is walking.')
# 定义一个函数,它可以接受一个Animal类型的变量
def walking(animal):
animal.walk()
animal = Animal()
dog = Dog()
pig = Pig()
cat = Cat()
walking(animal) # Animal is walking.
walking(dog) # Dog is walking.
walking(pig) # Dog is walking.
walking(cat) # Cat is walking.
当我们传入Animal的实例(包括子类实例)时,函数就会执行相应类型的walk()方法。另外,我们可以新增一个Animal的子类,不必对walking()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
多态的好处就是,当我们需要传入Dog、Cat等时,我们只需要接收Animal类型就可以了,因为Dog、Cat等都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有walk()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的walk()方法,这就是多态的意思
对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用walk()方法,而具体调用的walk()方法是作用在Animal、Dog、Cat还是其他Animal的子类对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保walk()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
对扩展开放:允许新增Animal子类
对修改封闭:不需要修改依赖Animal类型的walking()等函数。
静态语言 vs 动态语言
对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用walk()方法。
对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个walk()方法就可以了:
class X(object):
def walk(self):
print('...')
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子
以上多态部分参考:廖雪峰老师官方网站
四 组合
在上面我们提到继承可以完成代码的重用性,除此之外还有另外一种方式可以完成代码的重用性,即:组合
组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合
方式一:在__init__方法里面组合
# 创建一个性别类
class Gender:
def __init__(self, gender):
self.gender = gender
# 创建一个学生类
class Studnet:
def __init__(self,name):
self.name = name
self.gender = Gender('male') # 用Gender实例化一个对象,赋值给实例的gender属性
s = Studnet('Joe991')
print(s.gender.gender) # male
方式二:在外面组合
class Gender:
def __init__(self, gender):
self.gender = gender
class Studnet:
def __init__(self,name):
self.name = name
s = Studnet('Joe991')
s.gender = Gender('male') # 用Gender实例化一个对象,赋值给实例的gender属性
print(s.gender.gender) # male
组合与继承都是有效地利用已有类的资源的重要方式。但是二者的概念和使用场景皆不同,
1. 继承的方式
通过继承建立了派生类与基类之间的关系,它是一种'是'的关系,比如白马是马,人是动物
当类之间有很多相同的功能,提取这些共同的功能做成基类,用继承比较好,比如老师是人,学生是人
2. 组合的方式
用组合的方式建立了类与组合的类之间的关系,它是一种‘有’的关系,比如:老师有课程,学生有成绩,学生有课程,学校有学生等
class People: """ 人类 """ def __init__(self, name, age, gender): self.name = name self.age = age self.sex = gender class Course: """ 课程类 """ def __init__(self, name, period, price): self.name = name self.period = period self.price = price def course_info(self): print('<课程名称:{0}--课程周期:{1}--价格:{2}>' .format(self.name, self.period, self.price)) class Teacher(People): """ 老师类 """ def __init__(self, name, age, gender, position): # People().__init__(self, name, age, gender) super().__init__(name, age, gender) self.position = position self.course = [] self.students = [] class Student(People): """ 学生类 """ def __init__(self, name, age, gender): # People().__init__(self, name, age, gender) super().__init__(name, age, gender) self.course = [] t = Teacher('xxx',18,'male','讲师') s = Student('joe',18,'male') python = Course('Python', '6months', 3000) linux = Course('Linux', '6months', 3000) # 为老师和学生添加课程 t.course.append(python) t.course.append(linux) s.course.append(python) # 为老师添加学生 t.students.append(s) # 使用 for obj in t.course: obj.course_info() for obj in s.course: obj.course_info()
提醒:当类之间有显著不同,并且较小的类是较大的类所需要的组件时,用组合比较好
五 其他
5.1 id、type和value
在python中一切皆对象,每产生一个对象会对应三个属性:id、类型type和数值value
id可以理解为指向对象在内存中的位置,通过id()可以获取对象的地址
id相同,数值肯定相同;id不相同,数值可能相同。我们可以通过is判断id是否相同,利用==判断数值是否相同
>>> x=10 >>> y=10 >>> id(x) 1644307440 >>> id(y) 1644307440 >>> x is y True >>> x == y True >>> x=1000 >>> y=1000 >>> id(x) 1923155316688 >>> id(y) 1923156892720 >>> x is y False >>> x ==y True
注意:python中数字[-5,256]id是固定的。
5.2 __init_的作用
构造对象的同时被对象自动调用,完成对事物的初始化,一个类只要生成一个类对象,它一定会调用__init__构造函数。有以下特点:
1. 一个类中只能有一个初始化构造函数
2. 不能有返回值
3. 可以用它来为每个实例定制自己的特征
5.3 self关键字的用法
为了区分正在处理哪个对象,self指针变量指向当前时刻正在处理的对象,即构造出来的对象
在构造方法中self代表的是:self指针变量指向当前时刻正在创建的对象
构造函数中self.name = name 的含义:将局部变量name的数值发送给当前正在创建的对象中的name成员
class Student():
def __init__(self):
print("当前对象的地址是:%s" %self)
if __name__ == '__main__':
s1 = Student()
print(s1)
s2 = Student()
print(s2)
# 运行结果
当前对象的地址是:<__main__.Student object at 0x0000020623A7D8D0>
<__main__.Student object at 0x0000020623A7D8D0>
当前对象的地址是:<__main__.Student object at 0x0000020623A83E10>
<__main__.Student object at 0x0000020623A83E10>
在上面的程序中,s1、s2、self实际上都是指针变量,存放的是地址,指定当前时刻正在调用的那个对象
5.4 类和对象在内存中的保存
类以及类中的方法在内存中只有一份,而根据类创建的每一个对象都在内存中需要存一份,大致如下图:
如上图所示,根据类创建对象时,对象中除了封装 name 和 age 的值之外,还会保存一个类对象指针,该值指向当前对象的类
当通过 obj1 执行func1() 时,过程如下:
- 根据当前对象中的类对象指针找到类中的方法
- 将对象 obj1 当作参数传给方法的第一个参数 self




浙公网安备 33010602011771号