第046讲:魔法方法:描述符(Property的原理)

版权声明:转载请标明出处 https://blog.csdn.net/qq_41556318/article/details/84799423

目录

 

0. 请写下这一节课你学习到的内容:格式不限,回忆并复述是加强记忆的好方式!

测试题(笔试,不能上机哦~)

0. 请尽量用自己的语言来解释什么是描述符(不要搜索来的答案,用自己的话解释)?

1. 描述符类中,分别通过哪些魔法方法来实现对属性的 get、set 和 delete 操作的?

2. 请问以下代码,分别调用 test.a 和 test.x,哪个会打印“getting…”?

3. 请问以下代码会打印什么内容?

4. 请问以下代码会打印什么内容?

动动手(一定要自己动手试试哦~)

0. 按要求编写描述符 MyDes:当类的属性被访问、修改或设置的时候,分别做出提醒。

1. 按要求编写描述符 MyDes:记录指定变量的读取和写入操作,并将记录以及触发时间保存到文件:record.txt

2. 再来一个有趣的案例:编写描述符 MyDes,使用文件来存储属性,属性的值会直接存储到对应的pickle(腌菜,还记得吗?)的文件中。如果属性被删除了,文件也会同时被删除,属性的名字也会被注销。


0. 请写下这一节课你学习到的内容:格式不限,回忆并复述是加强记忆的好方式!

前面的课程中我们提到了 property 函数,那 property 到底是怎样实现的呢?今天我们谈论的问题是描述符,

描述符就是将某种特殊类型的类的实例指派给另一个类的属性。大家对于这个定义可能还不是很理解,等会会举例说明。首先,什么是特殊类型呢?特殊类型的要求是至少要实现以下三个方法其中一个或全部实现。

(一)•__get__(self, instance, owner)

用于访问属性,它返回属性的值

(二)•__set__(self, instance, value)

将在属性分配操作中调用,不返回任何内容

(三)•__delete__(self, instance)

控制删除操作,不返回任何内容

这三个方法和我们上节课提到的 __getattr__, __setattr__, delattr__是很相像的,但是这三个我们称之为属于描述符属性的方法。__get__用于访问属性的时候直接调用,__set__用于将属性分配,也就是赋值的时候被调用,__delete__用于删除属性的时候被调用。

先来一个最直观的例子:

>>> class MyDescriptor:
	def __get__(self, instance, owner):
		print("getting...", self, instance, owner)
	def __set__(self, instance, value):
		print("setting...", self, instance, value)
	def __delete__(self, instance):
		print("deleting...", self, instance)

		
>>> class Test:
	x = MyDescriptor()

先来写一个描述符 MyDescriptor,并且把所有方法的参数给打印出来。

再来一个真正的Test类来测试一下,就给一个属性 x ,把 MyDescriptor() 的实例指派给Test类的属性 x。我们就说 MyDescriptor 类就是 x 的描述符。(是不是一下子感觉到了 property 的影子)。

我们下面实例化 Test 类,对 x 属性进行各种操作,看看描述符类 MyDescriptor 会有怎样的响应。

>>> test = Test()
>>> test.x
getting... <__main__.MyDescriptor object at 0x000002681DA3D1D0> <__main__.Test object at 0x000002681DA25390> <class '__main__.Test'>
>>> test
<__main__.Test object at 0x000002681DA25390>
>>> Test
<class '__main__.Test'>
>>> test.x = "x-man"
setting... <__main__.MyDescriptor object at 0x000002681DA3D1D0> <__main__.Test object at 0x000002681DA25390> x-man
>>> del test.x
deleting... <__main__.MyDescriptor object at 0x000002681DA3D1D0> <__main__.Test object at 0x000002681DA25390>

我们尝试直接打印 test.x ,我们看到会调用描述符的 __get__,并且参数的意义也很明确, self 是描述符类本身的实例,instance 参数是 它的拥有者 Test 的实例 test,我们直接打印 test,就和 instance 的内容一样,然后 owner 就是它的拥有者 Test 类本身,我们直接打印 Test,就和 owner 的内容一样。

