Loading

Python面向对象三大特性之封装

一、引入

面向对象编程有三大特性:封装、继承、多态;其中最重要的一个特性就是封装。

封装指的就是把数据与功能都整合到一起,听起来是不是很熟悉,没错,我们之前所说的”整合“二字其实就是封装的通俗说法。除此之外,针对封装到对象或者类中的属性,我们还可以严格控制对它们的访问,分两步实现:隐藏与开放接口。

img

二、隐藏属性

Python的隐藏机制采用双下划线开头的方式将属性隐藏起来(设置成私有的),但其实这仅仅只是一种变形操作,类中所有双下滑线开头的属性都会在类定义阶段检测语法时自动变成“_类名__属性名”的形式:

class Foo:
    __N = 0  # 变形为_Foo__N

    def __init__(self):  # 定义函数时,会检测函数语法,所以__开头的属性会变形
        self.__x = 10  # 变形为self._Foo__x

    def __f1(self):  # 变形为_Foo__f1
        print('__f1 run')

    def f2(self):  # 定义函数时,会检测函数语法,所以__开头的方法也会变形
        self.__f1()  # 变形为self._Foo__f1()


print(Foo.__N)  # 报错AttributeError:类Foo没有属性__N

obj = Foo()
print(obj.__x)  # 报错AttributeError:对象obj没有属性__x

这种变形需要注意的问题是:

1、在类外部无法直接访问双下划线开头的属性,但知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如:Foo._Foo__N,所以说这种操作并没有严格意义上地限制外部访问,仅仅只是一种语法意义上的变形。

>>> Foo.__dict__
{..., '_Foo__N': 0, ...}
 
>>> obj.__dict__
{'_Foo__x': 10}
 
>>> Foo._Foo__N
0
>>> obj._Foo__x
10
>>> obj._Foo__N
0

2、在类内部是可以直接访问双下滑线开头的属性的(对外不对内),比如self.__f1(),因为在类定义阶段类内部双下滑线开头的属性统一发生了变形:

>>> obj.f2()
__f1 run

3、变形操作只在类定义阶段发生一次,在类定义之后的赋值操作,不会变形:

>>> Foo.__M = 100
>>> Foo.__dict__
{..., '__M': 100, ...}
>>> Foo.__M
100
 
>>> obj.__y = 20
>>> obj.__dict__
{'__y': 20, '_Foo__x': 10}
>>> obj.__y
20

三、开放接口

定义属性就是为了使用,所以隐藏并不是目的。

3.1 隐藏数据属性

将数据隐藏起来就限制了类外部对数据的直接操作,然后类内应该提供相应的接口来允许外部间接地操作数据,接口之上可以附加额外的逻辑来对数据的操作进行严格的控制:

class Teacher:
    def __init__(self, name, age):  # 将名字和年纪都隐藏起来
        self.__name = name
        self.__age = age

    def tell_info(self):  # 对外提供访问老师信息的接口
        print('姓名:%s,年龄:%s' % (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

t = Teacher('lili', 18)
t.set_info('LiLi', '19')

可以看到代码的参数传递中,年龄不为整形,会抛出如下异常:

Traceback (most recent call last):
  File "C:/Program Files/PycharmProjects/Python面向对象/面向对象相关/三大特性之封装.py", line 19, in <module>
    t.set_info('LiLi', '19')
  File "C:/Program Files/PycharmProjects/Python面向对象/面向对象相关/三大特性之封装.py", line 13, in set_info
    raise TypeError('年龄必须是整型')
TypeError: 年龄必须是整型

必须符合名字为字符串类型,年龄为整形的规则,才可以正常设置:

t.set_info('LiLi', 19)  # 名字为字符串类型,年龄为整形,可以正常设置
t.tell_info()  # 姓名:LiLi,年龄:19

3.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()

总结:

隐藏属性与开放接口,本质就是为了明确地区分内外,类内部可以修改封装内的东西而不影响外部调用者的代码;而类外部只需拿到一个接口,只要接口名、参数不变,则无论设计者如何改变内部实现代码,使用者均无需改变代码。这就提供一个良好的合作基础,只要接口这个基础约定不变,则代码的修改不足为虑。

四、property的使用

BMI指数是用来衡量一个人的体重与身高对健康影响的一个指标,计算公式为:

体质指数(BMI)= 体重(kg)÷身高^2(m)
EX:70kg ÷(1.75×1.75)= 22.86

身高或体重是不断变化的,因而每次想查看BMI值都需要通过计算才能得到,但很明显BMI听起来更像是一个特征而非功能,为此Python专门提供了一个装饰器property,可以将类中的函数“伪装成”对象的数据属性,对象在访问该特殊属性时会触发功能的执行,然后将返回值作为本次访问的结果,例如:

class People:
    def __init__(self, name, weight, height):
        self.name = name
        self.weight = weight
        self.height = height

    @property
    def bmi(self):
        return self.weight / (self.height ** 2)


obj = People('lili', 75, 1.85)
print(obj.bmi)  # 打印结果: 21.913805697589478
"""触发方法bmi的执行,将obj自动传给self,执行后返回值作为本次引用的结果"""

使用property有效地保证了属性访问的一致性。另外property还提供设置和删除属性的功能,如:

class Foo:
    def __init__(self, val):
        self.__NAME = val  # 将属性隐藏起来

    @property
    def name(self):
        return self.__NAME

    @name.setter
    def name(self, value):
        if not isinstance(value, str):  # 在设定值之前进行类型检查
            raise TypeError('%s must be str' % value)
        self.__NAME = value  # 通过类型检查后,将值value存放到真实的位置self.__NAME

    @name.deleter
    def name(self):
        raise PermissionError('Can not delete')

        
f = Foo('lili')
print(f.name)  # 打印结果: lili

f.name = 'LiLi'  # 触发name.setter装饰器对应的函数name(f, 'LiLi')
f.name = 123  # 触发name.setter对应的的函数name(f, 123),抛出异常TypeError: 123 must be str
del f.name  # 触发name.deleter对应的函数name(f),抛出异常PermissionError: Can not delete

上述代码中Foo类的写法还可以使用另外一种形式,即不使用装饰器语法糖的语法格式,如下:

class Foo:
    def __init__(self, val):
        self.__NAME = val  # 将属性隐藏起来

    def name(self):
        return self.__NAME

    def set_name(self, value):
        if not isinstance(value, str):  # 在设定值之前进行类型检查
            raise TypeError('%s must be str' % value)
        self.__NAME = value  # 通过类型检查后,将值value存放到真实的位置self.__NAME

    def del_name(self):
        raise PermissionError('Can not delete')

    name = property(name, set_name, del_name)  # 括号内对应函数的名字需按照读取,修改,删除这个顺序来放置

后续的使用方式与之前一样,没有任何区别,该方式仅作为了解即可,这里还是更推荐第一种写法。

posted @ 2021-12-06 22:58  JZEason  阅读(90)  评论(0编辑  收藏  举报