带你了解Python面向对象(5)高级篇:描述符、制作一系列装饰器

描述符(Descriptor)

描述符的使用面很广,不过其主要的目的在于让我们的调用过程变得可控制。因此我们在一些需要对我们调用过程实行精细控制的时候,使用描述符。

描述符本质就是一个新式类,在这个新式类中,至少具备了get()set()delete()其中一种,这也被称为:描述符协议

get():调用一个属性时触发
set():为一个属性赋值时触发
delete():使用del删除一个属性时触发


1、描述符的了解阶段


定义

class An:
    def __get__(self, instance, owner):
        pass

    def __set__(self, instance, value):
        pass

    def __delete__(self, instance):
        pass

这种定义的类,已经具备了我们所列出的3个方法其中之一了,此时它可以称为:描述符

我们可以尝试来调用它

res = An()
res.name = 'jack'
print(res.name)
> 'jack'

可以看到,里面的set()get()都没有触发,说明并不是这样使用描述符的。


描述符的分类

而描述符为分为:数据描述符、非数据描述符

1、数据描述符:数据描述符定义了set 或 delete() 其中一个方法。而通常数据描述会具备:set() 与 get() 两个方法
2、非数据描述符:只具备set()方法

官方文档的解释

在这里插入图片描述


作用

描述符的作用:它是在引用一个对象属性时自定义要完成的工作,可以很好规定我们定义属性的数据类型,避免代码的重复率

使用原则:用来代理另外一个类的属性的,必须把描述符定义成类的属性,且不能定义到构造函数中。

在此之前,先来看一下属性的查找顺序

class People:
	name = '20'

    def __init__(self,name,age):
        self.name = name
        sel.name = name
        self.age = age

p = People('jack',18)
print(p.name)
> 'jack'

未产生意外,属性先在自身获取 -> 类的属性 -> … 为什么要提这个,因为在使用描述符后,属性的查找顺序将发生改变!


描述符的注意事项

描述符本身应该定义成新式类,被代理的类也应该是新式类(Python3中都是新式类)

必须把描述符定义成这个类的类属性,不能为定义到构造函数中

要严格遵循该优先级,优先级由高到底分别是

1、类属性
2、数据描述符
3、实例属性
4、非数据描述符
5、找不到的属性触发getattr()

这就是为何上面提到了属性的查找顺序,因为在我们使用描述符以后,查找将会被这个优先级所代替。


描述符的使用

我们通过实例来了解:

# 描述符类
class An:
    def __get__(self, instance, owner):
        print('get触发了')

    def __set__(self, instance, value):
        print(f'set触发了')

    def __delete__(self, instance):
        print('delete触发了')

# 被描述的类
class People:

	# 类属性name代理了An()这个描述符类
    name = An()

    def __init__(self,n,age):

		# 重点!!!!

        # self.name 此时,这里的self.name已经不是给对象的属性了
        # 而是调用了类属性name,也就是上面那个
        self.name = 123 # 这里表示调用类属性name代理的描述符里的set()方法并传了123这个值进去
        # 简单来说就是,我们通过self找到的类属性里面的name,而并不是给对象赋值

		self.nnm = n # 这里的nnm未被代理,所以可以赋给对象作为属性
        self.age = age

p = People('jack',18)

和上面对比,我们的查找顺序已经发生了改变,直接类的属性了

直接运行:由于我们在类里面就调用了代理的描述符,所以触发了set方法
在这里插入图片描述
我们通过调用,类代理的描述符属性都会触发描述符里面的方法

p = People('jack',18)

p.name 

这个name并不是对象的,而是类的,且对象也无法创建name这个属性了,因为在类里面name已经被代理,无论对name赋值,还是查询,都是指向到name代理的描述符里面去

执行效果

'get触发了'

删除代理描述符的类属性时

del p.name

执行效果

'delete触发了'
使用小结:

已经发现,优先级的查找顺序在代理描述符的那刻起,就发生了改变。只要类的属性有,那么就用类的属性。而类的属性对应的则是描述符。所以赋值还是查询等操作都是传到了描述符类的方法里面去了


描述符的查找优先级

数据描述符(也就是包含了set或delete方法)查找属性:优先在类里面查找的,也就是我们上面所演示的

非数据描述符(只包含get方法的)查找属性:是优先在实例(对象)里面查找的

# 非数据描述符类
class An:
    def __get__(self, instance, owner):
        print('get方法执行了')

# 被描述符类
class People:
    name = An()
    def __init__(self,name):
        self.name  = name

p = People('jack')
print(p.name)

执行结果

'jack'

可以看到并没有触发描述符的get方法

因为非数据描述符,没有set方法,所以属性就添加到了对象里面,而查找的话,如果对象自身没有找到,则会去调用非数据描述符的get方法

# 非数据描述符类
class An:
    def __get__(self, instance, owner):
        print('get方法执行了')

# 被描述符类
class People:
    name = An()
    def __init__(self,age):
        self.age  = age # 将age属性赋给对象了

p = People(18)
print(p.name) # 查找的是name属性,对象自身没有,则触发了描述符里面的get()方法

执行结果

'get方法执行了'
None

描述符类的方法详解及调用触发

在调用描述符时,会传递一系列值进去。我们需要知道传递的值都是些什么。及什么情况下会触发

class An:
    def __get__(self, instance, owner):
        print(f'get触发了')

		# 这个描述符自身的对象,就像我们之前定义的类里面,每个方法都会有一个self
        print(f'描述符类的对象:{self} ')

		# 也就是p对象
        print(f'调用代理描述符的对象:{instance}')

		# 实例化p对象的类,也就是People
        print(f'调用代理描述符的对象所属的类:{owner}')
        print(instance.__dict__)

    def __set__(self, instance, value):
        print(f'set触发了')
        print(f'描述符类的对象:{self} ')
        print(f'调用代理描述符的对象:{instance}')
        print(f'调用代理描述符所传递的值:{value}')
        print(instance.__dict__)
        # 打印就可以知道,是不是调用代理描述符的对象了

    def __delete__(self, instance):
        print(f'delete触发了')
        print(f'调用代理描述符的对象:{instance}')
        print(instance.__dict__)

通过调用来查看结果:

class People:
    name = An()

    def __init__(self,name,age):
        self.age = age

p = People('jack',18)

p.name = '123' # 调用描述符的代理(name),并进行传值,执行了描述符的set()

执行结果

'''
set触发了
描述符类的对象:<__main__.An object at 0x7fe60a286760> 
代理描述符的对象:<__main__.People object at 0x7fe60a56ffd0>
调用描述符所传递的值:123
'''
{'age': 18}

可以看到,调用代理描述符时,instance参数接收的接收调用代理描述符的对象,也就是我们这里的p对象,因为是它调用的

调用描述符

p.name # 执行描述符的get()
'''
get触发了
描述符类的对象:<__main__.An object at 0x7fdcb667e760> 
代理描述符的对象:<__main__.People object at 0x7fdcb706ffd0>
代理描述符的对象所属的类:<class '__main__.People'>
'''
{'age': 18}

删除描述符代理

del p.name # 执行描述符的delete()方法
'''
delete触发了
代理描述符的对象:<__main__.People object at 0x7facc116ffd0>
'''
{'age': 18}

别使用类调用描述符

类来调用,就可以看到代理描述符的对象为None,基本不会这样调用

People.name # 执行描述符的get()
'''
get触发了
描述符类的对象:<__main__.An object at 0x7f9c0a186760> 
调用代理描述符的对象:None
调用代理描述符的对象所属的类:<class '__main__.People'>
'''
AttributeError: 'NoneType' object has no attribute '__dict__'

发生报错,因为instance接收到的不是对象,所以值为None,打印__dict__对象属性时就产生了报错,了解即可,基本不会这样调用描述符。

我们再使用类来调用数据描述符,会发现一些奇怪的事情

People.name = '2'

执行效果

# 什么也没有发生,这种表示People修改了name属性,那么此时name属性就不再是一个代理

调用查看

print(People.name)
> '2'

可以发现,描述符只能作为类属性使用,即不适合被类使用,也不能放入构造函数__init__内,两者其一都会失去描述符的意义。所以我们通过实例化的对象来调用才是最适合的用法


向描述符传值

向描述符传值操作与我们创建对象时的操作是一致的,但是不同点在于描述符类需要被调用才能生效。

