0. 背景
在Python的大量类属性或是实例属性中,由于python语法的特性,属性并不带有类型,可以随意赋值,当然了也可以通过@property
用来限制某个属性,但是对于需要大量重用代码的情况下,描述符会更有用。使用描述符,与其说是类型限制,不如说是赋值时的类型检查。问题解决的目标是:
针对某个自定义的类,自定义有目标类型(比如
Integer
,String
,Float
)的类属性或是实例属性。如果在初始化或赋值操作中,需要使得属性的类型发生了转化,则会出现Error
。
1. 思路与方案
如果想创建一个具有类型限制的实例属性,可以以描述符(了解python描述符的用法)的形式定义其功能。实例如下:
# 使用描述符来对以Integer类型为属性的限制
class Integer(object):
def __init__(self, name):
self.name = name
def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError("Expected an int")
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
所谓的描述符就是以特殊方法__get__()
,__set__()
,__delete__()
的形式实现了三个核心的属性访问操作(对于与get
,set
和delete
)的类。这些方法通过接受类实例作为输入来工作。之后,底层的实例字典会根据需要适当地进行调整。
要使用一个描述符,我们把描述符的实例放置在类的定义中作为类变量来用。示例如下:
# 属性x,y以Integer类为类型的使用
class Point(object):
x = Integer('x')
y = Integer('y')
def __init__(self, x, y):
self.x = x
self.y = y
当这么做时,所有针对描述符属性(即,这里的x或y)的访问都会被__get__()
,__set__()
,__delete__()
方法所捕获
if __name__ == '__main__':
p = Point(2,3) # 调用Point.__init__,进而调用 Point.x.__set__(),Point.y.__set__()
print p.x # 调用Point.x.__get__()
p.y = 5 # 调用Point.y.__set__()
p.x = 2.3
运行的结果是:
C:\Python27>python C:\Users\snwang\Desktop\wsn_attr.py
2
Traceback (most recent call last):
File "C:\Users\snwang\Desktop\wsn_attr.py", line 36, in <module>
p.x = 2.3
File "C:\Users\snwang\Desktop\wsn_attr.py", line 17, in __set__
raise TypeError("Expected an int")
TypeError: Expected an int
每个描述符方法都会接受被操纵的实例作为输入。要执行所请求的操作,底层的实例字典(即__dict__
属性)会根据需要适当的进行调整。描述符的self.name
属性会保存字典的key,通过这些key可以找到存储在实例字典中的实际数据。
2. 方案深化
上述的方案有一些不足:作为Integer
类型的已经写完了,如果我们想要写String
,或是Float
,这要我们又要重新写一遍类似的代码。遵从代码重用原则,我们应该建立一个描述符基类,然后让这些类型继承它。
下面的代码使用描述符实现了一个类型系统以及对值进行检查的框架:
# 描述符基类,用于类型检查
class Typed(object):
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError("Expected " + str(self.expected_type))
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
创建了描述符类之后,如果属性是int类型的,那么只需设置expected_type=int,str,float
同理。
接下来我们需要写一个类装饰器,用来装饰自定义的类:在自定义类之前,将类属性设置为某特定类型的Typed
实例,代码如下:
#类装饰器,用来装饰自定义的类, 且将类属性设置为某特定类型的Typed实例
def typeassert(**kwargs):
def decorate(cls):
for name, expected_type in kwargs.items():
# 将名字name和期望类型创建一个Typed实例
iTyped = Typed(name, expected_type)
# 将名字name和iType,设置到cls中,作为对应cls.__dict__中的key,value
setattr(cls, name, iTyped)
return cls
return decorate
然后就是使用这个类装饰器的测试代码:
# 例子使用,装饰器使用
@typeassert(name=str, shares=int, price=float)
class Stock(object):
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
# 调用了 Stock.name.__set__(s, 'csdn')
# Stock.shares.__set__(s, 529)
# Stock.name.__set__(s, 22.08)
s = Stock(name = 'csdn', shares = 529, price = 22.08)
# 调用了 Stock.name.__get__(s, Stock)
# Stock.shares.__get__(s, Stock)
# Stock.price.__get__(s, Stock)
print s.name, str(s.shares), str(s.price)
# 调用了Stock.shares.__set__(s, 'this_is_a_str'),但是期望是int类型,所以会有error
s.shares = 'this_is_a_str'
输出结果:
C:\Python27>python C:\Users\snwang\Desktop\typeassert.py
csdn 529 22.08
Traceback (most recent call last):
File "C:\Users\snwang\Desktop\typeassert.py", line 56, in <module>
s.shares = 'this_is_a_str'
File "C:\Users\snwang\Desktop\typeassert.py", line 18, in __set__
raise TypeError("Expected " + str(self.expected_type))
TypeError: Expected <type 'int'>
最后,应该强调的是,如果只是想访问某个特定的类中的一种属性,并对此做定制化处理,那么最好不要编写描述符来实现。对于这个任务,用@property属性方法来完成会更加简单。当在需要大量重用代码的情况下,描述符会更加有用,例如,我们希望在自己的代码中大量使用描述符提供的功能,或者将其作为库来使用。
3. 总结
本文主要讲述了怎样使用描述符来对类属性进行类型限制。若想查看上面的例子全部代码,请移步我的github https://github.com/csdz/SnapToSnap/tree/master/descriptor,也欢迎关注。