当前位置:
首页 > Python基础教程 >
-
【2020Python修炼记】面向对象编程——封装
一、引入
面向对象编程有三大特性:封装、继承、多态,其中最重要的一个特性就是封装。
封装指的就是把数据与功能都整合到一起,之前所说的”整合“二字其实就是封装的通俗说法。
除此之外,针对封装到对象或者类中的属性,我们还可以严格控制对它们的访问,分两步实现:隐藏与开放接口
二、隐藏属性
1、为何要隐藏属性
-
隐藏数据属性,将数据隐藏起来就限制了类外部对数据的直接操作。
-
隐藏函数属性,目的是隔离程序的复杂度 。
2、如何隐藏属性?
python的class机制采用双下划线开头的方式将属性隐藏起来(设置成私有的)。
但其实这仅仅只是一种变形操作,类中所有双下滑线开头的属性,
都会在类定义阶段、检测语法时自动变成“ _类名__属性名 ”的形式:
## 定义类 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(obbj.__x) # 报错AttributeError:对象obj没有属性__x
3、需要注意的几点问题
(1)在类外部无法直接访问双下滑线开头的属性,但知道了类名和属性名就可以拼出名字: _类名__属性,然后就可以访问了,如foo._a__n所以说这种操作并没有严格意义上地限制外部访问,仅仅只是一种语法意义上的变形。
class Foo: __x = 1 # _Foo__x def __f1(self): # _Foo__f1 print('from test') print(Foo.__dict__) # 查看类Foo的属性 print(Foo._Foo__x) # 访问隐藏的数据属性 __x print(Foo._Foo__f1) #访问隐藏的函数属性 __f1
(2) 这种隐藏对外不对内,即在类内部是可以直接访问双下滑线开头的属性的,比如self.__f1(),
因为在类定义阶段,类内部双下滑线开头的属性统一发生了变形。
class Foo: __x = 1 # _Foo__x = 1 def __f1(self): # _Foo__f1 print('from test') def f2(self): print(self.__x) # print(self._Foo__x) print(self.__f1) # print(self._Foo__f1) ,得到 _Foo__f1 的内存地址 print(self.__f1()) # print(self._Foo__f1()),打印函数 _Foo__f1的返回结果,先是打印输出 from test,再打印返回值,若未赋值,默认为None # print(Foo.__x) # AttributeError: type object 'Foo' has no attribute '__x' # print(Foo.__f1) # AttributeError: type object 'Foo' has no attribute '__f1' obj=Foo() obj.f2() # 输出结果: 1 <bound method Foo.__f1 of <__main__.Foo object at 0x000001B109AF9760>> from test None #为何返回一个 None 值? print(self.__f1()) # print(self._Foo__f1()),打印函数 _Foo__f1的返回结果,先是打印输出 from test,再打印返回值,若未赋值,默认为None
#为何返回一个 None 值?
折腾了一下子---View Code
(3)变形操作只在类定义阶段发生一次,在类定义完之后的赋值操作,即之后定义的__开头的属性,都不会变形。
class Foo: __x = 1 # _Foo__x = 1 def __f1(self): # _Foo__f1 print('from test') def f2(self): print(self.__x) # print(self._Foo__x) print(self.__f1) # print(self._Foo__f1) obj=Foo() obj.f2() Foo.__y=3 # 在定义完类Foo之后,定义的__y,不会变形,不属于被隐藏的属性 print(Foo.__dict__) print(Foo.__y) 输出结果: 1 # print(self.__x) <bound method Foo.__f1 of <__main__.Foo object at 0x000001FB91269760>> # print(self.__f1),返回 __f1的内存地址 {'__module__': '__main__', '_Foo__x': 1, '_Foo__f1': <function Foo.__f1 at 0x000001FB912DA1F0>, 'f2': <function Foo.f2 at 0x000001FB912DA310>, '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None, '__y': 3} # print(Foo.__dict__),可以看到,'__y': 3,即在定义完类Foo之后,定义的__y,没有变形,不属于被隐藏的属性 3 # print(Foo.__y)
三、开放接口
定义属性,就是为了使用,所以隐藏并不是目的
1、隐藏数据接口
(1)隐藏数据属性的一个栗子
class Foo: __x = 1 # _Foo__x = 1 def __init__(self,name,age): self.__name=name self.__age=age # obj=Foo() # 传参必须满足函数定义设置的参数个数,否则报错 TypeError: __init__() missing 2 required positional arguments: 'name' and 'age' obj=Foo('egon',18) print(obj.__dict__) # 输出结果:{'_Foo__name': 'egon', '_Foo__age': 18} # print(obj.name,obj.age) # __name 和 __age 属性均为隐藏的数据属性,是无法直接访问的, 输出结果报错: AttributeError: 'Foo' object has no attribute 'name'
(2)为何要隐藏数据属性,并开放接口
隐藏数据属性,将数据隐藏起来就限制了类外部对数据的直接操作。
然后类内应该提供相应的接口来允许类外部间接地操作数据,接口之上可以附加额外的逻辑来对数据的操作进行严格地控制:
# 设计者:egon class People: def __init__(self, name): self.__name = name # __name 为隐藏的数据属性 def get_name(self): # 通过该接口就可以间接地访问到名字属性 print('小垃圾,不让看') print(self.__name) def set_name(self,val): if type(val) is not str: print('小垃圾,必须传字符串类型') return self.__name=val # 使用者:王鹏 obj = People('egon') # People('egon'),self.__name = name最先初始化name,将值egon的内存地址传给__name # print(obj.name) # 无法直接用名字属性 AttributeError: 'People' object has no attribute 'name' obj.set_name('EGON') # 调用函数set_name,将初始化的值 egon 改为了 EGON,将 即此时__name保存的内存地址指向 EGON obj.get_name() #再调用函数get_name, print(self.__name),打印得到的值就是 EGON # 输出结果: # 小垃圾,不让看 # EGON obj.set_name(123123123) # 修改名字失败,返回原值 obj.get_name() # 输出结果: # 小垃圾,必须传字符串类型 # 小垃圾,不让看 # egon
没事瞎折腾版本:
两个瞎折腾的栗子--View Code
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()
3、总结—隐藏属性与开放接口
隐藏属性与开放接口,本质就是为了明确地区分内外,类内部可以修改封装内的东西,而不影响外部调用者的代码;
而类外部只需拿到一个接口,只要接口名、参数不变,则无论设计者如何改变内部实现代码,使用者均无需改变代码。
这就提供一个良好的合作基础,只要接口这个基础约定不变,则代码的修改不足为虑。
四、装饰器property
Python专门提供了一个装饰器property,可以将类中的函数“伪装成”对象的数据属性,
对象在访问该特殊属性时会触发功能的执行,然后将返回值作为本次访问的结果。
栗子如下:bmi 指数是用来衡量一个人的体重与身高对健康影响的一个指标,计算公式为——
体质指数(BMI)=体重(kg)÷身高^2(m) EX:70kg÷(1.75×1.75)=22.86
>>> 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) >>> obj.bmi #触发方法bmi的执行,将obj自动传给self,执行后返回值作为本次引用的结果 21.913805697589478
使用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') >>> f.name lili >>> f.name='LiLi' #触发name.setter装饰器对应的函数name(f,’Egon') >>> f.name=123 #触发name.setter对应的的函数name(f,123),抛出异常TypeError >>> del f.name #触发name.deleter对应的函数name(f),抛出异常PermissionError
栏目列表
最新更新
nodejs爬虫
Python正则表达式完全指南
爬取豆瓣Top250图书数据
shp 地图文件批量添加字段
爬虫小试牛刀(爬取学校通知公告)
【python基础】函数-初识函数
【python基础】函数-返回值
HTTP请求:requests模块基础使用必知必会
Python初学者友好丨详解参数传递类型
如何有效管理爬虫流量?
SQL SERVER中递归
2个场景实例讲解GaussDB(DWS)基表统计信息估
常用的 SQL Server 关键字及其含义
动手分析SQL Server中的事务中使用的锁
openGauss内核分析:SQL by pass & 经典执行
一招教你如何高效批量导入与更新数据
天天写SQL,这些神奇的特性你知道吗?
openGauss内核分析:执行计划生成
[IM002]Navicat ODBC驱动器管理器 未发现数据
初入Sql Server 之 存储过程的简单使用
这是目前我见过最好的跨域解决方案!
减少回流与重绘
减少回流与重绘
如何使用KrpanoToolJS在浏览器切图
performance.now() 与 Date.now() 对比
一款纯 JS 实现的轻量化图片编辑器
关于开发 VS Code 插件遇到的 workbench.scm.
前端设计模式——观察者模式
前端设计模式——中介者模式
创建型-原型模式