然后赋值(test.x = "x-man")的时候,就会调用描述符的 __set__,删除(del test.x)的时候也是一样,调用__delete__。

我们刚才说过了,只要我们弄清楚描述符,那么 property 的秘密就不再是秘密了,没错,property 就是一个描述符类,我们这里就来定义一个属于自己的 property(MyProperty),来实现property的所有功能:(我们这里定义的MyProperty只是把 property 的功能进行照搬,大家可以加入自己的创意)

>>> class MyProperty:
	def __init__(self, fget = None, fset = None, fdel = None):
		self.fget = fget
		self.fset = fset
		self.fdel = fdel
	def __get__(self, instance, owner):
		return self.fget(instance)
	def __set__(self, instance, value):
		self.fset(instance, value)
	def __delete__(self, instance):
		self.fdel(instance)

		
>>> class C:
	def __init__(self):
		self._x = None
	def getx(self):
		return self._x
	def setx(self, value):
		self._x = value
	def delx(self):
		del self._x
	x = MyProperty(getx, setx, delx)

>>> c = C()
>>> c.x = "x-man"
>>> c._x
'x-man'
>>> del c.x
>>> c._x
Traceback (most recent call last):
  File "<pyshell#62>", line 1, in <module>
    c._x
AttributeError: 'C' object has no attribute '_x'

接下来,我们来做一个练习:

先定义一个温度类,然后定义两个描述符类用于描述摄氏度和华氏度两个属性

要求个属性会自动进行转换,也就是说你可以给摄氏度这个属性赋值,然后打印的华氏度属性是自动转换后的结果。

公式:摄氏度 * 1.8 + 32 = 华氏度

