[Python学习日记-67] 封装
简介
从封装本身的意思去理解,封装就好像是拿来一个麻袋,把小猫、小狗、小王八和小猪一起装进麻袋,然后把麻袋封上口子。照这种逻辑看,封装起来的麻袋相当于就是把里面的内容隐藏了起来,这样是不是就可以把封装与"隐藏"划上等号呢?其实这种理解是相当片面的。
如何隐藏类中的属性
在 Python 中用双下划线开头的方式将属性隐藏起来(设置成私有的属性),这里需要与前后都加双下划线的属性作区分,前后都加双下划线的属性是 Python 的内置属性,并不是隐藏属性,下面的代码就是类中把属性隐藏起来的做法
python
class A:
__x = 1
def __init__(self,name):
self.__name = name
def __foo(self):
print('run foo')
在上面的代码中,在属性前面加上__就是隐藏属性了,那它是怎么把这个属性隐藏起来的呢?我们不妨先试一下用常规方法调用一下看看是什么效果吧,如下
python
# 尝试一下用常规的调用方法是否可以调用
print(A.__x)
print(A.__foo)
# 使用实例化后的对象是否可以呢?
a = A('jove')
print(a.__name)
代码输出如下:
从输出可以看出,我们使用常规的方法来调用类当中的隐藏属性都是失败报错的,说是找不到对应的属性,这到底是怎么一回事呢?我们回想一下前面学习类和对象的知识,我们知道调用这些属性其实都是调用类和对象的命名空间中的数据而已,那我们直接来看看在命名空间中到底发生了什么,如下
python
print(A.__dict__)
print(a.__dict__)
代码输出如下:
{'module': 'main', '_A__x': 1, 'init': <function A.init at 0x000001EB109A8AE0>, '_A__foo': <function A.__foo at 0x000001EB109A99E0>, 'bar': <function A.bar at 0x000001EB109A9EE0>, 'dict': <attribute 'dict' of 'A' objects>, 'weakref': <attribute 'weakref' of 'A' objects>, 'doc': None}
{'_A__name': 'jove'}
可以看出命名空间中原本应该是 __x、__foo、__name 的属性全都加上了 _A,这个 _A 的组合到底是什么意思呢?其实这个是 _+ 该属性所属类的类名。这个时候我们是不是只要在原来调用属性的基础上加上 _类名 就可以正常调用了呢?如下
python
print(A._A__x)
print(A._A__foo)
a = A('jove')
print(a._A__name)
代码输出如下:
到这里可以得知在 Python 当中,隐藏属性其实仅仅这是一种变形操作而已,类中所有双下划线开头的名称如 __x 都会自动变形成:_类名__x 的形式。而我们可以通过这种形式来访问到隐藏属性,即这种操作并不是严格意义上的限制外部访问,仅仅只是一种语法意义上的变形。
而在类当中调用自身的函数的时候也需要这样操作吗?好像并没有,如下
python
class A:
__x = 1
def __init__(self,name):
self.__name = name
def __foo(self):
print('run foo')
def bar(self):
self.__foo()
print('from bar')
a = A('jove')
a.bar()
代码输出如下:
从输出可以看出,bar 方法在调用 __foo 的时候并没有加上 _A 就可以成功调用,这是为什么呢?这是因为 Python 在执行程序的时候 A 类中的代码除了函数代码不会执行外,其它代码都会在定义阶段执行,而且 Python 解释器也会过一遍进行词法分析,所以在类的定义阶段执行代码的时候遇到 __ 开头的属性时解释器就会把它进行变形,如下
python
class A:
__x = 1 # 类的数据属性 _A__x = 1
def __init__(self,name):
self.__name = name # self._A__name = name
def __foo(self): # 类的函数属性 def _A__foo(self):
print('run foo')
def bar(self):
self.__foo() # self._A__foo()
print('from bar')
从上面代码的注释当中可以看出为什么在类当中调用隐藏属性时并不需要加上 _类名,这是因为在类的定义阶段时就会一起进行变形 。那隐藏属性存不存在子类覆盖父类属性的情况呢?我们先看一段代码,如下
python
class Foo:
def __func(self): # def _Foo__func(self):
print('from foo')
class Bar(Foo):
def __func(self): # def _Bar__func(self):
print('from bar')
b = Bar()
b._Bar__func()
b._Foo__func()
代码输出如下:
输出的结果来看,在隐藏属性的情况下,由于在类定义中的词法分析阶段已经变形了,所以实际上两个函数属性并没有重名。
一、这种自动变形的特点
- 类中定义的 _x 只能在内部使用,例如 self.__x,引用的就是变形的结果
- 这种变形其实正是针对外部的变形,在外部是无法通过 __x 这个名字访问到该属性
- 在子类定义的 __x 不会覆盖在父类定义的 __x,因为子类中变形成了 _子类名__x,而父类中变形成了 _父类名__x,即双下滑线开头的属性在继承给子类时,子类是无法覆盖的
二、这种变形需要注意的问题
1、 这种机制并没有真正意义上限制我们从外部直接访问属性,只要知道了类名和属性名就可以拼出名字:_类名__属性名,然后就可以访问了,例如 a._A__x
2、变形的过程只在类的定义是发生一次,在定义后的赋值操作,并不会变形,如下
python
class B:
__x = 1
def __init__(self, name):
self.__name = name
B.__y = 2
print(B.__dict__)
b = B('jove')
print(b.__dict__)
b.__age = 18
print(b.__dict__)
代码输出如下:
{'module': 'main', '_B__x': 1, 'init': <function B.init at 0x000001C57BC48CC0>, 'dict': <attribute 'dict' of 'B' objects>, 'weakref': <attribute 'weakref' of 'B' objects>, 'doc': None, '__y': 2}
{'_B__name': 'jove'}
{'_B__name': 'jove', '__age': 18}
3、在继承中,父类如果不想让子类覆盖自己的方法,可以将方法定义为私有的
之前学习的集成当中,如果父类和子类当中都有同名的方法,将会先去对象命名空间找,然后再去子类当中找,最后才去父类中找,如下
python
# 正常情况
class A:
def foo(self):
print('A.foo')
def bar(self):
print('A.bar')
self.foo() # b.foo()
class B(A):
def foo(self):
print('B.foo')
b = B()
b.bar()
代码输出如下:
但是总有想要直接使用父类的情况出现,这个时候我们就需要将父类中的方法定义为私有的了,如下
python
# 把 foo 定义成私有的,即 __foo
class A: # 实现了只在自己类里面找对应的函数
def __foo(self): # 在定义时就变形为 _A__foo
print('A.foo')
def bar(self):
print('A.bar')
self.__foo() # 只会与自己所在的类为准,即调用 self._A__foo
class B(A):
def __foo(self): # _B__foo
print('B.foo')
b = B()
b.bar()
代码输出如下:
封装并不是单纯意义的隐藏
一、封装数据
其实将属性隐藏起来这并不是真正的目的。真正的目的是,将属性隐藏起来后,对外提供操作该属性的接口,然后我们可以在接口附加上对该数据操作的限制,以此完成对数据属性操作的严格控制。
python
# 一: 封装数据属性: 明确的区分内外,控制外部对隐藏的属性的操作行为
class People:
def __init__(self,name,age):
self.__name = name
self.__age = age
def tell_info(self):
print('Name:<%s> Age:<%s>' % (self.__name,self.__age))
def set_info(self,name,age):
if not isinstance(name,str): # 通过封装可以在自己的函数接口当中进行逻辑判断等操作
print('名字必须是字符串类型')
elif not isinstance(age,int):
print('年龄必须是数字类型')
else:
self.__name = name
self.__age = age
p = People('jove',18)
p.tell_info() # 获取Name Age
p.set_info('JOVE',38) # 通过定义的接口来修改Name Age
p.tell_info()
p.set_info(123,38)
p.set_info('JOVE','38')
代码输出如下:
二、封装方法
封装方法的目的是为了隔离复杂度,我们举个银行取款的例子来看一下,如下
python
# 二: 封装方法的目的 --> 隔离复杂度
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()
a = ATM()
a.withdraw() # 使用者只需要调用withdraw()就可以按照流程执行,并不需要理内部的其他流程方法
代码输出如下:
上面的代码中,取款是功能,而这个功能有很多其他功能组成:插卡、用户认证、输入取款金额、打印账单、取款;对于使用者来说,只需要知道取款这个功能即可,其余功能我们都可以隐藏起来,很明显这么做,即隔离了复杂度,同时也提升了安全性。
封装方法的其他举例:
- 你的身体没有一处不体现着封装的概念,例如你的身体把膀胱尿道等等,这些排尿的功能隐藏了起来,然后为你提供一个尿的接口就可以了(接口就是你的...),你总不能用你的意识来操控膀胱,然后控制怎么尿的吧
- 电视机本身是一个黑盒子,隐藏了所有细节,但是一定会对外提供了一堆按钮,这些按钮也正是接口的概念,所以说,封装并不是单纯意义的隐藏
- 快门就是傻瓜相机为傻瓜们提供的方法,该方法将内部复杂的照相功能都隐藏起来了
注意:在编程语言里,对外提供的接口(可理解为了一个入口),可以是函数(接口函数),但这与接口的概念还不一样的,接口是代表一组接口函数的集合体
封装与扩展性
封装在于明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者的代码;而外部使用用者只知道一个接口(函数),只要接口(函数)名、参数不变,使用者的代码永远无需改变。这就提供一个良好的合作基础------或者说,只要接口这个基础约定不变,则代码改变不足为虑。
python
# 类的设计者
class Room:
def __init__(self,name,owner,width,length,high):
self.name = name
self.owner = owner
self.__width = width
self.__length = length
self.__high = high
def tell_area(self): # 对外提供的接口,隐藏了内部的实现细节,此时我们想求的是面积
return self.__width * self.__length
# 使用者
r1 = Room('卧室','jove',20,20,20)
print(r1.tell_area()) # 使用者调用接口tell_area
代码输出如下:
当类需要扩展功能时,只需要类的设计者直接扩展就好了,使用者无须改变自己的代码,如下
python
# 类的设计者
class Room:
def __init__(self,name,owner,width,length,high):
self.name = name
self.owner = owner
self.__width = width
self.__length = length
self.__high = high
def tell_area(self): # 对外提供的接口,隐藏内部实现,此时我们想求的是体积,内部逻辑变了,只需求修该下列一行就可以很简答的实现,而且外部调用感知不到,仍然使用该方法,但是功能已经变了
return self.__width * self.__length * self.__high
# 使用者
r1 = Room('卧室','jove',20,20,20)
print(r1.tell_area()) # 使用者调用接口tell_area
代码输出入下:
对于仍然在使用 tell_area 接口的使用者来说,根本无需改动自己的代码,就可以用上新功能了。
特性(property)
下面我们以 BMI 指数为例来演示特性(property)。
BMI 指数是计算而来的,是一种用来衡量一个人是否处于健康体重范围的指标。下面是它的一些指标和计算方法:
成人的BMI数值:
过轻:低于18.5
正常:18.5-23.9
过重:24-27
肥胖:28-32
非常肥胖,高于32
体质指数(BMI)=体重(kg) ÷ 身高^2(m)
例如:70kg÷(1.75x1.75) =22.86
常规代码实现:
python
class People:
def __init__(self,name,weight,height):
self.name = name
self.weight = weight
self.height = height
p = People('jove',75,1.81)
p.bmi = p.weight / (p.height ** 2)
print(p.bmi)
代码输出如下:
使用常规方法成功实现了一个对象的 BMI 指数,但是如果多个对象需要计算的话那我们就需要写多次的 BMI 指数的计算公式赋值给各个对象的 bmi 数据属性,于是我们把这部分计算公式放到类里的一个函数属性里面,从而减少重复代码,如下
python
class People:
def __init__(self,name,weight,height):
self.name = name
self.weight = weight
self.height = height
def bmi(self):
return self.weight / (self.height ** 2)
p = People('jove',75,1.81)
print(p.bmi()) # 改变了调用方式
代码输出如下:
但是这就出现了新的问题,这样改变了使用者的调用方式,从原来的数据属性调用不用加括号,变成了函数属性的调用需要加括号了,不过好在功能的实现是没问题的。但很明显它听起来像是一个数据属性而非函数属性,如果我们将其做成一个属性,更便于理解,这个时候就到我们的 property 出场了。
使用 property 实现:
property 也是一个装饰器,它是一种特殊的属性,访问它时会执行一个功能(函数)然后返回值。使用方法如下
python
class People:
def __init__(self,name,weight,height):
self.name = name
self.weight = weight
self.height = height
@property # 可以让使用者像访问数据属性那样去访问bim(),让使用者感知不到是在调用一个函数
def bmi(self):
return self.weight / (self.height ** 2)
p = People('jove',75,1.81)
print(p.bmi) # 触发方法bmi的执行,将p自动传给self,执行后返回值作为本次引用的结果
# p.bmi = 3333 不能直接赋值,因为实际上是一个方法,会报错: AttributeError: can't set attribute
代码输出如下:
使用 property 有效地保证了属性访问的一致性。另外 property 还提供设置和删除属性的功能,如下
python
class People:
def __init__(self,name):
self.__name = name # 将属性隐藏起来
@property # 相当于伪装
def name(self):
print('------get------')
return self.__name
@name.setter # 修改 @name.setter 必须是def name 被 @property 装饰过了才能使用
def name(self,val):
print('------set------')
if not isinstance(val,str): # 在设定值之前进行类型检查
print('名字必须是字符串类型')
return
self.__name = val # 通过类型检查后,将值val存放到真实的位置self.__name
@name.deleter # 删除 同上
def name(self):
print('------del------')
print('不允许删除')
p = People('jove')
print(p.name) # 获取值 会触发 @property 下的函数
p.name = 'JOVE' # 修改值 会触发 @name.setter 下的函数name(p,'JOVE')
p.name = 123 # 触发name.setter对应的的函数name(p,123),不会进行修改,并进行提示
print(p.name)
del p.name # 删除属性 会触发 @name.deleter 下的函数name(p),不会进行删除,并进行提示
代码输出如下: