python基础知识学习——描述符

本文深入探讨Python中的描述符,解释其含义、作用和分类。描述符作为Python类特性的底层实现,用于代理类的属性。文章详细讲解了数据描述符和非数据描述符的区别,以及描述符的优先级规则。通过实例展示了描述符在限制属性类型、类装饰器和延迟计算等方面的实用应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.描述符的含义:

  • 首先我们来看官方怎么给我们定义描述符的吧
  • 描述符就是一个“绑定行为“的对象属性,在描述符协议中,它可以通过方法充写属性的访问。这些方法有**get(),set(),delete()**,如果这些方法中任何一个被定义在一个对象中,这个对象就是一个描述符.
  • 我理解的描述符就是对一个“辅助类”的作用,它负责对另一个类进行代理,描述类的对象(包括属性、方法等等)。

2.描述符的作用:

  • 描述符的作用是用来代理一个类的属性,需要注意的是描述符不能定义在被使用类的构造函数中,只能定义为类的属性(因为这与描述符的优先级相关),它只属于类的,不属于实例,我们可以通过查看实例和类的字典来确认这一点。

  • 描述符是实现大部分Python类特性中最底层的数据结构的实现手段,我们常使用@classmethod、@staticmethd、@property、甚至是__slots__等属性都是通过描述符来实现的。它是很多高级库和框架的重要工具之一,是使用到装饰器或者元类的大型框架中的一个非常重要组件。

3.描述符的分类:

  1. 数据描述符:必须实现__get__()和__set__() ,可以没有实现__delete__()方法
  2. 非数据描述符:没有实现__set__()

4.描述符的注意点:

  1. 描述符本身应该定义成新式类,被代理的类也应该是新式类
  2. 必须把描述符定义成这个类的类属性,不能定义到这个类的构造函数中
  3. 要严格遵循该优先级,优先级由高到低分别为:
    1. 类属性
    2. 数据描述符
    3. 实例属性
    4. 非数据描述符
    5. 找不到的属性触发__getattr__()

5.描述符格式:

  • 说了这么多,那到底描述符长什么样呢?来看一段代码吧
class myDescriptor:
    def __get__(self, instance, owner):
        print("\n--->这是get方法")
        print("self:", self)
        print("instance:", instance)
        print("owner:", owner)

    def __set__(self, instance, value):
        print("\n--->这是set方法")
        print("self:", self)
        print("instance:", instance)
        print("value:", value)

    def __delete__(self, instance):
        print("\n--->这是delete方法")
        print("self:", self)
        print("instance:", instance)

class People:
    name = myDescriptor()
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = People("xiaoming", 18)
p.name
del p.name
#下面是测试输出的结果:
--->这是set方法
self: <__main__.myDescriptor object at 0x00000247BC153588>
instance: <__main__.People object at 0x00000247BC153A90>
value: xiaoming

--->这是get方法
self: <__main__.myDescriptor object at 0x00000247BC153588>
instance: <__main__.People object at 0x00000247BC153A90>
owner: <class '__main__.People'>

--->这是delete方法
self: <__main__.myDescriptor object at 0x00000247BC153588>
instance: <__main__.People object at 0x00000247BC153A90>
  • 我们看这测试结果,我们可以分析到的内容:
  1. 实例化类People会触发对象调用到myDescriptor.set()方法:
    1. self:是myDescriptor的实例对象,本质上是People类的name属性
    2. instance:People的实例对象,本质上是p
    3. owner: 即谁拥有这些东西,当然是 TestDesc这个类,它是最高统治者,其他的一些都是包含在它的内部或者由它生出来的

6. 优先级示例:

1.类属性的优先级大于数据描述符

class myDescriptor:
    def __get__(self, instance, owner):
        print("\n--->这是get方法")

    def __set__(self, instance, value):
        print("\n--->这是set方法")

    def __delete__(self, instance):
        print("\n--->这是delete方法")

class People:
    name = myDescriptor()
    
