面向对象编程(Object Oriented Programming,简称OOP),是一种编程方式。它把对象作为程序的基本单元,在python中,所有数据类型都可以视为对象,还可以自定义对象,自定义的对象数据类型就是面向对象中的类(Class)的概念。
一.面向对象编程概述
对象的特征
1.世间万物皆对象
2.每个对象都是唯一的
3.对象具有属性和行为(对象的行为包括具有的功能以及具体的实现)
4.对象具有状态(对象的某些行为往往会改变自身的状态,即属性的取值)
5.对象分为类对象和实例对象两大类(类对象是具有相同属性和实例对象的抽象,类对象是实例对象的模板,实例对象是由类对象创造出来的。举个栗子:类对象就是实例对象的模板,就是做月饼的模具,实例对象就是由类对象创建出来的,就由模具做出来不同馅儿的月饼。)
面向对象编程的大体步骤
1.抽象出类对象
2.抽象出类对象的属性
3.抽象出类对象的行为
4.根据类对象创建实例对象
5.通过实例对象访问属性和方法
定义类对象的语法格式
class 类名(object):
#属性与方法
通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承下来的类。
其中:
1.类名有一个或多个单词组合而成,每个单词的首字母大写且其余字母全部小写,例如SomeClass。
2.(object)表示该类对象继承自Python内置的类对象object,Python中所以的类对象都继承自一个统一的基类:object。
就拿电脑举例,电脑具有的属性有品牌、大小、价格等,为了在创建实例对象后对其进行初始化(给实例对象绑定一些属性)可以在类对象中定义一个名为__init__ 的特殊方法,它的第一个参数永远都是self,表示创建实例本身,在__init__方法内部,可以把各种属性绑定到self,因为self指向创建的实例本身。有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,注意self不需要传,python解释器自己会把实例变量传进去。像这个栗子就把属性brand、size、price传进去
#初识属性:
class Computer(object):
def __init__(self, brand, size, price):
self.brand = brand #品牌
self.size = size #大小
self.price = price #价格
#初识类的方法:
def read(self):
print("reading by computer")
def write(self):
print("writing by computer")
def play(self):
print("playing by computer")
类的方法与函数的区别:
方法是定义在类对象中的函数。方法与函数的区别在于:
- 定义方法时,方法的第一个形参表示调用该方法的实例对象,第一个形参的名称是self,也可以是其他名称。
- 调用方法时,系统自动将调用该方法的实例对象作为实参传递给第一个形象,第一个实参会传递给第二个形参,第二个实参会传递给第三个形参,依次类推。
根据类对象创建实例对象的语法格式为:
类名([实参])
二.实例对象属性
实例属性
(指的是实例对象所绑定的属性)
绑定实例属性的方式:
- 在类对象内部(方法中)
语法格式:self.属性名 = 属性名
推荐在特殊方法 init 中进行绑定,这样,在创建实例对象后就会自动调用特殊方法__init__对实例对象进行初始化,从而绑定实例属性。 - 在类对象外部
语法格式:实例对象.属性名 = 属性名
在实例对象创建后,可以对其进行动态的绑定属性。如果指定名称的实例属性已经存在,则是对实例属性进行修改。
访问实例属性的方式:
- 在类对象内部(方法中)
语法格式:self.属性名 - 在类对象外部
语法格式:实例对象.属性名
同一个类对象的不同实例对象所绑定的实例属性是相互独立的,给一个实例对象所绑定的实例属性对另一个实例对象是不起作用的。
实例对象.__ dict __ 可获得该实例对象所绑定的所有属性及其值的字典。
三.类对象属性
类属性指的是类对象所绑定的属性。
绑定类属性的方式:
- 在类对象内部(方法外)
语法格式:属性名 = 属性名 - 在类对象外部
语法格式:类对象.属性名 = 属性名
类对象也可以对其进行动态的绑定属性。如果指定名称的类属性已经存在,则是对类属性进行修改。
访问类属性的方式:
- 在类对象内部(方法中)
语法格式:类对象.属性名 - 在类对象外部
语法格式:类对象.属性名 或 : 实例对象.属性名
类对象的所有实例对象都有一个指向类对象的指针,所以,类对象的所有实例对象都可以访问类属性。
访问实例属性和类属性都可以通过“实例对象.属性名”的方式。当通过该方式访问时,会先查找指定的实例对象中有没有指定名称的实例属性。如果没有,再查找对应的类对象中有没有指定名称的类属性。所以,当通过该方式访问时,如果实例属性和类属性同名,实例属性会屏蔽掉类属性。
当通过“实例对象.属性名 = 属性值”的方式绑定属性时,这里的属性只表示实例属性(因为类属性没有这样的绑定方式),所以,只会查找指定的实例对象有没有绑定指定名称的实例属性,如果没有则进行绑定;如果绑定了则对属性值进行修改。
四.函数装饰器
对于某个函数,若希望在不改变函数代码的前提下,为该函数增加额外的功能,那么就可以使用装饰器来装饰该函数。
装饰器是一个函数,装饰器接受一个函数作为参考(传入的实参是被装饰的函数)装饰器的内部嵌套定义另一个函数,内函数中会引用装饰器的参数,且装饰器的返回值是内函数。为了让内函数接受任意类型的参数,将内函数的形参定义为(*args, **kwargs)
在函数中首先完成为被装饰函数添加的新功能,然后调用被装饰的函数。把装饰器应用到被装饰函数的语法为:在被装饰函数的前面添加"@装饰的函数名"。装饰器的作用就是为已经存在的对象添加额外的功能。
五.类方法
类方法指的是类对象中使用装饰器@classmethod进行装饰的方法。
在类对象定义类方法时,必须使用装饰器@classmethod进行装饰,此外,第一个形参表示类对象,其对应的实参由系统自动传入。第一个形参的名称通常是cls,也可以是其它名称。
- 类方法可以被类对象所调用,语法格式为 : 类对象.方法名([实参]) 或cls.方法名([实参])。
- 类方法也可以被实例对象所调用,语法格式为 : 实例对象.方法名([实参]) 或self.方法名([实参])。
类对象的所有实例对象都有一个指向类对象的指针,所以,类对象的所有实例对象都可以调用类对象中定义的类方法。调用类方法时,系统将自动将类对象作为实参传递给第一个形参。第一个实参会传递给第二个形参,第二个实参会传递给第三个形参,依次类推。
可以用实例对象调用类方法,但是不能用类对象调用实例方法
六.静态方法
类对象的静态方法只是一个普通函数,把某个普通函数归属于类对象,可能只是为了易于代码管理。在类对象中定义静态方法时,必须使用装饰器@staticmethod进行装饰。静态方法只是一个普通函数,因此,第一个形参没有特殊含义和要求。
- 静态方法可以被类对象所调用,语法格式为:类对象.方法名([实参])或:cls.方法名([实参])。
- 静态方法也可以实例对象所调用,语法格式为:实例对象.方法名([实参])或:self.方法名([实参])。
调用静态方法时的参数传递与调用普通函数是一样的。
七.访问控制
访问控制指的是:控制类对象的属性和方法在类对象的外部是否可以直接访问。
如果在类对象的某个属性或方法前添加两个下划线__,那么在类对象的外部就不能直接访问该属性或方法了。
之所以不能在类对象的外部直接访问以双下划线开头的属性或方法,是因为python解释器对外把属性或方法__xxx改成了另一个名字:_ 类名__xxx 。 所以在类对象的外部仍然可以通过 _ 类名__xxx。但是,强烈建议不要这样访问,因为不同版本的python解释器可能会把属性或方法__xxx改成不同的名字。
仍然可以在类对象的外部动态绑定名为__ xxx的属性或方法,这与类对象内部名为__xxx的属性或方法是不同的。
tw.__year = "1999"
print(tw.__year)
#1999
除了在类对象的属性或方法前添加两个下划线 _,还可以在类对象的属性或方式前添加单下划线,这表示:虽然可以在类对象的外部访问该属性或方法,但是最好不要访问。
八.封装
封装是面向对象编程的三大特征之一
封装有两方面的含义:
- 将数据(属性)和行为(方法)包装到类对象中,在方法内部对属性进行操作,在类对象的外部调用方法。这样,无需关心方法内部的具体实现细节,从而隔离了复杂度。
- 在类对象的内部通过访问控制把某些属性和方法隐藏起来,不允许在类对象的外部直接访问,而是在类对象的内部对外提供公开的接口方法(例如getter和setter)以访问隐藏的信息。这样,就对隐藏的信息进行了保护。
九.继承
除了封装,继承也是面向对象编程的三大特征之一。继承是是实现代码的复用的重要手段,相同的代码不需要重复的编写。当几个类对象中有共同的属性和方法时,就可以把这些属性和方法抽象并提取到一个基类中,每个类对象特有的属性和方法还是在本类对象中定义,这样只需要让每个类对象都继承这个基类,就可以访问基类中的属性和方法了。继承基类的每个类对象被称为派生类,基类也称为父类或超类,派生类也被称为子类。
这里可以看到,狗和鸟都有两个共同的属性,吃喝,将这两个属性抽象成一个动物的基类,狗和鸟这两个类都继承这个基类。
子类会继承所有父类(包括所有直接父类和所有间接父类)的所有属性和方法,子类可以添加父类中没有的属性和方法
子类可以在父类功能上进行重写,扩展类的功能。
单继承:子类只有一个直接父类时称为单继承
假设子类和父类分别为ChildClass和ParentClass,
子类继承父类的语法格式为:
class ChildClass(ParentClass):
pass
多继承:子类有多个直接父类时称为多继承
子类继承父类的语法格式为:(父类为ParentClass1, ParentClass2, …, ParentClassn)
class ChildClass(ParentClass1, ParentClass2, ..., ParentClassn):
pass
子类会继承父类(包括直接和间接父类)的所有属性和方法
十.重写
如果子类对继承父类的某个属性或方法不满意,可以在子类中对其进行重写从而提供自定义的实现,重写的方式为:在子类中定义与父类中同名的属性或方法(包括装饰器)。子类重写父类的属性后,通过子类或其实例对象只能访问重写后的属性,而无法访问被重写的父类的属性。子类重写父类的方法后,通过子类或其实例对象只能访问重写后的方法,而无法访问被重写的父类的方法。
class ParentClass(object):
ca = "ca(父类)"
def __init__(self):
print("__init__()被调用了(父类)")
def im(self):
print("im()被调用了(父类)")
@classmethod
def cm(cls):
print("cm()被调用了(父类)")
class ChildClass(ParentClass):
ca = "ca(子类)"
def __init__(self):
print("__init__被调用了(子类)")
def im(self):
print("im()被调用了(子类)")
@classmethod
def cm(cls):
print("cm()被调用了(子类)")
cc = ChildClass()
#__init__被调用了(子类)
print(ChildClass.ca)
#ca(子类)
print(cc.ca)
#ca(子类)
cc.im()
#im()被调用了(子类)
ChildClass.cm()
#cm()被调用了(子类)
cc.cm()
#cm()被调用了(子类)
根据上面栗子,虽然子类继承父类,但是子类属性都是重写的,所以输出的都是重写的结果,没有输出父类属性。But!如果想调用父类中的属性和方法,可以通过super().xxx调用。
class ChildClass(ParentClass):
ca = "ca(子类)"
def __init__(self):
super().__init__()
print("__init__被调用了(子类)")
def im(self):
super().im()
print("im()被调用了(子类)")
@classmethod
def cm(cls):
super().cm()
print("cm()被调用了(子类)")
cc = ChildClass()
#__init__()被调用了(父类)
#__init__被调用了(子类)
print(ChildClass.ca)
#ca(子类)
print(cc.ca)
#ca(子类)
cc.im()
#im()被调用了(父类)
#im()被调用了(子类)
ChildClass.cm()
#cm()被调用了(父类)
#cm()被调用了(子类)
cc.cm()
#cm()被调用了(父类)
cm()被调用了(子类)
十一.MRO(方法解析顺序)
Method Resolution Order简称MRO,它指的是对于一棵类继承树,当调用最底层类对象所对应实例对象的方法时,python解释器在类继承树上搜索方法的顺序。对于一棵类继承树,可以调用最底层类对象的方法mro()或访问最底层类对象的特殊属性__ mro __,获得这棵继承树的MRO。
class A(object):
def f(self):
print("A.f")
class B(A):
def f(self):
print("B.f")
class C(A):
def f(self):
print("C.f")
class D(B, C):
def f(self):
print("D.f")
print(D.mro())
#或print(D.__mro__)
#区别在于:mro()输出的为列表;__mro__输出的为元组
#[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
自下向上,从左到右,D->B->C->A->object
在子类重写后的方法中通过super()调用父类中被重写的方法时,在父类中搜索方法的顺序基于以该子类为最底层类对象的类继承树的MRO。若想调用指定父类中被重写的方法,可以给super()传入两个实参:super(a_type, obj),其中,a_type是个类对象,obj是实例对象,这样,被指定的父类是:obj所对应类对象的MRO中,a_type后面的那个类对象。
十二.多态
(在不考虑对象类型的情况下使用对象)
除了封装和继承,多态也是面向对象编程的三大特征之一。简单地说,多态就是“具有多种形态“,它指的是:即便不知道一个变量所引用的对象到底是什么类型,仍然可以通过这个变量调用方法,在运行过程中根据变量所引用对象的类型,动态地决定调用哪个对象中的方法。如果子类中不存在指定名称的方法,回到父类中去查找,如果在父类中找到了,则调用父类中的方法。
Python是动态语言,在调用函数时不会检查参数类型,动态语言的多态崇尚“鸭子类型”:当看到一只鸟走起来像鸭子,游泳起来像鸭子,叫起来也像鸭子,那么这只鸟就可以被称为鸭子。==也就是说,它不关注对象的类型,而是关注对象具有的行为