文章目录
- 1. 前言
- 2. __init__方法
- 3. __new__方法
- 4. __call__方法
- 5. __str__方法
- 6. __repr__方法
- 7. __getitem__方法
- 8. __setitem__方法
- 9. __delitem__方法
- 10. __len__方法
- 11. 富比较特殊方法
- 12. __iter__方法和__next__方法
- 13. __getattr__方法、__setattr__方法、__delattr__方法
- 14. __enter__方法和__exit__方法
1. 前言
python的内置方法具有特殊的功能,这些内置方法我们也称之为魔术方法(magic method)或特殊方法(special method)。魔术方法就是前后各有两个下划线__
的方法,像__init__
方法就是一个魔术方法。python中的类提供了很多双下划线开头和结尾__xxx__
的方法,这些方法是Python运行的基础,很多功能的背后都是通过调用这些内置方法来实现的。例如len()
函数调用对象的__len__
方法;print(obj)
函数调用对象的__str__
方法;for循环遍历语句for item in iterable_obj
调用对象的__next__
、__iter__
方法。下面对常见的魔术方法进行介绍。
上面的图片来自于:【python】魔术方法大全——基础篇
2. __init__方法
__init__
方法是一个构造方法,在创建类对象的时候用于初始化的设置。__init__
方法在创建类的实例对象时自动调用。
class Student:def __init__(self,name,age,id) -> None:self.name = nameself.age = ageself.id = iddef show_info(self) -> None:print(f"name = {self.name},age = {self.age},id = {self.id}")stu = Student("zhangsan",18,"001") # 会自动调用__init__方法
stu.show_info() # 输出:name = zhangsan,age = 18,id = 001
3. __new__方法
记住重要的一点:__new__
方法用于创建对象实例,而__init__
方法用于实例对象的初始化,且__new__
方法在__init__
方法之前被调用。也就是说,我们必须先创建对象(先调用__new__
方法),然后才可以进行对象的初始化(再调用__init__
方法)。
(1)__new__
方法负责实例对象的创建,在对象实例化的时候,它是第一个被调用的方法。
(2)__new__
是一个类方法(类方法使用cls作为第一个参数,cls==class(类)),因此在调用__new__
方法时使用类本身而不是实例对象。
(3)__new__
方法必须返回一个实例对象,这个实例对象通常是由super().__new__(cls)
创建的,即调用父类的__new__
方法。通常情况下,你不需要直接调用__new__
方法,因为它在实例化时自动被调用。
(4)当构造方法__init__
是一个无参数构造时,在__new__
方法中无需传递参数,并使用super().__new__(cls)
来创建实例对象。如下所示:
class Student:def __new__(cls):instance = super().__new__(cls)print("__new__被执行了...")return instancedef __init__(self) -> None:print("__init__被执行了...")stu = Student()输出结果:
__new__被执行了...
__init__被执行了...
(5)当构造方法__init__
是一个有参数构造时,在__new__
方法中需要传递相应的参数,并使用super().__new__(cls)
来创建实例对象。如下所示:
class Student:def __new__(cls,name,age,id):# instance = super(Student,cls).__new__(cls,name,age,id) # python2写法,会报错# 下面是python3的写法instance = super().__new__(cls)print("__new__被执行了...")return instancedef __init__(self,name,age,id) -> None:self.name = nameself.age = ageself.id = idprint("__init__被执行了...")stu = Student("zhangsan",18,"001")输出结果:
__new__被执行了...
__init__被执行了...
4. __call__方法
__call__
方法的作用是把一个类的实例化对象变成可调用对象。例如:在对象进行调用方法时,实例对象.__call__()
等价于实例对象()
。我们可以使用内置函数callable(obj)
来判断对象obj是否为可调用对象。
class Student:def __init__(self,name,age,id) -> None:self.name = nameself.age = ageself.id = iddef __call__(self, str1: str, str2: str):print(f"str1+str2 = {str1+str2}")stu = Student("zhangsan",18,"001") # 会自动调用__init__方法
stu.__call__("hello ","world") # 输出:str1+str2 = hello world
stu("hello ","world") # 输出:str1+str2 = hello world
print(callable(stu)) # 输出:True
5. __str__方法
__str__
方法的作用是把一个类的实例对象变成字符串(str)。
(1)不使用__str__
方法:返回实例对象的内存地址。
class Student:def __init__(self,name: str, age: int, id: str) -> None:self.name = nameself.age = ageself.id = idstu = Student("zhangsan", 18, "001")
print(stu) # 输出:<__main__.Student object at 0x0000020E72F6E088>
(2)使用__str__
方法:返回一个字符串。
class Student:def __init__(self,name: str, age: int, id: str) -> None:self.name = nameself.age = ageself.id = iddef __str__(self) -> str:return f"name = {self.name}, age = {self.age}, id = {self.id}"stu = Student("zhangsan", 18, "001")
print(stu) # 输出:name = zhangsan, age = 18, id = 001
(3)由上面代码可知:当使用__str__
方法时,输出实例对象(print(stu)
)的结果由<__main__.Student object at 0x0000020E72F6E088>
变成name = zhangsan, age = 18, id = 001
。即:__str__
方法的作用是把一个类的实例对象变成字符串(str)。
6. __repr__方法
__repr__
方法用来返回一个实例对象的字符串(str)表示形式。
class Student:def __init__(self,name: str, age: int, id: str) -> None:self.name = nameself.age = ageself.id = iddef __repr__(self) -> str:return f"Student(name='{self.name}',age={self.age},id='{self.id}')"stu = Student("zhangsan", 18, "001")
#当我们给repr()函数传入对象时,会调用__repr__方法。
print(repr(stu)) # 输出:Student(name='zhangsan',age=18,id='001')
__str__
方法和__repr__
方法的相同点和不同点:
(1)这两个方法都可以用来输出实例对象的字符串表示形式。
(2)当我们打印一个实例对象时,Python会自动调用__str__
方法。如果该对象没有实现(或定义)__str__
方法,Python会寻找对象的__repr__
方法。如果该对象也没有实现__repr__
方法,则输出默认的对象表示形式(返回实例对象的内存地址)。如果我们需要显式的指定以何种方式进行实例对象到字符串的转化,可以使用内置的str()
和repr()
函数,它们会调用类中对应的双下划线方法。也就是说str()
函数会调用__str__
方法,repr()
函数调用__repr__
方法。参考下面代码:
class Student:def __init__(self,name: str, age: int, id: str) -> None:self.name = nameself.age = ageself.id = iddef __str__(self) -> str:return f"执行__str__..., name = {self.name}"def __repr__(self) -> str:return f"执行__repr__..., name = {self.name}"stu = Student("zhangsan", 18, "001")
print(stu) # 输出:执行__str__..., name = zhangsan。自动调用__str__方法
print(str(stu)) # 输出:执行__str__..., name = zhangsan。调用__str__方法
print(repr(stu)) # 输出:执行__repr__..., name = zhangsan。调用__repr__方法
(3)__repr__
方法的返回结果更多地用于开发者调试和重新创建对象,并且可以准确地重现该对象的状态和属性。而__str__
方法的返回结果对用户更加友好,提供一个易于理解的字符串表示 。参考下面代码:
import datetime
today = datetime.datetime.today()
print(str(today)) # 输出:2024-08-05 11:17:11.422536
print(repr(today)) #输出:datetime.datetime(2024, 8, 5, 11, 17, 11, 422536)"""
(1)__str__ 的返回结果可读性强。也就是说,__str__ 的意义是得到便于人们阅读的信息,就像上面的 "2024-08-05 11:17:11.422536" 一样。
(2)__repr__ 的返回结果应更准确。__repr__ 存在的目的在于调试,便于开发者使用。将__repr__ 返回的结果datetime.datetime(2024, 8, 5, 11, 17, 11, 422536) 直接复制到命令行上,是可以直接执行的。
"""
(4)我们在写类的时候,最好至少添加一个__repr__
方法来保证实例对象到字符串的转换具有自定义的有效性。__str__
是可选的,因为在默认情况下,__str__
方法默认调用__repr__
方法,所以在实例对象转字符串的时候,找到底层__str__
方法之后,会调用重写__repr__
方法。
参考文章:浅谈python中__str__和__repr__的区别
7. __getitem__方法
__getitem__
方法,用于索引和切片操作,允许我们通过索引或切片的方式访问对象的元素。__getitem__
方法返回所给键对应的值。当对象是序列类型(像列表、元组和字符串)时,键是整数;当对象是映射(字典)时,键是任意值。使用场景是:在定义类时,如果希望能够按照键取类的值,则需要定义__getitem__
方法。
在Python中,可使用索引器运算符[]
来访问对象元素,例如:我们定义了一个列表mylist = [2,4,6,8]
,然后可以通过索引器运算符[]
来访问列表对象的元素,即my_list[2]
。其实,my_list[2]
大致等价于my_list.__getitem__(2)
。同理,我们定义了一个字典my_dict = {"name": "ZS", "age": 18}
,那么my_dict['name']
大致等价于my_dict.__getitem__("name")
。利用索引器运算符[]
来访问列表和字典对象元素的代码如下:
my_list = [2,4,6,8]
my_dict = {"name": "ZS", "age": 18}print(my_list[2]) # 输出:6
print(my_list.__getitem__(2)) # 输出:6
print(my_dict["name"]) # 输出:ZS
print(my_dict.__getitem__("name")) # 输出:ZS
当传递给索引器运算符[]
的参数不止一个时,那么这些参数会被隐式的转换成元组。例如:列表切片my_list[1:4]
等价于my_list[slice(1,4)]
;my_list[1:4, 0]
等价于my_list[ (slice(1,4), 0)]
。
(1)当对象是序列类型(像列表、元组和字符串)时,键是整数,使用__getitem__
方法来实现一个自定义的可索引对象。
class Student:def __init__(self, *args) -> None:# args是一个元组类型,它可以接收可变数量的参数。print(args) # 输出:(2, 4, 6, 8, 10)print(type(args)) # 输出:<class 'tuple'>self.my_list = list(args)def __getitem__(self, item):print("__getitem__被执行了...")print(type(item))return self.my_list[item]stu = Student(2,4,6,8,10)
# 通过实现__getitem__方法,我们可以使用索引或切片操作来获取my_list列表中的元素
print(stu[1]) # 输出:"__getitem__被执行了..."、<class 'int'> 和 4
print(stu[1:4]) # 输出:"__getitem__被执行了..."、<class 'slice'> 和 [4, 6, 8]# stu[1] 等价于 stu.__getitem__(1)
# stu[1:4] 等价于 stu.__getitem__(slice(1,4))
(2)当对象是映射(字典)时,键是任意值。使用__getitem__
方法来实现一个自定义的可索引对象。
class Student:def __init__(self, **kwargs) -> None:# kwargs是一个字典类型,它可以接收以键-值对形式传递的可变数量的参数。print(kwargs) # 输出:{'name': 'zs', 'age': 18, 'id': '001'}print(type(kwargs)) # 输出:<class 'dict'>self.my_dict = dict(kwargs)def __getitem__(self, item):print("__getitem__被执行了...")print(type(item))if isinstance(item, tuple):"""判断item是否为元组类型的对象,如果是的话,则通过列表推导式将字典中键(key)对应的值(value)存放到列表中,并返回该列表。"""return [self.my_dict[key] for key in item]else:return self.my_dict[item]stu = Student(name="zs", age = 18, id="001")print(stu["name"]) # 输出:"__getitem__被执行了..."、<class 'str'>、zs
print(stu["name","age","id"]) # stu["name","age","id"]等价于stu[("name","age","id")],参数被转换成元组。# 输出:"__getitem__被执行了..."、<class 'tuple'>、['zs', 18, '001']
参考文章:Python中__getitem__()方法和索引器[]的详细用法
8. __setitem__方法
__setitem__
方法的作用是让类按照一定的方法存储和键(key)映射的值(value),该值可以使用__getitem__
方法来获取。使用场景:当期望定义的类具备按照键存储值时,即类能够执行obj[“key”]=value(等价于obj.__setitem__(key,value)
)。代码示例如下:
class Student:def __init__(self, **kwargs) -> None:self.my_dict = dict(kwargs)def __getitem__(self, item):print("__getitem__被执行了...")if isinstance(item, tuple):return [self.my_dict[key] for key in item]else:return self.my_dict[item]def __setitem__(self, key, value):print("__setitem__被执行了...")self.my_dict[key] = valuedef __str__(self) -> str:obj_str = str()# 遍历字典中的键值对for key, value in self.my_dict.items():obj_str = obj_str + f"{key} = {value} "return obj_strstu = Student(name="zs", age = 18)
print(stu) # 输出:name = zs age = 18
# 调用__setitem__方法, stu["id"] = "001"等价于stu.__setitem__("id","001")
stu["id"] = "001" # 输出:"__setitem__被执行了..."
print(stu) # 输出:name = zs age = 18 id = 001
print(stu["id"]) # 输出:"__getitem__被执行了..."、001
9. __delitem__方法
__delitem__
方法用于删除给定键对应的元素。使用del
关键字来删除指定键对应的元素,即del obj[key]
,等价于obj.__delitem__(key)
。
class Mylist:def __init__(self, *args) -> None:self.obj_list = list(args)def __delitem__(self, key):print("__delitem__被调用...")del self.obj_list[key]obj = Mylist(1,2,3,4,5,6)
print(obj.obj_list) # 输出:[1, 2, 3, 4, 5, 6]
del obj[2] # 输出:__delitem__被调用...
print(obj.obj_list) # 输出:[1, 2, 4, 5, 6]
10. __len__方法
__len__
方法用于返回对象的长度或元素个数。我们可以在类中定义__len__
方法来实现对该类的对象使用 len()
函数。
class Mylist:def __init__(self, *args) -> None:self.obj_list = list(args)def __len__(self):print("__len__被调用...")return len(self.obj_list)obj = Mylist(1,2,3,4,5,6)
print(len(obj)) # 输出:"__len__被调用..."、6
print(obj.__len__()) # 输出:"__len__被调用..."、6
# len(obj)等价于obj.__len__()
11. 富比较特殊方法
富比较(rich comparison)特殊方法是一组用于实现对象之间比较操作的特殊方法。这些方法使得自定义对象可以支持 <, <=, ==, !=, >=, >
这样的比较操作。
参考文章:Python 富比较特殊方法
下面以__lt__
方法和__eq__
方法为例,代码如下:
class Student:def __init__(self,name,age,id) -> None:self.name = nameself.age = ageself.id = iddef __lt__(self, other: object) -> bool:# 使用对象中的age属性来比较大小return self.age < other.agedef __eq__(self, other: object) -> bool:# 使用对象中的age属性来比较大小return self.age == other.agestu1 = Student("zhangsan",18,"001")
stu2 = Student("zhangsan",30,"001")
print(stu1<stu2) # 等价于:stu1.__lt__(stu2)。输出:True
print(stu1 == stu2) # 等价于:stu1.__eq__(stu2)。输出:False
12. __iter__方法和__next__方法
在 Python 中,迭代器是一个实现了__iter__
和__next__
方法的对象。__iter__
方法返回迭代器对象自身,而 __next__
方法返回下一个元素。换句话说,迭代器是一个可以逐个返回元素的对象。如果在python的类中定义了__next__
和__iter__
方法,生成的实例对象可以通过for循环遍历来取,并且先调用__iter__
方法,再调用__next__
方法。代码如下:
class MyList:def __init__(self,*args) -> None:self.data = list(args)self.start = 0def __iter__(self):print("__iter__被执行了...")return selfdef __next__(self):print("__next__被执行了...")if self.start >= len(self.data):# raise StopIteration用于提前终止一个迭代器中的循环raise StopIterationitem = self.data[self.start]self.start += 1return itemobj_list = MyList(2,4,6)for i in obj_list:print(i)输出:
__iter__被执行了...
__next__被执行了...
2
__next__被执行了...
4
__next__被执行了...
6
__next__被执行了...
13. __getattr__方法、__setattr__方法、__delattr__方法
(1)__getattr__
方法:当我们访问对象中一个不存在的属性时,会抛出异常,提示我们该对象没有该属性。而这个异常就是__getattr__
方法抛出的,其原因在于它是访问一个不存在的属性的最后落脚点。
class Student:def __init__(self,name,age,id) -> None:self.name = nameself.age = ageself.id = iddef __getattr__(self,item):# 这里的item就是那个不存在的属性名print("__getattr__被执行...")print(item)return f"该对象不存在属性:{item}"stu = Student("ZS",16,"001")
print(stu.name) # name属性存在,可以访问
print(stu.gender) # gender属性不存在,会调用__getattr__方法。输出:
ZS
__getattr__被执行...
gender
该对象不存在属性:gender
(2)__setattr__
方法:在对一个属性设置值的时候,会调用到这个方法,每个设置值的方式都会进入这个方法。如下代码所示:在实例化对象的时候,会调用__init__
方法进行初始化。在__init__
方法中对属性age进行值的设置,此时也会调用__setattr__
方法。我们还可以新建一个属性name,并对其赋值,此时会调用__setattr__
方法。
class Student:def __init__(self,_age) -> None:self.age = _agedef __setattr__(self,key, value):# 这里的key为属性名,value是该属性对应的值print("__setattr__被执行了...")if value == 16:print("这里的key和value来自__init__方法")object.__setattr__(self, key, value)
# object.__setattr__(self, key, value)、
# self.__dict__[key] = value、
# super().__setattr__(key,value),这三种方法等价,都可以使用stu = Student(16)
print(stu.age)stu.age = 30
print(stu.age)stu.name = "ZS"
print(stu.name)输出:
__setattr__被执行了...
这里的key和value来自__init__方法
16
__setattr__被执行了...
30
__setattr__被执行了...
ZS
注意:在重写__setattr__
方法的时候千万不要重复调用,以防造成死循环。下面的代码就是一个死循环,在__init__
方法中执行语句self.age = _age
的时候会调用__setattr__
方法。而__setattr__
方法中又存在语句self.key = value
,因此会继续调用__setattr__
方法,造成死循环。
class Student:def __init__(self,_age) -> None:self.age = _agedef __setattr__(self,key, value):self.key = valuestu = Student(16)
(3)__delattr__
方法:用于删除对象中的某个属性。
class Student:def __init__(self,name,age,id) -> None:self.name = nameself.age = ageself.id = iddef __getattr__(self,item):print("__getattr__被执行了...")return f"该对象不存在属性:{item}"def __delattr__(self, item):print("__delattr__被执行了...")object.__delattr__(self,item)stu = Student("ZS",16,"001")
del stu.id # 会调用__delattr__方法
print(stu.id)输出:
__delattr__被执行了...
__getattr__被执行了...
该对象不存在属性:id
14. __enter__方法和__exit__方法
(1)with
上下文管理器:关键字with
使用最多的就是打开文件,然后进行读写操作。如下代码:
with open("data.txt","w+") as f_obj:f_obj.write("hello java")# 上面的代码等价于下面的代码
f_obj = open("data.txt","w+")
f_obj.write("hello java)
f_obj.close()
使用with语句的时候不需要显式地去关闭文件资源,因为它会自动关闭。那么这个自动关闭是怎么实现的呢,这其实就是__enter__
和__exit__
魔法方法在起作用。
(2)__enter__
和__exit__
方法的工作原理:我们使用with ... as ...
来打开文件,首先进入__enter__
方法,__enter__
方法会返回一个对象,然后将该对象赋给关键字as
后面的变量;当with ... as ...
语句体结束时,会自动调用__exit__
方法。
class OpenFile:def __init__(self,file_name="",mode="r"):self.__file = open(file_name, mode)def __enter__(self):print("__enter__被执行...")return selfdef write(self,ctx):print("writer方法被执行...")self.__file.write(ctx)print("writer方法执行结束...")def __exit__(self,exec_type,exec_value,exec_tb):print("__exit__方法被执行...")self.__file.close()with OpenFile("demo.txt","w+") as f:f.write("hello world")输出:
__enter__被执行...
writer方法被执行...
writer方法执行结束...
__exit__方法被执行...
(3)__exit__
方法中的三个参数:exec_type(异常类型),exec_value(异常对象),exec_tb(异常追踪信息),这些参数在异常处理的时候相当有用。如果with
代码块正常执行完毕,这些参数都为None
。通过下面代码来理解它是怎样工作的。
class Test:def __enter__(self):print("__enter__被执行...")return selfdef compute(self):print(1/0)def __exit__(self,exec_type,exec_value,exec_tb):print("__exit__方法被执行...")print(f"exec_type = {exec_type}")print(f"exec_value = {exec_value}")print(f"exec_tb = {exec_tb}")with Test() as obj_test:obj_test.compute()输出:
__enter__被执行...
__exit__方法被执行...
exec_type = <class 'ZeroDivisionError'>
exec_value = division by zero
exec_tb = <traceback object at 0x000001F61AB8F388>
Traceback (most recent call last):File "d:/code/day2/test.py", line 37, in <module>obj_test.compute()File "d:/code/day2/test.py", line 28, in computeprint(1/0)
ZeroDivisionError: division by zero
with后面的代码块抛出任何异常时,__exit__
方法都会被执行。与之关联的type,value和stack trace会传给__exit__
方法的参数exec_type,exec_value,exec_tb。我们可以在__exit__
方法中执行必要的清理操作(如关闭文件或释放资源)。
参考文章:Python----魔法函数__enter__/__exit__的用法