面向对象基础
面向对象和面向过程
编程思想是什么,就是用代码解决现实生活中问题的思路。
面向过程
核心点在过程二字,过程指的是解决问题的步骤,说白了就是先做什么再干什么。这种解决问题的思路就好比是工厂中的流水线。
运维同学工作中接触到的shell脚本就是典型的按步骤做事。
优点:复杂的问题流程化,进而简单化。
缺点:可扩展性差,比如,一个脚本就是干一件事情的。
面向对象
核心点是对象二字,对象指的是具有相同属性和动作的结合体叫对象。
面向对象编程就好比在代码中创造一个世界,创造若干对象,就像现实世界中的万物一样,每个物体都可以有自己的属性和动作。
优点:可扩展性强
缺点:编程的复杂度高于面向过程
对比一下
问:把大象装进冰箱,总共分几步?
答:三步啊,
- 第一步,把冰箱门打开。
- 第二步,把大象装进去。
- 第三步,把冰箱门关上。
这就是典型的面向过程解题的思维方式。
那面向对象编程是怎么样解决这个问题呢?
首先,我们需要有两个对象--冰箱和大象。冰箱可以开门和关门,大象可以被放进冰箱。
- 冰箱开门。
- 大象走进冰箱。
- 最后冰箱门关上。
这就是面向对象的思维方式。
我们面向的不再是事物发展的流程,而是操纵某个事物个体。
面向对象的实现
说了那么多,那究竟如何在代码中使用面向对象呢?
首先,我们来思考这样一个问题,在现实世界中,我们如何造一辆汽车?
我们肯定是先由设计师来设计,在图纸上勾勒出汽车应该有的东西,以及这辆汽车应该有的功能,然后再交给工厂去生产真正的汽车。
类的定义
在程序中也是一样,我们需要先设计一个图纸,图纸上描述着物体应该有的特征和功能,然后依照这个图纸创建对象。
在Python中,“画图纸”的这个过程,我们通过定义类来实现。
类的语法很简单,像下面这样:
class 类名: pass
比如我们创建一个车类:
class Car: pass
像这样我们就创建了一个汽车类,图纸有了,那怎么创建一辆汽车对象呢?
根据图纸创建物体的过程,我们称之为实例化。实例化就是由一个概念到实际物体的过程。
Python中实例化,只需要“类名()”就可以了。
c1 = Car() # 造一辆车 c2 = Car() # 再造一辆车
车现在有了,这车至少得有一些颜色、排量、品牌等信息吧。不同的车,可以有不同的颜色、排量、品牌等信息。
我们可以通过下面的代码来实现:
c1.color = 'red' c1.code = 'SNIS-517' c1.power = '2.0T' c2.color = 'green' c2.code = 'SNIS-594' c2.power = '3.0T'
然后,我们可以检查一下这两辆车的信息。
print(c1.color) print(c1.code) print(c1.power) print(c2.color) print(c2.code) print(c2.power)
我们就这样造了两辆完全不同的车,但是我们发现这两辆车是在造好之后再添加不同的信息的。可是这些信息应该是在创造汽车的时候就设计好的,不应该后面再设计的。
构造函数
好了,我们现在的需求就是在创建对象的时候就能给对象设置一些初始的属性信息,在Python中我们是通过在类中定义__init__()“函数”,来进行初识化操作。这个“函数”被称为构造函数(方法)。
class Car: def __init__(self, color, code, power): # self表示当前创建的这个对象,你创建的是谁self就指的谁 self.color = color self.code = code self.power = power c1 = Car('red', 'SNIS-517', '2.0T') c2 = Car('green', 'SNIS-594', '3.0T') print(c1.color) print(c1.code) print(c1.power) print(c2.color) print(c2.code) print(c2.power)
通过执行上面的代码,我们发现我们由一个Car类,创造了两个不同特征(属性)的汽车对象。
我们的这个例子还没有讲完,因为汽车不光有特征,它还应该有动作--车还会跑。而且是所有的汽车都会跑,这怎么实现呢?
方法
我们只需要在类中像定义函数一样定义一个方法即可。类中的方法比普通的函数多一个self参数。其实本质上就是函数,只不过这个函数被绑定给实例对象使用,不能随意通过名字调用执行它。
class Car: def __init__(self, color, code, power): # self表示当前创建的这个对象,你创建的是谁self就指的谁 self.color = color self.code = code self.power = power def run(self, speed): print('车可以跑{}迈'.format(speed)) c1 = Car('red', 'SNIS-517', '2.0T') c1.run(70) # 此时,Python会自动把c1这个对象当成run()的第一个参数传进去
self
构造函数中的self表示创建的当前对象。
方法中的self表示谁调用的方法,self就是谁。
总结
类是对事物的总结,是一种抽象的概念。类用来描述对象。
对象是类实例化的结果,对象能执行哪些方法,都是由类来定义的。类中定义了什么,对象就拥有什么。
再来一个例子,帮助理解面向对象。
from math import pi class Circle: """ 定义了一个圆形类; 提供计算面积(area)和周长(perimeter)的方法 """ def __init__(self, radius): self.radius = radius # 圆的半径 def area(self): return pi * self.radius * self.radius def perimeter(self): return 2 * pi * self.radius circle = Circle(10) # 实例化一个圆 area1 = circle.area() # 计算圆面积 per1 = circle.perimeter() # 计算圆周长 print(area1, per1) # 打印圆面积和周长
练习题
- 创建一个武松,武松可以打老虎、杀嫂子。
- 用面向对象的思维来模拟王者荣耀中亚瑟上阵杀敌。
- 用面向对象的思维来实现用户登录。
面向对象和面向过程的比较
你可能会问:面向对象和面向过程到底哪个好?
首先这不能看做是一个谁好谁不好的问题,要具体问题具体分析。没有绝对的好和不好,这一点非常重要!
我们借助几个例子,再来看一下面向对象和面向过程的区别:
大象装冰箱示例
面向过程版
# 非函数版 print('开冰箱门') print('把大象装进冰箱') print('关冰箱门') # 函数版 def open_door(): print('开冰箱门') def zhuang(): print('把大象装进冰箱') def close_door(): print('关冰箱门') open_door() zhuang() close_door()
面向对象版:
class Elephant: def open_door(self): print('开冰箱门') def zhuang(self): print('走进冰箱') def close_door(self): print('把门带上') dx = Elephant() dx.open_door() dx.zhuang() dx.close_door()
这...面向对象有点麻烦啊...别着急,继续往下看:
小猪佩奇大战奥特曼
以一只猪,名叫佩奇,今年18岁,会使用必杀技“嘟嘟嘴”。他不光能大战奥特曼,还能大战蝙蝠侠,蜘蛛侠。
面向过程
# 面向过程 def da_out_man(name, age, jn): print('{}, 今年{}岁,正在使用{}技能对奥特曼疯狂输出...'.format(name, age, jn)) def da_bat_man(name, age, jn): print('{}, 今年{}岁,正在使用{}技能对蝙蝠侠疯狂输出...'.format(name, age, jn)) def da_spider_man(name, age, jn): print('{}, 今年{}岁,正在使用{}技能对蜘蛛侠疯狂输出...'.format(name, age, jn)) da_out_man('小猪佩奇', 18, '嘟嘟嘴') da_bat_man('小猪佩奇', 18, '嘟嘟嘴') da_spider_man('小猪佩奇', 18, '嘟嘟嘴')
面向对象:
class Pig: def __init__(self, name, age, skill): self.name = name self.age = age self.skill = skill def da_out_man(self): print('{}, 今年{}岁,正在使用{}技能对奥特曼疯狂输出...'.format(self.name, self.age, self.skill)) def da_bat_man(self): print('{}, 今年{}岁,正在使用{}技能对蝙蝠侠疯狂输出...'.format(self.name, self.age, self.skill)) def da_spider_man(self): print('{}, 今年{}岁,正在使用{}技能对蜘蛛侠疯狂输出...'.format(self.name, self.age, self.skill)) pq = Pig('小猪佩奇', 18, '嘟嘟嘴') pq.da_out_man() pq.da_bat_man() pq.da_spider_man()
看完这个例子,是不是有点眉目了。在小猪佩奇这个示例中,我们明显能够发现使用面向对象的思想来解决问题会更清晰一些。
代码也更容易编写一些。
所以,使用哪种编程思想来解决问题不是绝对的,我们需要根据实际的需求来抉择。
面向对象的三大特征
面向对象有三大特性:封装、继承和多态。
封装
把很多数据封装到一个对象中,把固定功能的代码封装到⼀个代码块函、数、对象, 打包成模块,这都属于封装的思想。
具体的情况具体分析,比如:你写了⼀个很牛B的函数,那这个也可以被称为封装。在⾯向对象思想中是把一些看似无关紧要的内容组合到一起,统一进行存储和使用,这就是封装。
继承
子类可以自动拥有父类中除了私有属性外的其他所有内容。说⽩了, ⼉⼦可以随便用爹的东西。但是朋友们, 一定要认清楚⼀个事情。必须先有爹, 后有⼉⼦。 顺序不能乱,在Python中实现继承非常简单。在声明类的时候, 在类名后⾯面添加一个小括号,把要继承的类传进去就可以完成继承关系。
那么什么情况可以使用继承呢?
单纯的从代码层⾯上来看,当两个类具有相同的功能或者特征的时候,可以采用继承的形式。提取一个父类,这个父类中编写两个类中相同的部分。然后两个类分别去继承这个类就可以了。这样写的好处是我们可以避免写很多重复的功能和代码。
如果果语境中出现了了x是一种y。这时,y就是⼀种泛化的概念,x比y更加具体。那这时x就是y的子类。举个现实中的例子:猫是⼀种动物,猫继承动物。动物能动,猫也能动。这时猫在创建的时候就有了动物的"动"这个属性。再比如⽩骨精是⼀个妖怪,妖怪天生就有⼀个比较不好的功能叫"吃人", 白骨精⼀出生就知道如何"吃人"。此时 ⽩骨精继承妖精。
话不多说,上代码:
# 妖怪类 class Yao: def eat(self): print("我是妖怪, 我天生就会吃人") # ⽩骨精继承妖怪 class BaiGuJing(Yao): pass bgj = BaiGuJing() # 我是妖怪, 我天⽣生就会吃⼈人 # 虽然白骨精类中没有编写eat,但是他爹有啊,直接拿来⽤ bgj.eat()
继承是实例对象能够访问到自己类及父类的属性和方法。
先来看几个例子:
class A: def __init__(self, num): self.num = num def func1(self): print(self.num) class B(A): def func1(self): print("B.func1", self.num) obj = B(123) obj.func1()
再来一个:
class A: def __init__(self, num): self.num = num def func1(self): print(self.num) self.func2() def func2(self): print("A.func2") class B(A): def func2(self): print("B.func2") obj = B(123) obj.func1()
总结一下:
示例对象访问属性或调用方法时,永远都是先找自己的,找不到就往父类找。
最后来两个例子,巩固一下:
class A: def __init__(self, num): self.num = num def func1(self): print(self.num) self.func2() def func2(self): print("A.func2", self.num) class B(A): def func2(self): print("B.func2", self.num) list1 = [A(1), A(2), B(3)] for i in list1: i.func2()
来一个更绕的:
class A: def __init__(self, num): self.num = num def func1(self): print(self.num) self.func2() def func2(self): print("A.func2", self.num) class B(A): def func2(self): print("B.func2", self.num) list1 = [A(1), A(2), B(3)] for i in list1: i.func1()
在Python中,一个类可以同时继承多个父类。说白了, 现在一个⼉子可能会有多个爹了。
既然是可以有多个爹, 那总得有远有近。比如. 有一个这样的牛B的人物, 叫郭丑丑,就有很多个爹。
class QinDie: def eat(self): print("亲爹给你好吃的") def play(self): print("亲爹会陪你玩") class GanDie: def qian(self): print("干爹给钱啊") def play(self): print("干爹会陪你玩") class GuoChouChou(QinDie, GanDie): pass mm = GuoChouChou() mm.eat() mm.qian() mm.play() # 继承中,实例对象查找方法的顺序 # 亲爹没有, 找干爹 # 亲爹有了, 就不找干爹了
注意:多继承中的MRO(Method Resolution Order)算法,我们在后面会具体分析和讲解。
多态
同一个对象,多种形态。
这个在Python中其实是很不容易说明白的,因为我们一直在用,只是没有具体的说。比如我们创建⼀个变量a = 10,我们知道此时a是整数类型。但是我们可以通过程序让a = "Bob", 这时a又变成了字符串类型。这是我们都知道的。这个就是多态性。同一个变量a可以是多种形态。再比如一个打开的文件、一个列表、一个生成器都可以被for循环迭代,它们都是可迭代对象。可能这样的程序和说法你还get不到具体什么是多态。
接下来,我们来看⼀个程序。动物园里饲养员一天的⼯作,从早上开始喂养猪, 中午喂狗。
来我们用代码实现这样的代码:
class Animal: def eat(self): print("动物就知道吃") class Pig(Animal): def eat(self): print("猪在吃") class Dog(Animal): def eat(self): print("狗在吃") class Feeder: def feed_animal(self, ani): ani.eat() p = Pig() d = Dog() f = Feeder() f.feed_animal(p) f.feed_animal(d)
当子类和父类都存在相同的eat方法时,子类的eat方法覆盖了父类的eat方法。在代码运行的过程中,子类对象总是会调用自己的eat方法。
此时,虽然都是Animal类的对象,都有eat方法,但是实现的功能却可以不同。
多态的好处:程序具有超高的可扩展性。
面向对象基础2
类的成员简介
什么是类的成员呢?很简单,你在类中定义的内容都可以称为类的成员。
到目前为止,我们已经学过一些成员了:
class Person: # 方法 def __init__(self, name, age): # 属性 self.name = name self.age = age # 方法 def say(self): print('我是{self.name},我今年{self.age}岁!'.format(self=self))
在上面的代码中,__init__和say都是属于类的成员方法,又称为实例方法。
self.name和self.age都是实例对象的属性,这些称为成员变量或实例变量。
也就是说在类中,可以定义实例变量和实例方法,那还有些什么呢?
变量
实例变量
先说什么是实例变量,说白了,就是每个实例都应该拥有的变量。比如,人的名字,人的年龄,每个人的信息都属于具体的人,这些都可以称为实例变量。
class Person: # 方法 def __init__(self, name, age): # 属性 self.name = name self.age = age # 方法 def say(self): print('我是{self.name},我今年{self.age}岁!'.format(self=self)) p1 = Person('张三', 18) p2 = Person('李四', 20) print(p1.name, p1.age) # 张三 18 print(p2.name, p2.age) # 李四 20 print(id(p1.name), id(p2.name)) # 4567249208 4567250088 print(id(p1.age), id(p2.age)) # 4564114656 4564114720
类变量(静态变量)
那什么是类变量呢?类变量就是这一类事物统一拥有的变量,比如,在座的各位都是中国人,大家都拥有一个同一个祖国--中国。
class Person: # 类变量,所有实例对象都共享这个变量 country = '中国' # 方法 def __init__(self, name, age): # 属性 self.name = name self.age = age # 方法 def say(self): print('我是{self.name},我今年{self.age}岁!'.format(self=self)) p1 = Person('张三', 18) p2 = Person('李四', 20) print(p1.country, p2.country) # 中国 中国 print(id(p1.country), id(p2.country)) # 4567249032 4567249032
我们可以发现,实例对象p1和p2的country属性都是类中定义的那个,公用的。
我们尝试修改一下:
Person.country = 'China' print(p1.country, p2.country) # China China print(id(p1.country), id(p2.country)) # 4551075464 4551075464
从上面的代码中,我们可以看到我们修改了类变量country,各实例对象中的country属性也跟着变了。
接下来,我们再来看一个例子。
class Person: # 类变量,所有实例对象都共享这个变量 country = '中国' # 方法 def __init__(self, name, age): # 属性 self.name = name self.age = age # 方法 def say(self): print('我是{self.name},我今年{self.age}岁!'.format(self=self)) p3 = Person('王五', 3000) p3.country = '大秦' print(p3.name) # 王五 print(p3.country) # 大秦 p4 = Person('赵六', 30) print(p4.name) # 赵六 print(p4.country) # 中国
为什么王五的country是大秦,而赵六的country是中国呢?
注意,p3.country = '大秦'的时候,并没有去改变类中的country,而是给自己添加了一个实例属性,这个属性只有在p3这个实例对象中才是存在的,在p4中是不存在的。
总结
类变量,是定义在类中的变量,该类的实例对象都可以访问到该变量,但是示例对象只能查看不能修改。想要修改类变量,只能通过类名来修改。
示例变量,给实例对象用的。
类变量,实例对象共享的。
案例:通过类变量来记录创建的实例个数:
class Foo: count = 0 def __init__(self): Foo.count += 1 print(Foo.count) # 0 Foo() Foo() Foo() print(Foo.count) # 3
方法
类中的方法分为实例方法、静态方法和类方法。
实例方法
实例方法是我们接触和使用最多的,实例对象可以直接访问的方法就叫实例方法(成员方法)。
class Computer: # 实例方法 def play(self): print('我的电脑可以玩游戏') c1 = Computer() c1.play() # 实例对象直接调用实例方法
静态方法
静态方法不需要传递“self”参数,也就是说,当类中的方法不需要传入实例变量的时候,就可以使用静态方法。
定义静态方法时需要我们在该方法上添加@static,示例如下:
class Computer: # 实例方法 def play(self): print('我的电脑可以玩游戏') @staticmethod def fire(): # 方法不需要传入实例对象变量 print('我的电脑非常牛B,可以煎鸡蛋') c1 = Computer() c1.play() # 实例对象直接调用实例方法 c1.fire()
静态方法和静态变量一样,都可以使用类名直接访问。
Computer.fire()
类方法
类方法是由类调用的方法,他的第一个参数必须是类本身。类方法在被调用的时候,不需要调用者传递该参数,解释器会自动的把类当成第一个参数,类方法在定义的时候需要在类方法上添加@classmethod
class Computer: # 实例方法 def play(self): print('我的电脑可以玩游戏') @staticmethod def fire(): # 方法不需要传入实例对象变量 print('我的电脑非常牛B,可以煎鸡蛋') @classmethod def calc(cls, a, b): print(cls) return a + b c1 = Computer() c1.play() # 实例对象直接调用实例方法 c1.fire() ret = Computer.calc(10, 20) # <class '__main__.Computer'> print(ret) # 30
相关面试题
类方法、静态方法和实例方法有什么区别?
属性方法
属性方法其实就是把类中的方法改造成属性的一种写法,该写法需要在方法上加上@property,例如:
from datetime import datetime from dateutil.relativedelta import relativedelta class Person: def __init__(self, name, birthday): self.name = name self.birthday = birthday @property def age(self): # 人的年龄是一个属性,但是需要由生日和当前时间计算得出 # 所以我们把这个方法包装成属性 ret = relativedelta(datetime.now(), datetime.strptime(self.birthday, '%Y-%m-%d')) return ret.years p1 = Person('李国庆', '1990-10-01') # 像访问实例属性一样 print(p1.age)
在面向对象中,类中通常会定义属性和方法,属性用来描述实例对象的特征,方法用来描述实例对象的动作。
像年龄这种需要一定动作来得出的属性,Python中我们会把这个方法描述成一个属性。
补充property的进阶用法:
class Goods: def __init__(self): # 原价 self.original_price = 100 # 折扣 self.discount = 0.8 @property def price(self): # 实际价格 = 原价 * 折扣 new_price = self.original_price * self.discount return new_price @price.setter def price(self, value): self.original_price = value @price.deleter def price(self): del self.original_price obj = Goods() print(obj.price) # 获取商品价格 obj.price = 200 # 修改商品原价 print(obj.price) del obj.price # 删除商品原价 print(obj.price) # AttributeError: 'Goods' object has no attribute 'original_price'
私有成员
在某些场景下,我们可能会在类中存储一些只有我们自己能访问的信息。在Python中我们可以使用两个连续的双下划线将属性或方法变为私有的。
私有变量
我们定义一个Person类,有name属性和salary属性,薪资是保密,所以在salary的前面加上两个连续的下划线,把它变成私有的。
class Person: def __init__(self, name, salary): self.name = name self.__salary = salary # 薪资是秘密,设置为私有 p1 = Person('李狗剩', 200) print(p1.name) print(p1.__salary) # AttributeError: 'Person' object has no attribute '__salary'
上面的代码报错,因为我们无法直接访问实例对象的私有属性。
我们可以曲线救国,通过另外一个方法来获取这个属性。
class Person: def __init__(self, name, salary): self.name = name self.__salary = salary # 薪资是秘密,设置为私有 def dazuiba(self): return self.__salary p1 = Person('李狗剩', 200) print(p1.name) salary = p1.dazuiba() print(salary) # 200
私有属性不能直接访问,但是可以通过其他的实例方法来访问私有属性。这样做的好处是实现了私有属性只能查看不能修改。
不仅仅实例属性可以私有,类属性也可以:
class Person: __secret = '人都是自私的' def __init__(self, name, salary): self.name = name self.__salary = salary # 薪资是秘密,设置为私有 def dazuiba(self): return self.__salary p1 = Person('李狗剩', 200) print(p1.name) salary = p1.dazuiba() print(salary) # 200 print(Person.__secret) # AttributeError: type object 'Person' has no attribute '__secret'
私有方法
私有方法,顾名思义就是只能自己调用的方法,别人都不能随便调用。
class Person: def __init__(self): pass def __play(self): print('嘿嘿嘿') def work(self): print('工作使我快乐!') p1 = Person() p1.work() # 工作使我快乐! p1.__play() # AttributeError: 'Person' object has no attribute '__play'
__play是一个私有方法,只能够在类的内部调用,类的外部无法调用。
我们可以在类的内部调用私有方法:
class Person: def __init__(self): pass def __play(self): print('嘿嘿嘿') def work(self): print('工作使我快乐!') def dazuiba(self): self.__play() p1 = Person() p1.work() p1.dazuiba() # 嘿嘿嘿
在实际的应用场景中,我们通常会把那些不想暴露给实例对象的方法,定义为私有方法。
另外还需要注意的一点是,对于私有成员,子类是无法继承的。
class A: __secret = '密码' def __init__(self, salary): self.__salary = salary def __play(self): print('嘿嘿嘿') class B(A): def dazuiba(self): self.__play() b1 = B(200) print(b1.__salary) # 'B' object has no attribute '__salary' b1.__play() # 'B' object has no attribute '__play' b1.dazuiba() # AttributeError: 'B' object has no attribute '_B__play' print(b1.__secret) # AttributeError: 'B' object has no attribute '__secret' print(B.__secret) # AttributeError: type object 'B' has no attribute '__secret'
类的使用
依赖关系
class Elephant: def __init__(self, name): self.name = name def open(self, ref): print("⼤象要开门了。默念三声!开!") # 由外界传递进来⼀个冰箱, 让冰箱开门。这时,⼤象不用背着冰箱到处跑。 # 类与类之间的关系也就不那么的紧密了,换句话说只要是有open_door()⽅法的对象都可以接收运⾏ ref.open_door() def close(self, ref): print("⼤象要关门了。默念三声!关!") ref.close_door() def enter(self): print("钻进去") class Refrigerator: def open_door(self): print("冰箱门被打开了") def close_door(self): print("冰箱门被关上了") # 造冰箱 r = Refrigerator() # 造⼤大象 el = Elephant("神奇的大象") el.open(r) # 注意:此时是把一个冰箱作为参数传递进去了。也就是说大象可以指定任何一个冰箱 el.enter() el.close(r)
关联关系
class Boy: def __init__(self, name, girl_friend=None): self.name = name self.girl_friend = girl_friend def have_a_dinner(self): if self.girl_friend: print("{}和{}一起去吃晚餐".format(self.name, self.girl_friend.name)) else: print("单身狗。吃什么饭!") class Girl: def __init__(self, name): self.name = name b = Boy('张大锤') b.have_a_dinner() # 突然找到女朋友了 g1 = Girl("如花") b.girl_friend = g1 g2 = Girl("李小花") bb = Boy("张二锤", g2) # 娃娃亲,出⽣就有女朋友 bb.have_a_dinner() # 多么幸福的一家 # 突然.bb失恋了。娃娃亲不跟他好了 bb.girl_friend = None bb.have_a_dinner() # 又单身了