背景
今天在B站学习“零基础入门学习 Python”中的第45节“魔法方法:属性访问”,这也是我们组织的 Python基础刻意练习活动 的学习任务,其中有这样的一个题目。
练习要求:
- 写一个矩形类,默认有宽和高两个属性。
- 如果为一个叫
square
的属性赋值赋值,那么说明这是一个正方形,值就是正方形的边长,此时宽和高都应该等于边长。
技术分析
我们先来看看有关于属性的四个魔法方法:
__getattr__(self, name)
: 定义当用户试图获取一个不存在的属性时的行为。__getattribute__(self, name)
:定义当该类的属性被访问时的行为(先调用该方法,查看是否存在该属性,若不存在,接着去调用__getattr__
)。__setattr__(self, name, value)
:定义当一个属性被设置时的行为。__delattr__(self, name)
:定义当一个属性被删除时的行为。
class Test:
def __getattr__(self, name):
print('__getattr__')
def __getattribute__(self, name):
print('__getattribute__')
def __setattr__(self, name, value):
print('__setattr__')
def __delattr__(self, name):
print('__delattr__')
t = Test()
t.x
# __getattribute__
如上述代码所示,x
并不是Test
实例对象t
的一个属性,首先去调用 __getattribute__()
方法,得知该属性并不属于该实例对象。但是,按照常理,t.x
应该打印 __getattribute__
和__getattr__
,但实际情况并非如此,为什么呢?
实例对象属性寻找的顺序如下:
① 首先访问 __getattribute__()
魔法方法(隐含默认调用,无论何种情况,均会调用此方法)。
② 接着,去t.__dict__
中查找是否具备该属性。
③ 若在 t.__dict__
中找不到对应的属性, 则去t.__class__.__dict__
中寻找。
④ 若在实例的类中也找不到该属性,则去父类中寻找,即 t.__class__.__bases__.__dict__
中寻找
⑤ 若以上均无法找到,则会调用 __getattr__
方法,执行内部的命令(若未重载 __getattr__
方法,则直接报错:AttributeError
)
以上几个流程,即完成了属性的寻找。
但是,以上的说法,并不能解释为什么执行 t.x
时,不打印 __getattr__
啊?
问题就出在了步骤的第④步,因为,一旦重载了 __getattribute__()
方法,如果找不到属性,则必须要手动加入第④步,否则无法进入到 第⑤步 (__getattr__
)的。
验证一下以上说法是否正确:
方法一:采用 object
(所有类的基类)
class Test:
def __getattr__(self, name):
print('__getattr__')
def __getattribute__(self, name):
print('__getattribute__')
object.__getattribute__(self, name)
def __setattr__(self, name, value):
print('__setattr__')
def __delattr__(self, name):
print('__delattr__')
t = Test()
t.x
# __getattribute__
# __getattr__
方法二:采用 super()
方法
class Test:
def __getattr__(self, name):
print('__getattr__')
def __getattribute__(self, name):
print('__getattribute__')
super().__getattribute__(name)
def __setattr__(self, name, value):
print('__setattr__')
def __delattr__(self, name):
print('__delattr__')
t = Test()
t.x
# __getattribute__
# __getattr__
以上介绍完毕,那么 __setattr__
和 __delattr__
方法相对简单多了:
class Test:
def __getattr__(self, name):
print('__getattr__')
def __getattribute__(self, name):
print('__getattribute__')
object.__getattribute__(self, name)
def __setattr__(self, name, value):
print('__setattr__')
def __delattr__(self, name):
print('__delattr__')
t = Test()
t.x = 1
# __setattr__
del t.x
# __delattr__
对了,再补充一点哈!
class Test:
def __init__(self):
self.count = 0
def __setattr__(self, name, value):
print('__setattr__')
self.count += 1
t = Test()
# AttributeError: 'Test' object has no attribute 'count'
看报错信息很容易明白,这是因为:
① __init__()
时,给内部属性 self.count
进行了赋值;
② 赋值默认调用 __setattr__()
方法
③ 当调用 __setattr__()
方法时,首先打印 __setattr__
字符串,而后执行 self.cout += 1
操作
④ 当执行 self.cout + 1
操作时,将会去寻找 count
这个属性,然而,由于此时 __init__
尚未完成,并不存在 count
这个属性,因此导致 AttributeError
错误。
那么该如何更改呢?可以这样的:
class Test:
def __init__(self):
self.count = 0
def __setattr__(self, name, value):
print('__setattr__')
super().__setattr__(name, value + 1)
t = Test()
print(t.count)
# __setattr__
# 1
以上代码虽然解决了报错的问题,深入体会一下,你会发现,采用此方法只是给 基类object
增加了一个属性 count
,而并不是实例的属性,因此,以上这种写法避免使用。
另外,再次将代码改进一下,如下:
class Test:
def __setattr__(self, name, value):
self.name = value
t = Test()
t.x = 'lsgo'
# RecursionError: maximum recursion depth exceeded
当我们给 t.x
赋值时,调用了 __setattr__()
方法,进入该方法。该方法中,又来了一次赋值(self.name = value
),又会去调用 __setattr__()
方法,持续这个死循环。
所以,我们只好改变上述的问题了:
class Test:
def __setattr__(self, name, value):
print('__setattr__() been called')
super().__setattr__(name, value)
t = Test()
t.x = 'lsgo'
# __setattr__() been called
print(t.x)
# lsgo
代码实现
上面详细介绍了关于属性的四个魔法方法,下面我们来看实现要求的具体代码:
class Rectangle:
def __init__(self, width=0, height=0):
self.width = width
self.height = height
def __setattr__(self, key, value):
if key == 'square':
self.width = value
self.height = value
else:
super().__setattr__(key, value)
def getArea(self):
return float(self.width) * float(self.height)
r = Rectangle(4, 5)
print(r.getArea()) # 20.0
r.square = 10
print(r.__dict__) # {'width': 10, 'height': 10}
print(r.getArea()) # 100.0
总结
魔法方法是 Python 面向对象编程中最核心的内容,需要花费一定的精力才能将其掌握,参加 Python基础刻意练习的小伙伴们加油! See You!
参考文献:
- https://www.bilibili.com/video/av4050443/?p=46
- https://www.runoob.com/python3/python3-tutorial.html
- https://www.cnblogs.com/Jimmy1988/p/6804095.html
相关图文: