类的封装

引入

面向对象三大特性: 封装, 继承, 多态, 其中最重要的特性就是封装.

什么是封装?

  • 封装指的就是把数据与功能都整合到一起, 这里的”整合”就是封装的通俗说法

封装的意义

  • 封装到对象或者类中的属性, 我们可以严格控制它们的访问, 分两步实现: 隐藏属性与开放接口

为什么要使用封装?

  • 提高安全性 : 避免用户对类中属性或方法进行不合理的操作

  • 隔离复杂度 : 你只需使用开发者提供给你的接口, 内部结构不需要知道

  • 保证内部数据结构完整性 : 很好的避免了外部对内部数据的影响,提高了程序的可维护性

  • 提供代码的复用性

封装的原则

  • 将不需要对外提供的功能都给隐藏起来

  • 把属性都隐藏, 提供公共方法对其方法

一、隐藏

隐藏介绍

  • 在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()            # shawnP2.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
​
    @property 
    def name(self):       # 获取
        print("设置了名字")
        return self.__name
​
    @name.setter
    def name(self,name): # 设置
        print("设置名字成功")
        self.__name=name
​
    @name.deleter        # 删除
    def name(self):
        print("删除了名字")
        del self.__name
​
    @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'>
​
    @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
​
    @property  # name = property(name) # 这里返回的name给下面setter或deleter操作(注意: 这里要提供返回值, 因为伪装"对象.数据属性"这种查看方式, 既然查看, 那么必须就要提供返回值)
    def name(self):
        return self.__name
​
    # 注意: 下面的方法一定要基于property装饰之后, 才能使用. 因为需要拿到上面property(name)调用后的返回结果name进行setter或deleter操作
    @name.setter
    def name(self, value):
        if type(value) is not str:
            print("请输入字符串!")
            return
        self.__name = value
​
    @name.deleter
    def name(self):
        print("对不起, 不能删除!")
        # del self.__name
​
​
obj = People('egon')
​
# 人正常的思维逻辑
print(obj.name)
​
obj.name = 18
print(obj.name)
​
del obj.name