class An:
    def __init__(self, sex): # 接收到被描述类的代理属性传递的属性名
        self.sex = sex # 此时这个sex代表'sex'是我们类属性传递过来的

    def __get__(self, instance, owner):
        print(f'get触发了')
        return instance.__dict__[self.sex]
        # 查询时返回查询的属性
        
    def __set__(self, instance, value):
        print(f'set触发了')
        instance.__dict__[self.sex] = value
        # 将接收到的值放入调用这个描述符的对象属性内(也就是p)

    def __delete__(self, instance):
        print(f'delete触发了')


class People:
    # 通过An这个描述符实例化出一个对象,但暂时并不使用,只是起到一个传值操作
    sex = An('sex')

    def __init__(self, age):
        self.age = age


p = People(18)

p.sex = 'male' # 调用描述符的set()方法并传值'male'到value参数

print(p.__dict__)
print(p.sex)

执行结果

'set触发了'
{'age': 18, 'sex': 'male'}
'get触发了'
'male'

上序提到过,如果我们通过类名去调用会因为没有传递对象进去而报错,那么我们这里再来处理一下

People.sex
> AttributeError: 'NoneType' object has no attribute '__dict__'

我们只需要在查询描述符代理执行的get()方法时,做一个判断,那么就可以避免掉这个问题

def __get__(self, instance, owner):
     print(f'get触发了')
     if instance is None:
         return '当前未通过对象调用描述符的代理'
     return instance.__dict__[self.sex]

print(People.sex)

执行结果

'get触发了'
'当前未通过对象调用描述符的代理'

2、描述符进阶部分

限制属性设置的类型

可以限制我们对属性的传递为何种类型,如果不对则给出提示信息,既然涉及到了传递,那么就要对set()方法做点手脚

class An: # 两个方法进行调整
	def __init__(self, attri,data_type):  # 接收到被描述类的代理属性传递的属性名
		# 接收传递进来的第一个参数,我们将它作为属性名使用
	    self.attri = attri
	
		# 接收传递进来的第而个参数,我们将它数据类型,待会与传到set里面的值做匹配
	    self.data_type = data_type
	
	def __set__(self, instance, value):
		# 判断我们通过=赋给代理属性的值是否匹配我们传给这个描述符这个数据类型
	    if not isinstance(value,self.data_type):
	    	# 如果不支持,抛出类型错误的信息
	        raise TypeError(f'{self.attri}属性传递的不是:{self.data_type}类型数据')
	    instance.__dict__[self.arrti] = value
	    # 将属性添加到对象里面,这对象指定是调用代理描述符的那个类属性,也就是p

class People:
    # 将参数传给了描述符里__init__
    name = An('name',str)
					 # 这个name接收到的是000,因为是在调用类是传递进来的
    def __init__(self,name, age):
    
    	# 把000赋给name = An('name',str)这个描述符代理
    	# 执行了描述符里面的set()方法,然后将000传给value参数
        self.name = name
        self.age = age # 未被代理,所以可以实例化给对象


p = People(000,18)

print(p.name)

执行结果:
在这里插入图片描述

因为传递的数据类型不匹配,所以抛出异常

我们将传递给描述符set()的的值进行纠正

p = People('jack',18)
print(p.name)

执行结果

'get触发了'
'jack'

进一步优化,限制多个属性的类型

传递多个属性,代码稍作改动

# 被描述符类
class People:
			  # 第一个参数作为给p对象的属性名
    name = An('name',str)
    age = An('age', int)
    sex = An('sex',str)

					  # 这些参数作为给p对象的属性值
    def __init__(self,name, age,sex):
    
		# 将它们赋给不同的描述符代理
        self.name = name
        self.age = age
        self.sex = sex

p = People('jack',18,'sex')

print(p.name)
print(p.age)
print(p.sex)

执行结果

'''
get触发了
jack
get触发了
18
get触发了
sex
'''

到这里,我们已经可以实现我们的目的了,但是这样随着后期的属性增加,将会出现一堆的描述符代理,low,继续优化!

先来了解一下类的装饰器,哈哈,没听错,是给类使用的装饰器!它可以帮助我们优化描述符代理叠加的问题


类的装饰器

1、无参装饰器

先来了解无参的,有简入难

def decorate(cls):
    print('类的装饰器开始运行------>')
    cls.name = 'jack' # 给People类加上name属性
    return cls # 返回了People这个类

@decorate
class People: # People = decorate(People)
    pass

p = People() # 接收到People这个类,加上()调用类实例化了对象p

print(p.name) # p这个对象自身没有name这个属性,去People类里面找到

执行结果

'jack'
2、有参装饰器
def arrti_datatype(**kwargs):
    def decorate(cls):
        for attri,data_type in kwargs.items():
            setattr(cls,attri,data_type)
        print('有参装饰器!-> %s' % kwargs)
        return cls
    return decorate

@arrti_datatype(name = str,age = int,sex = str)
class People:
    def __init__(self,name,age,sex):
        self.name = name
        self.age = age
        self.sex = sex

print(People.__dict__)
p = People('jack',18,'20')

执行效果

有参装饰器!-> {'name': <class 'str'>, 'age': <class 'int'>, 'sex': <class 'str'>}
省略...<class 'str'>, 'age': <class 'int'>, 'sex': <class 'str'>}

可以看到,我们已经将这个我们制定好的参数变成我们的类属性了,只是目前这些类属性没有代理描述符!我们只需要类属性变成代理描述符的类属性即可大功告成!


最终优化结果

不使用重复代码完成,限制属性设置的数据类型

class An:
    def __init__(self,attri,data_type):
        self.attri = attri
        self.data_type = data_type

    def __get__(self, instance, owner):
        print(f'get触发了')
        if instance is None:
            return '当前未通过对象调用描述符的代理'
        return instance.__dict__[self.attri]

    def __set__(self, instance, value):
        if not isinstance(value, self.data_type):
            raise TypeError(f'{self.attri}属性传递的不是:{self.data_type}类型数据')
        instance.__dict__[self.attri] = value

    def __delete__(self, instance):
        print('执行了delete方法:')


def arrti_datatype(**kwargs):
    def decorate(cls):
        for attri,data_type in kwargs.items():
        	# 注意这里,将传给类的属性变成代理描述符的类属性
			# cls:People这个类
			# attri:给People类设置的属性名
			# An(attri,data_type) 将属性名与数据类型传递到描述符里的__init__内
			# 得到一个代理描述符的对象,然后赋给cls,也就是People类
            setattr(cls,attri,An(attri,data_type))
            # setattr就是给对象增加属性的,类也可以是一个对象,也可以进行属性增加
        return cls
    return decorate

@arrti_datatype(name = str,age = int,sex = str)
class People:
    def __init__(self,n,a,s):
    	# 此时name、age、sex都被描述符所代理,所以我们的赋值操作都是传到了描述符内的set方法里
        self.name = n
        self.age = a
        self.sex = s
        
print(People.__dict__)

我们可以看到,这些类属性已经变成了代理描述符的类属性

省略... 'name': <__main__.An object at 0x7fbc1536ffd0>, 'age': <__main__.An object at 0x7fbc153b28b0>, 'sex': <__main__.An object at 0x7fbc153b2c40>}

我们查看执行效果,先故意传错一个属性,看看会抛出异常

p = People('jack',18,222)

在这里插入图片描述
可以,已经出现效果了,我们再将属性进行纠正

p = People('jack',18,'male')

print(p.name)
print(p.age)
print(p.sex)

执行效果

'''
get触发了
jack
get触发了
18
get触发了
male
'''

我们的描述符类基本没有改变,关键点在于装饰器那里,给类属性赋值时,调用描述符,获得一个代理描述符的对象,然后赋给类作为属性。

到这里的相信已经会使用描述符进行操作了,根据自身需求制定描述符来对程序进行精确控制。请自行发挥!!


拓展内容


1、自定制@property

@property装饰器是我们在学习封装时所用到的,它的作用是将方法伪装成一个属性,来回顾一下

class People:
    @property
    def test(self):
        print('test')


p = People()
p.test

执行结果

'test'

我们也可以通过描述符来实现,自己写一个效果类似的

class customized:
    def __init__(self, func):
        self.func = func  # 将bmi方法拿到

    def __get__(self, instance, owner):
        print('执行了定制pro方法')
        if instance is None:
            return '请使用对象调用'
        # instance=就是调用bmi方法的对象,也就是p
        return self.func(instance)  # 将它传入进bmi方法内,它就可以使用self了
        # 调用bmi方法,相当于 bmi(p) 拿到返回值,返回给调用者


class People:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @customized
    def bmi(self):  # bmi = customized(bmi),但是并没有把self传递进去
        return self.width / (self.height ** 2)


p = People(70, 1.90)

print(round(p.bmi, 2))  # 通过round方法,将返回值保留小数点后两位

执行结果

'执行了定制pro方法'
'19.39'

制作缓存

效果就是,我们调用过一次bmi方法后,第二次不会再去调用,而是拿着第一次调用的结果直接打印,拿上边举例优化

p = People(70, 1.90)

print(round(p.bmi, 2))  # 通过round方法,将返回值保留小数点后两位
print(round(p.bmi, 2))  # 通过round方法,将返回值保留小数点后两位
print(round(p.bmi, 2))  # 通过round方法,将返回值保留小数点后两位

执行效果

'''
执行了定制pro方法
19.39
执行了定制pro方法
19.39
执行了定制pro方法
19.39
'''

可以看到,我们每次调用它的bmi方法,所以我们让它第一次拿到结果以后,不再去运行bmi方法计算获取结果

修改装饰器内的代码即可

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

    def __get__(self, instance, owner):
        print('执行了定制pro方法')
        if instance is None:
            return '请使用对象调用'

		# 将方法名(bmi)作为属性名添加到对象内,再将计算的值添加进去
		# 这样下次对象调用就能在自身获取到,不会再调用相同的方法来获取了
        instance.__dict__[self.func.__name__] = self.func(instance)
        return self.func(instance)

我们再来执行查看效果

print(round(p.bmi, 2))  # 通过round方法,将返回值保留小数点后两位
print(round(p.bmi, 2))  # 通过round方法,将返回值保留小数点后两位
print(round(p.bmi, 2))  # 通过round方法,将返回值保留小数点后两位
print(p.__dict__)

执行结果

'''
执行了定制pro方法
19.39
19.39
19.39
'''
{'width': 70, 'height': 1.9, 'bmi': 19.390581717451525}

这样一来,我们第一次输入bmi是去调动bmi这个方法,调用以后在装饰器内,将结果作为属性保存到了我们对象内,所以下一次调用则是去自身属性内找到了,所以直接打印。


2、制作类绑定方法

在之前使用类绑定方法的话,需要为类里面的某个函数加上@classmethod装饰器,该函数就与类进行了绑定。

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

	@classmethod
	def test(cls): # 我们在使用@classmethod装饰器创建方法时,默认就在括号内加上了cls,这个代表了我们这个People类
		print(cls)

p = People('jack')
p.test() # 通过对象来调用类绑定方法

执行结果:

<class '__main__.People'>

那么我们也可以通过描述符类实现这一操作

class MyClassMethod:
    def __init__(self, func):
        self.func = func  # 接收到传递过来的test函数内存地址

    def __get__(self, instance, owner):
        # owner:调用get方法对象的类,也就是p对象的类

        return self.func(owner)  # 将类作为实参传递给test函数


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

    @MyClassMethod  # test = MyClassMethod(test),将函数内存地址传递给了描述符
    def test(cls):  # 定义一个参数来接收
        print(cls)


p = People('jack')
p.test  # 调用了MyClassMethod,触发了get方法

执行结果

<class '__main__.People'>

3、制作静态方法

类里面的静态方法,就是一个普通函数,创建时无任何参数,我们通常使用@staticmethod装饰器来创建

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

    @staticmethod
    def test():
        print('这是一个普通函数')


p = People('jack', '男', 18)
p.test()

执行结果

'这是一个普通函数'

我们使用描述符来使用这一操作,且让它可以接收参数

class MyStaticMethod:
    def __init__(self, func):
        self.func = func  # 接收到传递过来的test函数内存地址

    def __get__(self, instance, value):
    	# 定义一个与制作的静态方法名称相同
        def test(*args, **kwargs):
            return self.func(*args, **kwargs) # 将值传递给test函数

        return test


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

    @MyStaticMethod
    def test(*args, **kwargs):
        print(args, kwargs)


p = People('jack')
p.test(1, 2, 3, 4, a=50, b=60) # 将参数传递描述符的get方法下面的test

执行结果

(1, 2, 3, 4) {'a': 50, 'b': 60}

技术小白记录学习过程,有错误或不解的地方欢迎在评论区留言,如果这篇文章对你有所帮助请点赞、评论、收藏+关注 子夜期待您的关注,谢谢支持!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值