目录
0. 请写下这一节课你学习到的内容:格式不限,回忆并复述是加强记忆的好方式!
0. 请尽量用自己的语言来解释什么是描述符(不要搜索来的答案,用自己的话解释)?
1. 描述符类中,分别通过哪些魔法方法来实现对属性的 get、set 和 delete 操作的?
2. 请问以下代码,分别调用 test.a 和 test.x,哪个会打印“getting…”?
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)