前言

在python中,有一些很特别的方法,它以”__”双下划线开头和结尾,这类方法我们称之为魔术方法。这些方法有特殊的用途,有的不需要我们自己去定义,有的我们通过简单的定义可以实现一些神奇的功能。

__init__、__new__和__del__

构造器:__init__和__new__

__init__应该是我们最为熟悉和常见的一个魔术方法,很多时候,我们会直接把它叫做构造函数,其实不然,在实例一个对象的时候,它并不是第一个被调用的函数,而是一个__new__的方法,这两者共同作用才是真正的构造函数。

看栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
def __init__(self):
print('调用init方法')

@classmethod
def __new__(cls, *args, **kwargs):
print('调用new方法')


a = A()
# 输出结果: 调用new方法
print(a)
# 输出结果: None

结果中我们可以发现有两个问题:

1、当实例对象a时,会自动先去调用__new__方法,但是为什么 __init__没有被调用呢?

2、为什么这个实例对象没有创建成功,返回的是None呢?

__init__没有被调用的原因是对象没有创建成功,那为什么对象没有创建成功呢?

原因是,python3之后,所有的类都是默认继承object类的,我们可以点进object类查看源码:

1
2
3
4
5
6
7
8
9
10
11
class object:
"""
The base class of the class hierarchy.

When called, it accepts no arguments and returns a new featureless
instance that has no instance attributes and cannot be given any.
"""
@staticmethod # known case of __new__
def __new__(cls, *more): # known special case of object.__new__
""" Create and return a new object. See help(type) for accurate signature. """
pass

可以看到,基类中有__new__方法,而在上面的例子中,我们其实是重写了基类的__new__方法!导致它不能真正的去创建对象!(所以__new__要慎用,一般情况最好不要去重写)

我们修改下上面的例子,让类A中的new方法继承基类中的new方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A:
def __init__(self,name,age=None):
self.name = name
self.age = age
print('调用init方法')

@classmethod
def __new__(cls, *args, **kwargs):
print('调用new方法')
print('我是参数args:',args)
print('我是参数kwargs',kwargs)
return super().__new__(cls)


a = A('古一',age=18)
print(a)
# 输出结果:
调用new方法
我是参数args: (<class '__main__.A'>, '古一')
我是参数kwargs {'age': 18}
调用init方法
<__main__.A object at 0x00000280B925C520>

可以看到,实例对象a时,自动先调用__new__方法,由它去创建对象并且由*args, **kwargs来接收创建对象时的参数,第二步,对象创建好后,自动调用__init__方法,由它来完成实例对象的属性初始化操作。因此,由__new____init__组成的构造器,完成了对象的创建和初始化。

前面说到最好不要重写__new__方法,但在做单例模式时,我们会这么做。

单例模式

单例模式(Singleton Pattern)是软件设计中的一种常用设计模式,目的主要是使某个类只存在一个实例(节约内存),比如在我们自动化测试框架中,像日志模块和数据库操作等模块,这两个操作类中,其实我们只需要实例一个日志输出对象和数据库操作对象即可,这种情况,我们即可采用单例。

具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Singleton:
__obj = None

@staticmethod
def __new__(cls, *args, **kwargs):
# 如果该类不存在对象,则创建一个对象
if not cls.__obj:
cls.__obj = super().__new__(cls)
return cls.__obj
else:
# 如果该类已创建过对象,则返回上一个对象
return cls.__obj

a = Singleton()
b = Singleton()
c = Singleton()
print(id(a))
print(id(b))
print(id(c))
# 输出:
1537050624832
1537050624832
1537050624832

在创建对象会自动调用__new__方法,那么我们则可以通过重写__new__的方式,来限制一个类的对象创建。定义一个类属性__obj,如果该类没有创建过对象,那么就正常创建这个对象,并把这个对象赋值给类属性__obj,如果创建过对象,那么直接返回__obj,即之前创建的对象,所以最后可以看到,这个类创建的对象id都是一样的,即是同一个对象。

**ps:**通过装饰器的方式,也可以实现单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def singleton(cls):
# 创建一个字典来存储类和对象的映射关系
dic = {}
def wrapper(*args, **kwargs):
# 如果dic中有key为cls的类,则直接返回其对象
if dic.get(cls):
return dic[cls]
else:
# 如果dic中没有key为cls的类,那么将cls和其对象cls()放到dic中,并返回该对象
dic[cls] = cls(*args, **kwargs)
return dic[cls]
return wrapper

