一.面向对象初识
什么是面向过程
首先明确一点,在面向对象之前我们一直都是按照面向过程的方式来编写程序!
面向过程的程序设计:核心是过程二字,过程指的是解决问题的步骤,即先干什么再干什么......面向过程的设计就好比精心设计好一条流水线,是一种机械式的思维方式。
优点是:复杂度的问题流程化,进而简单化(一个复杂的问题,分成一个个小的步骤去实现,实现小的步骤将会非常简单)
缺点是:一套流水线或者流程就是用来解决一个问题,生产汽水的流水线无法生产汽车,即便是能,也得是大改,改一个组件,牵一发而动全身。
应用场景:一旦完成基本很少改变的场景,著名的例子有Linux內核,git,以及Apache HTTP Server等。
为什么需要面向对象?
当今时代背景下,通常应用程序对扩展性和维护性要求都是非常高的,为什么?想想qq,微信,是不是不断的在添加新功能?,也就是说一款应用程序诞生后,都需要不断的更新维护
什么是面向对象编程
面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。
它将对象作为程序的基本单元
将数据和处理数据的程序封装到对象中
以提高软件的重用性、灵活性和扩展性为首要目的
面向对象编程优缺点
案例分析1:把大象装进冰箱如何实现
案例分析2:要开一家公司
面向对象编程的优点:
-
程序员的角色发送了改变,从一个操作者,变成了指挥者,不再需要关心,每个功能具体的实现细节,从而可以专注处理业务逻辑,是一种思想上的转变
-
大大提高了程序的扩展性,当一个对象发生了修改时,对其他对象时没有任何影响的,对象之间相互独立,耦合度变得更低了
-
提高了程序的灵活性,例如游戏中,每个玩家的操作都是自由的,而不是机械般固定的!你可以买武器,也可以买护甲
缺点:
-
编程的复杂度远高于面向过程,不了解面向对象而立即上手基于它设计程序,极容易出现过度设计的问题。一些扩展性要求低的场景使用面向对象会徒增编程难度,如果用面向对象来设计linux,估计现在都没写完
-
无法向面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题,即便是上帝也无法准确地预测最终结果。
应用场景:
-
需求经常变化的软件,一般需求的变化都集中在用户层,互联网应用,企业内部软件,游戏等都是面向对象的程序设计大显身手的好地方
用一个例子来说明面向对象与面向过程的区别:
话说三国时期曹军于官渡大败袁绍,酒席之间,曹操诗兴大发,吟道:喝酒唱歌,人生真爽! 众将直呼:"丞相好诗",于是命印刷工匠刻板印刷以流传天下;
待工匠刻板完成,交与曹操一看,曹操感觉不妥,说道:"喝酒唱歌,此话太俗,应改为'对酒当歌'较好",于是名工匠重新刻板,当时还没有出现活字印刷术,如果样板要改,只能重新刻板,工匠眼看连夜刻版之工,彻底白费,心中叫苦不迭。可也只得照办。
版样再次出来请曹操过目,曹操细细一品,觉得还是不好,说”人生真爽太过直接,应该改问语才够意境,因此应改为‘对酒当歌,人生几何?’“,于是....
在活字印刷术还没出现之前,如果版样有改动,只能重新雕刻。而且在印刷完成后,这个样板就失去了它的价值,如果需要其他样板只能重新雕刻。而活字印刷术的出现就大大改善了印刷技术。如上例”喝酒唱歌,人生真爽“,如果用活字印刷,只需要改四个字就可,其余工作都未白做。岂不快哉!!
活字印刷也反应了OOP。当要改动时,只需要修改部分,此为 可维护;当这些字用完后,并非就完全没有价值了,它完全可以在后来的印刷中重复使用,此乃 可复用;此诗若要加字,只需另刻字加入即可,这就是 可扩展;字的排列可以横排,也可以竖排,此是 灵活性好。
上述案列反应了OOP的优点,即可维护性高,扩展性强,复用性高! 这些特点非常适用于用户需求变化频繁的互联网应用程序,这是学习OOP的重要原因
但是OOP设计的程序需涉及类与对象,相应的复杂度会提高!
并非所有程序都需要较高的扩展性,例如系统内核,一旦编写完成,基本不会再修改,使用面向过程来设计则更适用
二.类与对象
类和对象是面向对象编程中最核心的两个概念
对象是什么
对象是特征与技能的结合体
如:演员张一山,姓名和职业是他的特征,演戏是他的行为,按照这样的定义,生活中到处都是对象
在程序中:
用变量来表示对象的特征,用函数表示对象的技能
将这些变量和函数结合在一起,形成一个整体,就是对象,这是面向对象的精髓所在!
对象的另一种理解方式
变量的作用数存储数据,函数的作用数处理数据
对象是将数据与处理数据的函数绑定在一起
类是什么
类就是类型,类别,种类; 是一系列对象中相似特征与技能的结合体
在生活中是一种抽象概念,例如人类,是不具体的
如某个对象属于人类,可以通过类别,了解这个对象具备的特征和技能
反过来看类就是对象的模板,同一类的对象,具备相同的特征和行为
为什么需要类
现实生活中,通过对对象的分析总结,得到类型;用类型来标识不同对象之间的差异;
在程序中,同样用于标识不同对象之间的差异
另一个重要的功能是作为对象的模板,例如学生类,无论是哪个学生都具备学习这个方法,既然是相同的就没必要为每个对象单独编写,学生类负责提供这些相同的方法;
OOP第一步要做的就是定义需要的类
三.创建类和对象
定义类
以学生类Student为例,在Python中,定义类通过class
关键字:
class Student: pass
class
后面紧接着是类名,即Student
,遵循python编码规范,类名通常是大写开头的单词,多个单词时使用驼峰命名法
创建对象
创建对象也称之为实例化,定义好Student
类后,就可以根据Student
类创建出Student
的实例,创建实例通过类名加上()实现:
stu1 = Student() print(stu1) #输出 <__main__.Student object at 0x10b11d588> print(Student) #输出 <class '__main__.Student'>
根据输出可以看到
变量名stu1
指向一个Student
类的实例,0x10b11d588
是实例的内存地址,每个实例的地址都不相同,
Student
本身则是一个类(class)
对象的属性操作
对象是特征(属性)与行为(方法)的结合体
stu
这个对象目前不具备任属性和方法,要为其添加属性可以在创建对象后使用点语法(变量名加 . )
比如为stu
对象添加name属性
stu1.name = "Jerry"
同样通过点语法来获取对象的属性值
print(stu1.name) #输出 Jerry
四.初始化方法init
什么是初始化方法
用于为对象的属性设置初始值的函数
为什么需要初始化方法
在类的实例(对象)中,一些属性是必须存在的,就可以使用初始化函数来完成,比如Student
对象中的name
属性,它是必须的,用于唯一标识一个学生
尝试一下
class Student: def __init__ (self,name): print("init run") self.name = name
执行过程:
在创建对象时Student("jack")
会申请新的内存空间用于保存对象数据,接着自动调init函数
注意:
__init__
函数要求第一个参数必须是self,该参数表示需要被初始化的对象本身,这样就可以将name属性绑定到对象上
可以将self改为其他任意的名称,但为了保证易读性通常是self,额外的参数须位于self之后
有了__init__
方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__
方法匹配的参数,但self
不需要传,Python解释器自己会把实例变量传进去:
# stu1 = Student() # 以上代码将抛出异常:TypeError: __init__() missing 1 required positional argument: 'name' stu1 = Student("jack") # 输出 init run print(stu1.name) # 输出 jack
小结:
-
init函数用于为对象属性设置初始值
-
在创建对象时会自动调用
-
自动传入对象本身
五.属性查找顺序
可以将类中的内容都称之为属性,变量称为数据属性,函数就叫函数属性
类中可以声明变量来表示数据属性,为Student
类添加数据属性和函数属性
class Student: school = "Tsinghua" #数据属性 def say_hello(self):#函数属性 print("hello i am a student") def __init__ (self,name): #初始化函数 self.name = name
也可以使用点语法在创建对象后为对象增加数据属性
stu = Student("Maria") stu.age = 20
问题1:在类中声明的数据属性和创建对象后为增加的数据属性,有什么区别?
类中的数据属性是所有对象共享的
创建对象后为增加的数据属性,是这个对象特有的,去其他对象无关
问题2:类中声明的数据属性和创建对象后为增加的数据属性,其访问属性是怎样的?
优先查找对象自己的名称空间,如果没有则在类中找,如果类中也没有则到父类中找,直到找到为止,如果父类中也没有则抛出异常
!!!注意!!!
此处父类可能也有父类,会一直沿着继承关系查找到最终的父类Object,该继承关系,后续会详细讨论!
案列:网页中折叠此处
stu1 = Student("Jack") stu2 = Student("Rose") #1.类中的数据属性是所有对象共享的 print(stu1.school) print(stu2.school) #输出 Tsinghua #输出 Tsinghua #2.类中的数据属性访问的是同一块内存 print(id(stu1.school)) print(id(stu2.school)) #输出 4470412656 #输出 4470412656 #3.类的函数属性是绑定给对象使用的,bound method称为绑定方法,每个对象的绑定方法内存地址不一样 print(stu1.say_hello) print(stu2.say_hello) #输出 <bound method Student.say_hello of <__main__.Student object at 0x10cc405f8>> #输出 <bound method Student.say_hello of <__main__.Student object at 0x10cc40630>> #4.优先访问对象自己的名称空间 # 修改stu1的学习属性为北京大学 会在自stu1的名称空间增加school属性 stu1.school = "Beijing" print(stu1.__dict__) print(stu2.__dict__) #输出 {'name': 'Jack', 'school': 'Beijing'} #输出 {'name': 'Rose'} #4.1再次查看学校属性 print(stu1.school) print(stu2.school) #输出 Beijing #输出 Tsinghua #__dict__用于访问对象的名称空间 本质是一个字典类型数据,存储名称与值的映射关系
属性查找顺序:对象->类->父类
六.绑定方法与非绑定方法
什么是方法?
先理清方法,函数,技能的关系:
生活中对象的技能在程序中用函数表示
函数在面向对象中称之为方法,换种称呼而已!
如此说来,绑定方法也就是绑定函数
10为什么要绑定?
在使用面向对象之前,数据与处理数据的函数是独立的没有任何联系,在调用函数时需要手动传入参数,如果要处理的数据有很多,参数的传递就是一个非常麻烦的事情,
原始的处理方式:函数 传参
问题1 调用函数时传入参数,如果要处理的数据有很多,编写了很多重复的代码,代码的阅读性很差
问题2 后期如果每次处理的数据个数变多了,函数需要修改参数列表,导致以前写的所有代码都需要修改,扩展性非常差
问题3 如果传入了错误的数据,比如需要整型却传入了字符串,造成函数无法正常工作
绑定方法的处理方式:
1.调用方法时传入对象,对象中包含了需要的所有数据,减少重复代码
2.后期数据变化时,修改类对象中的属性,方法中增加相应的处理代码,而方法参数不会发生变化,提高了扩展性
3.方法与对象进行绑定,没有对象则无法使用方法,并且在创建对象的初始化方法中,已经确定了各个属性数据时正确的,如此一来避免了传入使用错误数据执行函数造成的问题
简单的说,就是将数据与处理数据的函数绑定在一起,没有数据则根本不需要处理数据的函数,反过来要执行处理数据的函数则必须提供要被处理的数据
类中定义的函数分成两大类
一:绑定方法
1.绑定到对象的方法:没有被任何装饰器装饰的方法。
在类中定义的函数默认都是绑定到对象的方法
特点:参数的第一个必须是self 表示当前对象本身,使用对象来调用,调用时会自动传入对象
2.绑定到类的方法:用classmethod装饰器装饰的方法。
特点:参数的第一个必须是cls表示当前类本身,使用类名来调用,调用时会自动传入类
二:非绑定方法:用staticmethod装饰器装饰的方法
特点:不与类或对象绑定,类和对象都可以调用,但是没有自动传值那么一说。就是一个普通函数
不过由于作用域在类中所以需要使用类或对象类调用
class Student: school = "Tsinghua" def say_hello(self):# 绑定到对象的方法 print(self) print("hello i am a student my name is %s" % self.name) def __init__ (self,name): #绑定到对象的方法 self.name = name @classmethod # 绑定到类的方法 def school_info(cls): print(cls) print("the student from %s" % cls.school) stu1 = Student("Jack") print(stu1) #输出 <__main__.Student object at 0x1063112e8> #1.调用对象绑定方法 stu1.say_hello() #输出 <__main__.Student object at 0x1063112e8> #输出 hello i am a student my name is Jack #查看对象绑定方法 print(stu1.say_hello) #输出 <bound method Student.say_hello of <__main__.Student object at 0x10552b2e8>> #含义 这个绑定方法是Student类中的say_hello函数,绑定到地址为0x10552b2e8的Student对象 #绑定方法本质上也是函数 只要能找到它就能调用它所以你可以这样来调用它 Student.say_hello(stu1) #输出 <__main__.Student object at 0x103818390> #输出 hello i am a student my name is Jack print(Student) #输出 <class '__main__.Student'> #2.调用类绑定方法 Student.school_info() #输出 <class '__main__.Student'> #输出 the student from Tsinghua #查看类绑定方法 print(Student.school_info) #输出 <bound method Student.school_info of <class '__main__.Student'>> #含义 这个绑定方法是Student类中的school_info函数,绑定到Student这个类
绑定到类的方法与绑定到对象的方法总结
异同点:
相同
绑定对象调用时都有自动传参的效果
绑定到谁给谁就由谁来调用
不同
绑定到类的方法自动传入当前类
绑定到对象的方法自动传入当前对象
另外:
绑定方法中的self 和 cls参数名 是可以随意修改的,但是self和cls是约定俗成的写法,为了提高可读性不建议修改