Python面向对象—专题(一)
1.初识面向对象
Key Point
- 面向对象概念
- 类的定义
- 对象的创建
- isinstance函数
- 类本身也是一个对象
面向对象概念
- 类,描述一类对象的特征集合。
- 对象,符合类定义特征的具体实例。
- 属性,分为类属性和实例属性。
- 方法,分为类方法和实例方法。
类的定义
class Student:pass
使用class关键字来定义类,类名的首字母默认大写,类的命名需要直观明了。
对象的创建
student_1 = Student()
print(hex(id(student_1)))# hex 函数,将十进制数转换成十六进制数
# 语法: hex(x)# id 函数,返回对象的唯一标识符,标识符是一个整数。
# 语法: id([object])
使用类的名字+括号,实现类的对象的创建。
isinstance()函数
描述
isinstance() 函数来判断一个对象是否是一个已知的类型,类似 type()。
isinstance() 与 type() 区别:
- type() 不会认为子类是一种父类类型,不考虑继承关系。
- isinstance() 会认为子类是一种父类类型,考虑继承关系。
如果要判断两个类型是否相同推荐使用 isinstance()。
语法
isinstance(object, classinfo)
- object — 实例对象。
- classinfo — 直接或间接类名、基本类型或者由它们组成的元组。
- 返回值 — 如果object类型与classinfo相同则返回 True,否则返回 False。
实例
# 基本使用>>>a = 2
>>> isinstance (a,int)
True
>>> isinstance (a,str)
False
>>> isinstance (a,(str,int,list)) # 是元组中的一个返回 True
True
# type()与isinstance()区别class A:passclass B(A):passisinstance(A(), A) # returns True
type(A()) == A # returns True
isinstance(B(), A) # returns True
type(B()) == A # returns False
类本身也是一个对象
class Student:passprint(type(Student)) # return <class 'type'>
Python中类类本身是’type’类型的对象。
2.类变量
Key Point
- 类变量概念&定义
- 类变量值的获取&设置
- 类变量的删除&存储
类变量概念&定义
概念
- 属于类本身这个对象的属性
- 所有该类的对象都共享变量
# 说明:__name__ 、 __dict__ 属于系统定义变量属于类变量
class Student:passprint(Student.__name__) # return Student
print(Student.__dict__) # return a dict
# return {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}
定义类变量
# 说明:定义的类变量,该类的对象都可共享的变量
class Student:student_count = 1 # 定义的类变量stu_1 = Student()
stu_2 = Student()print(stu_1.student_count) # retrun 1
print(stu_2.student_count) # retrun 1
print(Student.student_count) # retrun 1
类变量值的获取&设置
获取类变量的值
- 直接通过引用进行取值。
- 使用getattr()函数取值。
class Student:student_count = 0# 直接引用
print(Student.student_count)# 如果引用不存在的类变量,出AttributeError
print(Student.unknown)# 使用getattr()函数获取类变量的值
print(getattr(Student,"student_count"))
设置类变量的值
- 直接通过赋值进行设置。
- 使用setattr()函数进行设置。
# 设置类变量的值
class Studentstudent_count = 0# 直接设置类变量的值
Studnet.student_count = 10# 使用setattr()函数设置类变量的值
setattr(Student,"student_count",10)# 如果类变量不存在,动态添加
Student.unkown = "hello"
类变量的删除&存储
删除类变量
- 使用del操作符。
- 使用delattr()函数。
# 删除类变量
class Student:student_count = 0del Student.student_count
delattr(Student,"student_count")
类变量的存储
所有类变量均存储在__dict__
字典中,但不能直接修改__dict__
的内容。
# pprint打印出来的格式更直观from pprint import pprint
class Student:student_count = 0print(Student.__dict__)
pprint(Student.__dict__)# print的结果格式
"""
{'__module__': '__main__', 'student_count': 0, '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}
"""# pprint的结果格式
"""
mappingproxy({'__dict__': <attribute '__dict__' of 'Student' objects>,'__doc__': None,'__module__': '__main__','__weakref__': <attribute '__weakref__' of 'Student' objects>,'student_count': 0})
"""
3.实例变量与函数
Key Point
- 认识
__init__
函数- 实例函数的定义
- 定义实例变量
- 实例函数中访问实例变量
- 外部访问实例变量与函数
认识__init__
函数
class Student:def __init__(self):print("doing initial stuff")std_1 = Student()
__init__()
函数是用于初始化对象的实例函数。
实例函数的定义
class Studentdef say_hello(self,msg:str):print(f"hello{msg}")
与普通函数定义方式相同,值得注意的是第一参数必须是self。
定义实例变量
class Student:def __init__(self,name:str):self.name = namestd_1 = Student("Nick")
构造对象的实例变量提前通过参数传入构造函数中。
实例函数中访问实例变量
class Student:def __init__(self,name):self.name = namedef introduce():print(f"I'm {self.name}.")
外部访问实例变量与函数
class Student:def __init__(self,name:str):self.name = namedef introduce():print(f"I'm {self.name}.")std_1 = Student('Nick') # Student类实例化
std_1.introduce() # 外部访问实例函数
std_1.name = "Tom" # 外部访问并修改实例变量
4.私有属性与函数
Key Point
- 私有属性与函数的用途
- 定义私有属性与函数
- 访问私有属性与函数
私有属性与函数的用途
面向对象的封装中,私有属性与函数主要目的在于防止它们在类的外部被使用,区别于其他语言,Python中没有严格的权限限定符去进行限制,Python中主要是通过命名来进行区分。
定义私有属性与函数
class Student:def __init__(self,name,gender):self._name = name # 定义私有属性self.__gender = gender # 定义私有属性def _change_gender(): # 定义私有函数passdef __change_name(): # 定义私有函数 pass
通过给属性和函数名称添加前缀_
或__
,以此实现私有函数与属性的定义。
访问私有属性与函数
class Student:def __init__(self, name: str, gender: str):self._name = nameself.__gender = genderdef __say_hello(self, msg: str):print(f"Hello {msg} {self._name}, you're a {self.__gender}")def main():s1 = Student("Jack", "man")print(s1._name)s1._name = "Lily"print(s1._name)# print(s1.__gender) # return AttributeError: 'Student' object has no attribute '__gender'print(s1._Student__gender) # 带有前缀 __ 的私有属性使用 _类名__私有属性的方式进行强制调用s1._Student__gender = "woman"print(s1._Student__gender)# s1.__say_hello("521") # return AttributeError: 'Student' object has no attribute '__say_hello's1._Student__say_hello("521") # 带有前缀 __ 的私有函数使用 _类名__私有属性的方式进行强制调用
带有前缀_
的属性与函数可以在外部直接访问与修改,但是带有__
前缀的属性与函数不可以在外部直接访问与修改,需要按照_类名__私有属性或函数的方法进行强制调用。
5. 类方法与静态方法
Key Point
- 类方法
- 静态方法
类方法
class Student: school = "abc" # 类变量def __init__(self,name:str): # 实例函数中的构造函数self.name = name def print_name(self): # 类实例方法print(self.name)@classmethod # 类方法def hello(cls):print(f"hello {cls.school}") # 使用cls.school实现类变量的调用std_1 = Student.hello() # 使用类名来进行调用类方法
类方法需要使用@classmethod装饰器来定义,类方法的第一个参数是类本身与实例函数中的__init__()
的参数self一样业界行规而已。类方法推荐使用类名直接调用,当然也可以使用实例对象来调用(不推荐)。
静态方法
class Student:school = "abc"def __init__(self, name:str):self.name = namedef print_name(self):print(self.name)@staticmethod # 静态方法装饰器def out():print(f"hello {Student.school}") # 使用Student.school实现类变量的调用@staticmethoddef size(value: int) -> float: return value * 1.5def speak(self): n = 12# n = Student.size(n) # 使用类名实现静态函数的内部调用n = self.size(n) # 使用类对象的方法实现类内部调用 print(n)def main():print(Student.school)Student.out() # 使用类名实现静态方法的调用 Student('Jack').speak()
静态方法使用@staticmethod装饰器来定义,静态方法只是定义在类范围内的一个函数而已,静态方法的调用,既可以使用类名,也可以使用类对象。通常使用静态方法实现一些工具性质的函数。静态方法没有类似 self、cls 这样的特殊参数,因此 Python 解释器不会对它包含的参数做任何类或对象的绑定。也正因为如此,类的静态方法中无法调用任何类属性和类方法。
6.常用的特殊方法
Key Point
python中方法名如果是
__XXX__
,代表有特殊的功能,因此叫做魔法方法。可以根据自己的需求进行重构,当然也可以使用默认的。
__str__
__repr__
__eq__
__hash__
__bool__
__del__
__str__()
class Date:def __init__(self, year, month, day):self.year = yearself.month = monthself.day = daydef __str__(self):print("str is called")return f"{self.year}-{self.month}-{self.day}"def main():my_date_1 = Date(2022, 11, 3)print(str(my_date_1)) # 调用类对象的特殊函数__str__print(my_date_1) # 使用print函数打印对象名会自动调用特殊函数__str__
__str__
方法主要返回一个描述对象本身的字符串,该描述主要面对用户。当print输出对象时,如果自行定义了__str__(self)
方法,就会打印次方法中return中的数据。
__repr__()
class Date:def __init__(self, year, month, day):self.year = yearself.month = monthself.day = daydef __repr__(self):print("repr is called")return f"MyDate:{self.year}-{self.month}-{self.day}"def main():my_date_1 = Date(2022, 11, 3)print(repr(my_date_1))print(my_date_1)main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_advance-main\metaclass_main_.py
repr is called
MyDate:2022-11-3
repr is called
MyDate:2022-11-3Process finished with exit code 0
__repr__
方法用于返回一个描述对象本身的字符串,该描述的主要目标是机器或开发者。通常情况下,直接输出某个实例化对象,本意往往是想了解该对象的基本信息,例如该对象有哪些属性,它们的值各是多少等等。但默认情况下,我们得到的信息只会是“类名+object at+内存地址”,对我们了解该实例化对象帮助不大。
那么,有没有可能自定义输出实例化对象时的信息呢?答案是肯定,通过重写类的 __repr__()
方法即可。事实上,当我们输出某个实例化对象时,其调用的就是该对象的__repr__()
方法,输出的是该方法的返回值。
以上面的程序为例,执行 print(my_date_1) 等同于执行 print(my_date_1.repr()),程序的输出结果是一样的(输出的内存地址可能不同)。
和 __init__(self)
的性质一样,Python中的每个类都包含 __repr__()
方法,因为 object 类包含 __reper__()
方法,而 Python 中所有的类都直接或间接继承自 object 类。
默认情况下,__repr__()
会返回和调用者有关的 “类名+object at+内存地址”信息。当然,我们还可以通过在类中重写这个方法,从而实现当输出实例化对象时,输出我们想要的信息。
__eq__()
class MyDate:def __init__(self, year, month, day):self.year = yearself.month = monthself.day = daydef __eq__(self, other):print("eq is called")if not isinstance(other,MyDate):return Falsereturn self.year == other.year \and self.month == other.month \and self.day == other.daydef main():date_1 = MyDate(2022, 11, 11)date_2 = MyDate(2022, 12, 12)date_3 = date_1date_4 = MyDate(2022, 11, 11)print(date_1 == date_2) # return False,调用 __eq__()print(date_1) # return <__main__.MyDate object at 0x0000019D87C483D0>print(date_2) # return <__main__.MyDate object at 0x0000019D87C48490>print(date_1 == date_4) # return True,调用 __eq__()print(date_1 is date_2) # return Falseprint(date_1 is date_3) # return Trueprint(date_4 is date_1) # return False
__eq__
方法用于实现对比两个对象是否相等的逻辑,在我们定义一个类的时候,常常想对一个类所实例化出来的两个对象进行判断这两个对象是否是完全相同的。一般情况下,我们认为如果同一个类实例化出来的两个对象的属性全都是一样的话,那么这两个对象是相同的。
但是如果我们直接用"==”来判断这两个对象是否相等,那么结果一定是不相等的,因为这两个对象的地址一定不同,它们在内存当中的不同区域,比如上面的代码26行。创建了两个Mydate对象,但它们的属性不一样。
那么怎么才能够让它们只要属性相同两个对象就相等呢?那就是利用__eq__
方法来进行判断,这个方法默认有两个参数,一个是self,另一个是other.也就是用自身的属性和other对象的属性分别进行比较,如果比对成功则返回True,失败则返回False。
__hash__()
class MyDate:def __init__(self, year, month, day):self.year = yearself.month = monthself.day = daydef __hash__(self):print("hash is called")return hash(self.year + self.month * 101 + self.day * 101)def main():date_1 = MyDate(2022, 12, 12)print(hash(date_1))
__hash__
方法用于实现根据对象生成hash值的逻辑。
__bool__()
# 对__bool__()进行重构class MyDate:def __init__(self, year, month, day):self.year = yearself.month = monthself.day = daydef __bool__(self):print("bool is called")return self.year > 2021def main():date_1 = MyDate(2022, 12, 12)print(bool(date_1)) # return True,调用__bool__()
# 不对__bool__()进行重构,换言之不定义__bool__()class MyDate:def __init__(self, year, month, day):self.year = yearself.month = monthself.day = day'''def __bool__(self):print("bool is called")return self.year > 2021
'''def main():date_1 = MyDate(2022, 12, 12)print(bool(date_1)) # 没有定义__bool__(),但是我调用了,此时背后真正执行的是__len__()
__bool__
方法用于在对象被bool函数求解时返回一个布尔值,如果类没有这个方法,那么使用后__len__
被调用。
__del__()
__del__
与__init__
作用相反,其作用是销毁实例化对象。其次,Python 跟根据程序的自动执行,自动调用__del__
释放内存,当让也可使用del语句调用。
class MyDate:def __init__(self, year, month, day):self.year = yearself.month = monthself.day = daydef __del__(self):print("del is called ")def main():date_1 = MyDate(2022, 12, 12)date_del = MyDate(2022, 11, 11)data_del = Noneprint(date_1)'''
运行结果<__main__.MyDate object at 0x0000022433C04E80>
del is called
del is called '''
上述代码可见__del__
方法在对象被垃圾回收前调用,由于不知道对象何时被回收,因此不能依赖于这个方法去做一些重要的事情。
当然,不要以为实例对象调用__del__
该对象所占的内存控件就会释放。证明代码如下:
class CLanguage:def __init__(self):print("调用 __init__() 方法构造对象")def __del__(self):print("调用__del__() 销毁对象,释放其空间")clangs = CLanguage()
#添加一个引用clangs对象的实例对象
cl = clangs
del clangs
print("hihi")
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_advance-main\metaclass\__main__.py 调用 __init__() 方法构造对象 hihi 调用__del__() 销毁对象,释放其空间
Process finished with exit code 0
区别于__init__
,__del__
的调用往往不可控制,原因在于 Python 的垃圾回收(GC)即如果之前创建的类实例化对象后续不再使用,最好在适当位置手动将其销毁,释放其占用的内存空间。难道这就真的把握不住了吗?
实则不然,Python 采用自动引用计数(简称 ARC)的方式实现垃圾回收机制。该方法的核心思想是:每个 Python 对象都会配置一个计数器,初始 Python 实例对象的计数器值都为 0,如果有变量引用该实例对象,其计数器的值会加 1,依次类推;反之,每当一个变量取消对该实例对象的引用,计数器会减 1。如果一个 Python 对象的的计数器值为 0,则表明没有变量引用该 Python 对象,即证明程序不再需要它,此时 Python 就会自动调用 __del__()
方法将其回收。
以上面程序中的 clangs 为例,实际上构建 clangs 实例对象的过程分为 2 步,先使用 CLanguage() 调用该类中的 __init__()
方法构造出一个该类的对象(将其称为 C,计数器为 0),并立即用 clangs 这个变量作为所建实例对象的引用( C 的计数器值 + 1)。在此基础上,又有一个 cl 变量引用 clangs(其实相当于引用 CLanguage(),此时 C 的计数器再 +1 ),这时如果调用del clangs
语句,只会导致 C 的计数器减 1(值变为 1),因为 C 的计数器值不为 0,因此 C 不会被销毁(不会执行 __del__()
方法)。
因此,修改至如下代码:
class CLanguage:def __init__(self):print("调用 __init__() 方法构造对象")def __del__(self):print("调用__del__() 销毁对象,释放其空间")clangs = CLanguage()
# 添加一个引用clangs对象的实例对象
cl = clangs
del clangs
print("hihi")
del cl
print("hihi")
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_advance-main\metaclass\__main__.py 调用 __init__() 方法构造对象 hihi 调用__del__() 销毁对象,释放其空间
Process finished with exit code 0
可以看到,当执行 del cl 语句时,其应用的对象实例对象 C 的计数器继续 -1(变为 0),对于计数器为 0 的实例对象,Python 会自动将其视为垃圾进行回收。
此外,还需注意继承中 __del__
方法的细节,如果我们重写子类的 __del__()
方法(父类为非 object 的类),则必须显式调用父类的 __del__()
方法,这样才能保证在回收子类对象时,其占用的资源(可能包含继承自父类的部分资源)能被彻底释放。
示例代码如下:
class CLanguage:def __del__(self):print("调用父类 __del__() 方法")class cl(CLanguage):def __del__(self):print("调用子类 __del__() 方法")# 注意:面对带有继承对象进行__del__操作时,必须先调用父类的 __del__,再调用子类的__del__,
# 而且,父类__del__的调用必须用非绑定方法进行调用。只有这样才能“干净”地将对象删除。
c = cl()
CLanguage.__del__(c)
del c
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_advance-main\metaclass\__main__.py 调用父类 __del__() 方法 调用子类 __del__() 方法
Process finished with exit code 0
7.property类
Key Point
- 防止实例变量被外部错误修改
- setter和getter方法的编写
- property类的引入
防止实例变量被外部错误修改
class Student:def __init__(self, name:str, age:int )self.name = nameself.age = agestd_1 = Student('lxs',18)
std_1.age = -2 # 对于年龄这个变量来说是一个明显错误的值,防止这种错误的外部修改
编写setter和getter方法
class Student:def __init__(self, name:str, age:int)self.name = nameself.__age = age # 定义为私有属性防止外部错误修改def set_age(self,age) # 编写setter方法来防止外部错误修改if age < 0:raise Exception("age cannot be less than 0")self.__age = agedef get_age(self):return self.__agestd_1 = Student("Jack",16)
std_1.set_age(-2) # 弹出异常
std_1.set_age(3)
print(std_1.get_age()) # return 3
通过编写setter和getter方法防止实例变脸被外部错误修改。调用时仍然需要使用setter和getter方法名来完成调用。
property类的引入
class Student:def __init__(self, name: str, age: int):self.name = nameself.__age = agedef get_age(self):return self.__agedef set_age(self, age):if age < 0 or age > 200:raise Exception(f"Age {age} is not valid")self.__age = ageage = property(fget=get_age, fset=set_age) # 引入property类def main():std_1 = Student("Nick", 18)print(std_1.age) # 执行age的fget中的函数std_1.age = -3 # 执行age的fset中的函数
此时的property理解为中间代理,只要是调用age私有属性便执行fget中的get_age,但凡是设置age私有属性便执行fset中的set_age。这就是使用property类解决代码不兼容问题。
8.property装饰器
Key Point
- @property装饰器
- @property.setter装饰器
@property装饰器
既要保护类的封装特性,又要让开发者可以使用“对象.属性”的方式操作操作类属性,除了使用 property() ,Python 还提供了 @property 装饰器。通过 @property 装饰器,可以直接通过方法名来访问方法,不需要在方法名后添加一对“()”小括号。
class Student:def __init__(self, name: str, age: int):self.name = nameself.__age = age # 私有属性@property # 使用property装饰器def age(self):return self.__agedef __str__(self):return f"{self.name}, {self.__age}"def main():student = Student("Jack", 18)print(student.age)
上面程序中,使用 @property 修饰了 age() 方法,这样就使得该方法变成了 age私有属性的 getter 方法。需要注意的是,如果类中只包含该方法,那么 age私有 属性将是一个只读属性。也就是说,在使用 Student类时,无法对 age 属性重新赋值,即运行如下代码会报错。
student.age = 3 # return AttributeError: can't set attribute
@property 的语法格式
@property
def 方法名(self):
代码块
@property.setter装饰器
class Student:def __init__(self, name: str, age: int):self.name = nameself.__age = age@property def age(self):return self.__age@age.setter # setter装饰器 def age(self, age: int):if age < 0 or age > 200:raise Exception(f"Age {age} is not valid")self.__age = agedef __str__(self):return f"{self.name}, {self.__age}"def main():student = Student("Jack", 18)student.age = -3print(student.age)
而要想实现修改 age 属性的值,还需要为 area 属性添加 setter 方法,就需要用到 setter 装饰器,它的语法格式如下。
@方法名.setter
def 方法名(self, value):
代码块
值得注意的是,此时 age 属性就有了 getter 和 setter 方法,该属性就变成了具有读写功能的属性。
删除与读取 property
只读property(小练习)
通过一个简短的练习,进一步理解property的使用。
Key Point
- 标记一个方法为property
- 缓存property的值
设计一个正方形的类,能够缓存面积,并使用将所有属性定义为私有属性,通过property装饰器实现私有属性的调用与赋值。
class Square:def __init__(self, width):self.__width = widthself.__area = None@propertydef width(self):return self.__width@width.setterdef width(self, width):self.__width = widthself.__area = None@property # 实现私有属性面积的缓存def area(self):if self.__area is None:self.__area = self.__width * self.__widthreturn self.__areadef main():square = Square(5)print(square.area)square.width = 6print(square.area)
删除property
@property.deleter装饰器,实现删除property的操作,默认的@property.deleter执行的是del的相关操作,当然也可以自行重构,现在不执行del操作,执行清空操作的相关代码如下:
class Square:def __init__(self, width):self.__width = widthself.__area = None@propertydef width(self):return self.__width@width.setterdef width(self, width):self.__width = width@propertydef area(self):if self.__area is None:self.__area = self.__width * self.__widthreturn self.__area@area.deleter # 使用@property.deleter装饰器def area(self):self.__area = None # 清空私有属性areadef main():square = Square(5)print(square.area)square.width = 6del square.area # 调用@area.deleter所装饰的函数print(square.area)
10.运算符重载
可重载的运算符
重载运算符,指的是在类中定义并实现一个与运算符对应的处理方法,这样当类对象在进行运算符操作时,系统就会调用类中相应的方法来处理。
列表类型支持直接做加法操作实现添加元素的功能,字符串类型支持直接做加法实现字符串的拼接功能,同样的运算符对于不同序列类型的意义是不一样的,这都归功于可重载的运算符。
重载运算的一个栗子:
class MyClass: #自定义一个类def __init__(self, name , age): #定义该类的初始化函数self.name = name #将传入的参数值赋值给成员交量self.age = agedef __str__(self): #用于将值转化为字符串形式,等同于 str(obj)return "name:"+self.name+";age:"+str(self.age)__repr__ = __str__ #转化为供解释器读取的形式def __lt__(self, record): #重载 self<record 运算符if self.age < record.age:return Trueelse:return Falsedef __add__(self, record): #重载 + 号运算符return MyClass(self.name, self.age+record.age)myc = MyClass("Anna", 42) #实例化一个对象 Anna,并为其初始化
mycl = MyClass("Gary", 23) #实例化一个对象 Gary,并为其初始化
print(repr(myc)) #格式化对象 myc,
print(myc) #解释器读取对象 myc,调用 repr
print (str (myc)) #格式化对象 myc ,输出"name:Anna;age:42"
print(myc < mycl) #比较 myc<mycl 的结果,输出 False
print (myc+mycl) #进行两个 MyClass 对象的相加运算,输出 "name:Anna;age:65"
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
name:Anna;age:42
name:Anna;age:42
name:Anna;age:42
False
name:Anna;age:65Process finished with exit code 0
常用的重载运算符
重载运算符 | 含义 |
---|---|
_new_ | 创建类,在 _init_ 之前创建对象 |
_init_ | 类的构造函数,其功能是创建类对象时做初始化工作 ,类似于x = classname() |
_del_ | 析构函数,其功能是销毁对象时进行回收资源的操作 |
_add_ | 加法运算符 +,当类对象 X 做例如 X+Y 或者 X+=Y 等操作,内部会调用此方法。但如果类中对_iadd_ 方法进行了重载,则类对象 X 在做 X+=Y 类似操作时,会优先选择调用 __iadd__ 方法。 |
_radd_ | 类对象 X 做类似 Y+X 的运算时,会调用此方法。 |
_iadd_ | 重载 += 运算符,也就是说,当类对象 X 做类似 X+=Y 的操作时,会调用此方法。 |
_or_ | 或”运算符 |,如果没有重载 _ior_,则在类似 X|Y、X|=Y 这样的语句中,“或”符号生效 |
__repr__,__str__ | 格式转换方法,分别对应函数 repr(X)、str(X) |
_call_ | 函数调用,类似于 X(*args, **kwargs) 语句 |
_getattr_ | 点号运算,用来获取类属性 |
_setattr_ | 属性赋值语句,类似于 X.any=value |
_delattr_ | 删除属性,类似于 del X.any |
_getattribute_ | 获取属性,类似于 X.any |
_getitem_ | 索引运算,类似于 X[key],X[i:j] |
_setitem_ | 索引赋值语句,类似于 X[key], X[i:j]=sequence |
_delitem_ | 索引和分片删除 |
__get__, __set__, __delete__ | 描述符属性,类似于 X.attr,X.attr=value,del X.attr |
__len__ | 计算长度,类似于 len(X) |
__lt__,__gt__,__le__,__ge__,__eq__,__ne__ | 比较,分别对应于 <、>、<=、>=、=、!= 运算符 |
__iter__,__next__ | 迭代环境下,生成迭代器与取下一条,类似于 I=iter(X) 和 next() |
__contains__ | 成员关系测试,类似于 item in X |
__index__ | 整数值,类似于 hex(X),bin(X),oct(X) |
__enter__,__exit__ | 在对类对象执行类似 with obj as var 的操作之前,会先调用 __enter__ 方法,其结果会传给 var;在最终结束该操作之前,会调用 __exit__ 方法(常用于做一些清理、扫尾的工作) |
重载运算符实现自定义序列
Python 中,可以通过重写几个特殊方法,实现自定义一个序列类。下表展示出自定义序列相关的特殊方法。
方法名 | 功能 |
---|---|
_len_(self) | 返回序列类中存储元素的个数。 |
_contains_(self, value) | 判断当前序列中是否包含 value 这个指定元素。 |
_getitem_(self, key) | 通过指定的 key(键),返回对应的 value(值)。 |
_setitem_(self, key,value) | 修改指定 key(键)对应的 value(值)。 |
_delitem_(self, key) | 删除指定键值对。 |
注意,在对上表中的这些特殊方法进行重写时,在实现其基础功能的基础上,还可以根据实际情况,对各个方法的具体实现进行适当调整。以 _setitem_() 方法为例,当在序列中未找到指定 key 的情况下,该方法可以报错,当然也可以将此键值对添加到当前序列中。
在实现自定义序列类时,并不是必须重写表 1 中全部的特殊方法。如果该自定义序列是一个不可变序列(即序列中的元素不能做修改),则无需重写 _setitem__() 和 __delitem_() 方法;反之,如果该自定义序列是一个可变序列,可以重写以上 5 个特殊方法。
练习:
编写一个简短序列类,要求此类是一个字典,只能存储 int 类型的元素:
class IntDic:def __init__(self):# 用于存储数据的字典self.__date = {}def __contains__(self, value):return value in self.__date.values()def __len__(self):return len(list(self.__date.values()))def __getitem__(self, key):# 如果在self.__changed中找到已经修改后的数据if key in self.__date:return self.__date[key]return Nonedef __setitem__(self, key, value):# 判断value是否为整数if not isinstance(value, int):raise TypeError('必须是整数')# 修改现有 key 对应的 value 值,或者直接添加self.__date[key] = valuedef __delitem__(self, key):if key in self.__date: del self.__date[key]dic = IntDic()
# 输出序列中元素的个数,调用 __len__() 方法
print(len(dic))
# 向序列中添加元素,调用 __setitem__() 方法
dic['a'] = 1
dic['b'] = 2print(len(dic))
dic['a'] = 3
dic['c'] = 4
print(dic['a'])
# 删除指定元素,调用 __delitem__() 方法
del dic['a']
print(dic['a'])
print(len(dic))
# 判断是否拥有元素
print(dic.__contains__(2))
print(2 in dic) # 可见 in 会调用 __contains__() 方法
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\11.py
0
2
3
None
2
True
TrueProcess finished with exit code 0
9.类的常用特殊内置属性和方法总结
- __foo__: 定义的是特殊方法,一般是系统定义名字 ,类似 __init__() 之类的。
- _foo: 以单下划线开头的表示的是 protected 类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *
- __foo: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问。
特殊属性
Class._doc_ # 类型帮助信息 ‘Class1 Doc.’
Class.__name__ # 类型名称 ‘Class1’
Class.__module__ # 类型所在模块 ‘main’
Class.bases # 类型所继承的基类 (<type ‘object’>,)
Class.dict # 类型字典,存储所有类型成员信息。 <dictproxy object at 0x00D3AD70>
obj.class # 类型 <class ‘main.Class1’>
obj.module # 实例类型所在模块 ‘main’
obj.dict # 对象字典,存储所有实例成员信息。 {‘i’: 1234}
示例代码如下:
class A:'''A的帮助信息''' #给__doc__使用def __init__(self, name, price):self.name = nameself.price = pricedef foo(self):passdef bar(self):passa = A('口罩', 0.5)
#-----------------class---------------------
print(A.__doc__) #类的帮助信息
print(A.__name__) # 类的名字:A
print(A.__module__) ## __main__
print(A.__base__) ## A的基类:<class 'object'>
print(A.__dict__) #类别字典,类存储的成员信息#---------------------obj-----------------------
print(a.__class__) #<class '__main__.A'>
print(a.__module__) # __main__
print(a.__dict__) #对象字典{'name': '口罩', 'price': 0.5}
类的基础方法
目的 | 代码 | 实际调用 |
---|---|---|
初始化一个实例 | x = MyClass() | x.__init__() |
字符串的“官方”表现形式 | repr(x) | x.__irepr__() |
字符串的“非正式”值 | str(x) | x.__str__() |
字节数组的“非正式”值 | bytes(x) | x.__ibytes__() |
格式化字符串的值 | format(x, format_spec) | x.__iformat__(format_spec) |
与迭代器类似(__iter__ 与 __next__ 方法)
目的 | 代码 | 实际调用 |
---|---|---|
遍历某个序列 | iter(seq) | seq.__iter__() |
从迭代器中获取下一个值 | next(seq) | seq.__next__() |
按逆序创建一个迭代器 | reversed(seq) | seq.__reversed__() |
行为方式与函数类似(__call__方法)
目的 | 代码 | 实际调用 |
---|---|---|
可调用对象 | my_instance() | my_instance.__call__() |
行为方式与序列类似
目的 | 代码 | 实际调用 |
---|---|---|
序列长度 | len(seq) | seq.__len__() |
判断是否是否包含特点value | x in seq | seq.__contains__(x) |
行为方式与字典类似
目的 | 代码 | 实际调用 |
---|---|---|
通过键来获取值 | x[key] | x.__getitem__(key) |
通过键来设置值 | x[key] = value | x.__setitem__(key, value) |
删除一个键值对 | del x[key] | x.__delitem__(key) |
为缺失键提供默认值 | x[nonexistent_key] | x.__missing__(nonexistent_key) |
可比较的类
目的 | 代码 | 实际调用 |
---|---|---|
相等 | x == y | x._eq_(y) |
不相等 | x != y | x._ne_(y) |
小于 | x < y | x._lt_(y) |
小于或等于 | x <= y | x._le_(y) |
大于 | x > y | x._gt_(y) |
大于或等于 | x >= y | x._ge_(y) |
布尔上上下文环境中的真值 | if x: | x._bool_() |
属性操作
目的 | 代码 | 实际调用 |
---|---|---|
获取一个计算属性(无条件的) | x.my_property | x.__getattribute__(‘my_property’) |
获取一个计算属性(后备) | x.my_property | x.__getattr__(‘my_property’) |
设置某属性 | x.my_property = value | x.__setattr__(‘my_property’,value) |
删除某属性 | del x.my_property | x.__delattr__(‘my_property’) |
列出所有属性和方法 | dir(x) | x.__dir__() |
11.类的继承
Key Point
- 类继承机制和定义
- 类继承的机制
- 类继承内部过程
- isintance() 和 isssubclass()
类继承机制
继承机制经常用于创建和现有类功能类似的新类,又或是新类只需要在现有类基础上添加一些成员(属性和方法),但又不想直接将现有类代码复制给新类。也就是说,通过使用继承这种机制,可以轻松实现类的重复使用。
举个例子,假设现有一个 Shape 类,该类的 draw() 方法可以在屏幕上画出指定的形状,现在需要创建一个 Form 类,要求此类不但可以在屏幕上画出指定的形状,还可以计算出所画形状的面积。要创建这样的类,笨方法是将 draw() 方法直接复制到新类中,并添加计算面积的方法。实现代码如下所示:
class Shape:def draw(self,content):print("画",content)
class Form:def draw(self,content):print("画",content)def area(self):#....print("此图形的面积为..."
当然还有更简单的方法,就是使用类的继承机制。实现方法为:让 Form 类继承 Shape 类,这样当 Form 类对象调用 draw() 方法时,Python 解释器会先去 Form 中找以 draw 为名的方法,如果找不到,它还会自动去 Shape 类中找。如此,我们只需在 Form 类中添加计算面积的方法即可,示例代码如下:
class Shape:def draw(self,content):print("画",content)
class Form(Shape):def area(self):#....print("此图形的面积为...")
上面代码中,class Form(Shape) 就表示 Form 继承 Shape。
类继承定义
Python 中,实现继承的类称为子类,被继承的类称为父类(也可称为基类、超类)。因此在上面这个样例中,Form 是子类,Shape 是父类。
子类继承父类时,只需在定义子类时,将父类(可以是多个)放在子类之后的圆括号里即可。语法格式如下:
class 类名(父类1, 父类2, …):
#类定义部分
注意,如果该类没有显式指定继承自哪个类,则默认继承 object 类(object 类是 Python 中所有类的父类,即要么是直接父类,要么是间接父类)。另外,Python 的继承是多继承机制(和 C++ 一样),即一个子类可以同时拥有多个直接父类。
“派生”这个词汇,它和继承是一个意思,只是观察角度不同而已。换句话话,继承是相对子类来说的,即子类继承自父类;而派生是相对于父类来说的,即父类派生出子类。
类继承内部过程
class Person:def __init__(self):print("Person: init is called")self.name = "Jack"class Student(Person):def __init__(self):print("Student: init is called")self.school = "abc"class Child(Person):passdef main():child_1 = Child()std_1 = Student()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_11\test.py
Person: init is called
Student: init is called
Student类继承自Person类,但没有执行Person的构造函数,Child类继承自Person类,执行了Student的构造函数,在这里说明两点:
- 类在继承过程中,子类如果没有定义自己的构造函数,默认执行父类的构造函数。
- 子类继承父类即继承父类的所有成员(属性与方法),仅仅使用
class 类名(父类1, 父类2, ...):
还不能实现真正的继承过程。
需要加入super函数后,才能实现完整的继承过程。代码如下:
class Person:def __init__(self):print("Person: init is called")self.name = "Jack"class Student(Person):def __init__(self):super().__init__()print("Student: init is called")self.school = "abc"class Child(Person):passdef main():child_1 = Child()std_1 = Student()print(std_1.name)
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_11\test.py
Person: init is called
Person: init is called
Student: init is called
Jack
####isintance() 和 isssubclass()
class Person:def __init__(self):print("Person:init is called")self.name = "Jack"class Student(Person):def __init__(self):super().__init__()print("Student:init is called")self.school = "Abc"class Stone:passdef main():student = Student()print(student.name)print(student.school)print(isinstance(student, Student)) # 判断student对象是否是Student的实例print(isinstance(student, Person)) # 判断student对象是否是Person的实例person = Person()print(isinstance(person, Student)) # 判断person对象是否是Student的实例print(issubclass(Student, Person)) # 判断Student类是否是Perosn的子类print(issubclass(Person, Student)) # 判断Person类是否是Student的子类stone = Stone() print(issubclass(Stone, Student)) # 判断Stone类是否是Student的子类print(isinstance(stone, Person)) # 判断stone对象是否是Person的实例
输出结果
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_11\main.py
Person:init is called
Student:init is called
True
True
Person:init is called
False
True
False
False
False
12.类中方法与属性的重写
Key Point
- 方法重写
- 类属性重写
- 类内部调用被重写的父类方法
- 类外部调用被重写的父类方法
方法重写
Python 中,子类继承了父类,那么子类就拥有了父类所有的类属性和类方法。通常情况下,子类会在此基础上,扩展一些新的类属性和类方法。但是对于某些特殊情况,,即子类从父类继承得来的类方法中,大部分是适合子类使用的,但有个别的类方法,并不能直接照搬父类的,如果不对这部分类方法进行修改,子类对象无法使用。针对这种情况,我们就需要在子类中重写父类的方法。
举个例子,鸟通常是有翅膀的,也会飞,因此我们可以像如下这样定义个和鸟相关的类:
class Bird:#鸟有翅膀def isWing(self):print("鸟有翅膀")#鸟会飞def fly(self):print("鸟会飞")
但是,对于鸵鸟来说,它虽然也属于鸟类,也有翅膀,但是它只会奔跑,并不会飞。针对这种情况,可以这样定义鸵鸟类:
class Ostrich(Bird):# 重写Bird类的fly()方法def fly(self):print("鸵鸟不会飞")
综上,方法重写特指在子类中重新定义父类的方法。以下面代码为例展示了重写方法后,方法的调用顺序。
class Person:def __init__(self):self.name = "Jack"def say(self):print("Hello from person")class Student(Person):def __init__(self):self.school = "Abc"def say(self):print("Hello from student")class Worker(Person):passdef main():student = Student()student.say()person = Person()person.say()person = Student()person.say()person = Worker()person.say()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_12\test.py
Hello from student
Hello from person
Hello from student
Hello from person
从上述代码中看出,子类中重写父类中的方法后,子类对象可以调用重写后的方法。如果,子类中不对方法进行重写,则仍然调用父类的方法。
类属性重写
class Person:color = 1def __init__(self):self.name = "Jack"def say(self):print("Hello from person")def print_color(self):print(self.color)class Student(Person):color = 2 # 重写类属性def __init__(self):super().__init__()self.school = "Abc"def say(self):print("Hello from student")class Worker(Person):passdef main():student = Student()print(student.color) # return 2student.print_color() # return 2person = Person()print(person.color) # return 1person = Worker()print(person.color) # return 1
输出出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_12\test.py
2
2
1
1
综上,无论是类属性重写还是类方法重写,子类优先调用重写的类属性与类方法,如果没有在到父类中去调用的类属性与类方法。
类内部调用被重写的父类方法
class Person:color = 1def __init__(self):self.name = "Jack"def say(self):print("Hello from person")class Student(Person):color = 2def __init__(self):super().__init__()self.school = "Abc"def say(self):super().say()print("Hello from student")
子类中可以通过super来调用父类的方法。
类外部调用被重写的父类方法
如果我们在子类中重写了从父类继承来的类方法,那么当在类的外部通过子类对象调用该方法时,Python 总是会执行子类中重写的方法。如果想调用父类中被重写的这个方法,该怎么办呢?
Python 中的类可以看做是一个独立空间,而类方法其实就是出于该空间中的一个函数。而如果想要全局空间中,调用类空间中的函数,只需要在调用该函数时备注类名即可。具体操作如下:
class Bird:#鸟有翅膀def isWing(self):print("鸟有翅膀")#鸟会飞def fly(self):print("鸟会飞")
class Ostrich(Bird):# 重写Bird类的fly()方法def fly(self):print("鸵鸟不会飞")# 创建Ostrich对象
ostrich = Ostrich()#调用 Bird 类中的 fly() 方法
Bird.fly(ostrich)
注意的一点是,使用类名调用其类方法,Python 不会为该方法的第一个 self 参数自定绑定值,因此采用这种调用方法,需要手动为 self 参数赋值。通过类名调用实例方法的这种方式,又被称为未绑定方法。
13.抽象的类
Key Point:
- 抽象类和方法概念
- 抽象类的定义
- 抽象方法的定义
抽象类的概念
抽象类是包含一个或多个抽象方法的类。 抽象方法是已声明但不包含任何实现的方法。 抽象类无法实例化,并且需要子类来提供抽象方法的实现。下面给出一个不是抽象类的例子:
class AbstractClass:def do_something(self):passclass B(AbstractClass):passa = AbstractClass()
b = B()
之所以不是抽象类,是因为如下原因:
- 可以从AbstractClass实例化一个实例
- 不需要在B的类定义中实现do_something
抽象类的定义
Python本身并不提供抽象类。 但是,Python附带了一个模块,该模块提供了用于定义抽象基类(ABC,Abstract Base Class)的基础结构。此模块称为abc。包含有抽象方法的抽象类本身无法实例化,通过以下代码来证明:
# action.py
from abc import ABC, abstractmethodclass Action(ABC):@abstractmethoddef execute(self):pass
# main.py
from action.action import Actiondef main():act_1 = Action()if __name__ == "__main__":main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
Traceback (most recent call last):
File “E:\python\code\pythonProject\main.py”, line 9, in
main()
File “E:\python\code\pythonProject\main.py”, line 5, in main
act_1 = Action()
TypeError: Can’t instantiate abstract class Action with abstract method executeProcess finished with exit code 1
抽象类方法的定义
抽象类类方法需要使用@abstractmethod进行装饰,抽象方法是一个没有具体实现的方法,该种方法只有函数名和返回值但没有函数体。具体例子如下:
from abc import ABC, abstractmethodclass Action(ABC):@abstractmethoddef excute(self):pass
注意:除非所有抽象方法都被重写,否则不能实例化从一个抽象类派生的类。通过以下代码来证明:
# action.py
from abc import ABC, abstractmethodclass Action(ABC):@abstractmethoddef play_football(self):pass@abstractmethoddef play_basketball(self):pass
# play.py
from action.action import Actionclass Play(Action):def play_football(self):print("play football")
# main.py
from action.play import Playdef main():play_1 = Play()play_1.play_football()if __name__ == "__main__":main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
Traceback (most recent call last):
File “E:\python\code\pythonProject\main.py”, line 10, in
main()
File “E:\python\code\pythonProject\main.py”, line 5, in main
play_1 = Play()
TypeError: Can’t instantiate abstract class Play with abstract method play_basketballProcess finished with exit code 1
下面,使用正确的方法来定义抽象类方法,代码如下:
# action.py 定义抽象类
from abc import ABC, abstractmethodclass Action(ABC):@abstractmethoddef execute(self):pass
# create_student_action.py
from action.action import Actionclass CreateStudentAction(Action):def execute(self):print("Create a new student")
# delete_student_actioin.py
from action.action import Actionclass DeleteStudentAction(Action):def execute(self):print("Delete a student")
# main.py
from action.action import Action
from action.create_student_action import CreateStudentAction
from action.delete_student_action import DeleteStudentActiondef execute_action(action: Action): #抽象类的设计模式具备多人联合编程的优势action.execute()def main():create_student_action = CreateStudentAction()delete_student_action = DeleteStudentAction()execute_action(create_student_action)execute_action(delete_student_action)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_13\main.py
Create a new student
Delete a studentProcess finished with exit code 0
综上述,抽象类可以理解为一种定义规范。此外,子类继承父类,如果父类是抽象类,子类如果不改动则仍是抽象类。
14.枚举类
Key Point
- 枚举类概念
- 枚举类定义
- 枚举类调用
- 枚举类注意事项
- Enum() 函数创建枚举类
枚举类概念
枚举类实例化对象个数固定,是一种具有特殊含义的类,例如:一个类表示月份,则该类的实例对象最多有12个;再比如一个类表示性别,则该类的实例对象最多有2个。
枚举类的作用在于提高代码的可读性,并且能够提供一种利于差错的方法。
枚举类定义
通过继承enum.Enum类定义一个枚举类型,通过一个例子说明,代码如下:
from enum import Enum
class Color(Enum):red = 1 # 成员1green = 2 # 成员2blue = 3 # 成员3
将一个类定义为枚举类,只需要令其继承自 enum 模块中的 Enum 类即可。例如在上面程序中,Color 类继承自 Enum 类,则证明这是一个枚举类。
在Color枚举类中,red、green、blue 都是该类的成员(可以理解为是类变量)。注意,枚举类的每个成员都由 2 部分组成,分别为 name 和 value,其中 name 属性值为该枚举值的变量名(如 red),value 代表该枚举值的序号(序号通常从 1 开始)。
注意:区别于传统的普通类,枚举类不能用于实例化对象,通过一下代码来证明:
from enum import Enumclass Color(Enum):red = 1green = 2blue = 3def main():colo_1 = Color()if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_13\main.py
Traceback (most recent call last):
File “E:\python\code\github\python_oop-main\chapter_13\main.py”, line 15, in
main()
File “E:\python\code\github\python_oop-main\chapter_13\main.py”, line 11, in main
colo_1 = Color()
TypeError:__call__()
missing 1 required positional argument: ‘value’Process finished with exit code 1
枚举类调用
虽然枚举类不能够实例化对象,但是我们仍然可以访问其中的成员,通过一下几种方式实现枚举类中成员的调用:
from enum import Enumclass Color(Enum):red = 1green = 2blue = 3def main():# 调用枚举成员的 3 种方式print(Color.red)print(Color['red'])print(Color(1))# 调取枚举成员中的 name 和 valueprint(Color.red.value)print(Color.red.name)# 遍历枚举类中所有成员的 2 种方式for color in Color:print(color)for name,member in Color.__members__.items(): print(name,"->",member)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_13\main.py
Color.red
Color.red
Color.red
1
red
Color.red
Color.green
Color.blue
red -> Color.red
green -> Color.green
blue -> Color.blueProcess finished with exit code 0
枚举类成员的变量名就是它的name,所赋值的整数就是它的value,当我们拿到字符串的name或整数的value,就可以拿到对应的枚举成员。由于枚举类本身是可以迭代的,因此可使用for循环的方式遍历其中的每一个成员,此外,该枚举类还提供了一个 members 属性,该属性是一个包含枚举类中所有成员的字典,通过遍历该属性,也可以访问枚举类中的各个成员。
对于枚举类的 members 属性,当出现有多个别名成员时,别名成员输出的name和value与原始成员保持一致。例子如下:
from enum import Enumclass Status(Enum):SUCCESS = 1OK = 1FAIL = 2WRONG = 2def main():for s in Status:print(s.name)print(Status.__members__)print(Status.SUCCESS == Status.OK)print(Status.SUCCESS is Status.OK)if __name__ == "__main__":main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
SUCCESS
FAIL
{‘SUCCESS’: <Status.SUCCESS: 1>, ‘OK’: <Status.SUCCESS: 1>, ‘FAIL’: <Status.FAIL: 2>, ‘WRONG’: <Status.FAIL: 2>}
True
TrueProcess finished with exit code 0
枚举类注意事项
第一,枚举类成员之间不能比较大小,但可以使用 == 或 is进行比较是否相等。如下:
print(Color.red == Color.green) # rerurn : False
print(Color.red.name is Color.green.name) # return : True
第二,枚举类中各成员的值不能在类外部做任何修改,因此下列代码是不被允许的:
Color.red = 4
第三,Python枚举类中各个成员必须保证 name 互不相同,但 value 可以相同。如下:
from enum import Enumclass Color(Enum):# 为序列值指定value值red = 1green = 1blue = 3def main():print(Color['green'])if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_13\main.py
Color.redProcess finished with exit code 0
Color 枚举类中 red 和 green 具有相同的值(都是 1),Python 允许这种情况的发生,它会将 green 当做是 red 的别名,因此当访问 green 成员时,最终输出的是 red。
因此,在实际编程过程中,为了避免发生这种情况,使用@unique装饰器,当枚举类中出现相同值得成员时,程序会出现如下错误。
from enum import Enum, unique # 记得import unique@unique
class Color(Enum):# 为序列值指定value值red = 1green = 1blue = 3def main():print(Color['green'])if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_13\main.py
Traceback (most recent call last):
File “E:\python\code\github\python_oop-main\chapter_13\main.py”, line 5, in
class Color(Enum):
File “D:\ProgramData\Anaconda3\lib\enum.py”, line 1013, in unique
raise ValueError(‘duplicate values found in %r: %s’ %
ValueError: duplicate values found in <enum ‘Color’>: green -> redProcess finished with exit code 1
Enum() 函数创建枚举类
除了通过继承 Enum 类的方法创建枚举类,还可以使用 Enum() 函数创建枚举类。Enum() 函数可接受 2 个参数,第一个用于指定枚举类的类名,第二个参数用于指定枚举类中的多个成员。如下:
from enum import Enum#创建一个枚举类
Color = Enum("Color",('red','green','blue'))#调用枚举成员的 3 种方式
print(Color.red)
print(Color['red'])
print(Color(1))#调取枚举成员中的 value 和 name
print(Color.red.value)
print(Color.red.name)#遍历枚举类中所有成员的 2 种方式
for color in Color:print(color)for name,member in Color.__members__.items(): print(name,"->",member)
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_13\main.py
Color.red
Color.red
Color.red
1
red
Color.red
Color.green
Color.blue
red -> Color.red
green -> Color.green
blue -> Color.blueProcess finished with exit code 0
15.定制和扩展枚举
Key Point
- 定制
__str__
函数- 定制
__eq__
函数- 定制
__lt__
函数- auto()函数
定制__str__
函数
打印一个实例化对象时,打印的其实时一个对象的地址。而通过__str__()
函数就可以帮助我们打印对象中具体的属性值,或者你想得到的东西。
因为再python中调用print()打印实例化对象时会调用__str__()
如果__str__()
中有返回值,就会打印其中的返回值,通过以下例子定制枚举类__str__
函数。
from enum import Enumclass Gender(Enum):MALE = 1FEMALE = 2def __str__(self):return f"{self.name}({self.value})"def main():for i in Gender:print(i)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
MALE(1)
FEMALE(2)Process finished with exit code 0
定制__eq__
函数
定制枚举类的__eq__
函数,其作用在于可以自行定义相等函数,根据使用者的需求定义相等的逻辑,例子如下:
from enum import Enum, unique@unique
class Gender(Enum):MALE = 1FEMALE = 2def __eq__(self, other):if isinstance(other, int):return self.value == otherif isinstance(other, str):return self.value == other.upper()if isinstance(other, Gender):return self is otherdef main():print(Gender.MALE == 1)print(Gender.MALE == "female")if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
True
FalseProcess finished with exit code 0
定制__lt__
函数
定制枚举类的__lt__
函数需要在定义函数前用@total_orderting进行装饰,__lt__
方法是用于定义或实现小于运算符 “<” 功能的魔术方法,定制后的__lt__
函数可以自定义运算符 ”<“ 的逻辑。例子如下:
from enum import Enum, unique
from functools import total_ordering@unique
@total_ordering
class WorkFlowStatus(Enum):OPEN = 1IN_PROGRESS = 2REVIEW = 3COMPLETE = 4def __lt__(self, other):if isinstance(other, int):return self.value < otherif isinstance(other, WorkFlowStatus):return self.value < other.valuedef main():print(WorkFlowStatus.IN_PROGRESS < 2)print(WorkFlowStatus.IN_PROGRESS < WorkFlowStatus.COMPLETE)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
False
TrueProcess finished with exit code 0
auto()函数
当需要自动给枚举成员自动赋值时,使用 auto() 函数。示例代码如下:
from enum import Enum, unique, auto@unique
class WorkFlowStatus(Enum):OPEN = auto()IN_PROGRESS = auto()REVIEW = auto()COMPLETE = auto()def main():for name, member in WorkFlowStatus.__members__.items():print(name, "->", member)print(member.value)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
OPEN -> WorkFlowStatus.OPEN
1
IN_PROGRESS -> WorkFlowStatus.IN_PROGRESS
2
REVIEW -> WorkFlowStatus.REVIEW
3
COMPLETE -> WorkFlowStatus.COMPLETE
4Process finished with exit code 0
16. 多继承
Key Point
- 多继承的定义与概念
- 调用不同父类的方法
多继承的定义与概念
大部分面向对象的语言仅仅支持单继承,即子类只有一个父类,而 Python 支持多继承,多继承相对于单继承容易让代码逻辑复杂,建议尽量不要使用。
多继承需要解决两个问题,第一是面对子类有多个父类,父类包含同名类方法时的调用情况,Python 中的调用顺序:排在前面的父类中的类方法会覆盖在后面父类中的同名类方法。示例代码如下:
class Parent1:def __init__(self):print("paren1 called init")def render(self):print(f"parent 1")def hello(self):print(f"hello parent 1")class Parent2:def __init__(self):print("paren2 called init")def render(self):print(f"parent 2")def hello(self):print(f"hello parent 2 ")class Parent3(Parent1, Parent2):def __init__(self):passdef main():par_1 = Parent3()par_1.render()par_1.hello()if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
parent 1
hello parent 1Process finished with exit code 0
调用不同父类的方法
当子类继承多个父类,例如子类继承顺序为:父类1、父类2,如何调用父类1与父类2中的方法呢?
对于第一顺位的父类可以直接使用 super().method
实现调用,非第一顺位的父类通过 类名.方法(self)
实现调用,示例代码如下:
class Parent1:def __init__(self):print("paren1 called init")def render(self):print(f"parent 1")def hello(self):print(f"hello parent 1")class Parent2:def __init__(self):print("paren2 called init")def render(self):print(f"parent 2")def hello(self):print(f"hello parent 2 ")class Child(Parent1, Parent2):def __init__(self):Parent2.__init__(self)def render(self):super().render()def hello(self):Parent2.hello(self)def main():child = Child()child.hello()child.render()if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\pythonProject\main.py
paren2 called init
hello parent 2
parent 1Process finished with exit code 0
17.描述符(descriptor)
Key Point
- Python 中对象属性的访问顺序
- 描述符概念
- 描述符的魔法方法
- 描述符中的细节问题
对象属性访问顺序
对象属性的访问按照如下顺序(无描述符时):
- 实例属性
- 类属性
- 父类属性
__getattr__()
方法
下面通过一个例子说明实例属性和类属性遇到访问与修改时二者的运行顺序:
class Student(object):cls_val = 1def __init__(self):self.ins_val = 2def main():st_1 = Student()print(f"不更改实例属性")print(Student.__dict__)print(st_1.__dict__)st_1.cls_val = 3print(f"更改实例属性,该实例属性与类型属性同名")print(Student.__dict__)print(st_1.__dict__)Student.cls_val = 0print(f"更改同名类属性")print(Student.__dict__)print(st_1.__dict__)if __name__ == "__main__":main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\main.py
不更改实例属性
{'__module__'
: ‘main’, ‘cls_val’: 1,'__init__'
: <function Student.__init__
at 0x0000020263E7D280>,'__dict__'
: <attribute'__dict__'
of ‘Student’ objects>,'__weakref__'
: <attribute'__weakref__'
of ‘Student’ objects>,'__doc__'
: None}
{‘ins_val’: 2}
更改实例属性,该实例属性与类型属性同名
{'__module__'
:'__main__'
, ‘cls_val’: 1,'__init__'
: <function Student.init at 0x0000020263E7D280>,'__dict__'
: <attribute'__dict__'
of ‘Student’ objects>,'__weakref__'
: <attribute'__weakref__'
of ‘Student’ objects>,'__doc__'
: None}
{‘ins_val’: 2, ‘cls_val’: 3}更改同名类属性
{'__module__'
:'__main__'
, ‘cls_val’: 0,'__init__'
: <function Student.__init__
at 0x00000240C23ED670>,'__dict__'
: <attribute'__dict__'
of ‘Student’ objects>,'__weakref__'
: <attribute'__weakref__'
of ‘Student’ objects>,'__doc__'
: None}
{‘ins_val’: 2, ‘cls_val’: 3}Process finished with exit code 0
上述代码 14~17 行说明,增加一个与类属性同名的实例属性并不影响对应的类属性,仅仅是在对象属性新增了一个同名的属性而已。
上述代码 19~22 行说明,更改同名类属性不会影响对应实例中的值,因此,同名的类属性和实例属性互不影响(井水不犯河水)。
描述符概念
概念,python描述符是一个“绑定行为”的对象属性,在描述符协议中,它可以通过方法重写属性的访问。这些方法有 __get__()
, __set__()
, 和__delete__()
。如果这些方法中的任何一个被定义在一个对象中,这个对象就是一个描述符。
概念说的很繁琐,本质上看描述符是一个类,它定义了另一个类中属性的访问方式,简而言之描述类是一个可以全权负责属性管理的类。
描述符是 Python 中复杂属性访问的基础,它在内部被用于实现 property、方法、类方法、静态方法和 super 类型。
说了那么多,只要一个类中内部定义了__get__()
, __set__()
, 和__delete__()
中的一个或多个,就可以称为描述符。
描述符的魔法方法
方法的原型如下:
__get__(self, instance, owner)
__set__(self, instance, value)
__del__(self, instance)
值得探究的是其中的 self 、instance 、owner 分别代表什么,通过以下代码说明:
class Desc(object):def __get__(self, instance, owner):print("__get__...")print("self : \t\t", self)print("owner : \t", owner)print('='*40, "\n")def __set__(self, instance, value):print('__set__...')print("self : \t\t", self)print("instance : \t", instance)print("value : \t", value)class TestDesc(object):x = Desc()def main():t = TestDesc()t.xif __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
__get__
…
self : <__main__
.Desc object at 0x000001614A403FD0>
instance : <__main__
.TestDesc object at 0x000001614A403FA0>
owner : <class ‘__main__
.TestDesc’>Process finished with exit code 0`>
实例化类TestDesc后,调用对象t访问其属性x,会自动调用类Desc的 __get__
方法,由输出信息可以看出:
- self: Desc的实例对象,其实就是TestDesc的属性x。
- instance: TestDesc的实例对象,其实就是t。
- owner: 即谁拥有这些东西,当然是 TestDesc这个类,它是最高统治者,其他的一些都是包含在它的内部或者由它生出来的。
描述符中的细节问题
问题1
上述代码访问 t.x 时,为什么会直接调用描述符的 __get__()
方法?
解:t为实例,访问t.x时,根据常规顺序:
首先,访问的__getattribute__()
方法,如果没有重载则调用的基类object的__getattribute__()
方法,在到实例属性中查找是否有对应的属性名,发现没有,然后去类属型中,找到了,整个过程查找顺序如下。
- 调用基类的
__getattribute__()
返回属型值;- 实例属性的
__dict__
有没有x,没有;- 类属性的
__dict__
有没有x, 有;- 如果类属没有,则看父类的
__dict__
有没有,- 如果类属性没有,则执行
__getattr__()
方法
但是,判断属性 x 为一个描述符,此时,它就会做一些变动了,将 t.x 转为t.__class__.__dict__['x'].__get__(t,TestDesc)
,即:
TestDesc.__dict__['x']
.__get__
(T, TestDesc) 来访问。最后,进入类Desc的 __get__()
方法,进行相应的操作。
通过以下例子进行清晰的说明:
class Desc(object):def __init__(self, name):self.name = namedef __get__(self, instance, owner):print("__get__...")print('name = ', self.name)print(f"self:", self)print(f"instance:", instance)print(f"owner:", owner)print(f"")class TestDesc(object):x = Desc('x')def __init__(self):self.y = Desc('y')def main():t = TestDesc()print(t.__dict__)print(t.__class__.__dict__)print(f"")t.__class__.__dict__['x'].__get__(t, TestDesc)t.xif __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
{‘y’: <__main__
.Desc object at 0x000002D05E4B3DC0>}
{‘__module__
’: ‘__main__
’, ‘x’: <__main__
.Desc object at 0x000002D05E4B3FD0>, ‘__init__
’: <function TestDesc.__init__
at 0x000002D05E48C700>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘TestDesc’ objects>, ‘__weakref__
’: <attribute '__weakref__'
of ‘TestDesc’ objects>, ‘__doc__
’: None}
__get__
…
name = x
self: <__main__
.Desc object at 0x000002D05E4B3FD0>
instance: <__main__
.TestDesc object at 0x000002D05E4B3F70>
owner: <class ‘__main__
.TestDesc’>
__get__
…
name = x
self: <__main__
.Desc object at 0x000002D05E4B3FD0>
instance: <__main__
.TestDesc object at 0x000002D05E4B3F70>
owner: <class ‘__main__
.TestDesc’>Process finished with exit code 0
上述代码可以看出,此种情况 t.x
与t.__class__.__dict__['x'].__get__(t, TestDesc)
等价。
问题2
问题1中描述符的对象 x 是类 TestDesc 的类属性,如果描述符的对象是实例属性调用时的情况会怎样呢?
通过以下代码说明:
class Desc(object):def __init__(self, name):self.name = namedef __get__(self, instance, owner):print("__get__...")print('name = ', self.name)print(f"self:", self)print(f"instance:", instance)print(f"owner:", owner)print(f"")class TestDesc(object):x = Desc('x')def __init__(self):self.y = Desc('y')def main():t = TestDesc()print(t.__dict__)print(t.__class__.__dict__)t.y # Statement seems to have no effect
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
{‘y’: <__main__
.Desc object at 0x000002D05E4B3DC0>}
{‘__module__
’: ‘__main__
’, ‘x’: <__main__
.Desc object at 0x000002D05E4B3FD0>, ‘__init__
’: <function TestDesc.__init__
at 0x000002D05E48C700>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘TestDesc’ objects>, ‘__weakref__
’: <attribute '__weakref__'
of ‘TestDesc’ objects>, ‘__doc__
’: None}Process finished with exit code 0
可见,当描述符对象作为实例属性是,并不会正常调用,Python 解释器不会输出任何结果(可认为是被忽略),但是,我此时想要调用作为实例属性的描述符,该如何调用呢?
t.__dict__['y'].__get__(t, TestDesc) # 当描述符对象作为实例属性,此时,不等价于执行了 t.y
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
{‘y’: <__main__
.Desc object at 0x000002D05E4B3DC0>}
{‘__module__
’: ‘__main__
’, ‘x’: <__main__
.Desc object at 0x000002D05E4B3FD0>, ‘__init__
’: <function TestDesc.__init__
at 0x000002D05E48C700>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘TestDesc’ objects>, ‘__weakref__
’: <attribute '__weakref__'
of ‘TestDesc’ objects>, ‘__doc__
’: None}
__get__
…
name = y
self: <__main__
.Desc object at 0x00000217FE743DC0>
instance: <__main__
.TestDesc object at 0x00000217FE743F70>
owner: <class ‘__main__
.TestDesc’>
问题3
类属性是描述符且该类属性和实例属性重名,此时的调用情况会是什么样呢?
通过以下代码说明:
# 描述类中既有 __get__ 也有 __set__
class Desc(object):def __init__(self, name):self.name = namedef __get__(self, instance, owner):print("__get__...")print('name = ', self.name)print('\n')def __set__(self, instance, value):self.value = valueclass TestDesc(object):x = Desc('x')def __init__(self, x):self.x = xdef main():t = TestDesc(10)print(t.__dict__)print(t.__class__.__dict__)print(t.x)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
{}
{‘__module__
’: ‘__main__
’, ‘x’: <__main__
.Desc object at 0x000001ADB68C3FD0>, ‘__init__
’: <function TestDesc.__init__
at 0x000001ADB689C790>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘TestDesc’ objects>, ‘__weakref__
’: <attribute ‘__weakref__
’ of ‘TestDesc’ objects>, ‘__doc__
’: None}
__get__
…
name = xNone
Process finished with exit code 0
# 描述类中只有 __get__
class Desc(object):def __init__(self, name):self.name = namedef __get__(self, instance, owner):print("__get__...")print('name = ', self.name)print('\n')class TestDesc(object):x = Desc('x')def __init__(self, x):self.x = xdef main():t = TestDesc(10)print(t.__dict__)print(t.__class__.__dict__)print(t.x)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
{‘x’: 10}
{‘__module__
’: ‘__main__
’, ‘x’: <__main__
.Desc object at 0x0000021B82BE3FD0>, ‘__init__
’: <function TestDesc.__init__
at 0x0000021B82BBC700>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘TestDesc’ objects>, ‘__weakref__
’: <attribute ‘__weakref__
’ of ‘TestDesc’ objects>, ‘__doc__
’: None}
10Process finished with exit code 0
从上面代码中看出,描述类中是否仅有__get__
会影响属性的调用顺序,我们将只定义了__get__()
方法的描述类称为非数据描述符,包含有__get__()
以外的如__set__()
、__delete__()
的方法称为数据描述符。
因此,当描述符对象的类属性与实例属性同名时,属性调用情况如下:
-
如果是数描述符,优先调用描述符属性,并将同名的实例属性覆盖(同名实例属性变空)。
-
如果是非数据描述符,优先调用实例属,而不再调用描述符的类属性。
属性查询优先级
__getattribute__()
, 无条件调用;- 数据描述符由 1. 触发调用 (若人为的重载了该
__getattribute__
() 方法,可能会导致无法调用描述符); - 实例对象的字典(若与描述符对象同名则会被覆盖)
- 类的字典
- 非数据描述符
- 父类的字典
__getattr__
() 方法
18. 详解__getattribute__
()
Key Point
- getattribute属性与逻辑
- getattribute注意事项
- getattribute属性拦截器
- getattributte循环调用闭坑
####getattribute属性与逻辑
getattribute作为Python的魔法方法,访问类的属性时会无条件调用。因此弄清楚运行逻辑是很有必要的。
__getattribute__(self,obj)
官方给出的原形,在重写这个函数前要了解这个函数的输入参数与输出参数。
通过以下代码来说明:
class Test(object):def __init__(self, subject1):self.Subject1 = subject1self.Subject2 = 'cpp'self.Subject3 = 1def __getattribute__(self, obj):if obj == 'Subject1':print('传入obj的是属性名')return 1else:print(type(object.__getattribute__(self, obj)))print(object.__getattribute__(self, obj))return object.__getattribute__(self, obj)def show(self):print('this is Test')def main():s = Test('hi')print(s.Subject1)print(f"")print(s.Subject2)print(f"")print(s.Subject3)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_06\main.py
传入obj的是属性名
1<class ‘str’>
cpp
cpp<class ‘int’>
1
1Process finished with exit code 0
从输出结果可见,传入重写__getattribute__
中 obj 的是对象的属性名,传入基类(object)__getattribute__
中obj的是对象属性名,基类__getattribute__
执行后返回的是属性值(正常基操罢了!!!)。
值得注意的是,如果在__init__()
中也调用了属性名,也会执行__getattribute__
函数,代码证明如下:
class Test(object):def __init__(self, subject1):self.Subject1 = subject1print("call init : %s" % self.Subject1)self.Subject2 = 'cpp'self.Subject3 = 1def __getattribute__(self, obj):if obj == 'Subject1':print('传入obj的是属性名')return 1else:print(type(object.__getattribute__(self, obj)))print(object.__getattribute__(self, obj))return object.__getattribute__(self, obj)def show(self):print('this is Test')def main():s = Test('hi')if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_06\main.py
传入obj的是属性名
call init : 1Process finished with exit code 0
综上,但凡是调用属性名,都会无条件先执行__getattribute()
,且重写必须要有返回值(即有return)!!!
getattribute在调用属性时会被无条件调用,是否方法也会这样呢,答案是必然的 !!! 时刻注意传入 obj 的是方法名或属性名。
getattribute注意事项
通过以下代码说明注意事项:
class Student(object):country = "china"def __init__(self, name, age):self.name = nameself.age = agedef __getattribute__(self, attr): # 注意:attr是传入的属性名,不是属性值print("开始属性校验拦截功能")print(attr)return object.__getattribute__(self, attr) # 返回属性值def main():s1 = Student("tom", 19)print(Student.country, s1.country, s1.name, s1.age) # 调用属性,会调用__getattribute__方法if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_06\main.py
开始属性校验拦截功能
country
开始属性校验拦截功能
name
开始属性校验拦截功能
age
china china tom 19Process finished with exit code 0
注意事项:
- 使用类名调用类属性时,不会经过
__getattribute__
方法,只争取实例对象对属性的调用,包括调用类属性。 __getattribute__(self,*args,**kwgs)
中传入的参数是属性名,返回的是属性值。- 重写了
__getattribute__
,则类会调用重写的方法,所以这个方法必须要有renturn返回值,返回传进去的属性,否则调用属性会出现失败的情况。
getattribute属性拦截器
Python中只要定义了继承object的类,就默认存在属性拦截器,只不过是拦截后没有进行任何操作,而是直接返回。所以我们可以自己改写__getattribute__
方法来实现相关功能,比如查看权限、打印log日志等。
通过一个例子说明:
class Student(object):country = "china" # 类属性不会放到__dict__中def __init__(self,name,age):self.name = nameself.age = agedef __getattribute__(self, attr): # 注意:attr是传入的属性名,不是属性值print("开始属性校验拦截功能")print(attr)if attr == "name": # 注意这里引用原属性名不用self,直接引号引起来即可。print("现在开始调用的是name属性")elif attr =="age":print("现在开始调用的是age属性")else:print("现在调用的是其他属性")return object.__getattribute__(self, attr) #返回属性值def main():s1 = Student("tom", 19)print(s1.name, s1.age, s1.country)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_06\main.py
开始属性校验拦截功能
name
现在开始调用的是name属性
开始属性校验拦截功能
age
现在开始调用的是age属性
开始属性校验拦截功能
country
现在调用的是其他属性
tom 19 chinaProcess finished with exit code 0
getattributte循环调用闭坑
对__getattribute__
重写时,要注意一个坑,具体bug如下:
class Tree(object):def __init__(self, name):self.name = nameself.cate = "plant"def __getattribute__(self, obj):if obj.endswith("e"):return object.__getattribute__(self, obj)else:return self.call_wind()def call_wind(self):return "树大招风"def main():aa = Tree("大树")aa.call_wind()if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_06\main.py
Traceback (most recent call last):
File “E:\python\code\github\python_oop-main\chapter_06\main.py”, line 22, in
main()
File “E:\python\code\github\python_oop-main\chapter_06\main.py”, line 18, in main
aa.call_wind()
File “E:\python\code\github\python_oop-main\chapter_06\main.py”, line 10, in__getattribute__
return self.call_wind()
File “E:\python\code\github\python_oop-main\chapter_06\main.py”, line 10, in__getattribute__
return self.call_wind()
File “E:\python\code\github\python_oop-main\chapter_06\main.py”, line 10, in__getattribute__
return self.call_wind()
[Previous line repeated 994 more times]
File “E:\python\code\github\python_oop-main\chapter_06\main.py”, line 7, in__getattribute__
if obj.endswith(“e”):
RecursionError: maximum recursion depth exceeded while calling a Python objectProcess finished with exit code 1
执行aa.wind
时,先调用__getattribute__
方法,经过判断后,它返回的是self.call_wind()
,即self.call_wind
的执行结果,但当去调用aa这个对象的call_wind
属性时,前提是又要去调用__getattribute__
方法,如此往复,形成递归调用,超过最大递归次数触发Python 的Error。
重写魔法方法时,会遇到一些坑,在此进行小总结:
- 在
__getattribute__
方法中,不要使用self.属性名,这个会再次触发调用__getatrribute__
方法。- 在
__setattr__
方法体中不要使用self.属性名去设置属性的值,也会使程序进入死循环。- 在
__delattr__
方法体中不要使用del self.属性名,因为del 会触发调用__delattr__
这个方法。
19.__getattr__
, __getattribute__
, __setattr__
, __delattr__
的属性访问
Key Point
__getattr__
,__getattribute__
,__setattr__
,__delattr__
作用- 属性访问时
__getattr__
,__getattribute__
的调用细节__setattr__
,__delattr__
的调用细节
__getattr__
, __getattribute__
, __setattr__
, __delattr__
作用
考虑到访问类或实例的属性时会调用一些魔法函数,在此说明一下主要所用的几个魔法函数的作用
__getattr__
(self, name): 访问不存在的属性时调用。__getattribute__
(self, name):访问存在的属性时调用(先调用该方法,查看是否存在该属性,若不存在,接着去调用1.)。__setattr__
(self, name, value):设置实例对象的一个新的属性时调用。__delattr__
(self, name):删除一个实例对象的属性时调用
属性访问时__getattr__
, __getattribute__
的调用细节
情况一
两个函数均不进行重写,正常调用顺序是先调用__getattribute__
,访问属性不存在时调用 __getattr__
此时会弹出AttributeError。
细节如下:
① 首先访问
__getattribute__
() 魔法方法② 去实例对象t中查找是否具备该属性: t.
__dict__
中查找,每个类和实例对象都有一个__dict__
的属性③ 若在 t.
__dict__
中找不到对应的属性, 则去该实例的类中寻找,即 t.__class__
.__dict__
④ 若在实例的类中也招不到该属性,则去父类中寻找,即 t.
__class_
_.__bases__
.__dict__
中寻找⑤ 若以上均无法找到,则会调用
__getattr__
方法,执行内部的命令(若未重载__getattr__
方法,则直接报错:AttributeError)
情况二
对__getattribute__
进行重写,__getattr__
不重写,属性访问时__getattribute__
先调用,此时不抛出AttributeError。
情况三
对__getattribute__
进行重写,__getattr__
重写,属性访问时__getattribute__
先调用,此时也不抛出AttributeError。
Python 中一旦重载
__getattribute__()
方法,如果找不到属性,则必须手动加入第④步,否则无法进入到 第⑤步 (__getattr__
)的。
下面展示,重写__getattribute__()
后,能够继续调用__getattr__()
的方法。代码如下:
# 方法一
class Test:def __getattr__(self, name):print('__getattr__')def __getattribute__(self, name):print('__getattribute__')object.__getattribute__(self, name) # 当找不到属性,加入此段代码能继续调用__getattr__()def main():t1 = Test()t1.xif __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_06\main.py
__getattribute__
__getattr__
Process finished with exit code 0
# 方法二
class Test:def __getattr__(self, name):print('__getattr__')def __getattribute__(self, name):print('__getattribute__')super().__getattribute__(name) # 当找不到属性,加入此段代码能继续调用__getattr__()def main():t1 = Test()t1.xif __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_oop-main\chapter_06\main.py
__getattribute__
__getattr__
Process finished with exit code 0
小结:
重构getattribute后,当找不到属性,想要继续调用getattr通过以下两种方法继续执行:
- 采用 object 类的即
object.__getattribute__(self, name)
- 采用supoer()方法即
super().__getattribute__(name)
- 可见使用1和2方法仅仅是给基类 object 增加一个属性,而不是增加实例属性,因此避免使用。
__setattr__
, __delattr__
的调用细节
看到这里,这两个的调用细节相对简单,通过以下代码说明:
class Test:def __init__(self):self.name = "Liu" # 此处会调用 __setattr__函数self.age = 12 # 此处会调用 __setattr__函数self.male = True # 此处会调用 __setattr__函数def __setattr__(self, name, value):print('__setattr__')self.__dict__[name] = valuedef __delattr__(self, name):print('__delattr__')test = Test()
test.id = 123
print(test.__dict__)
del test.id # 此处调用 __delattr__函数
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_advance-main\metaclass_main_.py
__setattr__ __setattr__ __setattr__ __setattr__ {'name': 'Liu', 'age': 12, 'male': True, 'id': 123} __delattr__
Process finished with exit code 0
注意:
- del语句作用于类实例和类实例属性是两种不同的情况,前者触发__del__(),而后者才触发__delattr__()。示例代码如下:
class Fun:def __init__(self):self.name = "Tom Ford"def __del__(self):print("calling __del__")def __delattr__(self, name):print("calling __delattr__")fun = Fun()
del fun.name
del fun
- del对象时删除的是“引用”,**所以当同一个对象存在多个引用时,仅在删除最后一个引用才会触发__del__()。**示例代码如下:
class Fun:def __init__(self):self.name = "Tom Ford"def __del__(self):print("calling __del__")def __delattr__(self, name):print("calling __delattr__")fun = Fun()
fun_1 = fun # fun_1 引用 fun
fun_2 = fun # fun_2 引用 fundel fun_1
print("h"*20)
del fun_2
print("h"*20)
del fun
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\github\python_advance-main\metaclass\__main__.py hhhhhhhhhhhhhhhhhhhh hhhhhhhhhhhhhhhhhhhh calling __del__
20.应用描述符
Key Point:
- 一点点前言
- 描述符的属性共享
- 描述符的属性隔离
一点点前言
现在面临一个成绩管理系统,让你去写一个类管理学生的三科成绩,作为 Python 菜鸟代码如下:
class Student:def __init__(self, name, math, chinese, english):self.name = nameself.math = mathself.chinese = chineseself.english = englishdef __repr__(self):return "<Student: {}, math:{}, chinese: {}, english:{}>".format(self.name, self.math, self.chinese, self.english)def main():std_1 = Student('小明', 80, 85, 90)print(std_1)if __name__ == "__main__":main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\test2.py
<Student: 小明, math:80, chinese: 85, english:90>Process finished with exit code 0
看似完成了,但是如果使用者由于失误写入了带有负数的成绩,上面的代码就不中看了。
因此,引入判断逻辑小改一下即可,修改的代码如下:
class Student:def __init__(self, name, math, chinese, english):self.name = nameif 0 <= math <= 100:self.math = mathelse:raise ValueError("Valid value must be in [0, 100]")if 0 <= chinese <= 100:self.chinese = chineseelse:raise ValueError("Valid value must be in [0, 100]")if 0 <= chinese <= 100:self.english = englishelse:raise ValueError("Valid value must be in [0, 100]")def __repr__(self):return "<Student: {}, math:{}, chinese: {}, english:{}>".format(self.name, self.math, self.chinese, self.english)def main():std_1 = Student('小明', -2, 85, 90)print(std_1)if __name__ == "__main__":main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\test2.py
Traceback (most recent call last):
File “E:\python\code\Project_1\test2.py”, line 31, in
main()
File “E:\python\code\Project_1\test2.py”, line 26, in main
std_1 = Student(‘小明’, -2, 85, 90)
File “E:\python\code\Project_1\test2.py”, line 7, in init
raise ValueError(“Valid value must be in [0, 100]”)
ValueError: Valid value must be in [0, 100]Process finished with exit code 1
嗯嗯,有点那味了,但是在构造函数中引入太多的判断逻辑导致代码可读性下降,突然灵光一闪上@property,说干就干,代码如下:
class Student:def __init__(self, name, math, chinese, english):self.name = nameself.math = mathself.chinese = chineseself.english = english@propertydef math(self):return self._math@math.setterdef math(self, value):if 0 <= value <= 100:self._math = valueelse:raise ValueError("Valid value must be in [0, 100]")@propertydef chinese(self):return self._chinese@chinese.setterdef chinese(self, value):if 0 <= value <= 100:self._chinese = valueelse:raise ValueError("Valid value must be in [0, 100]")@propertydef english(self):return self._english@english.setterdef english(self, value):if 0 <= value <= 100:self._english = valueelse:raise ValueError("Valid value must be in [0, 100]")def __repr__(self):return "<Student: {}, math:{}, chinese: {}, english:{}>".format(self.name, self.math, self.chinese, self.english)def main():std_1 = Student('小明', -2, 85, 90)print(std_1)if __name__ == "__main__":main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\test2.py
Traceback (most recent call last):
File “E:\python\code\Project_1\test2.py”, line 55, in
main()
File “E:\python\code\Project_1\test2.py”, line 48, in main
std_1 = Student(‘小明’, -2, 85, 90)
File “E:\python\code\Project_1\test2.py”, line 4, in__init__
self.math = math
File “E:\python\code\Project_1\test2.py”, line 17, in math
raise ValueError(“Valid value must be in [0, 100]”)
ValueError: Valid value must be in [0, 100]Process finished with exit code 1
小满意,整个代码可读性提高了,但是仔细一想,如果我管理的不知三科,如果是几百多科是不是每个都定义成@property
呢?代码复用率急速下降,小扣一下脑袋,想到了描述类,最终优化代码如下:
class Score:def __init__(self, default=0):self._score = defaultdef __set__(self, instance, value):if not isinstance(value, int):raise TypeError('Score must be integer')if not 0 <= value <= 100:raise ValueError('Valid value must be in [0, 100]')self._score = valuedef __get__(self, instance, owner):return self._scoredef __delete__(self):del self._scoreclass Student:math = Score(0)chinese = Score(0)english = Score(0)def __init__(self, name, math, chinese, english):self.name = nameself.math = mathself.chinese = chineseself.english = englishdef __repr__(self):return "<Student: {}, math:{}, chinese: {}, english:{}>".format(self.name, self.math, self.chinese, self.english)def main():std_1 = Student("小明", -2, 90, 95)if __name__ == "__main__":main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\test2.py
Traceback (most recent call last):
File “E:\python\code\Project_1\test2.py”, line 42, in
main()
File “E:\python\code\Project_1\test2.py”, line 38, in main
std_1 = Student(“小明”, -2, 90, 95)
File “E:\python\code\Project_1\test2.py”, line 27, in__init__
self.math = math
File “E:\python\code\Project_1\test2.py”, line 9, in__set__
raise ValueError(‘Valid value must be in [0, 100]’)
ValueError: Valid value must be in [0, 100]Process finished with exit code 1
虽然,完成了但是上述代码存在着一个致命缺点:没有做到属性隔离。
描述符的属性共享
讨论属性隔离问题前,先回顾下带描述符的属性调用,通过以下代码回顾一下:
class Score:def __init__(self, default=0):print("desc init called")self._score = defaultdef __set__(self, instance, value):print("__set__ called")if not isinstance(value, int):raise TypeError('Score must be integer')if not 0 <= value <= 100:raise ValueError('Valid value must be in [0, 100]')self._score = valuedef __get__(self, instance, owner):return self._scoredef __delete__(self):del self._scoreclass Student:def __init__(self, name, math):print("__init__ called")self.name = nameprint("self.__dict__:", self.__dict__)print("self.__class__.__dict__:", self.__class__.__dict__)self.math = mathmath = Score(0)def main():std_1 = Student("小明", 1)print("dict============")print(std_1.__dict__)print(std_1.__class__.__dict__)print(std_1.__class__.__dict__['math'].__dict__)print(std_1.math)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
desc init called
__init__
called
self.__dict__
: {‘name’: ‘小明’}
self.__class__
.__dict__
: {‘__module__
’: ‘__main__
’, ‘__init__
’: <function Student.__init__
at 0x0000026D537CD820>, ‘math’: <__main__
.Score object at 0x0000026D537F4A60>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘Student’ objects>, ‘__weakref__
’: <attribute ‘__weakref__
’ of ‘Student’ objects>, ‘__doc__
’: None}
__set__
called
dict============
{‘name’: ‘小明’}
{‘__module__
’: ‘__main__
’, ‘__init__
’: <function Student.__init__
at 0x0000026D537CD820>, ‘math’: <__main__
.Score object at 0x0000026D537F4A60>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘Student’ objects>, ‘__weakref__
’: <attribute ‘__weakref__
’ of ‘Student’ objects>, ‘__doc__
’: None}
{‘_score’: 1}
1Process finished with exit code 0
从上述结果中可以看出当类中有描述符对象时,无论它写在构造函数的前面还是后面,无一例外优先调用描述符对象的构造函数。现在我们重点看一下self.math = math的运行过程 = 左边的 math 传入 int 型的值1,self.name 即 std_1.name ,整个语句等价于std_1.math = math,此时由于 = 的存在会间接调用描述符中的__set__
函数。对于出入__set__
函数的参数中,self 对应的是 Score 类的对象 math ,instance 对应的是 Student 类的对象 std_1, value 对应的是 int 类的 math 即1。对于 std_1.math 则调用描述符类math中的__set__
函数。
描述符对象作为类属性时,存在实例共享描述符,实质是所有实例共享描述符这个对象,具体代码如下:
class Score:def __init__(self, default=0):self._score = defaultdef __set__(self, instance, value):if not isinstance(value, int):raise TypeError('Score must be integer')if not 0 <= value <= 100:raise ValueError('Valid value must be in [0, 100]')self._score = valuedef __get__(self, instance, owner):return self._scoredef __delete__(self):del self._scoreclass Student:def __init__(self, name, math, english, chinese):self.name = nameself.math = mathself.english = englishself.chinese = chinesedef __repr__(self):return "<Student: {}, math:{}, chinese: {}, english:{}>".format(self.name, self.math, self.english, self.chinese)math = Score(0)english = Score(0)chinese = Score(0)def main():std_1 = Student("小明", 0, 0, 0)print("原始的std_1:", std_1)std_2 = Student("小红", 0, 0, 0)print("原始的std_2:", std_2)std_1.math = 1print("修改std_1的math")print(std_1)print(std_2)print("===== dict ===== ")print("std_1的math", std_1.__class__.__dict__["math"])print("std_2的math", std_2.__class__.__dict__["math"])if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
原始的std_1: <Student: 小明, math:0, chinese: 0, english:0>
原始的std_2: <Student: 小红, math:0, chinese: 0, english:0>
修改std_1的math
<Student: 小明, math:1, chinese: 0, english:0>
<Student: 小红, math:1, chinese: 0, english:0>
===== dict =====
std_1的math <__main__
.Score object at 0x000001E0A5815FD0>
std_2的math <__main__
.Score object at 0x000001E0A5815FD0>Process finished with exit code 0
如果实例描述符共享属性会导致,修改一个对象的属性值而导致另一个对象的属性值变化,这样就全乱套了,有什么办法处理呢?请看下节。
描述符的属性隔离
math,chinese,english 这三个变量变成实例之间相互隔离的属性,在描述符__get__
、__set__
、__del__
中的应该使用instance。见下面代码中的重要标识!!!
class Score:def __init__(self, default):self._score = default # 关键!!!def __set__(self, instance, value):if not isinstance(value, int):raise TypeError('Score must be integer')if not 0 <= value <= 100:raise ValueError('Valid value must be in [0, 100]')instance.__dict__[self._score] = value # 关键!!!def __get__(self, instance, owner):return instance.__dict__[self._score] # 关键!!!def __delete__(self):del instance._scoreclass Student:def __init__(self, name, math, english, chinese):self.name = nameself.math = mathself.english = englishself.chinese = chinesedef __repr__(self):return "<Student: {}, math:{}, chinese: {}, english:{}>".format(self.name, self.math, self.english, self.chinese)math = Score("math") # 关键 !!!english = Score("english") # 关键 !!!chinese = Score("chinese") # 关键 !!!def main():std_1 = Student("小明", 0, 0, 0)print("原始的std_1:", std_1)std_2 = Student("小红", 0, 0, 0)print("原始的std_2:", std_2)std_1.math = 1print("修改std_1的math")print(std_1)print(std_2)print("===== dict ===== ")print("std_1的class", std_1.__class__.__dict__)print("std_2的class", std_2.__class__.__dict__)print("std_1的", std_1.__dict__)print("std_2的", std_2.__dict__)if __name__ == '__main__':main()
输出结果:
D:\ProgramData\Anaconda3\python.exe E:\python\code\Project_1\cha1.py
原始的std_1: <Student: 小明, math:0, chinese: 0, english:0>
原始的std_2: <Student: 小红, math:0, chinese: 0, english:0>
修改std_1的math
<Student: 小明, math:1, chinese: 0, english:0>
<Student: 小红, math:0, chinese: 0, english:0>
===== dict =====
std_1的class {‘__module__
’: ‘__main__
’, ‘__init__
’: <function Student.__init__
at 0x0000025B89C1D820>, ‘__repr__
’: <function Student.__repr__
at 0x0000025B89C1D8B0>, ‘math’: <__main__
.Score object at 0x0000025B89C45FD0>, ‘english’: <__main__
.Score object at 0x0000025B89C45F70>, ‘chinese’: <__main__
.Score object at 0x0000025B89C45CD0>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘Student’ objects>, ‘__weakref__
’: <attribute ‘__weakref__
’ of ‘Student’ objects>, ‘__doc__
’: None}
std_2的class {‘__module__
’: ‘__main__
’, ‘__init__
’: <function Student.__init__
at 0x0000025B89C1D820>, ‘__repr__
’: <function Student.__repr__
at 0x0000025B89C1D8B0>, ‘math’: <__main__
.Score object at 0x0000025B89C45FD0>, ‘english’: <__main__
.Score object at 0x0000025B89C45F70>, ‘chinese’: <__main__
.Score object at 0x0000025B89C45CD0>, ‘__dict__
’: <attribute ‘__dict__
’ of ‘Student’ objects>, ‘__weakref__
’: <attribute ‘__weakref__
’ of ‘Student’ objects>, ‘__doc__
’: None}
std_1的 {‘name’: ‘小明’, ‘math’: 1, ‘english’: 0, ‘chinese’: 0}
std_2的 {‘name’: ‘小红’, ‘math’: 0, ‘english’: 0, ‘chinese’: 0}Process finished with exit code 0
描述符属性隔离总结:
- 在定义类中实例化多个不同实参的描述符对象。
- 在
__get__
和__set__
中使用的是sintance而不是self。 - 牢记
__get__
和__set__
中使用__dict__
来设和获取值。
注意
整合总结面向对象
- Python视频教程_Python基础入门视频课程 - Python自学网 (wakey.com.cn)
- Python类和对象 (biancheng.net)
参考材料
Python 中下划线的 5 种含义 | 菜鸟教程 (runoob.com)
[Python实例方法、静态方法和类方法详解(包含区别和用法) (biancheng.net)](http://c.biancheng.net/view/4552.html#:~:text=Python类静态方法. 静态方法,其实就是我们学过的函数,和函数唯一的区别是,静态方法定义在类这个空间(类命名空间)中,而函数则定义在程序所在的空间(全局命名空间)中。. 静态方法没有类似 self、cls 这样的特殊参数,因此 Python 解释器不会对它包含的参数做任何类或对象的绑定。. 也正因为如此,类的静态方法中无法调用任何类属性和类方法。., def info (name%2Cadd)%3A print (name%2Cadd) 静态方法的调用,既可以使用类名,也可以使用类对象,例如:.)
Python的__hash__函数和__eq__函数 - KadyCui - 博客园 (cnblogs.com)
Python @property装饰器详解 (biancheng.net)