第六章 面向对象基础
在本章, 首先将会学习面向过程和面向对象之间的区别和联系 然后, 学习类(类对象, 类属性, 类方法, 静态方法), 对象(演变, 内存结构), 属性和方法(命名规范, 方法相关特性, 实例属性和实例方法, 私有属性和私有方法), 常用的类的方法(init, call, del, @property)以及关键字None 的相关介绍 最后, 进行实操来巩固所学习到的知识
Python完全采用了面向对象的思想,是真正面向对象的编程语言,完全支持面向对象的基本功能,例如:继承、多态、封装等。 Python中,一切皆对象. 我们在前面学习的数据类型、函数等,都是对象.
需要注意:
为了思考面向是什么, 我们不妨考虑"如何设计事物". 比如造车, 我们首先应该不去思考怎么按步骤造车 , 而是会去思考车怎么设计. 这就是面向过程到面向对象的转变 通过对相关知识的学习, 我们知道汽车主要是由轮胎, 发动机, 车壳, 座椅, 玻璃等几个零件组成 为了能够高效的实现汽车生产, 我们可以找轮胎厂完成制造轮胎的步骤, 发动机厂完成制造发动机的步骤… 这样, 汽车的各项零件就会并行的去制造, 最终去进行组装, 大大提高了汽车生产效率 但是, 具体到轮胎厂的一个流水线操作,仍然是有步骤的,还是离不开执行者、离不开面向过程
注意:
面向过程适合简单、不需要协作的事务,重点关注如何执行。 面向过程时,我们首先思考 “ 怎么按步骤实现?” 比如如何开车?我们很容易就列出实现步骤:
注意:
区别
联系
类:我们叫做class . 类一般看做是对象的抽象 我们把对象比作一个“饼干”,类就是制造这个饼干的“模具”. 类和对象的关系如下图所示:
前面讲的类定义格式为,
class 类名:
实际上, 当解释器执行class 语句时, 就会创建一个类对象
class Student:
pass # 空语句
print(type(Student)) # <class 'type'>
print(id(Student)) # 1772468512176
Stu2 = Student
s1 = Stu2()
print(s1) # <__main__.Student object at 0x0000019CAF6E3D90>
注意:
类属性是从属于“类对象”的属性, 也称为“类变量”. 由于类属性从属于类对象, 因此可以被所有实例对象共享类属性的定义方式:
class 类名:
类变量名= 初始值
注意:
类名.类变量名
来读写实操代码
class Student:
company = "sxd" # 类属性
s1 = Student()
print(s1.company)
类方法是从属于“类对象”的方法. 类方法通过装饰器
@classmethod
来定义
格式如下:
@classmethod
def 类方法名(cls [, 形参列表]):
方法体
注意:
@classmethod
必须位于方法上面一行cls
必须有; cls
指的就是“类对象”本身类名.类方法名(参数列表)
. 参数列表中, 不需要也不能给 cls
传值实操代码
class Student:
company = 'sxd'
@classmethod
def printCompany(cls):
print(cls.company)
Student.printCompany()
Python中允许定义与“类对象”无关的方法, 称为“静态方法” “静态方法”和在模块中定义普通函数没有区别, 只不过“静态方法”放到了“类的名字空间里面”, 需要通过“类调用” 静态方法通过装饰器
@staticmethod
来定义,
格式如下
@staticmethod
def 静态方法名([形参列表]):
方法体
注意
@staticmethod
必须位于方法上面一行实操代码
class Student:
company = "sxd"
@staticmethod
def add(a, b):
print("{0}+{1}={2}".format(a, b, a+b))
return a + b
Student.add(111, 222)
对象:我们叫做object , instance (实例). 以后我们说某个类的对象,某个类的实例. 是一样的意思. 对象一般看做是类的具体体现
随着编程面临的问题越来越复杂,编程语言本身也在进化. 从主要处理简单数据开始,随着数据变多进化“数组”; 数据类型变复杂,进化出了“结构体”; 处理数据的方式和逻辑变复杂,进化出了“对象”.
演变过程如下图所示:
类是抽象的,也称之为“对象的模板”. 而对象则是类的具体实现 我们需要通过类这个模板,创建类的实例对象,然后才能使用类定义的功能。 我们前面说过一个Python对象包含三个部分: id (identity识别码)、type (对象类型)、value (对象的值)
现在我们可以更进一步的说,一个Python对象包含如下部分:标识, 类型, 属性和方法. 完整对象内存结构图如下图所示:
相较于Java 使用了关键字对类和方法以及属性进行访问权限控制. Python 使用下划线来对类, 对象, 属性和方法进行权限控制
具体区别如下:
_xxx
(方法名称前一个下划线):保护成员, 不能用 from module import * 导入, 只有类对象和子类对象能访问这些成员.__xxx__
(方法名称前后两个下划线):系统定义的特殊成员__xxx
(方法名称前两个下划线): 类中的私有成员, 只有类对象自己能访问, 子类对象也不能访问. (但, 在类外部可以通过 对象名. _类名__xxx 这种特殊方式访问. Python不存在严格意义的私有成员)实例属性是从属于实例对象的属性, 也称为“实例变量”.
注意:
实例方法是从属于实例对象的方法.
实例方法的定义格式如下:
def 方法名(self [, 形参列表]):
函数体
方法的调用格式如下:
对象.方法名([实参列表])
注意:
实例对象和实例方法实操代码:
class Student:
def __init__(self, name, score):
self.name = name # 实例属性
self.score = score
self.age = 18 # 增加age属性
def say_score(self): # 实例方法
print(self.name, "的分数是: ", self.score)
s1 = Student("时间静止", 18)
s1.say_score() # 调用示例方法
print(s1.age)
s1.salary = 3000 # s1对象增加salary属性
s2 = Student("不是简史", 81)
s2.say_score()
print(s2.age)
dir(obj)
可以获得对象的所有属性、方法. 返回的是一个字典类型的数据obj.__dict__
对象的属性字典pass
空语句. 用于定义空白逻辑的函数/方法isinstance(对象,类型)
判断“对象”是不是“指定类型”都是用来完成一个功能的语句块, 本质一样. 方法调用时, 通过对象来调用. 方法从属于特定实例对象, 普通函数没有这个特点 直观上看, 方法定义时需要传递self, 函数不需要
如果我们在类体中定义了多个重名的方法, 只有最后一个方法有效. 建议: 不要使用重名的方法!Python中方法没有重载. 这一点和java大不相同
注意:
实操代码
class Person:
def make_money(self):
print("我要挣钱啦~")
def make_money(self, money):
print("我要挣钱啦~今天挣到{0}元".format(money))
p1 = Person()
# p1.make_money() # 不带参, 报: TypeError: Person.makeMoney() missing 1 required positional argument: 'money'
p1.make_money(666)
Python是动态语言, 我们可以动态的为类添加新的方法, 或者动态的修改类的已有的方法
实操代码
我们可以看到, Person 动态的新增了 play_game 方法, 以及用 work2 替换了 work 方法
class Person:
def work(self):
print("努力上班!")
def play_game(self): # 静态方法
print("玩游戏")
def work_plus(s):
print("上班好好工作, 下班好好玩")
Person.play = play_game
Person.work = work_plus
p = Person()
p.play()
p.work()
Python对于类的成员没有严格的访问控制限制, 这与其他面向对象语言有区别.
关于私有属性和私有方法, 有如下要点:
_类名__私有属性(方法)名
”访问私有属性(方法)注意:
实操代码
class Employee:
__company = "sxd" # 私有.通过dir查到_Employee__company
def __init__(self, name, age):
self.name = name
self.__age = age
def get_company_name(self):
print("我的公司是: ", Employee.__company) # 类内部可以直接访问私有属性
print("{0}的年龄是{1}".format(self.name, self.__age))
def __work(self): # 私有实例方法, 通过dir可查到_Employee__work
print("工作是人类进步的阶梯")
p1 = Employee("timePause", 18)
print(dir(Employee))
print(Employee._Employee__company) # 在将company属性私有化后,直接调用报错, 通过dir可以查到属性_Employee__company,这种方式调用输出sxd, 从而验证规则4
# print(p1.age) # 在age属性未私有化时, 这样可以访问
print(p1._Employee__age) # 在age属性未私有化后, 通过这种方式可以直接访问到私有属性_Employee__age. 也验证了规则4
p1.get_company_name()
# p1.work() # 未私有之前可以调用, 私有后调用报错: AttributeError: 'Employee' object has no attribute 'work'
p1._Employee__work() # 私有之后. 通过这种方式可以直接访问私有熟悉_Employee__work. 也验证了规则4
我们以下面代码为例,分析整个创建过程:
class Student:
company = "sxd" # 类属性
count = 0 # 类属性
def __init__(self, name, score):
self.name = name # 实例属性
self.score = score
Student.count = Student.count + 1
def say_score(self): # 实例方法
print("我的公司是: ", Student.company)
print(self.name, "的分数是: ", self.score)
s1 = Student("时间静止", 18) # s1是实例对象, 自动调用__init__()方法
s2 = Student("不是简史", 81)
s1.say_score()
print('一共创建{0}个Student对象'.format(Student.count))
print("s1的引用:", id(s1))
print("s2的引用:", id(s2))
print("Student的引用:", id(Student))
实例对象和类对象创建过程如下图所示
初始化对象, 我们需要定义构造函数
__init__()
方法 构造方法用于执行“实例对象的初始化工作”, 即对象创建后, 初始化当前对象的相关属性, 无返回值 如果将对象比作一座房子, 则构造方法(__init__)
是负责初始化(装修) , 不是建对象(房子)
__init__()
的要点如下(约等于Java的构造方法):
__init__()
self
. self
指的就是刚刚创建好的实例对象类名(参数列表)
”来调用构造函数. 调用后, 将创建好的对象返回给相应的变量. 比如: s1 = Student("时间静止", 80)
__init__()
方法: 初始化创建好的对象, 初始化指的是: “给实例属性赋值”__init__
方法, 系统会提供一个默认的 __init__
方法.
如果我们定义了带参的 __init__
方法, 系统不创建默认的 __init__
方法__new__()
方法: 用于创建对象, 但我们一般无需重定义该方法注意:
__del__()
称为“析构方法”, 用于实现对象被销毁时所需的操作. 比如: 释放对象占用的资源, 例如: 打开的文件资源、网络连接等
注意:
__del__()
__del__()
__del__
方法 , 一般不需要自定义析构方法.实操代码
class Person:
def __del__(self):
print("销毁对象: {0}".format(self))
p1 = Person()
p2 = Person()
del p2
print("程序结束")
__call__
方法和可调用对象凡是可以将 () 直接应用到自身并执行, 都称为可调用对象.
注意:
__call__()
的对象, 称为“可调用对象”, 即该对象可以像函数一样被调用.实操代码
def f1():
print("f1")
f1() # 本质上也是调用了 __call__()方法
class Car:
def __call__(self, age, money):
print("调用__call__()方法")
print("车龄:{0}, 金额:{1}".format(age, money))
f2 = Car()
f2(3, 289000) # 像函数那样调用, 本质上也是调用了 __call__()
@property
装饰器相当于在Java中实现了对属性的get 方法
注意:
@property
可以将一个方法的调用方式变成“属性调用”.@property
主要用于帮助我们处理属性的读操作、写操作emp1.salary = 30000
如上的操作读操作、写操作
但是这种做法不安全. 比如, 我需要限制薪水必须为 1-10000 的数字. 这时候我们就需要通过使用装饰器 @property
来处理.实操代码
class Employee:
def __init__(self, name, salary):
self.name = name
self.__salary = salary
@property # 相当于salary属性的getter方法
def salary(self):
print("月薪为{0}, 年薪为{1}".format(self.__salary, 12 * self.__salary))
return self.__salary
@salary.setter
def salary(self, salary): # 相当于salary属性的setter方法
if 0 < salary < 1000000:
self.__salary = salary
else:
print("薪水录入错误! 只能在0-1000000之间")
emp1 = Employee("时间静止", 8848.48)
print(emp1.salary)
emp1.salary = 12580
print(emp1.salary)
实操代码
obj = None
obj2 = None
print(type(None))
print(id(None))
print(id(obj))
print(id(obj2))
"""输出结果
<class 'NoneType'>
140706372487160
140706372487160
140706372487160
"""
实操代码
# None不是False, None不是0, None不是空字符串. None和任何其他的数据类型比较永远返回False.
a = None
if a is None and a == None:
print("a是None") # 会执行
if a == False or a == 0:
print("None不等于False") # 不会被打印
a = []
b = ()
c = {}
d = ""
e = 0
f = None
# if语句判断时,空列表[]、空字典{}、空元组()、0等一系列代表空和无的对象会被转换成False
if (not a) and (not b) and (not c) and (not d) and (not e) and (not f):
print("if判断时, 空列表[]、空字符串、0、None等代表空和无的对象会被转换成False")
# == 和 is 判断时, 空列表、空字符串不会自动转成 False
if a == False or d == False:
print("==时, 空列表、空字符串不是False!") # 不会执行
if(e == False):
print("==时, 0会转成False")
设计一个名为MyRectangle的矩形类来表示矩形。这个类包含:
考察核心
实操代码
import turtle
class MyRectangle:
"""
设计一个名为MyRectangle的矩形类来表示矩形. 这个类包含:
(1) 左上角顶点的坐标:x, y
(2) 宽度和高度:width、height
(3) 构造方法:传入x, y, width, height. 如果(x, y)不传
则默认是0, 如果width 和height不传, 则默认是100.
(4) 定义一个getArea() 计算面积的方法
(5) 定义一个getPerimeter(), 计算周长的方法
(6) 定义一个draw()方法, 使用海龟绘图绘制出这个矩形
"""
__x = 0
__y = 0
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
def get_area(self):
print("该矩形面积为:", self.width * self.height)
def get_perimeter(self):
print("该矩形周长为:", (self.width + self.height) * 2)
def draw(self):
turtle.showturtle()
turtle.write("画出该矩形, 左上角坐标为:({0},{1}), 宽为:{2}, 高为:{3}".format(self.x, self.y, self.width, self.height))
turtle.speed(1)
turtle.goto(self.x, self.y)
turtle.goto(self.x, self.y - self.height)
turtle.goto(self.x + self.width, self.y - self.height)
turtle.goto(self.x + self.width, self.y)
turtle.goto(self.x, self.y)
m1 = MyRectangle(0, 100, 50, 100)
m1.get_area()
m1.get_perimeter()
m1.draw()