🍖类的封装
引入
先来举个例子 :
当你摁下电脑开机键, 你不需要考虑主板是怎么通电的、 磁盘是怎么转动的、系统的信息是怎么加载的、里面的一系列化学或者物理变化是怎么样的,你面对的就是一个开关键, 摁下它, 电脑就开起来了.
又比如:
一个玩具制造厂, 制作一个机器人, 我们需要去考虑机器人的每一个细节: 手臂、腿、头、躯干等等,制作简单的玩具不要紧, 如果设计的玩具非常的复杂, 并且这工厂还有很多其他类型的玩具生产(小熊, 佩奇, 芭比公主,飞机模型等等)
上面我们使用面向过程的思想去设计, 在程序中就会让我们的代码很长, 很杂乱
于是工厂听取了小王的建议, 引进了一台制造机器人玩具的机器, 我们只需要摁下机器的开关, 它就会自动制造机器人
这个时候我们不再去关注是先制造手还是脚还是躯干这些细节, 我们面对的就只是一台机器, 这机器就是一个对象
原理 : 以前我们需要关注制作玩具的每个步骤, 现在我们将步骤都封装到一个机器里面, 留给我们的只剩一个开关, 我们只需要摁开关就可以生产玩具了, 这就是面向对象的三大特性之一 : 封装, 下面我们将详细展开介绍
一.封装
1.什么封装
- 封装是Python中面向对象的三大特性之一
- 封装就是隐藏对象的属性和实现细节, 仅对外提供公共访问方式
2.为什么使用封装
- 提高安全性 : 避免用户对类中属性或方法进行不合理的操作
- 隔离复杂度 : 就以上面的例子为例, 你只需要机器提供给你的开关, 内部结构不需要知道
- 保证内部数据结构完整性 : 很好的避免了外部对内部数据的影响,提高了程序的可维护性
- 提供代码的复用性
3.封装的原则
- 将不需要对外提供的功能都给隐藏起来
- 把属性都隐藏, 提供公共访问方法
二.隐藏
在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.第一层面(公有 : 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
这种方式进行导入