@singleton
class A:
pass

a = A()
b = A()
c = A()
print(id(a))
print(id(b))
print(id(c))
# 输出:
2412994972400
2412994972400
2412994972400

还有更多的单例实现方式,可以参考听风大佬的博文

析构器:__del__

python中通过__del__就是一个析构函数了,当对象被销毁时,会调用他本身的析构函数,另外当对象在某个作用域中调用完毕,在跳出其作用域的同时析构函数也会被调用一次,这样可以用来释放内存空间。

析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。

简单来说,就是擦屁股的,这个对象删除不用了,ok,我帮你善后。

1
2
3
4
5
6
7
8
9
10
class A:
def __del__(self):
print('我来善后!')

a = A()
print('对象a在该作用域中调用完毕') # 对象a在该作用域中调用完成后,立即调用__del__

# 输出:
'对象a在该作用域中调用完毕'
'我来善后!'
1
2
3
4
5
6
7
8
9
10
class A:
def __del__(self):
print('我来善后!')

a = A()
del a # 删除对象a后,立即调用__del__
print('对象a没了')
# 输出:
'我来善后!
'对象a在该作用域中调用完毕'

__enter__和__exit__

在平常操作文件时,通常需要先打开文件、操作读写、再关闭文件,而当使用with 关键字去操作文件时,却可以自动关闭文件,这是为什么呢?why?

1
2
3
4
5
6
7
# 方法一:
f = open('test.txt','w')
f.write('python')
f.close()
# 方法二:
with open('test.txt','w') as f:
f.write('python')