class Celsius:
        def __init__(self, value = 26.0):
                self.value = float(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()
>>> temp.cel
26.0
>>> temp.fah
78.80000000000001
>>> temp.fah = 100
>>> temp.cel
37.77777777777778

测试题(笔试,不能上机哦~)

0. 请尽量用自己的语言来解释什么是描述符(不要搜索来的答案,用自己的话解释)?

答:有时候,某个应用程序可能会有一个相当微妙的需求,需要你设计一些更为复杂的操作来响应(例如每当属性被访问时,你也许想创建一个日志记录)。最好的解决方案就是编写一个用于执行这些“更复杂的操作”的特殊函数,然后指定它在属性被访问时运行。那么一个具有这种函数的对象被称之为描述符。

往再简单了说,描述符就是一个类,一个至少实现 __get__()、__set__() 或 __delete__() 三个特殊方法中的任意一个的类。

1. 描述符类中,分别通过哪些魔法方法来实现对属性的 get、set 和 delete 操作的?

答:__get__、__set__ 和 __delete__

__get__(self, instance, owner)
- 用于访问属性,它返回属性的值

__set__(self, instance, value)i
- 将在属性分配操作中调用,不返回任何内容

__delete__(self, instance)
- 控制删除操作,不返回任何内容

2. 请问以下代码,分别调用 test.a 和 test.x,哪个会打印“getting…”?

>>> class MyDes:
        def __get__(self, instance, owner):
                print("getting...")
 
>>> class Test:
        a = MyDes()
        x = a
 
>>> test = Test()

答:都会打印滴。

3. 请问以下代码会打印什么内容?

class MyDes:
    def __init__(self, value = None):
        self.val = value
 
    def __get__(self, instance, owner):
        return self.val - 20
 
    def __set__(self, instance, value):
        self.val = value + 10
        print(self.val)
 
class C:
    x = MyDes()
 
if __name__ == '__main__':  # 该模块被执行的话,执行下边语句。
    c = C()
    c.x = 10
    print(c.x)

答:需要注意的是 print(c.x) 访问了 c 的 x 属性,因此值减 20。

>>> 
20
0

4. 请问以下代码会打印什么内容?

>>> class MyDes:
        def __init__(self, value = None):
                self.val = value
        def __get__(self, instance, owner):
                return self.val ** 2
 
>>> class Test:
        def __init__(self):
                self.x = MyDes(3)
 
>>> test = Test()
>>> test.x

答:如果你认为小甲鱼考的是 3 de 平方 == 9,那你就 too young too simple了!这其实是一个“陷阱”,我们先来看下会打印什么:

>>> test.x
<__main__.MyDes object at 0x1058e6f60>

如你所见,访问实例层次上的描述符 x,只会返回描述符本身。为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么 Python 无法自动为你调用 __get__ 和 __set__ 方法。


动动手(一定要自己动手试试哦~)

0. 按要求编写描述符 MyDes:当类的属性被访问、修改或设置的时候,分别做出提醒。

程序实现如下:

>>> class Test:
        x = MyDes(10, 'x')
 
>>> test = Test()
>>> y = test.x
正在获取变量: x
>>> y
10
>>> test.x = 8
正在修改变量: x
>>> del test.x
正在删除变量: x
噢~这个变量没法删除~
>>> test.x
正在获取变量: x
8

答:其实大家如果自己认真思考了代码,会发现我们这里描述符起到的作用是间接地保存指定变量的数据。

代码清单:

class MyDes:
    def __init__(self, initval=None, name=None):
        self.val = initval
        self.name = name

    def __get__(self, instance, owner):
        print("正在获取变量:", self.name)
        return self.val

    def __set__(self, instance, value):
        print("正在修改变量:", self.name)
        self.val = value

    def __delete__(self, instance):
        print("正在删除变量:", self.name)
        print("噢~这个变量没法删除~")

1. 按要求编写描述符 MyDes:记录指定变量的读取和写入操作,并将记录以及触发时间保存到文件:record.txt

程序实现如下:

>>> class Test:
        x = Record(10, 'x')
        y = Record(8.8, 'y')
 
>>> test = Test()
>>> test.x
10
>>> test.y
8.8
>>> test.x = 123
>>> test.x = 1.23
>>> test.y = "I love FishC.com!"
>>>

产生文件:record.txt

答:这道题考察的点比较多,例如字符串的转换、文件的操作、time 模块的用法、描述符……大家哪里不会补哪里~

代码清单:

import time

class Record:
    def __init__(self, initval=None, name=None):
        self.val = initval
        self.name = name
        self.filename = "record.txt"

    def __get__(self, instance, owner):
        with open(self.filename, 'a', encoding='utf-8') as f:
            f.write("%s 变量于北京时间 %s 被读取,%s = %s\n" % \
                    (self.name, time.ctime(), self.name, str(self.val)))
        return self.val

    def __set__(self, instance, value):
        filename = "%s_record.txt" % self.name
        with open(self.filename, 'a', encoding='utf-8') as f:
            f.write("%s 变量于北京时间 %s 被修改, %s = %s\n" % \
                    (self.name, time.ctime(), self.name, str(value)))
        self.val = value

2. 再来一个有趣的案例:编写描述符 MyDes,使用文件来存储属性,属性的值会直接存储到对应的pickle(腌菜,还记得吗?)的文件中。如果属性被删除了,文件也会同时被删除,属性的名字也会被注销。

举个栗子:

>>> class Test:
        x = MyDes('x')
        y = MyDes('y')
        
>>> test = Test()
>>> test.x = 123
>>> test.y = "I love FishC.com!"
>>> test.x
123
>>> test.y
'I love FishC.com!'

产生对应的文件存储变量的值:

如果我们删除 x 属性:

>>> del test.x
>>>

对应的文件也不见了:

代码清单:

import os
import pickle

class MyDes:
    saved = []

    def __init__(self, name = None):
        self.name = name
        self.filename = self.name + '.pkl'

    def __get__(self, instance, owner):
        if self.name not in MyDes.saved:
            raise AttributeError("%s 属性还没有赋值!" % self.name)

        with open(self.filename, 'rb') as f:
            value = pickle.load(f)

        return value

    def __set__(self, instance, value):
        with open(self.filename, 'wb') as f:
            pickle.dump(value, f)
            MyDes.saved.append(self.name)

    def __delete__(self, instance):
        os.remove(self.filename)
        MyDes.saved.remove(self.name)

猜你喜欢

转载自blog.csdn.net/qq_41556318/article/details/84799423