引入
面向对象三大特性: 封装, 继承, 多态, 其中最重要的特性就是封装.
什么是封装?
-
封装指的就是把数据与功能都整合到一起, 这里的”整合”就是封装的通俗说法
封装的意义
-
封装到对象或者类中的属性, 我们可以严格控制它们的访问, 分两步实现: 隐藏属性与开放接口
为什么要使用封装?
-
提高安全性 : 避免用户对类中属性或方法进行不合理的操作
-
隔离复杂度 : 你只需使用开发者提供给你的接口, 内部结构不需要知道
-
保证内部数据结构完整性 : 很好的避免了外部对内部数据的影响,提高了程序的可维护性
-
提供代码的复用性
封装的原则
-
将不需要对外提供的功能都给隐藏起来
-
把属性都隐藏, 提供公共方法对其方法
一、隐藏
隐藏介绍
-
在python中用双下划线开头的方式将属性隐藏起来(设置成私有的)
-
其实这仅仅这是一种变形操作且仅仅只在类定义阶段发生变形
-
类中所有双下划线开头的名称如
__xxx
都会在类定义时自动变形成:_[类名]__xxx
的形式
如何隐藏属性?
在属性名前加__
前缀, 就会实现一个对完隐藏属性效果(注意: 对外, 不是对内.且__值__
, 这种情况下不会隐藏, 这种情况下这个值会被当做内置属性来看待)
class Eat:
__breed = "五花肉" # 变形 : "_Eat__breed"
def __init__(self):
self.__meat = "大块" # 变形 : "_Eat__meat"
def __eat(self): # 变形 : "_Eat__eat"
print(f"吃了{self.__meat}的{self.__breed}")
def eat_meat(self):
self.__eat() # 只有在类的内部才可以通过"__eat"的形式访问到
P1 = Eat()
# 调用类提供的接口(方法)
P1.eat_meat() # 吃了大块的五花肉
# 调用隐藏的属性/方法,报错
print(P1.breed) # 报错 "AttributeError" 没有该属性
print(P1.__breed) # 报错 "AttributeError" 没有该属性
P1.__eat() # 报错 "AttributeError" 没有该属性
#调用变形后的属性/方法(不推荐这么做)
print(P1._Eat__breed) # 五花肉
print(P1._Eat__meat) # 大块
P1._Eat__eat() # 吃了大块的五花肉
隐藏属性的查找示例
class Bar:
def __f1(self): # 变形 "_Bar__f1"
print("i am Bar_f1")
def f2(self):
print("i am Bar_f2")
self.__f1() # 等同于 "self._Bar__f1"
class Foo(Bar):
def __f1(self): # 变形 "_Foo__f1"
print("i am Foo_f1")
Foo().f2()
'''输出
i am Bar_f2
i am Bar_f1
'''
重要步骤 : 当找到
self.__f1()
的时候, 其实是找到self._Bar__f1
, 于是先去自己(Foo)里面去找, 结果找不到自己里面的, 然后到父类中去找, 最终打印 “i am Bar_f1”
针对这种变形需要注意的问题:
-
在类外部: 无法直接访问双下划线开头的属性, 但是知道了类名和属性名就可以拼接出名字:
_类名__属性名
, 然后就可以访问了. -
在类内部: 可以直接访问双下划线开头的属性.(提示: 这种隐藏对外不对内)
-
变形操作只在类定义阶段检测语法阶段发生一次, 在类定义之后的赋值操作都不会发生变形.
示例:
-
注意1: 在类外部: 无法直接访问双下划线开头的属性, 但是知道了类名和属性名就可以拼接出名字:
_类名__属性名
, 然后就可以访问了.
class Foo:
__x = 1 # _Foo__x
def __f1(self): # _Foo__f1
print('from __f1')
print(Foo.__dict__) # {..., '_Foo__x': 1, '_Foo__f1': <function Foo.__f1 at 0x000001CF201FAA60>}
# print(Foo.__x) # AttributeError: type object 'Foo' has no attribute '__x'
print(Foo._Foo__x) # 1
# print(Foo.__f1) # AttributeError: type object 'Foo' has no attribute '__f1'
print(Foo._Foo__f1) # <function Foo.__f1 at 0x000001C9F0A08A60>
-
在类内部: 可以直接访问双下划线开头的属性.(提示: 这种隐藏对外不对内)
class Bar: __x = 1 # _Bar__x def __f1(self): # _Bar__f1 print('from __f1') def f2(self): print(self.__x) # print(self._Bar__x) print(self.__f1) # print(self._Bar__f1) # print(Bar.__x) # AttributeError: type object 'Bar' has no attribute '__x' # print(Bar.__f1) # AttributeError: type object 'Bar' has no attribute '__f1' obj = Bar() obj.f2()
-
注意3: 变形操作只在类定义阶段发生一次, 在类定义之后的赋值操作都不会发生变形.
class Car: __x = 1 # _Car__x def __f1(self): # _Car__f1 print('from __f1') def f2(self): print(self.__x) # print(self._Car__x) print(self.__f1) # print(self._Car__f1) Foo.__y = 3 print(Foo.__dict__) print(Foo.__y) # 3
二、 开放接口: 隐藏并不是目的, 定义属性就是为了使用
1. 隐藏数据接口
-
将数据隐藏起来了就限制了类外部对数据的直接操作, 然后类内应该提供相应的接口来允许类外部间接的操作数据, 接口之上可以附加额外的逻辑来对数据的操作进行严格的控制.
-
好处: 作为接口的设计者, 可以在接口中附加任意的控制逻辑, 控制使用者对该属性的操作.(注意: 控制的是已经定义好的属性)
class Student: def __init__(self, name, age): self.__name = name self.__age = age def tell_info(self): print('控制逻辑控制使用者访问的方式'.center(50, '=')) print(f'姓名:{self.__name} 年龄:{self.__age}岁') def set_info(self, name, age): if not isinstance(name, str): raise TypeError("控制逻辑控制使用者修改姓名的方式:名字必须执行字符串类型") if not isinstance(age, int): raise TypeError("控制逻辑控制使用者修改年龄的方式:年龄必须执行整数") self.__name = name self.__age = age print("修改成功!") stu_obj = Student('egon', 18) # print(stu_obj.name) # 无法直接用名字属性 stu_obj.tell_info() # stu_obj.set_info(1, 19) # TypeError: 控制逻辑控制使用者修改姓名的方式:名字必须执行字符串类型 # stu_obj.set_info('EGON', '99') # TypeError: 控制逻辑控制使用者修改年龄的方式:年龄必须执行整数 stu_obj.set_info('EGON', 99)
2.隐藏函数接口
-
好处: 隔离复杂度
-
ATM程序的取款功能,该功能有很多其他功能组成,比如插卡、身份认证、输入金额、打印小票、取钱等,而对使用者来说,只需要开发取款这个功能接口即可,其余功能我们都可以隐藏起来
class ATM: def __card(self): # 插卡 print('插卡') def __auth(self): # 身份认证 print('用户认证') def __input(self): # 输入金额 print('输入取款金额') def __print_bill(self): # 打印小票 print('打印账单') def __take_money(self): # 取钱 print('取款') def withdraw(self): # 取款功能 self.__card() self.__auth() self.__input() self.__print_bill() self.__take_money() obj = ATM() obj.withdraw()
三、总结隐藏属性和开放接口
-
二者本质: 明确的区分内外, 类内部可以修改封装内的东西而不影响外部调用者的代码
-
类外部只需要拿到一个接口, 只要接口名不变, 参数不变, 则无论设计者如何改变内部实现代码, 使用者无需改变代码. 这就提供了一个良好的合作基础, 只要接口这个基础约定不变, 则代码的修改不足为虑
四、封装的两个层面
1.第一层面(公有 : public)
-
public : 公有属性的类变量和类函数,在类的外部、类内部以及子类中,都可以正常访问
🍔我们来制作一个机器人,分别要考虑制作不同部件,每个方法都得调用一下
class Rebot:
def Head(self):
print("制作头")
def Hand(self):
print("制作手")
def Foot(self):
print("制作脚")
def Body(self):
print("制作躯干")
def Fit(self):
print("机器人合体")
P1 = Rebot()
🍔没有进行封装,我们需要进行每一个部件的调用
P1.Hand() # 制作手
P1.Head() # 制作头
P1.Foot() # 制作脚
P1.Body() # 制作躯干
P1.Fit() # 机器人合体
🍔当我们进行封装,提供给用户一个"Auto"接口(方法)
def Auto(self): # 提示:这个Auto方法是在类里面的,方便讲解我才放在这个位置
self.Hand()
self.Head()
self.Foot()
self.Body()
self.Fit()
🍔用户只需要调用"Auto"这个方法就可以一步完成上面的所有步骤
P1.Auto()
'''输出
制作手
制作头
制作脚
制作躯干
机器人合体
'''
如果这样的话, 使用者还是可以调用里面机器人的制作细节, 如果我们不想让使用者使用到那些方法, 我们就可以将细节部分给隐藏起来, 只提供一个”Auto”方法,👇👇
2.第二层面 (私有 : private)
-
private:
私有属性的类变量和类函数,只有在类的内部使用,类的外部以及子类都无法使用
-
如果类中的变量和函数,其名称以双下划线
__
开头,则该变量或函数为私有的 -
如果以单下划线
_
开头的属性和方法,我们约定俗成的视其为私有, 就是上面隐藏里说到的变形后的结果, 虽然能正常调用, 但不建议这么做
-
class Rebot:
def __Head(self): # 变形 : "_Rebot__Head"
print("制作头")
def __Hand(self): # 变形 : "_Rebot__Hand"
print("制作手")
def __Foot(self): # 变形 : "_Rebot__Foot"
print("制作脚")
def __Body(self): # 变形 : "_Rebot__Body"
print("制作躯干")
def __Fit(self): # 变形 : "_Rebot__Fit"
print("机器人合体")
def Auto(self): # 提供给使用者的接口(方法)
self.__Hand()
self.__Head()
self.__Foot()
self.__Body()
self.__Fit()
P1 = Rebot()
P1.Auto()
'''输出
制作手
制作头
制作脚
制作躯干
机器人合体
'''
# P1.__Hand() # 报错 : "AttributeError" 没有该属性
# P1.__Head() # 报错 : "AttributeError" 没有该属性
🍔如果特别想调用, 可以这么调用, 但非常不建议
P1._Rebot__Hand() # 制作头
P1._Rebot__Head() # 制作手
如此就实现了给使用者只看到能让他用的东西
不给用户直接使用的东西隐藏起来
五、示例
-
定义一个人类, 然后实例出一个对象, 该对象的名字不能以 “sb” 开头
-
把 “name” 这个属性隐藏起来, 外部访问不到
-
“name” 隐藏起来后, 对象自己该如何获取这个属性? 提供一个打印名字的方法
-
定义一个修改名字的方法
class Person:
def __init__(self,name):
if not name.startswith("sb"):
self.__name = name
else:
print("名字不能以'sb'开头")
def print_name(self): # 打印名字的方法
print(self.__name)
def change_name(self,name): # 修改名字的方法
if not name.startswith("sb"):
self.__name = name
print("修改成功")
else:
print("名字不能以'sb'开头")
P1 = Person("sb_hahah") # 名字不能以'sb'开头
P2 = Person("shawn")
# print(P2.__name) # 报错 : "AttributeError" 没有该属性
# print(P2.name) # 报错 : "AttributeError" 没有该属性
P2.print_name() # shawn
P2.change_name("sb_kkkk") # 名字不能以'sb'开头
P2.change_name("xing") # 修改成功
P2.print_name() # xing
如此一来保证了数据的安全
补充 :
-
模块中也可以使用隐藏属性, 在属性前加 ““, 例 : “name” (单双下划线都可以)
-
希望只在模块内部使用, 而外部无法使用
-
针对的是
from xxx import *
方法中的*
星号 -
如果想使用, 可以以
from xxx impoer _name
这种方式进行导入
六、特性property
1.什么是 property 特性
-
property 装饰器可以用于装饰类里面的方法, 让其伪装成一个数据属性, 也就是在调用的时候可以不用加括号
-
提示: property是用类实现的装饰器, 所以说只要是可调用对象都可以作为装饰器. 类, 函数都是可调用对象因此它们都可以被当做装饰器.(可调用对象: 可以通俗的理解为加括号就能进行调用)
-
将类中的函数”伪装成”对象的数据属性, 对象在访问该特殊属性时会触发功能的执行, 然后将返回值作为本次访问的结果, 然后对象通过该返回值就可以直接进行对象的操作模式.
-
注意: 被修饰对象需要指定返回值, 明确本次访问的结果.
2.property作用
-
当你某个功能在逻辑层面上是个应该直接通过”对象.属性名”直接访问的, 且被访问的对象是可能更具对象的某些数据属性动态的变化的, 这个时候我们应该使用property.
3.property还提供了伪装设置和删除属性的功能:
-
设置: @property对象装饰完毕后拿到的返回值.setter
-
删除: @property对象装饰完毕后拿到的返回值.deleter
4.为什么要有property 特性
-
将类的一个函数(方法)定义成 property 特性之后, 不加括号的去使用
[对象].[方法]
的时候, 我们无法察觉自己是执行了一个函数(方法), 这种特性的使用方式遵循了统一访问的原则
5.property 属性的定义和调用的注意点
-
定义时 : 在实例方法的上方添加
@property
装饰器, 并且仅有一个self
参数 -
调用时 : 无需加括号
6.property 属性的使用两种方法
第一种 : 使用 property( )
函数 (古老用法, 了解即可)
class Person:
def __init__(self):
self.__name= None
#这是setter方法
def set_name(self,name):
print("设置了名字")
self.__name=name
#这是getter方法
def get_name(self):
print("获取了名字")
return self.__name
# 这是deleter方法
def del_name(self):
print("删除了名字")
del self.__name
name=property(get_name,set_name,del_name) # 这里存放的是"name"的所有操作
P1 = Person()
P1.name = 'Shawn' # 设置了名字 (直接赋值,等同于 P1.set_name('Shawn'))
n = P1.name # 获取了名字 (直接获取数据 ,等同于 P1.get_name())
print(n) # Shawn
del P1.name # 删除了名字 (删除属性)
print(P1.name) # 属性被删除了, 再次查看报错 : "AttributeError" 没有该属性
第二种 : 使用 @property
装饰器 (新方法)
-
第二种方法 getter 必须写在 setter 和 deleter 的前面 (说是这么说, 但是实验了一下可以写在后面)
-
@property 装饰器必须写在 三者最前面, 并且三者都必须使用被
@property
修饰的同一个函数名, 否则报错
class Person:
def __init__(self):
self.__name= None
def name(self): # 获取
print("设置了名字")
return self.__name
.setter
def name(self,name): # 设置
print("设置名字成功")
self.__name=name
.deleter # 删除
def name(self):
print("删除了名字")
del self.__name
.getter # 获取
def name(self):
print("查看了名字并+'哈哈'")
return self.__name + "哈哈"
P1 = Person()
P1.name = "shawn" # 设置名字成功
n = P1.name # 查看了名字并+'哈哈'
print(n) # shawn哈哈
由上面的例子实验, 我们可以发现一个问题:
@property
下修饰的功能其实是与@name.getter
的功能重复的, 于是我们可以省略@name.getter
不用写, 其实就是@property
替代了@name.getter
注意点
-
经典类中的属性只有一种访问方式,其对应被
@property
修饰的方法 -
新式类中的属性有三种访问方式,并分别对应了三个被
@property
、@方法名.setter
、@方法名.deleter
修饰的方法 (获取、修改、删除)
总结
-
定义property属性共有两种方式,分别是装饰器和类属性,而装饰器方式针对经典类和新式类又有所不同。
-
通过使用property属性,能够简化调用者在获取数据的流程, 就是重新实现一个属性的设置和读取方法
7、property 特性的应用
案例一: BMI指数应该作为数据属性的访问方式被调用
'''
1.同过计算BMI指数(体质指数),利用身高与体重的比例来衡量一个人是否过瘦或过肥
2.公式: BMI = 体重(kg) / (身高(m) ** 2)
3.范围:(过轻:低于18.5), (正常:18.5-23.9), (过重:24-27), (肥胖:28-32), (非常肥胖, 高于32)
'''
class People:
def __init__(self, name, weight, height):
self.name = name
self.weight = weight
self.height = height
# self.bmi = self.weight / (self.height ** 2) # 这里不能这样初始化, 因为对人obj对象来说, 它的bmi是动态的变化的, 我们一旦初始化以后, bmi就被固定死了.
print(property) # <class 'property'>
def bmi(self): # bmi听起来更像是一个数据属性,而非功能
return self.weight / (self.height ** 2)
obj = People('MY', 65, 1.75)
print(obj.bmi)
# 当我们长高了, 这个时候我们再次访问我们的bmi值应该也要随着变化.
obj.height = 1.78
print(obj.bmi)
案例二: 伪装对象查看,修改,删除数据属性的方式
# 案例二:
class People:
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, value):
if type(value) is not str:
print("请输入字符串!")
return
self.__name = value
def del_name(self):
print("对不起, 不能删除!")
# del self.__name
# 注意: 这里的name是提供使用者访问的方式. 比如使用者想要查看, 那么使用者就可以使用"对象.name"这种方式. 这个名字是要和使用者调用时所对应.
name = property(get_name, set_name, del_name)
'''
底层原理:
对象调用"obj.name"时property会触发get_name执行, 把对象本身传入, 并执行该方法体代码
对象调用"obj.name = '值'"时property会触发set_name执行, 把对象本身及"值"按照顺序窜给set_name, 执行该方法体代码
对象调用"del name"时property会触发del_name执行, 把对象本身传入, 并执行该方法体代码
'''
obj = People('egon')
# print(obj.get_name())
#
# obj.set_name("EGON")
# print(obj.get_name())
#
# obj.del_name()
# 人正常的思维逻辑
print(obj.name)
obj.set_name(1)
obj.set_name("EGON")
print(obj.name)
# 案例二另一种实现方式:
class People:
def __init__(self, name):
self.__name = name
# name = property(name) # 这里返回的name给下面setter或deleter操作(注意: 这里要提供返回值, 因为伪装"对象.数据属性"这种查看方式, 既然查看, 那么必须就要提供返回值)
def name(self):
return self.__name
# 注意: 下面的方法一定要基于property装饰之后, 才能使用. 因为需要拿到上面property(name)调用后的返回结果name进行setter或deleter操作
.setter
def name(self, value):
if type(value) is not str:
print("请输入字符串!")
return
self.__name = value
.deleter
def name(self):
print("对不起, 不能删除!")
# del self.__name
obj = People('egon')
# 人正常的思维逻辑
print(obj.name)
obj.name = 18
print(obj.name)
del obj.name