这背后的原理其实就是:上下文管理器。(上下文管理器具体原理看这篇《上下文管理器》

而实现上下文管理器的方法之一,涉及到两个魔术方法:__enter____exit__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo:

def __enter__(self):
print('我要进来了')
return self

def __exit__(self, exc_type, exc_val, exc_tb):
print('我要出去了')

with Demo(): # Demo()即为上下文管理器
print('我进来了')
# 输出:
我要进来了
我进来了
我要出去了

如上所示,Demo实现了__enter____exit__这两个上下文管理器协议,当Demo调用/实例化的时候,则创建了上下文管理器Demo

当上下文管理器遇到with关键字,上下文管理器就会被激活,先自动调用__enter__,然后进入运行时上下文环境,执行with_body中的语句,执行完成后,自动调用__exit__。其实这里的__enter____exit__就类似于我们测试中的前置后置

通常,with会和as一起使用,当有as从句时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Demo:

def __enter__(self):
print('执行前置条件')
return self

def __exit__(self, exc_type, exc_val, exc_tb):
print('执行后置条件')
print(exc_type)
print(exc_val)
print(exc_tb)

@staticmethod
def count(a, b):
return a / b
with Demo() as x:
print(x.count(4,2))

# 输出:
执行前置条件
2.0
None
None
None
执行后置条件

在执行__enter__方法后,会返回自身或另一个与运行时上下文相关的对象(此例返回了Demo对象),然后赋值给变量x,在上下文环境中,实例对象x执行了count方法。

此外,在上面的例子中,应该能注意到,我们分别打印了__exit__ 中的三个参数:exc_typeexc_valexc_tb,但是打印结果却都为None,why?

首先,这三个参数代表的含义为:

  • exc_type: 异常类型
  • exc_val:异常值
  • exc_tb:异常的溯源信息

当在执行with_body中的语句时,若出现了异常,则会自动执行__exit__ 方法,并且将异常信息分别对应这三个参数,传递进__exit__ 方法中进行处理。

我们用上面的例子,修改一下count的参数,构造一个异常:

1
2
with Demo() as x:
print(x.count(1,0))

此时的运行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
执行前置条件
<class 'ZeroDivisionError'>
division by zero
<traceback object at 0x00000231755FACC0>
执行后置条件
Traceback (most recent call last):
File "C:\Users\ancient\demo.py", line 34, in <module>
print(f.count(1,0))
File "C:\Users\ancient\demo.py", line 15, in count
return a / b
ZeroDivisionError: division by zero

可以看到,with代码块中的代码出现了异常后,立即触发了__exit__方法捕捉到这个异常,并分别打印了异常的类型、值、溯源信息,然后抛出了异常。

需要注意的是,如果__exit__ 方法中有return True,那么该方法则不会抛出异常。

__str__

先抛一个问题:为什么str(123)打印出来会是字符串"123"呢?

先不忙解释,我们先看如果打印一个类的实例对象,会是什么呢?

1
2
3
4
5
6
7
class Demo:
pass

a = Demo()
print(a)
# 输出:
<__main__.Demo object at 0x0000019C0B789070>

可以看到,打印结果是a的对象信息和内存地址。其实通过查看str()源码会发现,str其实是一个类,所有的字符串都是它的实例对象,那么为什么同样是类,我们自定义类的实例对象的打印结果却不一样呢?原因就在于__str__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class str(object):
"""
str(object='') -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or
errors is specified, then the object must expose a data buffer
that will be decoded using the given encoding and error handler.
Otherwise, returns the result of object.__str__() (if defined)
or repr(object).
encoding defaults to sys.getdefaultencoding().
errors defaults to 'strict'.
"""
def __init__(self, value='', encoding=None, errors='strict'):
# known special case of str.__init__
"""
str(object='') -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or
errors is specified, then the object must expose a data buffer
that will be decoded using the given encoding and error handler.
Otherwise, returns the result of object.__str__() (if defined)
or repr(object).
encoding defaults to sys.getdefaultencoding().
errors defaults to 'strict'.
# (copied from class doc)
"""
pass

def __str__(self, *args, **kwargs): # real signature unknown
""" Return str(self). """
pass
...............

__str__方法可以自定义实例对象的打印结果为指定的字符串,这里str() 的实例对象返回的是Return str(self),即返回的对象本身,所以,这就是为什么str(123)打印出来会是字符串"123"

我们也可以来自定义试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Car:

def __init__(self, name, price):
self.name = name
self.price = price

def __str__(self):
return f'{self.name}的价格是:{self.price}'


benz = Car('Benz', '40w')
audi = Car('Audi', '35w')

print(benz) # 输出:Benz的价格是:40w
print(audi) # 输出:Audi的价格是:35w

同理,int()bool()等其实也是类似的原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class int(object):
"""
int([x]) -> integer
int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments
are given. If x is a number, return x.__int__(). For floating point
numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string,
bytes, or bytearray instance representing an integer literal in the
given base. The literal can be preceded by '+' or '-' and be surrounded
by whitespace. The base defaults to 10. Valid bases are 0 and 2-36.
Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0)
4
"""
def __int__(self, *args, **kwargs): # real signature unknown
""" int(self) """
pass
...........
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class bool(int):
"""
bool(x) -> bool

Returns True when the argument x is true, False otherwise.
The builtins True and False are the only two instances of the class bool.
The class bool is a subclass of the class int, and cannot be subclassed.
"""
def __and__(self, *args, **kwargs): # real signature unknown
""" Return self&value. """
pass
def __or__(self, *args, **kwargs): # real signature unknown
""" Return self|value. """
pass
.........

python自2.2以后,对类和类型进行了统一,做法就是将int()、float()、str()、list()、tuple()这些BIF转换为工厂函数,所谓工厂函数,其实就是一个类对象,当你调用他们的时候,事实上就是创建一个相应的实例对象。

__add__

"hello," + "world"字符串为什么可以拼接成"hello,world"?

1+1为什么可以等于整型相加为什么可以等于2?

[1,2,3] + ["a","b"]列表相加为什么可以合并为[1,2,3,"a","b"]?

{"a":1} + {"b":2}字典又为什么不能直接相加合并呢?

原因是可以相加的类里都有__add__方法,而字典没有该方法。

1
2
3
def __add__(self, *args, **kwargs): # real signature unknown
""" Return self+value. """
pass

那为什么有这个方法就能让同类型的对象相加呢?

因为__add__方法定义了对象相加的逻辑和返回值,当使用+操作时,将会触发__add__()方法。

直接上例子:

1
2
3
4
5
6
7
8
class Person:
def __init__(self, age):
self.age = age


man = Person(22)
woman = Person(20)
print(man+woman)

这里Person类中并没有定义对象相加的方法,此时直接相加就会报错,提示不支持该操作:

1
2
3
4
Traceback (most recent call last):
File "C:\Users\ancient\demo.py", line 8, in <module>
print(man+woman)
TypeError: unsupported operand type(s) for +: 'Person' and 'Person'

加上__add__后:

1
2
3
4
5
6
7
8
9
10
11
12
class Person:
def __init__(self, age):
self.age = age

def __add__(self, other):
return self.age + other.age


man = Person(22)
woman = Person(20)
print(man + woman)
# 输出结果: 42

__add__方法中的参数selfother分别指Person的不同实例对象,这里self即对象manother即对象woman。配合上文提到的__str__,还可以自定义+的打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person:
def __init__(self, age):
self.age = age

def __str__(self):
return f'两人年龄之和为:{self.age}'

def __add__(self, other):
return Person(self.age + other.age)


man = Person(22)
woman = Person(20)
print(man + woman)
# 输出结果:两人年龄之和为:42

这里只提到了+的魔术方法,其实其他的运算符也都有对应的魔术方法,原理也差不多,本文就不再赘述。

附上其它运算符对应魔术方法:

__slots__

众所周知,python是一门动态语言,所谓动态就是运行代码时可以根据某些条件改变自身结构,例如创建一个类的实例对象,可以给该实例绑定任意的属性和方法。

绑定属性:

1
2
3
4
5
6
class A:
pass

a = A()
a.name = 'python'
print(a.name) # 输出:python

绑定方法:

1
2
3
4
5
6
7
8
from types import MethodType

def set_age(self, age):
self.age = age

a.set_age = MethodType(set_age, a) # 将setage()方法绑定为对象a的方法
a.set_age(18)
print(a.age) # 输出18

此外,每个类的实例对象都会被分配一个__dict__属性,它会维护该实例的所有属性。

1
2
3
4
5
6
7
8
9
10
class A:
def __init__(self,name,age):
self.name = name
self.age = age


a = A('古一',18)
print(a.__dict__)
# 输出结果:
{'name': '古一', 'age': 18}

如果我们再实例一个对象:

1
2
3
4
b = A('古二',19)
print(b.__dict__)
# 输出结果:
{'name': '古二', 'age': 19}

由此可见,类的每次实例化,都会为其对象分配一个__dict__属性,那么试想,如果需要创建大量实例,这个__dict__就会有点浪费内存了。

所以python也是提供了一种解决方案,就是在类中定义__slots__属性。

__slots__一是可以限制实例能添加的属性,二是阻止类实例化时分配__dict__属性。

1
2
3
4
5
6
7
8
9
10
11
class A:
__slots__ = ['name', 'age']

def __init__(self, name, age):
self.name = name
self.age = age


a = A('古一', 18)
a.gender = 'male'
print(a.gender)

输出结果:

1
2
3
4
Traceback (most recent call last):
File "C:\Users\ancient\demo.py", line 65, in <module>
a.gender = 'male'
AttributeError: 'A' object has no attribute 'gender'

报错提示A 对象没有gender这个属性,因为__slots__限制了这个类的所有实例对象只能有nameage这两个属性,不能再添加任何属性。

另外,A的实例对象也不再有__dict__属性了:

1
print(a.__dict__)
1
2
3
4
Traceback (most recent call last):
File "C:\Users\ancient\demo.py", line 66, in <module>
print(a.__dict__)
AttributeError: 'A' object has no attribute '__dict__'

注意

  • __slots__仅对当前类起作用,对继承的子类不起作用
  • 在子类中定义__slots__,子类允许定义的属性就是自身的__slots__加上父类的__slots__

说了这么多,有啥实际应用呢?

在接口自动化测试中,有个场景是需要验证查询日志的返回结果是否跟查询条件一致,比如返回了一组这样的json数据:

即我需要校验其中每个字典中某些字段的值和查询条件一致,那么我该怎么处理呢?

第一个方法很简单,直接用jsonpath提取查询字段,然后遍历比较就行,但是日志的数量通常是上万条的,这时候jsonpath的性能就不太行。

第二个方法,就是自己定义一个逻辑实现。我们可以看到,这样一组json数据中,列表中嵌套了许多字典,每个字典的结构其实都一样,key都是一样的,只是value不一定一样,那么,我们可以封装一个校验json的类,把每个字典都当做一个实例对象,里面的key就是实例属性,value就是实例属性值,然后我们还可以顶一个is_validated方法来对做相关的校验,比如这样:

1
2
3
4
5
6
7
8
9
10
class Validated:
__slots__ = ['happenTime', 'type', 'appName']

def __init__(self, json_data):
for k, v in json_data.items():
if k in self.__slots__:
setattr(self, k, v)

def is_validated(self,expr):
pass

另外有些key 对我们来说,是不需要的,我们只需要保留查询条件字段即可(比如我们的查询字段为happenTimetypeappName,那么我们就只需要保留这三个字段),因此我们可以设置__slots__来限制只保留我们需要的字段,相当于一个白名单,另外,也可以阻止__dict__的生成来节约内存。

这个方法的好处是,代码复用性会更强,扩展性也更强,如果下一次又加了几个新字段,那么继承一下,那么第一个版本和第二个版本分别是属性隔离、接口隔离的,也符合面向对象的开闭原则。

但是,目前这个方法的运行速度还不够快,还需要优化,暂时还没想到更好的方法,害。

自定义属性访问

python的自定义属性访问涉及到四个魔术方法,分别是:__getattribute____getattr____setattr____delattr__

__getattribute__

1
2
3
4
5
6
7
8
9
10
11
12
class A(object):
def __init__(self, x):
self.x = x

def __getattribute__(self, item):
print('调用__getattribute__')

a = A(123)
print(a.x)
# 输出:
调用__getattribute__
None

可以看到,当我们在一个类中定义了__getattribute__方法,在访问对象属性的时候,会去调用这个方法,但是这里返回的结果却是None,这是因为我们重写了父类的__getattribute__方法,它无法完成返回对应属性值的操作。

父类object中的__getattribute__方法:

1
2
3
def __getattribute__(self, *args, **kwargs): # real signature unknown
""" Return getattr(self, name). """ # 返回属性值
pass

注意:虽然看源码我们知道该方法其实就是Return getattr(self, name),但是我们重写的时候绝对不能这么写:

1
2
3
def __getattribute__(self, item):
print('调用__getattribute__')
return self.item # 相当于self.__getattribute__(item)

这样将会陷入无限递归~

因此我们需要调用父类的该方法,来实现返回属性值的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A(object):
def __init__(self, x):
self.x = x

def __getattribute__(self, item):
print('调用__getattribute__')
print(f'item:{item}') # item即属性名
return super().__getattribute__(item)

a = A(123)
print(a.x)
# 输出:
调用__getattribute__
item:x
123

a.x等价于a.__getattribute__(x)

__getattr__

接上面的例子,当访问一个存在的属性时:

1
2
3
4
5
6
7
8
9
10
11
class A(object):
def __init__(self, x):
self.x = x

def __getattribute__(self, item):
print('调用__getattribute__')
print(f'item:{item}') # item即属性名
return super().__getattribute__(item)

a = A(123)
print(a.y)

此时会直接报错:

1
2
3
4
5
6
7
8
调用__getattribute__
item:y
Traceback (most recent call last):
File "C:\Users\ancient\demo.py", line 16, in <module>
print(a.name)
File "C:\Users\ancient\demo.py", line 8, in __getattribute__
return super().__getattribute__(item)
AttributeError: 'A' object has no attribute 'y'

如果我们在这个类中定义一个__getattr__方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A(object):
def __init__(self, x):
self.x = x

def __getattribute__(self, item):
print('调用__getattribute__')
print(f'item:{item}')
return super().__getattribute__(item)

def __getattr__(self, item):
print('调用__getattr__')


a = A(123)
print(a.y)

输出结果:

1
2
3
4
调用__getattribute__
item:name
调用__getattr__
None

可以发现,当访问属性时,会先调用__getattribute__,如果这个属性存在,就返回属性值,如果这个属性不存在(发生AttributeError错误),将会触发__getattr__,这个方法会捕获异常。

其实可以总结出,访问属性时,属性的查找过程如下(如print(a.attr)):

1、首先会在对象的实例属性中寻找,未找到则执行第二步

2、在对象所在的类中查找类属性,未找到则执行第三步

3、到对象的继承链上寻找,未找到则只需第四步

4、最后调用__getattr__方法,如果该属性还是没有找到且没有定义__getattr__方法,那么就会抛出AttributeError,终止查找。

__setattr__

这个很好理解,就是在给对象定义属性时,就会触发__setattr__方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class A(object):
def __init__(self, x):
self.x = x

def __getattribute__(self, item):
print('调用__getattribute__')
print(f'item:{item}')
return super().__getattribute__(item)

def __getattr__(self, item):
print('调用__getattr__')
self.item = 123
return self.item

def __setattr__(self, key, value):
print(f'key:{key}')
print(f'value:{value}')
super().__setattr__(key, value)


a = A(123)
a.y = 'python'
setattr(a, 'z', 'java')
# 输出:
key:x
value:123
key:y
value:python
key:z
value:java

可以看到三种定义对象属性的方式,不管是哪一种,都会触发__setattr__

此外,这里重写__setattr__方法时,也必须调用父类__setattr__,否则属性不会真正设置成功。

__delattr__

很明显,最后一个方法就是在删除对象属性时触发的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A(object):
def __init__(self, x):
self.x = x

def __delattr__(self, item):
print('调用__delattr__')


a = A(123)
del a.x
print(a.x)
# 输出:
调用__delattr__
123

值得注意的是,当重写了__delattr__时,如果没有调用父类的__delattr__,其实属性是不会被删除的。要想删除,就必须调用父类方法。

1
2
3
def __delattr__(self, item):
print('调用__delattr__')
super().__delattr__(item)

小例子

上面说了那么多,结合一个小例子,来看看这几个魔术方法的作用。

1
2
3
4
5
6
7
8
定义一个Students类:
属性:
name: 属性值只能是字符串
age:属性值只能是int
grade:属性值只能是dict类型

name属性不能被删除
grade属性如果没有添加,则返回None
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Students:

def __setattr__(self, key, value):
if key == 'name':
if isinstance(value,str):
super().__setattr__(key,value)
else:
raise AttributeError('name属性只能是str类型')
elif key == 'age':
if isinstance(value,int):
super().__setattr__(key,value)
else:
raise AttributeError('age属性只能是int类型')
elif key == 'grade':
if isinstance(value,dict):
super().__setattr__(key,value)
else:
raise AttributeError('grade属性只能是dict类型')

def __delattr__(self, item):
if item == 'name':
raise AttributeError('name属性不能被删除')
else:
super().__delattr__(item)

def __getattr__(self, item):
if item == 'grade':
return None
else:
return super().__getattribute__(item)
a = Students()
a.name = 123 # 报错,提示name属性只能是str类型
a.age = '12' # 报错,提示age属性只能是int类型
print(a.grade) # 返回None
del a.name # 报错,提示name属性不能被删除

总结

上文中总结了一些常用的魔术方法,所谓魔术方法,我个人理解其实就像LOL中每个英雄的被动技能,当我们给一个类定义了一些魔术方法,就相当于赋予了它一个被动技能,这个被动技能会在特定的情况下被自动触发,从而使对象变得更加灵活强大,实现一些或高级或复杂的功能,是真正的黑魔法

当然,python中的魔术方法远不止上面所总结的,下面附上python中的魔术方法大全以供查阅。

附录

魔术方法大全

魔法方法 含义
基本的魔法方法
new(cls[, …]) __new__是在一个对象实例化的时候所调用的第一个方法
init(self[, …]) 构造器,当一个实例被创建的时候调用的初始化方法
del(self) 析构器,当一个实例被销毁的时候调用的方法
call(self[, args…]) 允许一个类的实例像函数一样被调用:x(a, b) 调用 x.call(a, b)
len(self) 定义当被 len() 调用时的行为
repr(self) 定义当被 repr() 调用时的行为
str(self) 定义当被 str() 调用时的行为
bytes(self) 定义当被 bytes() 调用时的行为
hash(self) 定义当被 hash() 调用时的行为
bool(self) 定义当被 bool() 调用时的行为,应该返回 True 或 False
format(self, format_spec) 定义当被 format() 调用时的行为
有关属性
getattr(self, name) 定义当用户试图获取一个不存在的属性时的行为
getattribute(self, name) 定义当该类的属性被访问时的行为
setattr(self, name, value) 定义当一个属性被设置时的行为
delattr(self, name) 定义当一个属性被删除时的行为
dir(self) 定义当 dir() 被调用时的行为
get(self, instance, owner) 定义当描述符的值被取得时的行为
set(self, instance, value) 定义当描述符的值被改变时的行为
delete(self, instance) 定义当描述符的值被删除时的行为
比较操作符
lt(self, other) 定义小于号的行为:x < y 调用 x.lt(y)
le(self, other) 定义小于等于号的行为:x <= y 调用 x.le(y)
eq(self, other) 定义等于号的行为:x == y 调用 x.eq(y)
ne(self, other) 定义不等号的行为:x != y 调用 x.ne(y)
gt(self, other) 定义大于号的行为:x > y 调用 x.gt(y)
ge(self, other) 定义大于等于号的行为:x >= y 调用 x.ge(y)
算数运算符
add(self, other) 定义加法的行为:+
sub(self, other) 定义减法的行为:-
mul(self, other) 定义乘法的行为:*
truediv(self, other) 定义真除法的行为:/
floordiv(self, other) 定义整数除法的行为://
mod(self, other) 定义取模算法的行为:%
divmod(self, other) 定义当被 divmod() 调用时的行为
pow(self, other[, modulo]) 定义当被 power() 调用或 ** 运算时的行为
lshift(self, other) 定义按位左移位的行为:<<
rshift(self, other) 定义按位右移位的行为:>>
and(self, other) 定义按位与操作的行为:&
xor(self, other) 定义按位异或操作的行为:^
or(self, other) 定义按位或操作的行为:|
反运算
radd(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rsub(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rmul(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rtruediv(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rfloordiv(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rmod(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rdivmod(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rpow(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rlshift(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rrshift(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rand(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
rxor(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
ror(self, other) (与上方相同,当左操作数不支持相应的操作时被调用)
增量赋值运算
iadd(self, other) 定义赋值加法的行为:+=
isub(self, other) 定义赋值减法的行为:-=
imul(self, other) 定义赋值乘法的行为:*=
itruediv(self, other) 定义赋值真除法的行为:/=
ifloordiv(self, other) 定义赋值整数除法的行为://=
imod(self, other) 定义赋值取模算法的行为:%=
ipow(self, other[, modulo]) 定义赋值幂运算的行为:**=
ilshift(self, other) 定义赋值按位左移位的行为:<<=
irshift(self, other) 定义赋值按位右移位的行为:>>=
iand(self, other) 定义赋值按位与操作的行为:&=
ixor(self, other) 定义赋值按位异或操作的行为:^=
ior(self, other) 定义赋值按位或操作的行为:|=
一元操作符
pos(self) 定义正号的行为:+x
neg(self) 定义负号的行为:-x
abs(self) 定义当被 abs() 调用时的行为
invert(self) 定义按位求反的行为:~x
类型转换
complex(self) 定义当被 complex() 调用时的行为(需要返回恰当的值)
int(self) 定义当被 int() 调用时的行为(需要返回恰当的值)
float(self) 定义当被 float() 调用时的行为(需要返回恰当的值)
round(self[, n]) 定义当被 round() 调用时的行为(需要返回恰当的值)
index(self) 1. 当对象是被应用在切片表达式中时,实现整形强制转换 2. 如果你定义了一个可能在切片时用到的定制的数值型,你应该定义 index 3. 如果 index 被定义,则 int 也需要被定义,且返回相同的值
上下文管理(with 语句)
enter(self) 1. 定义当使用 with 语句时的初始化行为 2. enter 的返回值被 with 语句的目标或者 as 后的名字绑定
exit(self, exc_type, exc_value, traceback) 1. 定义当一个代码块被执行或者终止后上下文管理器应该做什么 2. 一般被用来处理异常,清除工作或者做一些代码块执行完毕之后的日常工作
容器类型
len(self) 定义当被 len() 调用时的行为(返回容器中元素的个数)
getitem(self, key) 定义获取容器中指定元素的行为,相当于 self[key]
setitem(self, key, value) 定义设置容器中指定元素的行为,相当于 self[key] = value
delitem(self, key) 定义删除容器中指定元素的行为,相当于 del self[key]
iter(self) 定义当迭代容器中的元素的行为
reversed(self) 定义当被 reversed() 调用时的行为
contains(self, item) 定义当使用成员测试运算符(in 或 not in)时的行为

参考文章:《python魔术方法大全》