深入理解Python虚拟机中描述器的实现原理
作者:一无是处的研究僧 发布时间:2021-08-05 21:20:48
在本篇文章当中主要给大家介绍一个我们在使用类的时候经常使用但是却很少在意的黑科技——描述器,在本篇文章当中主要分析描述器的原理,以及介绍使用描述器实现属性访问控制和 orm 映射等等功能!在后面的文章当中我们将继续去分析描述器的实现原理。
描述器的基本用法
描述器是一个实现了 __get__
、__set__
或 __delete__
中至少一个方法的 Python 类。这些方法分别用于在属性被访问、设置或删除时调用。当一个描述器被定义为一个类的属性时,它可以控制该属性的访问、修改和删除。
下面是一个示例,演示了如何定义一个简单的描述器:
class Descriptor:
def __get__(self, instance, owner):
print(f"Getting {self.__class__.__name__}")
return instance.__dict__.get(self.attrname)
def __set__(self, instance, value):
print(f"Setting {self.__class__.__name__}")
instance.__dict__[self.attrname] = value
def __delete__(self, instance):
print(f"Deleting {self.__class__.__name__}")
del instance.__dict__[self.attrname]
def __set_name__(self, owner, name):
self.attrname = name
在这个例子中,我们定义了一个名为 Descriptor 的描述器类,它有三个方法:__get__
、__set__
和 __delete__
。当我们在另一个类中使用这个描述器时,这些方法将被调用,以控制该类的属性的访问和修改。
要使用这个描述器,我们可以在另一个类中将其定义为一个类属性:
class MyClass:
x = Descriptor()
现在,我们可以创建一个 MyClass 对象并访问其属性:
>>> obj = MyClass()
>>> obj.x = 1
Setting Descriptor
>>> obj.x
Getting Descriptor
1
>>> del obj.x
Deleting Descriptor
>>> obj.x
Getting Descriptor
在这个例子中,我们首先创建了一个 MyClass 对象,并将其 x 属性设置为 1。然后,我们再次访问 x 属性时,会调用 __get__
方法并返回 1。最后,我们删除了 x 属性,并再次访问它时,会调用 __get__
方法并返回 None。从上面的输出结果可以看到对应的方法都被调用了,这是符合上面对描述器的定义的。如果一个类对象不是描述器,那么在使用对应的属性的时候是不会调用__get__
、__set__
和 __delete__
三个方法的。比如下面的代码:
class NonDescriptor(object):
pass
class MyClass():
nd = NonDescriptor()
if __name__ == '__main__':
a = MyClass()
print(a.nd)
上面的代码输出结果如下所示:
<__main__.NonDescriptor object at 0x1012cce20>
从上面程序的输出结果可以知道,当使用一个非描述器的类属性的时候是不会调用对应的方法的,而是直接得到对应的对象。
描述器的实现原理
描述器的实现原理可以用以下三个步骤来概括:
当一个类的属性被访问时,Python 解释器会检查该属性是否是一个描述器。如果是,它会调用描述器的
__get__
方法,并将该类的实例作为第一个参数,该实例所属的类作为第二个参数,并将属性名称作为第三个参数传递给__get__
方法。当一个类的属性被设置时,Python 解释器会检查该属性是否是一个描述器。如果是,它会调用描述器的
__set__
方法,并将该类的实例作为第一个参数,设置的值作为第二个参数,并将属性名称作为第三个参数传递给__set__
方法。当一个类的属性被删除时,Python 解释器会检查该属性是否是一个描述器。如果是,它会调用描述器的
__delete__
方法,并将该类的实例作为第一个参数和属性名称作为第二个参数传递给__delete__
方法。
在描述器的实现中,通常还会使用 __set_name__
方法来在描述器被绑定到类属性时设置属性名称。这使得描述器可以在被多个属性使用时,正确地识别每个属性的名称。
现在来仔细了解一下上面的几个函数的参数,我们以下面的代码为例子进行说明:
class Descriptor(object):
def __set_name__(self, obj_type, attr_name):
print(f"__set_name__ : {obj_type } {attr_name = }")
return "__set_name__"
def __get__(self, obj, obj_type):
print(f"__get__ : {obj = } { obj_type = }")
return "__get__"
def __set__(self, instance, value):
print(f"__set__ : {instance = } {value = }")
return "__set__"
def __delete__(self, obj):
print(f"__delete__ : {obj = }")
return "__delete__"
class MyClass(object):
des = Descriptor()
if __name__ == '__main__':
a = MyClass()
_ = MyClass.des
_ = a.des
a.des = "hello"
del a.des
上面的代码输入结果如下所示:
__set_name__ : <class '__main__.MyClass'> attr_name = 'des'
__get__ : obj = None obj_type = <class '__main__.MyClass'>
__get__ : obj = <__main__.MyClass object at 0x1054abeb0> obj_type = <class '__main__.MyClass'>
__set__ : instance = <__main__.MyClass object at 0x1054abeb0> value = 'hello'
__delete__ : obj = <__main__.MyClass object at 0x1054abeb0>
__set_name__
这个函数一共有两个参数传入的参数第一个参数是使用描述器的类,第二个参数是使用这个描述器的类当中使用的属性名字,在上面的例子当中就是 "des" 。__get__
,这个函数主要有两个参数,一个是使用属性的对象,另外一个是对象的类型,如果是直接使用类名使用属性的话,obj 就是 None,比如上面的 MyClass.des 。__set__
,这个函数主要有两个参数一个是对象,另外一个是需要设置的值。__delete__
,这函数有一个参数,就是传入的对象,比如 del a.des 传入的就是对象 a 。
描述器的应用场景
描述器在 Python 中有很多应用场景。以下是其中的一些示例:
实现属性访问控制
通过使用描述器,可以实现对类属性的访问控制,例如只读属性、只写属性、只读/只写属性等。通过在 __get__
和 __set__
方法中添加相应的访问控制逻辑,可以限制对类属性的访问和修改。
class ReadOnly:
def __init__(self, value):
self._value = value
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
raise AttributeError("Read only attribute")
class MyClass:
read_only_prop = ReadOnly(42)
writeable_prop = None
my_obj = MyClass()
print(my_obj.read_only_prop) # 42
my_obj.writeable_prop = "hello"
print(my_obj.writeable_prop) # hello
my_obj.read_only_prop = 100 # raises AttributeError
在上面的例子中,ReadOnly
描述器只实现了 __get__
方法,而 __set__
方法则抛出了 AttributeError
异常,从而实现了只读属性的访问控制。
实现数据验证和转换
描述器还可以用于实现数据验证和转换逻辑。通过在 __set__
方法中添加数据验证和转换逻辑,可以确保设置的值符合某些特定的要求。例如,可以使用描述器来确保设置的值是整数、在某个范围内、符合某个正则表达式等。
class Bounded:
def __init__(self, low, high):
self._low = low
self._high = high
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
if not self._low <= value <= self._high:
raise ValueError(f"Value must be between {self._low} and {self._high}")
self._value = value
class MyClass:
bounded_prop = Bounded(0, 100)
my_obj = MyClass()
my_obj.bounded_prop = 50
print(my_obj.bounded_prop) # 50
my_obj.bounded_prop = 200 # raises ValueError
在上面的例子中,Bounded
描述器在 __set__
方法中进行了数值范围的检查,如果值不在指定范围内,则抛出了 ValueError
异常。
实现延迟加载和缓存
描述器还可以用于实现延迟加载和缓存逻辑。通过在 __get__
方法中添加逻辑,可以实现属性的延迟加载,即当属性第一次被访问时才进行加载。此外,还可以使用描述器来实现缓存逻辑,以避免重复计算。
class LazyLoad:
def __init__(self, func):
self._func = func
def __get__(self, instance, owner):
if instance is None:
return self
value = self._func(instance)
setattr(instance, self._func.__name__, value)
return value
class MyClass:
def __init__(self):
self._expensive_data = None
@LazyLoad
def expensive_data(self):
print("Calculating expensive data...")
self._expensive_data = [i ** 2 for i in range(10)]
return self._expensive_data
my_obj = MyClass()
print(my_obj.expensive_data) # Calculating expensive data...
print(my_obj.expensive_data)
上面的程序的输出结果如下所示:
Calculating expensive data...
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
从上面的结果可以看到,只有在第一次使用属性的时候才调用函数,后续再次调用函数将不会再调用函数而是直接返回缓存的结果。
实现 ORM 映射
ORM 的主要作用是把数据库中的关系数据转化为面向对象的数据,让开发者可以通过编写面向对象的代码来操作数据库。ORM 技术可以把面向对象的编程语言和关系数据库之间的映射关系抽象出来,开发者可以不用写 SQL 语句,而是直接使用面向对象的语法进行数据库操作。
我们现在需要实现一个功能,user.name 直接从数据库的 user 表当中查询 name 等于 user.name 的数据,user.name = "xxx" 根据 user 的主键 id 进行更新数据。这个功能我们就可以使用描述器实现,因为只需要了解如何使用描述器的,因此在下面的代码当中并没有连接数据库:
conn = dict()
class Field:
def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
print(f"{self.fetch = }")
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
print(f"{self.store = }")
def __get__(self, obj, objtype=None):
return conn.execute(self.fetch, [obj.key]).fetchone()[0]
def __set__(self, obj, value):
conn.execute(self.store, [value, obj.key])
conn.commit()
class User:
table = 'User' # Table name
key = 'id' # Primary key
name = Field()
age = Field()
def __init__(self, key):
self.key = key
if __name__ == '__main__':
u = User("Bob")
上面的程序输出结果如下所示:
self.fetch = 'SELECT name FROM User WHERE id=?;'
self.store = 'UPDATE User SET name=? WHERE id=?;'
self.fetch = 'SELECT age FROM User WHERE id=?;'
self.store = 'UPDATE User SET age=? WHERE id=?;
从上面的输出结果我们可以看到针对 name 和 age 两个字段的查询和更新语句确实生成了,当我们调用 u.name = xxx 或者 u.age = xxx 的时候就执行 __set__
函数,就会连接数据库进行相应的操作了。
来源:https://juejin.cn/post/7230110285766836283
猜你喜欢
- 本文实例讲述了Python文件与文件夹常见基本操作。分享给大家供大家参考,具体如下:1、判断文件(夹)是否存在。os.path.exists
- 镜像使用方法(三种办法任意一种都能解决问题,建议使用第三种,将配置写死,下次用的时候配置还在):1.通过config命令npm config
- <?php $search = array ("'<script[^>]*?>.*?</sc
- 不说什么,先上代码这里先求解形如的微分方程1.欧拉法def eluer(rangee,h,fun,x0,y0):
- 1.引子:函数也是对象木有括号的函数那就不是在调用。def hi(name="yasoob"):return "
- 一、实现原理其实实现原理很简单,我们的pptx文件其实是一个压缩包。我们可以直接修改pptx文件的后缀,改成zip然后解压,比如下面这个:这
- 参数strSQL 要导出的SQL查询语句strFields 字段名称列表,如果为空字符,则使用SQL语句中的字段名用法示例:1:export
- 前言:博主在刚接触Python的时候时常听到GIL这个词,并且发现这个词经常和Python无法高效的实现多线程划上等号。本着不光要知其然,还
- 一:分组函数的语句顺序 1 SELECT ... 2 FROM ...
- 首先mysql更新数据的某个字段,一般这样写:UPDATE mytable SET myfield = 'value' WH
- 在matplotlib中,errorbar方法用于绘制带误差线的折线图,基本用法如下plt.errorbar(x=[1, 2, 3, 4],
- 一、什么是数据库事务数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执
- 主程序import pygamefrom pygame.sprite import Groupfrom settings import Se
- API说明调用地址:http://yzxyzm.market.alicloudapi.com/yzx/verifySms请求方式:POST返
- Tkinter 实现上述功能并不复杂,只要使用 Tkinter 的相关组件和一些简单的逻辑处理即可,在编写这个案例的过程中大家要做到温故而知
- golang 最吸引人的地方可能就是并发了,无论代码的编写上,还是性能上面,golang 都有绝对的优势学习一个语言的并发特性,我喜欢实现一
- 最近在做搜索设计时,发现了两个容易纠结的小问题,在这里谈谈自己的一些分析。问题一:提交的关键字是哪个?凡客的这个例子中,搜索建议“时尚斜拉链
- 类型主要针对文本属性进行定义。理解“编辑字体列表”和“行高”。二、CSS规则定义之“背景”·背景有背景颜色和背景图像的选择设置。·利于背景图
- 本文介绍了PyTorch上搭建简单神经网络实现回归和分类的示例,分享给大家,具体如下:一、PyTorch入门1. 安装方法登录PyTorch
- 前言: 不要试图用强制方法杀掉一个python线程,这从服务设计上就存在不合理性。 多线程本用来任务的协