property、魔法属性和魔法方法、多重继承和多继承
1.5 property
学习目标
1. 能够说出什么要使用 set/get 方法操作属性
2. 能够说出如何对属性进行数据有效性控制
3. 能够说出使用 set/get 方法的好处
4. 能够说出说出 property 的作用
5. 能够说出 property 类和 @property 有什么区别
6. 能够说出语法糖的作用
--------------------------------------------------------------------------------
1.5.1 property 概述
property 本身的意义就是属性、性质,在 python 中主要为属性提供便利的操作方式。
1.5.2 思考
如果现在需要设计一个 银行帐户类 ,这个类中包含了帐户人姓名和帐户余额,不需要考虑具体的操作接口,你会怎么设计?
1.5.3 实现与复盘
简单实现
class Account(object):
def __init__(self, name, money):
self.name = name # 帐户人姓名
self.balance = money # 帐户余额
这样的设计有什么问题? 很显然,这样设计起来很简单方便,但是所有的属性外部都能访问修改,非常不安全。 如何改进呢?
改进一 隐藏实现细节
对于帐户的信息,特别是金额,这是不能够让用户直接修改的,如果要改变信息,需要窗口去办理。
程序实现也是一样,在使用对象时,尽量不要让使用者直接操作对象中的属性,这样会带来安全隐患。
改进办法,使用私有属性。
class Account(object):
def __init__(self, name, money):
self.__name = name # 帐户人姓名
self.__balance = money # 帐户余额
代码改进以后,将所有的属性都设计成了对象私有属性,确实从外部在使用时,并不知道对象内部的属性是什么,不能直接修改对象了,隐藏了实现的细节。
但是随之又产生了另外一个问题,如果确实需要对这两个属性要进行修改怎么办呢?
改进二 提供精确的访问控制
在之前的学习中,学习过 set/get方法,是专门来为类的私有属性提供访问接口。
class Account(object):
def __init__(self, name, money):
self.__name = name # 帐户人姓名
self.__balance = money # 帐户余额
# 帐户人姓名,在创建帐户时就已经确定,不允许修改,所以对外不提供姓名的 set 方法
def get_name(self):
return self.__name
def set_balance(self,money):
self.__balance = money
def get_balance(self):
return self.__balance
经过修改,外部使用这个类的对象时,想使用对象中的属性,只能通过类中提供的 set/get 接口来操作,提高了程序的安全性。
这样,程序基本达到了设计需求,但是能不能更加完善呢?
如果在使用这个类的对象过程中,由于误操作,传入了不正常的数据,导致数据异常。该如何以避免这种情况发生呢?
比如:设置金额时出现了负数,或字符串,或其它类型的对象。
改进三 保证数据有效性
在 set 方法中,对传入的数据进行判断有效性,如果是无效数据,提示用户出错。
class Account(object):
def __init__(self, name, money):
self.__name = name # 帐户人姓名
self.__balance = money # 帐户余额
def get_name(self):
return self.__name
def set_balance(self,money):
if isinstance(money, int):
if money >= 0:
self.__balance = money
else:
raise ValueError('输入的金额不正确')
else:
raise ValueError('输入的金额不是数字')
def get_balance(self):
return self.__balance
经过几个版本的迭代,程序越来越健壮。安全性也越来越高。
但是在使用的过程中,能不能更加精练一些呢?即然 set/get 方法是用来操作属性的方法,那么能不能以属性操作的方式来使用呢?
答案是肯定的。
1.5.4 property 类
在 Python 中,提供了一个叫做 property 的类,通过对这个创建这个类的对象的设置,在使用对象的私有属性时,可以不在使用属性的函数的调用方式,而像普通的公有属性一样去使用属性,为开发提供便利。
property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
property 是一个类,init方法由四个参数组成,实例后返回一个用来操作属性的对象 参数一:属性的获取方法 参数二:属性的设置方法 参数三:属性的删除方法 参数四:属性的描述
class Account(object):
def __init__(self, name, money):
self.__name = name # 帐户人姓名
self.__balance = money # 帐户余额
def __get_name(self):
return self.__name
def set_balance(self,money):
if isinstance(money, int):
if money >= 0:
self.__balance = money
else:
raise ValueError('输入的金额不正确')
else:
raise ValueError('输入的金额不是数字')
def get_balance(self):
return self.__balance
# 使用 property 类来为属性设置便利的访问方式
name = property(__get_name)
balance = property(get_balance, set_balance)
ac = Account('tom', 10)
print(ac.name)
print(ac.balance)
ac.balance = 1000
print(ac.balance)
通过 property 类实例对象以后,在使用对象中的属性时,就可以像使用普通公有属性一样来调用,但是实际调用的还是 set/get 方法。 在实例 property 对象时,不是所有的参数都需要写,比如示例中的 name 只提供了 get 方法,并且是一个私有的方法。这样就完全隐藏了内部的实现细节 。
1.5.5 @property 装饰器
Python 语法中,提供一种装饰器语法,在函数定义的上一行,使用 @xxx 的形式来使用装饰器。
装饰器的作用就是提供装饰的功能,在不改变原来函数功能的基础上,添加新的功能。(装饰器语法会在后面的课程中单独讲解)
这种形式被称为语法糖。
语法糖指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法。 语法糖往往给程序员提供了更实用的编码方式,有益于更好的编码风格,更易读。
利用 @property 装饰器,可以用来简化定义新的属性或修改现有的操作。
class Account(object):
def __init__(self, name, money):
self.__name = name # 帐户人姓名
self.__balance = money # 帐户余额
# property 只能对获取方法装饰,并且获取方法不需要再写 get
@property
def name(self):
return self.__name
# 如果 property 装饰的属性还有 set 方法,需要写到 get方法后定义
@property
def balance(self):
return self.__balance
# 实现 set 方法, 格式: @xxx.setter ,xxx 要和property装饰的方法名一致
@balance.setter
def balance(self, money):
if isinstance(money, int):
if money >= 0:
self.__balance = money
else:
raise ValueError('输入的金额不正确')
else:
raise ValueError('输入的金额不是数字')
ac = Account('tom', 10)
print(ac.name)
print(ac.balance)
ac.balance = 1000
print(ac.balance)
注意:
在使用 @property 装饰属性时,只能装饰获取方法
@property 装饰属性时, set/get 方法不需要再属性名前加 set 和 get ,直接写属性名即可
如果一个属性同时有 set/get 方法,那么要先实现 @property 对获取方法的定义
再实现设置方法的定义,定义时使用 @xxx.setter 装饰,xxx 要和获取方法名保持一致
1.5.5 总结
1. 在设计类时,尽量使用私有属性,然后使用 set/get 接口来提供读写操作
2. 使用 set/get 接口方法,可以方便控制数据的有效性,精细化控制访问权限,隐藏实现细节
3. 在定义 set/get 函数时,可以使用实例 property 类的对象,或 使用 @property 装饰器来对方法进行处理
4. 处理之后的 set/get 函数在使用时,可以像直接使用属性一样进行操作,但实际调用还是函数,简化操作
5. 一个类,一个是装饰器
6. Python 中提供了很多语法糖,语法糖的作用是用来简化操作,使代码开发更简单,是一种对开发人员‘更甜蜜’语法
1.6 魔法属性和魔法方法
学习目标
1. 能够说出什么是魔法方法
2. 能够说出魔法属性的作用
3. 能够说出不同的魔法方法调用的时机
--------------------------------------------------------------------------------
1.6.1 魔法属性和魔法方法概述
在 Python 中预先定义好了一些以 __xxx__ 形式的属性和方法。
这些属性和方法用来表示特定的意义和功能。
在程序运行过程中自动调用,或者根据开发的需求手动调用,甚至重写魔法方法的功能。
本节主要介绍一些在开发中经常用到的魔法属性和魔法方法。
1.6.2 __doc__ 属性
Python 中有个特性叫做文档字符串,即DocString,这个特性可以让你的程序文档更加清晰易懂。
DocString 通俗的说就是文件中的特殊注释。用来描述文件,类,函数等的功能。
DocString 有固定的格式,一般是放在自己函数定义的第一行,用 ‘ ’ 符号指示,在这 ‘ ’ 里面添加函数功能说明就可以了。
DocString 可以使用 xxx.__doc__(注意前后都是双_)属性,将 DocString 特性 print 打印出来。print(print.__doc__)
AA.py
''' This is File DocString '''
def display():
''' This is Display Function DocString. '''
pass
class Test(object):
''' This is Test Class DocString! '''
pass
def show(self):
''' This is Show Function DocString '''
pass
BB.py
import AA
t = AA.Test()
print(t.__doc__) # 对象使用描述是当前对象所属类的描述
print(AA.Test.__doc__) # 类的描述
print(t.show.__doc__) # 对象方法的描述,查看方法描述时,方法名后面不能有括号
print(AA.display.__doc__) # 公有函数的描述
print(AA.__doc__) # 文件(模块)的描述
Python的系统文件都在使用这个特性对功能进行描述。
print(print.__doc__)
调用的就是 print 函数的第一行描述内容
help函数 DocSting的典型用法是使用 help()调用。
当使用 help 函数时,help函数会通过__doc__魔法属性将参数中的 DocString 属性展示出来。
def display():
''' This is Display Function DocString. '''
pass
help(display)
程序运行结果:
Help on function display in module __main__:
display()
This is Display Function DocString.
1.6.3 __module__ 属性 、 __class__ 属性 、__bases__ 属性 、 __mro__ 属性
在 Python 程序开发过程中经常会导入很多其它模块或者创建很多类的实例对象。
并且,Python 是一个支持多继承和多重继承的编程语言。
多继承是指一个类同时继承多个类。 多重继承是指一个类所继承的类之前还有继承关系。
当使用模块较多时,可以通过 __module__ 来查看当前成员属于哪个模块,通过 __class__ 属性查看对象属于哪个类
当类中有复杂的继承关系时,可以通过 __bases__ 查看本类的父类有哪些,通过 __mro__ 属性查看类中方法的查找顺序。
AA.py
# 动物类
class Animal(object):
pass
# 人类继承动物类
class Person(Animal):
pass
# 鸟类继承动物类
class Bird(Animal):
pass
# 鸟人类继承人类和鸟类,即是多继承,也是多重继承
class BirdMan(Person, Bird):
pass
# 显示鸟人类的父类
print(BirdMan.__bases__)
# 显示鸟人类初始化或实例让用方法时的查找顺序
print(BirdMan.__mro__)
BB.py
from AA import *
# 使用 module 查看当前类属于哪个模块
print(BirdMan.__module__)
# 使用 class 查看指定类的对象属于哪个类
print(BirdMan().__class__)
# 使用 bases 来查看当前类的直接父类
print(BirdMan.__bases__)
# 使用 mro 来查看多重多继承时的继承关系
print(BirdMan.__mro__)
程序运行结果:
AA
<class 'AA.BirdMan'>
(<class 'AA.Person'>, <class 'AA.Bird'>)
(<class 'AA.BirdMan'>, <class 'AA.Person'>, <class 'AA.Bird'>, <class 'AA.Animal'>, <class 'object'>)
1.6.4 __new__ 方法 和 __init__ 方法
在 Python 中,__init__ 方法用来对实例中的属性进行初始化操作,在使用类创建实例对象时,就会自动调用这方法。
但是 __init__ 方法并不是在创建对象时第一个被执行的方法。
在创建对象时,Pyhton 会先在内存中申请对象的内存空间。如果申请成功,说明对象才创建成功,之后才是使用 __init__ 进行初始化操作。
而申请对象空间这个过程就是由 __new__ 方法来执行的。
也就是说在创建对象过程中,会先执行 __new__ 在内存中申请实例存储空间,然后再执行 __new__ 初始化实例对象空间。
可以通过下面的代码验证:
class A(object):
def __new__(cls, *args, **kwargs):
print('New Run...')
return super().__new__(cls, *args, **kwargs)
def __init__(self):
print('Init Run ...')
a = A()
程序运行结果:
New Run...
Init Run ...
__new__ 方法在开辟完内存空间后,会自动调用 __init__ 方法来初始化对象空间。
开发过程中,一般不会重写 __new__ 方法。一般都是重写 __init__ 方法。
Python官方文档的说法,__new__ 方法主要是当你继承一些不可变的class时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。 比如,要继承 int 类,实现一个永远是整数的类。 另外,在实现单例类的时候可以使用 __new__ 。
1.6.5 __call__ 方法
需求:记录一个函数执行的次数
在程序设计过程中,并不建议使用全局变量,因为会大量占用内存空间不释放,会破坏程序的封装性。 而且全局变量在使用过程中,任何人都可以访问,不安全,不符合面向对象的封装思想。
这样就可以使用 __call__ 方法来实现这个需求
实现__call__后,可以将对象当做函数一样去使用,称为仿函数或函数对象
那么普通函数和函数对象有什么区别?
普通对象在执行完成后就结束了,不能保存函数执行的状态。而且在扩展其它函数时,函数间的关联性不强。
函数对象可以保存函数的状态。比如要实现对象调用的次数。而且可以在类中扩展更多的功能。
class MyFun(object):
def __init__(self):
self.__call_num = 0
def __call__(self, *args, **kwargs):
print('MyFunc Run...')
self.__call_num += 1
def get_call_num(self):
return self.__call_num
mf = MyFun()
mf()
mf()
print(mf.get_call_num())
程序运行结果 :
MyFunc Run...
MyFunc Run...
2
1.6.6 __getitem__ 、__setitem__ 、__delitem__ 、__len__ 方法
魔术方法的作用:
__getitem__(self,key):返回键对应的值。
__setitem__(self,key,value):设置给定键的值
__delitem__(self,key):删除给定键对应的元素。
__len__():返回元素的数量
当我们对类的属性item进行下标的操作时,会被__getitem__()/__setitem__()/__delitem__()拦截,从而进行相应的操作,如赋值,修改内容,删除内容等等。
如果现在要设计一个学生管理的类,实现学生信息的添加,获取,删除等操作,应该怎么设计?
但是我们发现,这个管理系统在管理学生时,实际使用的是一个字典,在管理学生信息的时候,能否不使用类提供的函数,而像使用字典一样,直接通过操作字典的key的方式来直接操作呢?
类似如下操作: d = {} d['one'] = 1 print(d['one'])
程序改进
class StudentManager(object):
'''学生信息管理'''
def __init__(self):
# 使用字典来保存所有的学生信息
self.__students = {}
# 添加学生
def __setitem__(self, key, value):
self.__students[key] = value
# 获取学生
def __getitem__(self, item):
if item not in self.__students:
return None
return self.__students[item]
# 删除学生
def __delitem__(self, key):
if key in self.__students:
del self.__students[key]
# 获取学生人数
def __len__(self):
return len(self.__students)
# 创建学生管理对象
sm = StudentManager()
# 添加两个学生
sm[1] = 'tom'
sm[2] = 'jack'
# 查看学生个数
print(len(sm))
# 显示添加的学生
print(sm[1])
print(sm[2])
# 删除2号学生
del sm[2]
# 查看学生个数
print(len(sm))
# 查看是否删除
print(sm[1])
print(sm[2])
运行结果 :
2
tom
jack
1
tom
None
可以看出,结果完全相同,但是使用更加简洁。这才是Python语法的精髓。
在自定义对象可以使用 对象[ ] 形式来直接操作对象中的容器,使代码书写更加简洁,可读性更高。
1.6.7 __str__ 方法
当使用print输出对象的时候,只要自己在类中定义了__str__(self)方法,那么就会打印从在这个方法中return的数据
列表或字典在输出时,通过直接打印列表或字典的名字,会直接打印出列表或字典中的内容。
那么自定义类会打印出来什么呢?
<__main__.StudentManager object="" at="" 0x103a120f0="">
那么自定义类,能不能像系统列表一样,显示所有的信息呢?
这个功能可以通过 __str__ 方法来实现
...
def __str__(self):
return '[' + " : ".join(self.__students.values()) + ']'
程序运行结果 :
[tom : jack]
小结:
__str__ 的作用是一个自定义的类型,在转换成字符串类型时,程序是不知道怎么办的的,因为程序也不知道你倒底写了什么。
python提供 __str__ 这个方法,让开发人员重写这个方法,制定自定义对象转换成字符串的规则。
1.6.8 总结
魔法属性和魔法方法都是 Python 定义好的一些属性或方法,用来实现一些特定的功能。
魔法属性和魔法方法的主要作用是用来简化 Python 的操作,在编写代码时让使用方式更加简洁。
1.7 多重继承和多继承
学习目标
1. 能够说出什么是继承
2. 能够说出继承的作用
3. 能够说出多重继承的初始化过程
4. 能够说出多继承的初始化过程
5. 什么是钻石继承
--------------------------------------------------------------------------------
1.7.1 继承概述
在面向对象程序开发中,继承是一个非常重要的概念。也是一个非常贴近现实继承关系的一种思想。
通过继承,可以让子类去拥有父类的属性和方法,而不必重新编写相同的代码,并且可以在父类的基础上添加新的属性和功能。
在继承的同时,子类还可以重写父类中的方法,从而获取与父类不同的功能,实现多态。
在 Python 中 所有的类都是存在继承关系的,没有显示继承的,都是默认继承 object 类
1.7.2 继承的作用
子类在继承父类的同时可以向子类中添加新的属性和功能,提高开发效率,避免代码冗余。
实现多态。
1.7.3 继承回顾
单重单继承
通过继承,让子类调用方法,如果子类存在就直接调用执行,如果没有,就会到父类中去查找,如果父类中有就执行父类中的方法,如果没有就再去object 类中去查找,如果父类中没有就会报错。
1.7.4 多重单继承的初始化问题
在设计单类时,初始化数据时,只需要重写__init__方法即可,可是当有多个类发生关系时,类中的属性该何进行初始化呢?
在发生继承时,子类会继承父类的属性的方法。
当在初始化时,子类只需要初始化自己扩展的属性即可,父类中的属性交给父类去初始化。
使用 父类名.__init__()的形式来调用父类的初始化方法
为什么要这么做呢?
父类中的属性是私有的,子类根本不知道父类里的属性是什么。
子类也不知道父类在初始化操作过程中做了哪些工作。
所以谁的内容就交给谁去执行是最安全的。
class Person():
def __init__(self, name):
self.__name = name
print('Peron init...')
def get_name(self):
return self.__name
class Father(Person):
def __init__(self,name, age):
Person.__init__(self, name)
self.__age = age
print('Father init...')
def get_age(self):
return self.__age
class Son(Father):
def __init__(self,name,age, gender):
Father.__init__(self,name, age)
self.__gender = gender
print('Son init...')
def get_gender(self):
return self.__gender
s = Son('Tom', 18, '男')
print(s.get_name(),s.get_age(),s.get_gender())
程序执行结果:
Peron init...
Father init...
Son init...
Tom 18 男
1.7.5 多重多继承的初始化问题
多重多继承时使用 父类名.__init__()的形式来调用父类的初始化方法时,因为 Father 类和 Mother 类都是继承于 Person 类的,在自己的初始化方法中,都执行了父类的初始化方法,所以Person 类的 __init__ 方法被执行了两次。
多重多继承的初始化使用super 函数
super(类名, self).__init__(*args)
在 Python 中,提供 Super 函数来解决多继承时父类被重复初始化这个问题。
super函数的格式
super(CurrentClassName, self).__init__(*args, **kwargs)
class Person():
def __init__(self, name):
self.__name = name
print('Peron init...')
def get_name(self):
return self.__name
class Father(Person):
def __init__(self, age, *args):
super(Father, self).__init__(*args)
self.__age = age
print('Father init...')
def get_age(self):
return self.__age
class Mother(Person):
def __init__(self, job,*args):
super(Mother, self).__init__(*args)
self.__job = job
print('Mother init...')
def get_job(self):
return self.__job
class Son(Father, Mother):
def __init__(self, name, age, gender, job):
super(Son, self).__init__(age, job,name)
self.__gender = gender
print('Son init...')
def get_gender(self):
return self.__gender
s = Son('Tom', 18, '男','老师')
print(s.get_name(),s.get_age(),s.get_gender(),s.get_job())
程序执行结果:
Peron init...
Father init...
Mother init...
Son init...
Tom 18 男 老师
通过执行结果来看,使用 super 改进后的代码只初始化了一次 Person 的初始化方法。
这种初始化方式称为钻石继承(菱形继承),如图显示 :
当程序使用了 super 函数以后,可以正常初始化,是以__mro__方法解析顺序为依据。
1.7.6 __mro__
mro Method Resolution Order 方法解析顺序
类名.__mro__返回一个元组类型的继承链,来确定继承的顺序
在类出现继承时,每个类中都会保存一个当前类的继承关系的表。
super 函数在执行时,会在自己保存的这个继承关系中去查找第一个参数,也就是当前的类名的下一个类是谁。然后去调用下个类的初始化方法。
Python 中使用广度优先算法来决定继承关系的排序。(广度优先算法可自行查找资料了解)
广度优先算法:横向优先
深度优先算法:纵向优先
MRO顺序
print(Son.__mro__)
程序执行结果 :
(<class '__main__.Son'>, <class '__main__.Mother'>, <class '__main__.Father'>, <class '__main__.Person'>, <class 'object'>)
1.7.7 super的简化写法
在初始化父类时,也可以使用 super().__init__() 函数来调用,简化super函数的写法。
这时,super函数中没有参数,还是能正确执行,就是依照 __mro__ 中的顺序来执行的。
class Person():
def __init__(self, name):
self.__name = name
print('Peron init...')
def get_name(self):
return self.__name
class Mother(Person):
def __init__(self, name, age, job):
# super(Mother, self).__init__(name, age)
super().__init__(name,age)
self.__job = job
print('Mother init...')
def get_job(self):
return self.__job
class Father(Person):
def __init__(self,name, age):
# super(Father, self).__init__(name)
super().__init__(name)
self.__age = age
print('Father init...')
def get_age(self):
return self.__age
class Son(Mother, Father):
def __init__(self, name, age, gender, job):
# super(Son, self).__init__(name, age, job)
super().__init__(name,age,job)
self.__gender = gender
print('Son init...')
def get_gender(self):
return self.__gender
s = Son('Tom', 18, '男','老师')
print(s.get_name(),s.get_age(),s.get_gender(),s.get_job())
程序执行结果:
Peron init...
Father init...
Mother init...
Son init...
Tom 18 男 老师
对象在调用方法时,也会依照这个顺序来查找方法。
类在继承时,继承关系的书写顺序会影响 __mro__ 中的顺序。
class A():
pass
class B(A):
pass
class C(A):
pass
class D(B,C):
pass
class E(C,B):
pass
print(D.__mro__)
print(E.__mro__)
程序执行结果:
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
(<class '__main__.E'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
1.7.8 总结
继承的思想是为了避免代码的冗余,方便程序的扩展和复用
多重继承是继承关系中很多代(纵向)
多继承是一个类可以同时继承多个类(横向)
Python 中的类可以多重继承,也可以多继承
Python 通过 __mro__ 来确定继承的顺序,使用的是广度优先算法(横向优先)