背景今天在B站上学习“零基础入门学习Python”这门课程的第46讲“魔法方法:描述符”,这也是我们组织的 Python基础刻意练习活动 的学习任务,其中有这样的一个题目。 练习要求: 先定义一个温度类,然后定义两个描述符类用于描述摄氏度和华氏度两个属性。 要求两个属性会自动进行转换,也就是说你可以给摄氏度这个属性赋值,然后打印的华氏度属性是自动转化后的结果。 华氏度与摄氏度的转换关系:1 Fahrenheit = 1 Celsius*1.8 + 32
技术分析为了解决这个问题,我们首先回顾__dict__ 属性,以及__get__ ,__set__ ,__delete__ 魔法方法,然后总结描述符这个 Python 语言特有的语法结构,最后写代码完成要求的任务。 1. __dict__ 属性 class Test(object): cls_val = 1
def __init__(self): self.ins_val = 10
t = Test() print(Test.__dict__) # {'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x000000EBCB65F598>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None} print(t.__dict__) # {'ins_val': 10}
根据 Python 的语法结构,t 为实例对象,Test 为类对象。其对应的属性ins_val 和cls_val 称为实例属性和类属性。实例t 的属性并不包含cls_val ,cls_val 是属于类Test 的。 t.cls_val = 20 print(Test.__dict__) # {'__module__': '__main__', 'cls_val': 1, '__init__': <function Test.__init__ at 0x000000CB7EB5F598>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None} print(t.__dict__) # {'ins_val': 10, 'cls_val': 20}
可见,更改实例t 的属性cls_val ,只是新增了该属性,并不影响类Test 的属性cls_val 。 Test.cls_val = 30 print(Test.__dict__) # {'__module__': '__main__', 'cls_val': 30, '__init__': <function Test.__init__ at 0x000000DAB2BFC048>, '__dict__': <attribute '__dict__' of 'Test' objects>, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None} print(t.__dict__) # {'ins_val': 10, 'cls_val': 20}
可见,更改了类Test 的属性cls_val 的值,由于事先增加了实例t 的cls_val 属性,因此不会改变实例的cls_val 值。 2. __get__() ,__set__() ,__delete__() 魔法方法 get(self, instance, owner)
set(self, instance, value)
del(self, instance)
class Desc(object): def __get__(self, instance, owner): print("__get__...") print("self:", self) print("instance: ", instance) print("owner: ", owner)
def __set__(self, instance, value): print('__set__...') print("self:", self) print("instance:", instance) print("value:", value)
class TestDesc(object): x = Desc()
t = TestDesc() t.x
# __get__... # self: <__main__.Desc object at 0x0000009C9B980198> # instance: <__main__.TestDesc object at 0x0000009C9B9801D0> # owner: <class '__main__.TestDesc'>
可以看到,实例化类TestDesc 后,调用对象t 访问其属性x ,会自动调用类Desc 的__get__ 方法,由输出信息可以看出: self : Desc 的实例对象,其实就是TestDesc 的属性x
instance : TestDesc 的实例对象,其实就是t
owner : 即谁拥有这些东西,当然是 TestDesc 这个类,它是最高统治者,其他的一些都是包含在它的内部或者由它生出来的
3. 描述符的定义 某个类,只要是内部定义了方法__get__ ,__set__ ,__delete__ 中的一个或多个,就可以称为描述符。Desc 类就是一个描述符(描述符是一个类)。 t 为实例对象,访问t.x 时,根据常规顺序。
首先,访问Owner 的__getattribute__() 方法(其实就是 TestDesc.__getattribute__() ),访问实例属性,发现没有,然后去访问父类! 其次,判断属性x 为一个描述符,此时,它就会做一些变动了,将TestDesc.x 转化为TestDesc.__dict__['x'].__get__(None, TestDesc) 来访问。 最后,进入类Desc 的__get__() 方法,进行相应的操作。 - 问题2. 从上面代码我们看到了,描述符的对象
x 其实是类TestDesc 的类属性,那么可不可以把它变成实例属性呢?
class Desc(object): def __init__(self, name): self.name = name
def __get__(self, instance, owner): print("__get__...") print('name = ', self.name)
class TestDesc(object): x = Desc('x')
def __init__(self): self.y = Desc('y')
t = TestDesc() t.x t.y
# __get__... # name = x
咦,为啥没打印 t.y 的信息呢? 因为调用 t.y 时刻,首先会去调用TestDesc (即Owner )的 __getattribute__() 方法,该方法将 t.y 转化为TestDesc.__dict__['y'].__get__(t, TestDesc) ,但是呢,实际上 TestDesc 并没有y 这个属性,y 是属于实例对象的,所以,只能忽略了。 - 问题3. 如果 类属性的描述符对象 和 实例属性描述符的对象 同名时,咋整?
class Desc(object): def __init__(self, name): self.name = name print("__init__(): name = ", self.name)
def __get__(self, instance, owner): print("__get__() ...") return self.name
def __set__(self, instance, value): self.value = value
class TestDesc(object): _x = Desc('x')
def __init__(self, x): self._x = x
t = TestDesc(10) t._x
# __init__(): name = x # __get__() ...
不对啊,按照惯例,t._x 会去调用 __getattribute__() 方法,然后找到了 实例t 的 _x 属性就结束了,为啥还去调用了描述符的 __get__() 方法呢? 这就牵扯到了一个查找顺序问题:当 Python 解释器发现实例对象的字典中,有与描述符同名的属性时,描述符优先,会覆盖掉实例属性。 我们再将代码改进一下, 删除 __set__() 方法试试看会发生什么情况? class Desc(object): def __init__(self, name): self.name = name print("__init__(): name = ", self.name)
def __get__(self, instance, owner): print("__get__() ...") return self.name
class TestDesc(object): _x = Desc('x')
def __init__(self, x): self._x = x
t = TestDesc(10) print(t._x)
# __init__(): name = x # 10
可见,一个类,如果只定义了 __get__() 方法,而没有定义 __set__() , __delete__() 方法,则认为是非数据描述符;反之,则成为数据描述符。非数据描述符,优先级低于实例属性。 ① __getattribute__() , 无条件调用 ② 数据描述符 ③ 实例对象的字典 ④ 类的字典 ⑤ 非数据描述符 ⑥ 父类的字典 ⑦ __getattr__() 方法
代码实现class Celsius: def __init__(self, value=26.6): self.value = value
def __get__(self, instance, owner): return self.value
def __set__(self, instance, value): self.value = float(value)
class Fahrenheit: def __get__(self, instance, owner): return instance.cel * 1.8 + 32
def __set__(self, instance, value): instance.cel = (float(value) - 32) / 1.8
class Temperature: cel = Celsius() fah = Fahrenheit()
temp = Temperature() print(temp.cel) # 26.6 print(temp.fah) # 79.88 temp.cel = 30 print(temp.cel) # 30 print(temp.fah) # 86.0 temp.fah = 79.88 print(temp.cel) # 26.599999999999998 print(temp.fah) # 79.88
总结通过以上的介绍我们了解了 Python 中描述符的定义,以及属性调用的优先级。由于Python魔法方法非常复杂需要下很大的功夫才能把这块搞明白。今天就到这里吧,See you!
参考文献 https://www.runoob.com/python3/python3-tutorial.html https://www.bilibili.com/video/av4050443 http://c./view/2371.html https://www.cnblogs.com/seablog/p/7173107.html https://www.cnblogs.com/Jimmy1988/p/6808237.html
相关图文:
经过8年多的发展,LSGO软件技术团队在「地理信息系统」、「数据统计分析」、「计算机视觉」等领域积累了丰富的研发经验,也建立了人才培养的完备体系,目前深耕的领域为「机器学习与量化金融」,欢迎对计算机技术感兴趣的同学加入,与我们共同成长进步。
|