print("给类属性name赋值前:", People.__dict__)
People.name = 1
print("给类属性name赋值后:", People.__dict__)
#下面是测试输出的结果:
给类属性name赋值前: {'__module__': '__main__', 'name': <__main__.myDescriptor object at 0x0000028EC8790B00>, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None}
给类属性name赋值后: {'__module__': '__main__', 'name': 1, '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>, '__doc__': None}
  • 我们看这测试结果,我们可以分析到的内容:

name的值由myDescriptor的实例对象的内存地址变成了 1,而且给People.name赋值时,并没有调用myDescriptor的__set__()方法。我们可以发现类属性的优先级大于数据描述符的优先级。

2.数据描述符的优先级大于实例属性的优先级

class myDescriptor:
    def __get__(self, instance, owner):
        print("\n--->这是get方法")

    def __set__(self, instance, value):
        print("\n--->这是set方法")

    def __delete__(self, instance):
        print("\n--->这是delete方法")

class People:
    name = myDescriptor()

p = People()
p.name = "xiaoming"
print(p.name)
#下面是测试输出的结果:
--->这是set方法

--->这是get方法
None
  • 我们看这测试结果,我们可以分析到的内容:
    如果在没有描述符的情况下,这就是普通的为一个类创建实例对象,并对该实例对象的name属性赋值为“xiaoming”,输入实例对象的name属性的值。毫无疑问,赋值什么就输出什么呗。但在这好像跟我们想想的格格不入啊。
    看下输出的结果,我们不难分析到,给p.name赋值时触发了__set__()方法,输出的时候调用了__get__()方法。因此我们可以推断出来,实例对象的属性的优先级并没有数据描述符的优先级大。至于说,为什么输出的时None,这因为我们对__set__()并没有做其他操作,只是一条print语句。

3.实例属性的优先级大于非数据描述符

class myDescriptor:
    def __get__(self, instance, owner):
        print("\n--->这是get方法")

class People:
    name = myDescriptor()

p = People()
print(p.__dict__)
p.name= "xiaoming"
print(p.__dict__)
#下面是测试输出的结果:
{}
{'name': 'xiaoming'}
  • 我们看这测试结果,我们可以分析到的内容:
    通过p.name能够给实例p的属性字典添加新的属性。其实很好理解,因为这描述符没有定义__set__()方法,所以实例属性的优先级会高于非数据描述符。

描述符的应用

描述符应用1:

  • 尽然是描述符的应用,那么就应该有应用场景的啦。我们来看一下这个要求吧:
    1.对用户输入的name和age进行类型限制,name只能是字符串类型,age只能是整数类型
    2.如果用户输入的类型错误,直接报错,程序终止。
  • 这个运用场景还是很常见的,因为python是一种弱语言 。弱语言就是说我们在定义一个变量之前没有严格的限制该变量的类型。它既可以是字符串类型,也可以是整型,也可以是列表、字典等等。
class Des:
    def __init__(self, key, expect_type):
        self.key = key
        self.expect_type = expect_type

    def __get__(self, instance, owner):
        print("这是__get__")
        return instance.__dict__[self.key]

    def __set__(self, instance, value):
        print(value,"__set__()方法")
        if not isinstance(value, self.expect_type):	#如果value是self.expect_type的一个实例,返回True
            raise TypeError("'%s'输入的类型不正确,应该为%s"%(self.key,self.expect_type))
        instance.__dict__[self.key] = value		

    def __delete__(self, instance):
        print("这是__delete__")
        # print(instance)
        instance.__dict__.pop(self.key)
        
class Peopel:
    name = Des("name", str)
    age = Des("age",int)
    def __init__(self, name, age):
        self.name =name
        self.age = age

p = Peopel("baidu",18)
# p = Peopel(100,18)    #name的类型输入错误
# p = Peopel("baidu","18")  #age的类型输入错误
print(p.__dict__)

描述符应用2——类装饰器

  • 可以发现一点,我们在对类的属性进行限制属性类型的时候,我们写了很多重复的代码。虽然我们在这里只定义了两个变量,那如果我们写的是一个大程序,这变量就远远不止两个了,每次都写一句这个,就显得很啰嗦了。为一个属性,函数添加一个功能,不知道你们是否想到了装饰器了呢?接下来我们就了解类装饰器的运用吧。
	name = Des("name", str)
    age = Des("age",int)
  • 我们就保持描述符不变,将上述做成一个装饰器这样就能简化我们的代码,还能提高可读性。
def decorate(**kwargs): 
    def wrapped(obj):
        for key, val in kwargs.items():
            setattr(obj, key, myDescriptor(key, val))   # name = Des("name",str)
        return obj
    return wrapped

我们来分析下整段的代码吧

class myDescriptor:
    def __init__(self, key, myExpect_type):
        self.key = key
        self.myExpect_type = myExpect_type

    def __get__(self, instance, owner):
        return instance.__dict__[self.key]

    def __set__(self, instance, value):
        if not isinstance(value, self.myExpect_type):
            raise TypeError("%s输入的类型错误,应该为%s"%(self.key, self.myExpect_type))
        instance.__dict__[self.key] = value

    def __delete__(self, instance):
        instance.__dict__.pop(self.key)

def decorate(**kwargs):
    def wrapped(obj):
        for key, val in kwargs.items():
            setattr(obj, key, myDescriptor(key, val))   # name = Des("name",str)
        return obj
    return wrapped

@decorate(name = str, age = int)   
class Peopel:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Peopel("baidu")
print(p.__dict__)

首先,程序加载所有的类和函数。当加载到了@decorate(name = str, age = int),一个类名加(),那么就会调用这个类,创建一个实例对象。将name = str, age = int传入给可变参数kwargs,返回的是一个wrapped的内存地址。那么@decorate(name = str, age = int) 就等价于@wrapped,这又等价于People = wrapped(People),此时类People作为一个参数传入到wrapped函数中。在wrapped的函数设计中,我们需要做的是为传进去的People类添加限制属性,并将这个People类返回。这就完成了装饰器的目的。

描述符应用3——自制@property,解决延迟计算

class LazyProperty:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        # print(self)		    #<__main__.LazyProperty object at 0x0000014391DD0D30>
        # print(instance)		#<__main__.Room object at 0x0000014391DD3588>
        # print(owner)			#<class '__main__.Room'>
        print("这是get方法")
        if self is None:
            return self
        res = self.func(instance)			#res = Room.get_area(wc)
        instance.__dict__[self.func.__name__] = res   #wc.__dict__[Room.get_area.__name__] = res
        return res

class Room:
    def __init__(self, name, length, width):
        self.name = name
        self.length = length
        self.width = width

    @LazyProperty   #get_area = LazyProperty(get_area)
    def get_area(self):
        return self.length * self.width

wc = Room("wc", 1, 1)
print(wc.get_area)
print("---------------")
print(wc.get_area)
#下面是测试输出的结果:
这是get方法
1
---------------
1
  • 这个输出结果,我们可以发现第一次调用wc.get_area是,并没有再次调用__get__方法,这就解决了延迟计算的功能了,这能够提高程序的执行效率。
  • 我们来分析这一段代码吧,程序从上到下加载对象到内存中,当执行到 @LazyProperty,这相当于执行get_area = LazyProperty(get_area),这时候的LazyProperty已经被设置成了一个非数据描述符,所以会运行LazyProperty(get_area)中的__get__()方法,并将返回值返回给get_area,这就将一个函数方法转化成了一个变量了。
  • 假如说get_area()方法里面有好几百行代码(小的不才,就只能写几行代码),那么每次调用都需要每次执行,这就导致程序的执行效率就降低了,所以需要用到该描述符。
  • 描述符的设计还是很特别的:
  1. self:为LazyProperty的对象,描述符创建的一个对象
  2. instance: 为目标类创建的一个实例对象,wc
  • 描述符需要做的就是将get_area执行的结果保存到字典中,并通过描述符将结果返回,保存到属性字典的目的是为了下次再调用该属性时,能够在属性字典中找到该值,就不需要在类中执行,提高效率。

  • 关于描述符的记录就差不多结束了,可能内容并不是很多,但这是一个重要的知识,记录到博客中,以后还须经常回来复习,慢慢的使用它,让你变得更加强大吧。。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值