今日内容:
1、面向对象编程
2、类与对象
属性查找
3、封装
一、面向对象编程
面向对象编程:
核心是对象二字,对象就是一个用来盛放数据与功能的容器
基于该思想编写程序就是创造一个个的容器
优点:扩展性强
缺点:编程的复杂度提升
在程序中,先定义类,后产生对象
二、类与对象
类与对象
对象是"容器",用来存放数据与功能的
类也是"容器",用来存放同类对象共有的数据与功能
我们可以总结出一个学生类,用来存放学生们相同的数据与功能
# 学生类 相同的特征: 学校=清华大学 相同的功能: 选课
定义类:
class Student: # 类的命名应该使用'驼峰体' school = '清华大学' #数据 def chose(self): # 功能 print('%s is chosing a curse' ) print(Student.__dict__) # {'__module__': '__main__', 'school': '清华大学', 'chose': <function Student.chose at 0x0000022647CF90D0>, '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}
调用类的过程称为将类实例化,拿到的返回值就是程序中的对象,或称之为一个实例
stu1 = Student() print(stu1) # 每一次实例化Student类就得到一个学生对象 # <__main__.Student object at 0x0000027A0DBECFD0>
在实例化的过程中为学生定制各自独有的数据,姓名,性别,年龄
需要我们在类内部新增一个__init__方法
class Student: school = '清华大学' # 该方法会在对象产生之后自动执行,专门为对象进行初始化操作,可以有任意代码 # 但是一定不能返回None的值 def __init__(self,name,sex,age): self.name = name self.sex = sex self.age = age def choose(self): print('%s is choosing a course' %self.name)
然后我们重新实例化学生
stu1 = Student('王大力', '男', 28) stu2 = Student('李建刚', '女', 18) stu3 = Student('牛嗷嗷', '男', 38) # 会产生对象的名称空间,用__dict__查看 print(stu1.__dict__) print(stu2.__dict__) print(stu3.__dict__) # {'name': '王大力', 'sex': '男', 'age': 28} # {'name': '李建刚', 'sex': '女', 'age': 18} # {'name': '牛嗷嗷', 'sex': '男', 'age': 38}
单拿stu1的产生过程来分析,调用类会先产生一个空对象stu1,然后将stu1连同调用类时括号内的参数一起传给Student.__init__(stu1,'王大力','男',28)
def __init__(self, name, sex, age): self.name = name # stu1.name = '王大力' self.sex = sex # stu1.sex = '男' self.age = age # stu1.age = 28
会产生对象的名称空间,同样可以用__dict__查看
print(stu1.__dict__) # {'name': '王大力', 'sex': '男', 'age': 28}
至此,我们造出来了三个对象与一个类,对象存放各自独有的数据,类中存放对象们共有到的内容
属性的查找
1、类属性与对象属性
在勒种定义的名字,都是类的属性,细说的话,类有两种属性:数据属性和函数属性,可以通过__dict__访问属性的值,比如Student.__dict__['school'],但是Python提供了专门的属性访问语法:
print(Student.school) # 访问数据属性,等同于:Student.__dict__['school'] print(Student.__dict__['school']) # 清华大学 # 清华大学 print(Student.choose) # 访问函数属性,等同于:Student.__dict__['choose'] print(Student.__dict__['choose']) # <function Student.choose at 0x000001BB76179310> # <function Student.choose at 0x000001BB76179310>
2、操作对象的属性也是一样的
# 查看 print(stu1.name) # 等同于stu1.__dict__['name'] # 王大力 # 新增 stu1.course = 'python' # 等同于 stu1.__dict__['course'] = 'python' # 修改 stu1.age = 38 # 等同于stu1.__dict__['age'] = 38 # 删除 del stu1.course # 等同于del stu1.__dict__['course']
3、属性查找顺序与绑定方法
对象的名称空间里只存放着对象独有的属性,而对象们相似的属性是存放于类中的,对象在访问属性时,会优先从对象本身的__dict__中查找,未找到的,则去类的__dict__中查找。
1、类中定义的变量是类的数据属性,是共享给所有对象用的,指向相同的内存地址
# 1、id都一样 print(id(Student.school)) # 2051291861136 print(id(stu1.school)) # 2051291861136 print(id(stu2.school)) # 2051291861136 print(id(stu3.school)) # 2051291861136
2、类中定义的函数时类的函数属性,类可以使用,但是必须遵循函数的参数规则,有几个参数需要传几个参数
Student.choose(stu1) Student.choose(stu2) Student.choose(stu3) # 王大力 is choosing a course # 李建刚 is choosing a course # 牛嗷嗷 is choosing a course
但其实类中定义的函数主要是给对象使用的,内存地址都不一样
ps:id是python的实现机制,并不能真实的反映内存地址,如果有内存地址,还是以内存地址为准
print(id(stu1.choose)) print(id(stu2.choose)) print(id(stu3.choose)) # 2200856354304 # 2200856354368 # 2200856354304
绑定对象的方法特殊之处在于,绑定给谁就应该由谁来调用,谁来调用,就会将'谁'本身当做第一个参数自动传入。(方法__init__也是一样的道理)
stu1.choose() stu2.choose() stu3.choose() # 等同于: Student.choose(stu1) Student.choose(stu2) Student.choose(stu3) # 王大力 is choosing a course # 李建刚 is choosing a course # 牛嗷嗷 is choosing a course
绑定到不同对象的choose技能,虽然都是选课,但是王大力选的课,不会选给李建刚,这正是"绑定"二字的精髓
注意:绑定到对象方法的这种自动传值的特征,决定了在类中定义的函数都要默认写一个参数self,self可以是任意名字,但是命名为self是约定俗称的。
三、封装
一、引入
面向对象编程有三大特征:封装、继承、多态。
封装指的就是把数据与功能整合到一起。
针对封装到对象或类中的属性,我们还可以严格的控制对它们的访问,分两步实现:隐藏于开放接口
二、隐藏属性
Python的class机制采用双下划线的方式将隐藏属性隐藏起来(设置成私有的),但其实这仅仅只是一种变形操作,类中所有双下滑线开头的属性都会在类定义阶段、检测语法时自动变成 " _类名__属性名 " 的形式。
class Foo: __N = 0 # 变形为_Foo__N def __init__(self): # 定义函数时,会检测函数语法,所以__开头的属性也会变形 self.__x = 10 # 变形为_Foo_x def __f1(self): # 变形为_Foo__f1 print('__f1 run') def f2(self): # 定义函数时,会检测函数语法,所以__开头的属性也会变形 self.__f1() # 变形为self._Foo__f1
print(Foo.__N) # 报错 # AttributeError: type object 'Foo' has no attribute '__N' obj = Foo() print(obj.__x) # 报错 # AttributeError: 'Foo' object has no attribute '__x'
这种变形需要注意的问题是:
1、在类外部无法直接访问双下滑线的属性,但是知道了类名和属性名就可以拼出名字:_类名__属性。然后就可以访问了,例如:Foo._A_N。
所以说这种操作并没有严格意义上的限制外部访问,仅仅是语法意义上的变形。
2、在类内部是可以直接访问双下滑线开头的属性的,因为在类定义阶段内部双下划綫开头的属性同一发生了变形。
3、变形操作值在类定义阶段发生一次,在类定义之后的赋值操作,不会变形
三、开放接口
定义属性就是为了使用,所以隐藏并不是目的
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): print('姓名必须是字符串类型') return if not isinstance(age, int): print('年龄必须是整型') return self.__name = name self.__age = age t = Teacher('lili',18) t.set_info('Lili','19') # 年龄必须是整型 # 年龄必须是整型 t.tell_info() # 查看老师的信息 # 姓名:lili,年龄:18
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
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 ) # 触发方法bmi的执行,将obj自动传给self,执行后返回值作为本次引用的结果 # 21.913805697589478
使用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): # 在设定值之前进行类型检查 print('%s must be str' %value) return self.__NAME = value # 通过类型检查后,将值value存放到真实的位置self.__NAME @name.deleter def name(self): print('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