20. 面向对象之封装
1. 面向对象的三大特性
面向对象编程有三大特性:封装、继承、多态
2. 理论
2.1 概念
封装指的是把数据与功能都整合到一起,之前所说的”整合“二字其实就是封装的通俗说法。
除此之外,针对封装到对象或者类中的属性,可以严格控制对它们的访问,分两步实现:隐藏与开放接口
2.2 封装的目的
将某些属性和方法隐藏起来,在程序外部看不到,其它程序无法调用
保护数据,可以防止外部代码随意修改对象内部的数据
3. 如何进行封装---隐藏属性
3.1 隐藏属性方法
Python的Class机制采用双下划线开头的方式将属性隐藏起来(设置成私有的)
但其实这仅仅只是一种变形操作,类中所有双下滑线开头的属性都会在类定义阶段、检测语法时自动变成_类名__属性名的形式
3.2 隐藏属性访问
在类外部无法直接访问双下滑线开头的属性,但知道了类名和属性名就可以拼出名字:__类名__属性,然后就可以访问了,
如Person._Person__NAME,所以说这种操作并没有严格意义上地限制外部访问,仅仅只是一种语法意义上的变形
class Student:
_ _school = 'MIT' # 在需要被封装数据属性的位置,加_ _
def _ _init_ _(self, name, age, grade):
self.name = name
self.age = age
self.grade = grade
def show_info(self):
print(f'name is {self.name}, age is {self.age}, grade is {self.grade},'
f'school is {self.__school}')
def _ _lab(self): # 在需要被封装函数属性的位置,加__
print(f'{self.__school}的{self.name}正在做实验')
print(Student._ _dict_ _) # '_Student_ _school': 'MIT' 类的属性字典变成变成 _类名__属性名的形式
stu1 = Student('neymar', 32, 2)
stu1.show_info() # name is neymar, age is 32, grade is 2,school is MIT
print(stu1._ _dict_ _) # {'name': 'neymar', 'age': 32, 'grade': 2}
stu1.school = 'California' # 隐藏school数据属性后对象无法直接修改该属性
print(stu1._ _dict_ _) # {'name': 'neymar', 'age': 32, 'grade': 2, 'school': 'California'}
print(Student._ _dict_ _) # '_Student__school': 'MIT'
# 通过 类名._类名__属性名修改属性
Student._Student_ _school = 'Harvard'
print(Student._ _dict_ _) # '_Student__school': 'Harvard'
print(stu1._Student_ _school) # Harvard
stu1._Student_ _lab() # Harvard的neymar正在做实验
3.3 隐藏方法变形
在类内部是可以直接访问双下划线开头的属性的,因为在类定义阶段类内部双下划线开头的属性统一发生了变形。
class Student:
__school = 'MIT' # 本质上变形成了_Student__school
def __init__(self, name, age, grade):
self.__name = name # 本质上变形成了self._Student__name
self.age = age
self.grade = grade
def __lab(self): # 本质上变形成了_Student__lab
print(f'{self.__school}的{self.__name}正在做实验')
def run(self):
self.__lab() # 本质上变形成了self._Student__lab())
print(self.__school) # 本质上变形成了self._Student__school
3.4 变形操作只会发生一次
变形操作只在类定义阶段发生一次,在类定义之后的赋值操作,不会变形
class Student:
_ _school = 'MIT' # 本质上变形成了_Student_ _school
def _ _init_ _(self, name, age, grade):
self._ _name = name # 本质上变形成了self._Student_ _name
self.age = age
self.grade = grade
def _ _lab(self): # 本质上变形成了_Student__lab
print(f'{self._ _school}的{self._ _name}正在做实验')
def run(self):
self._ _lab() # 本质上变形成了self._Student_ _lab())
print(self._ _school) # 本质上变形成了self._Student_ _school
Student._ _school = 'singapore'
print(Student._ _dict_ _) # '_Student_ _school': 'MIT' 数据属性并没有发生修改
3.5 总结
(1)在类内部 将变量名 用双下划线 声明
在类初始化的时候就会由原来的变量名 __变量名 变换 变换成 _类名_ _变量名
(2)在类内部封装起来的属性
内部调用 self 去调用的时候直接 self.__变量名即可调用
如果是对象调用 隐藏起来的属性 那就需要 对象._类名__变量名 去调用
(3)在类内部封装属性后变形只会发生一次
当使用对象或者类.__变量名去修改属性的时候 修改的是 新的属性而不是原来的属性
4. 封装之后如何访问---开放接口
4.1 原理
将数据和功能隐藏起来就限制了类外部对数据和功能的直接操作,类内部应该提供相应的接口来允许类外部间接地操作数据和功能,接口之上可以增加额外的逻辑对数据和函数的操作进行严格限制
4.2 隐藏数据属性
class Student:
def __init__(self, name, age, grade):
self.__name = name
self.__age = age
self.__grade = grade
def show_info(self):
print(f'{self.__name}的年龄是{self.__age}年级是{self.__grade}')
def set_info(self, name1, grade1):
if not name1.startswith('mit_'):
raise ValueError('名字必须以mit_开头')
if not grade1.isdigit():
raise ValueError('年级必须是数字')
self.__name = name1 # 符合以上两个条件才允许修改属性
self.__grade = grade1
stu1 = Student(name='alan', age=30, grade=1)
stu1.show_info() # alan的年龄是30年级是1
# 对象无法直接修改类中数据属性
stu1.name = 'messi'
stu1.show_info() # alan的年龄是30年级是1
# 想要修改类中数据属性,必须通过接口进行修改
stu1.set_info(name1='messi', grade1='2') # ValueError: 名字必须以mit_开头
stu1.set_info(name1='mit_messi', grade1=2)
# AttributeError: 'int' object has no attribute 'isdigit' int类型没有isdigit方法
stu1.set_info(name1='mit_messi', grade1='2') # 修改成功
stu1.show_info() # mit_messi的年龄是30年级是2
4.3 隐藏函数属性
# ATM程序的取款功能,由许多其它功能组成:插卡、输入密码、输入取款金额、取款、打印回执等
# 而对于使用者来说,只需要withdraw这个功能接口即可,其余功能都可以隐藏起来
class ATM:
def __inert_card(self):
print('插卡')
def __input_pwd(self):
print('输入密码')
def __input_money(self):
print('输入取款金额')
def __take_money(self):
print('取款')
def __print_bill(self):
print('打印回执')
def withdraw(self):
self.__inert_card()
self.__input_pwd()
self.__input_money()
self.__take_money()
self.__print_bill()
obj = ATM()
obj.withdraw()
5. property属性
5.1 引入
计算bmi
class Person:
def __init__(self, name, height, weight):
self.__name = name
self.__height = height
self.__weight = weight
def bmi(self):
info = f'{self.__name}的BMI数值是:{self.__weight / (self.__height ** 2)}'
return info
person1 = Person(name='haaland', height=1.95, weight=75)
print(person1.bmi()) # haaland的BMI数值是:19.723865877712033
# bmi计算得到的结果是一个数字,应该和姓名、身高、体重一样是一个数据属性,而不是函数属性
# 给计算bmi的函数加一个property属性,将函数属性伪装成数据属性
class Person:
def __init__(self, name, height, weight):
self.__name = name
self.__height = height
self.__weight = weight
@property
def bmi(self):
info = f'{self.__name}的BMI数值是:{self.__weight / (self.__height ** 2)}'
return info
person1 = Person(name='haaland', height=1.95, weight=75)
# 函数属性变成了数据属性
print(person1.bmi) # haaland的BMI数值是:19.723865877712033
5.2 为什么要用property
将一个类的函数定义成特性以后,对象再去使用的时候obj.name,根本无法察觉自己的name是执行了一个函数然后计算出来的,这种特性的使用方式遵循了统一访问的原则
面向对象的封装有三种方式:
public 这种就是不封装,对外公开
protected 这种封装方式对外不公开,但对子类公开
private 这种方式对任何都不公开
python并没有在语法上将三种封装方式内建到class机制中,通过property方法可以实现
5.3 setter与deleter装饰器
setter与deleter装饰器一般与property结合使用
class Student:
def __init__(self, name, age, grade):
self.__name = name
self.__age = age
self.__grade = grade
@property # 将函数属性包装成数据属性
def name_tool(self):
return f'MIT_{self.__name}'
@name_tool.setter # 给以上包装后的数据属性设置值
def name_tool(self, value):
if not len(value) <= 3:
raise ValueError('名字必须是三个字及以下')
self.__name = value
@name_tool.deleter # 给以上包装后的数据属性删除值
def name_tool(self):
del self.__name
stu1 = Student(name='lavigne', age=19, grade=2)
# 获取完整名字 触发property,此时name_tool函数中的self都是对象stu1本身
print(stu1.name_tool) # MIT_lavigne
print(stu1.__dict__) # {'_Student__name': 'lavigne', '_Student__age': 19, '_Student__grade': 2}
# 修改名字 触发setter,所有self都是对象stu1本身
stu1.name_tool = 'lav'
print(stu1.__dict__) # {'_Student__name': 'lav', '_Student__age': 19, '_Student__grade': 2}
# 删除名字 触发deleter,所有self都是对象stu1本身
del stu1.name_tool
print(stu1.name_tool) # AttributeError: 'Student' object has no attribute '_Student__name'. Did you mean: '_Student__age'?
print(stu1.__dict__) # {'_Student__age': 19, '_Student__grade': 2}
5.4 使用property内置函数对外提供统一接口
class Student:
def __init__(self, name, age, grade):
self.__name = name
self.__age = age
self.__grade = grade
def __full_name(self):
return f'MIT_{self.__name}'
def __set_name(self, value):
if not len(value) <= 3:
raise ValueError('名字必须是三个字及以下')
print(self.__dict__)
self.__name = value
print(self.__dict__)
def __del_name(self):
print(self.__dict__)
del self.__name
print(self.__dict__)
# 不用装饰器,使用property内置函数,对外提供统一接口
name_tool = property(fget=__full_name, fset=__set_name, fdel=__del_name)
stu1 = Student(name='lavigne', age=20, grade=3)
# 获取完整名字 只需要调用接口即可,触发property第一个参数函数
print(stu1.name_tool) # MIT_lavigne
# 修改名字 只需要调用接口即可,触发property第二个参数函数
stu1.name_tool = 'lav'
# {'_Student__name': 'lavigne', '_Student__age': 20, '_Student__grade': 3}
# {'_Student__name': 'lav', '_Student__age': 20, '_Student__grade': 3}
# 删除名字 只需要调用接口即可,触发property第三个参数函数
del stu1.name_tool
# {'_Student__name': 'lav', '_Student__age': 20, '_Student__grade': 3}
# {'_Student__age': 20, '_Student__grade': 3}