Python 入门 ------ 描述器
文章目录
描述器
前面我们介绍了两种属性拦截的方式:特性(property
)以及重载属性访问运算符,还有一种方式是通过描述器来拦截属性访问,特性只是一种特定类型的描述器的简化使用方式,而描述器也是由 __getattribute__
函数调用。
描述器允许我们把特定属性的 get
、set
、del
操作指向独立的类对象方法,使对象能够自定义属性查找、存储和删除操作。
任何实现了:__get__
、__set__
、__delete__
方法中的一种的类都可以称为描述器。
描述器的主要目的是提供一个挂钩,允许存储在类变量中的对象控制在属性查找期间发生的情况。
传统上,调用类控制查找过程中发生的事情。但描述器反转了这种关系,并允许正在被查询的数据对此进行干涉。
简单示例
python
class Name:
"""Name descriptor doc
"""
def __get__(self, obj, objtype=None):
print('get')
return obj._name
def __set__(self, obj, value):
print('set')
obj._name = value
def __delete__(self, obj):
print('del')
del obj._name
class Person:
name = Name()
def __init__(self, name):
self.name = name
tom = Person('Tom')
# set
tom.name
# get
# 'Tom'
tom.name = 'Robert'
# set
tom.name
# get
# 'Robert'
vars(tom)
# {'_name': 'Robert'}
del tom.name
# del
虽然我们为 Person
定义了 name
属性,但是存储在 __dict__
中的还是 _name
(与 Name
描述符中使用的属性一致)。
注意:
必须将描述符赋值给一个类属性,如果赋值为实例属性,将无法工作,可以自己尝试一下。注意这三个方法的参数,
obj
表示Person
类实例对象,objtype
默认为Person
定制名称
在上面的例子中,我们定义的描述符只能用于 _name
属性,但是一般我们所定义的描述符都希望其能够适用于更多的属性,这显然不符合我们实际的需求。
我们可以添加 __set_name__
方法来记录字段名称,让每个描述符都有自己的公有属性和私有属性名称,例如
python
class Field:
def __set_name__(self, owner, attr):
self.public_name = attr
self.private_name = '_' + attr
def __get__(self, obj, objtype=None):
value = getattr(obj, self.private_name)
print('Accessing', self.public_name)
return value
def __set__(self, obj, value):
print('Updating', self.public_name, 'to', value)
setattr(obj, self.private_name, value)
class Person:
name = Field()
age = Field()
def __init__(self, name, age):
self.name = name
self.age = age
tom = Person('Tom', 19)
# Updating name to Tom
# Updating age to 19
tom.name
# Accessing name
# 'Tom'
vars(tom)
# {'_name': 'Tom', '_age': 19}
tom._name
# 'Tom'
vars(vars(Person)['name'])
# {'public_name': 'name', 'private_name': '_name'}
其中 owner
是使用描述器的类,name
是分配给描述器的类变量名。
vars
函数只是查找属性并不会触发方法,访问私有属性也不会经过描述符。
只读属性
使用描述符来创建只读属性,光定义 __get__
方法,而没有定义 __set__
是不够的,例如
python
class TestDesc:
def __get__(self, obj, obj_type=None):
print('Get')
class Test:
t = TestDesc()
a = Test()
a.t
# Get
vars(a)
# {}
a.t = 100
a.t
# 100
vars(a)
# {'t': 100}
对描述符属性赋值会将其覆盖,可以看到赋值后出现了一个常规的公开属性,要设置只读属性,要在 __set__
方法中让赋值行为引发一个异常
python
class TestDesc:
def __get__(self, obj, obj_type=None):
print('Get')
def __set__(self, obj, value):
raise AttributeError("Con't set value!")
class Test:
t = TestDesc()
a = Test()
a.t
# Get
a.t = 100
# AttributeError: Con't set value!
状态交互
描述符内也可以定义属性值,在 __get__
和 __set__
函数内也可以使用实例参数来获取包含描述符的类的属性值
python
class Money:
def __init__(self, value):
self.value = value
def __get__(self, obj, obj_type=None):
return self.value * getattr(obj, 'rate')
def __set__(self, obj, value):
self.value = value
class Exchange:
money = Money(1000)
def __init__(self, rate):
self.rate = rate
s = Exchange(0.567)
s.money
# 567.0
s.rate = 0.678
s.money
# 678.0
上面这段代码,属性 value
仅存在于描述符中,并不会与使用描述符的类中的属性有冲突。money
属性会随着 rate
值的变化自动计算出相应的值。
验证器类
验证器是一个用于托管属性访问的描述器。在存储任何数据之前,它会验证新值是否满足各种类型和范围限制。如果不满足这些限制,它将引发异常,从源头上防止数据损坏。
这一功能通常在前后端数据校验中比较常用,我们可以自己实现一个验证器。例如,我们定义一个抽象类,必须实现 validate
方法
python
from abc import ABC, abstractmethod
class Validator(ABC):
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
@abstractmethod
def validate(self, value):
pass
自定义验证器
我们可以对数据进行常见的几种校验,例如:
OneOf
:验证值是一组受约束的选项之一。
python
class OneOf(Validator):
def __init__(self, *options):
self.options = set(options)
def validate(self, value):
if value not in self.options:
raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
Number
:验证值是否为int
或float
。根据可选参数,它还可以验证值在给定的最小值或最大值之间。
python
class Number(Validator):
def __init__(self, minvalue=None, maxvalue=None):
self.minvalue = minvalue
self.maxvalue = maxvalue
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected {value!r} to be an int or float')
if self.minvalue is not None and value < self.minvalue:
raise ValueError(
f'Expected {value!r} to be at least {self.minvalue!r}'
)
if self.maxvalue is not None and value > self.maxvalue:
raise ValueError(
f'Expected {value!r} to be no more than {self.maxvalue!r}'
)
String
:验证值是否为str
。根据可选参数,它可以验证给定的最小或最大长度,还可以验证用户定义的predicate
。
python
class String(Validator):
def __init__(self, minsize=None, maxsize=None, predicate=None):
self.minsize = minsize
self.maxsize = maxsize
self.predicate = predicate
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f'Expected {value!r} to be an str')
if self.minsize is not None and len(value) < self.minsize:
raise ValueError(
f'Expected {value!r} to be no smaller than {self.minsize!r}'
)
if self.maxsize is not None and len(value) > self.maxsize:
raise ValueError(
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
)
if self.predicate is not None and not self.predicate(value):
raise ValueError(
f'Expected {self.predicate} to be true for {value!r}'
)
验证器的使用
我们定义一个类,包含三种带验证器的描述器属性
python
class Component:
name = String(minsize=3, maxsize=10, predicate=str.isupper)
kind = OneOf('wood', 'metal', 'plastic')
quantity = Number(minvalue=0)
def __init__(self, name, kind, quantity):
self.name = name
self.kind = kind
self.quantity = quantity
描述器会阻止错误实例的创建
python
Component('Widget', 'metal', 5) # 'Widget' 不是全大写
# Traceback (most recent call last):
# ...
# ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
Component('WIDGET', 'metle', 5) # 'metle' 不在取值范围
# Traceback (most recent call last):
# ...
# ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
Component('WIDGET', 'metal', -5) # quantity 的值要大于等于 0
# Traceback (most recent call last):
# ...
# ValueError: Expected -5 to be at least 0
Component('WIDGET', 'metal', 'V') # quantity 不是数值
# Traceback (most recent call last):
# ...
# TypeError: Expected 'V' to be an int or float
c = Component('WIDGET', 'metal', 5) # 通过
对象关系映射
我们可以使用描述器来实现一个简单的对象关系映射(ORM
)框架
其核心思路是将数据存储在外部数据库中,Python
实例仅持有数据库表中对应的的键,描述器负责对值进行查找或更新。
我们首先定义字段描述器
python
class Field:
def __set_name__(self, owner, name):
self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'
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()
然后用 Field
类来定义描述了数据库中每张表的模式(models
)。
python
class Movie:
table = 'Movies' # Table name
key = 'title' # Primary key
director = Field()
year = Field()
def __init__(self, key):
self.key = key
class Song:
table = 'Music'
key = 'title'
artist = Field()
year = Field()
genre = Field()
def __init__(self, key):
self.key = key
要使用模型,首先要创建一个数据库
python
def create_tables(conn):
with conn:
conn.execute('''
CREATE TABLE IF NOT EXISTS Movies (
title TEXT PRIMARY KEY,
director TEXT,
year INTEGER
)
''')
conn.execute('''
CREATE TABLE IF NOT EXISTS Music (
title TEXT PRIMARY KEY,
artist TEXT,
year INTEGER,
genre TEXT
)
''')
def insert_movie(conn, title, director, year):
with conn:
conn.execute('''
INSERT INTO Movies (title, director, year) VALUES (?, ?, ?)
''', (title, director, year))
def insert_song(conn, title, artist, year, genre):
with conn:
conn.execute('''
INSERT INTO Music (title, artist, year, genre) VALUES (?, ?, ?, ?)
''', (title, artist, year, genre))
conn = sqlite3.connect('example.db')
create_tables(conn)
insert_movie(conn, 'Inception', 'Christopher Nolan', 2010)
insert_movie(conn, 'The Matrix', 'Lana Wachowski, Lilly Wachowski', 1999)
insert_song(conn, 'Bohemian Rhapsody', 'Queen', 1975, 'Rock')
insert_song(conn, 'Imagine', 'John Lennon', 1971, 'Pop')
从数据库中检索数据及对其进行更新
python
Movie('Inception').director
# 'Christopher Nolan'
mat = Movie('The Matrix')
f'Released in {mat.year} by {mat.director}'
# 'Released in 1999 by Lana Wachowski, Lilly Wachowski'
Song('Imagine').artist
# 'John Lennon'
Movie('Inception').director = 'Nolan'
Movie('Inception').director
# 'Nolan'
关闭数据库连接
python
conn.close()