面向对象三大特性之封装
封装
不绕弯子,面向对象的编程思想有三大特性,分别是:封装、继承、多态。
其中,封装是面向对象的最基本的概念。我们之前提到,对象是一个容器,装的是数据和功能。这本身就是一种封装思想,将数据和功能封装到一个对象中。
对象封装了数据和功能,那对象(类)就有了管理数据和功能的权利,即对象或者类可以把数据和功能给隐藏起来或者开放给使用者,限制对象(类)的使用者权利,或者控制/规范使用者的使用方式。
隐藏属性
如果类的设计者不想使用者直接访问到属性,就可以将属性给隐藏起来。属性分为数据属性和功能属性,即这两类属性都可以被隐藏。
隐藏属性可以隐藏类中的共有属性和对象的私有属性。
# 使用双下划线开头命名的属性将会被隐藏
class Foo:
__x = 1 # 隐藏类中的共有数据属性
def __init__(self, name, age):
self.__name = name # 隐藏对象的私有属性
self.age = age
def __f1(self): # 隐藏类内定义的函数属性
print('from test')
def f2(self): # f2没有被隐藏,可以被访问到
print(self.__x)
print(self.__f1)
############################################################
print(Foo.__x) # 访问不到
print(Foo.__f1) # 访问不到
print(Foo.f2) # 可以访问到
print(Foo._Foo__x, Foo._Foo__f1) # 可以访问
obj = Foo('xliu', 18) # 实例化对象obj
print(obj.__name) # 访问不到
print(obj.age) # 可以访问到
Foo.__y = 2 # 增加类Foo的数据属性
print(Foo.__y) # 可以访问到
注意点:
# 1 在类外部无法直接访问双下滑线开头的属性,在类内部可以访问到
# 2 在类定义阶段,双下划先开头的属性会发生变形,变为 _Foo__x, _Foo__f1, _Foo__name, 所以在在类外无法直接通过过 .__x 的方式访问。但是可以通过变形后的 _Foo__x访问。但这是没有意义的。所以说这种操作并没有严格意义上地限制外部访问,仅仅只是一种语法意义上的变形。
# 3 之所以在类内部可以直接通过__x 访问,是因为__开头的属性会在检查类体代码语法时统一发生变形(类定义阶段)
# 4 这种变形操作只在检查类体语法的时候发生一次,之后再定义的__开头的属性都不会变形,所以可以直接__y访问到
开放接口
定义属性的目的是为了使用,所以隐藏属性的目的不是单纯的隐藏,而是为了更好的使用。
想要这些属性被使用,就必须提供给使用者一些接口,即没有被隐藏属性。
隐藏数据属性:将数据隐藏起来就限制了类外部对数据的直接操作,然后类内应该提供相应的接口来允许类外部间接地操作数据,接口之上可以附加额外的逻辑来对数据的操作进行严格地控制。
class Student:
def __init__(self, name):
self.__name = name
def get_name(self):
# 通过该接口就可以间接地访问到名字属性
print(self.__name)
def set_name(self, new_name):
# 通过改接口判断用户修改的新名字是否合法;非法则修改,不合法就不修改
if type(new_name) is not str:
print('小垃圾,名字必须是字符串类型')
return
self.__name = new_name
隐藏函数/方法属性:目的是为了隔离复杂度。例如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
python中有两种方式实现装饰器。第一种是函数装饰器,第二种是类装饰器。比如python3中的property就是一个类装饰器,将来绑定给对象的方法伪造成一个数据属性。它有下面两个基本使用场景。
场景1:将函数属性伪装成数据属性
比如人的BMI指数是体重健康的一种参数标准。
它是计算出来的,但是对用户来说BMI指数却更像数据属性,即用户更愿意通过数据属性来访问它。需求就来了。
解决办法:类内部在函数bmi上面加property语法糖,然后对象就可以像访问数据属性那样来使用bmi这个函数属性
class Poeple:
def __init__(self, name, w, h):
self.__name = name
self.w = w
self.h = h
@property
def bmi(self):
return self.w / (self.h ** 2)
obj1 = Poeple('xliu', 66, 1.75)
print(obj1.bmi)
场景2:统一数据属性的查、改、删操作
# 方案1
class Poeple:
def __init__(self, name, w, h):
self.__name = name
self.w = w
self.h = h
@property
def name(self):
return self.__name
@name.setter
def name(self, value):
self.__name = value
@name.deleter
def name(self):
print('不能删')
obj1 = Poeple('xliu', 66, 1.75)
print(obj1.name)
obj1.name = 'egon'
del obj1.name
# 当name 遇到查询时,触发被property装饰的函数的执行
# 当name 遇到赋值操作,即 = 时触发被property.setter装饰的函数的执行
# 当name 遇到删除操作,即 del 时触发property.deleter装饰的函数的执行
上述使用property的流程如下:
1 将想要伪装的系列方法命名成一个相同的名字
2 在查看功能上加上property语法糖
3 在修改和删除功能上加名字.setter和 .deleter语法糖
# 方案2
class People:
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, val):
if type(val) is not str:
print('必须传入str类型')
return
self.__name = val
def del_name(self):
print('不让删除')
# del self.__name
name = property(get_name,set_name,del_name)
obj1=People('egon')
print(obj1.get_name())
obj1.set_name('EGON')
print(obj1.get_name())
obj1.del_name()
# 将三个函数(get_name,set_name,del_name)统一为一个相同的符号name
# 当name 遇到查询时触发get_name函数的执行
# 当name 遇到赋值操作,即 = 时触发set_name函数的执行
# 当name 遇到删除操作,即 del 时触发del_name函数的执行
# 上面方案1是相同的模式
总结
不论是双下划线开头的属性命名来隐藏属性,还是使用类装饰器property来将函数属性伪装成数据属性,本质上都是类的设计者封装类的行为。这样做的目的都是控制和规范类的使用者。