描述符

描述符及一些其他

描述符是对类中的属性进行管理的一种方法. 描述符是实现了特定协议的类,只要一个类实现了__get__,__set__,__delete__方法中的任何一个,就可以将其称之为描述符,因为他已经具备了部分属性管理的功能. 大部分描述符只实现了__get____set__方法. 描述符实例是需要被管理的类的一个属性. 下面用几个例子总结描述符运行的逻辑.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Descriptor:
def __init__(self, name):
self.name = name

def __set__(self, instance, value):
print("现在调用了描述符的__set__方法!")
instance.__dict__[self.name] = value


class A:
quantity = Descriptor("weight")

def __init__(self, value):
self.quantity = value
1
2
a = A(10)
print(a.__dict__, a.weight, a.quantity, sep="\n")
现在调用了描述符的__set__方法!
{'weight': 10}
10
<__main__.Descriptor object at 0x0000022261B1E6A0>

上面这个运行的顺序是这样的,quantity是类A的一个属性,它是Descriptor类的一个实例,它的name属性的值是weight; A(10)运行了__init__方法,在__init__函数中,self.quantity实际上是通过实例a读取类A中的quantity对象,描述符quantity发现了正在对它进行赋值,自动调用了quantity的__set__方法,__set__方法的第一个参数是描述符实例,在这里就是quantity实例,第二个参数是调用他的对象实例,在这里就是实例a,第三个参数是尝试赋给它的值,在这里就是10. 于是,在这里就给实例a一个新的属性,weight,新属性的值是10.

一些注意

赋值和读取在python中是不等价的,这是理解描述符的关键,看下面的一个例子:

1
2
3
4
5
6
7
8
class B:
x = 1


b = B()
print(B.x, b.x, b.__dict__)
b.x = 2
print(B.x, b.x, b.__dict__)
1 1 {}
1 2 {'x': 2}

当实例b没有x属性时,他会自动找到类中的x属性,因此第一个b.x实际上就是B.x;但是在尝试为实例的x属性赋值时,python发现这个实例没有x属性,会生成一个新的实例属性x给b,并赋值为2,在这一过程中并不会改变类中的x的值

上面的例子中描述符的__set__方法为对象生成了一个名字与描述符不同的属性,如果名字相同会发生什么情况呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Descriptor:
def __init__(self, name):
self.name = name

def __set__(self, instance, value):
print("现在调用了描述符的__set__方法!")
instance.__dict__[self.name] = value


class A:
weight = Descriptor("weight")

def __init__(self, value):
self.weight = value
1
2
a = A(10)
print(a.__dict__, a.weight, sep="\n")
现在调用了描述符的__set__方法!
{'weight': 10}
10

现在这个属性的关系是这样的,类A有一个描述符叫weight,实例a又有一个属性叫weight,在尝试读取a.weight时,不会返回类A中的描述符,而是会返回a的weight属性的值. 但是在尝试改变a.weight的值时,又会调用描述符的__set__方法,理解这一点的关键就是python赋值和读取的不等价性.

1
2
a.weight = 100
print(a.__dict__, a.weight, sep="\n")
现在调用了描述符的__set__方法!
{'weight': 100}
100

__get__方法有什么用呢..其实从上面的讨论就可以看出,__set__方法实际上就相当于实现了为类中属性赋值的托管,__get__方法就是实现了属性读取的托管~

1
2
3
4
5
6
7
8
class Descriptor:
def __get__(self, instance, owner):
print("现在调用了描述符的__get__方法!")
return "不管发生了什么我都会返回这句话.."


class A:
weight = Descriptor()
1
2
a = A()
a.weight
现在调用了描述符的__get__方法!





'不管发生了什么我都会返回这句话..'

__get__方法的第一个参数是描述符实例,第二个是它所在类的实例,第三个是他所在的类,在这里就分别是weight,a,A. 但是单独使用__get__方法时,假如一旦给实例一个与描述符同名的属性时,就会把这个同名描述符属性给覆盖掉:

1
2
3
4
5
6
7
8
9
10
11
class Descriptor:
def __get__(self, instance, owner):
print("现在调用了描述符的__get__方法!")
return "不管发生了什么我都会返回这句话.."


class A:
weight = Descriptor()

def __init__(self, value):
self.weight = value
1
2
a = A(100)
a.weight
100

同时实现__set__方法与__get__方法时,通过二者的配合,可以实现属性的完全托管. 下面这个例子摘自fluent python,情形是这样的,LineItem是商品类,该类的每一个对象都有weightprice两个值,代表不同的商品参数,Quantity是描述符类,实现对这两个量的管理.

管理逻辑是这样的,这两个量分别对应着两个描述符实例,如果像本文最上面的描述符那样实现,用name属性来区分不同的描述符实例,则会很麻烦,每实例化一个描述符对象时,就要传给它一个名字. 这里巧妙地通过Quantity类属性__counter来区分,每实例化一个描述符时,它的值就+1,从而实现了对不同描述符中name值的区分. 每一个描述符实例都具有一个独一无二的storage_name

这里通过__set__方法和__get__,使得不需要直接访问这个sotrage_name,就可以直接获得对象的这两个值.. 相当于对象的storrage_name被隐藏了,唯一暴露的接口就是描述符方法..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Quantity:
__counter = 0

def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = "_{}#{}".format(prefix, index)
cls.__counter += 1

def __get__(self, instance, owner):
return getattr(instance, self.storage_name)

def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError("value must be > 0")


class LineItem:
weight = Quantity()
price = Quantity()

def __init__(self, weight, price):
self.weight = weight
self.price = price

def subtotal(self):
return self.weight * self.price
1
apple = LineItem(1, 5)
1
print(apple.__dict__, apple.weight, apple.price, sep="\n")
{'_Quantity#0': 1, '_Quantity#1': 5}
1
5
1
2
3
apple.weight = 10
apple.price = 100
print(apple.__dict__, apple.weight, apple.price, sep="\n")
{'_Quantity#0': 10, '_Quantity#1': 100}
10
100

这里生成了一个apple对象,apple对象有两个属性,_Quantity#0_Quantity#1,这两个属性是不可见的(这也是为什么要设置成这个名字)weightprice都是描述符,在通过apple对象给weightprice赋值时,自动调用描述符的__set__方法,检查数值的同时,给apple对象赋予_Quantity#0_Quantity#1两个属性;在读取weightprice的值时,由于apple对象没有对应的属性,于是自动调用了__get__方法,返回了_Quantity#0_Quantity#1的值. 实现了闭环.

其他

  1. python中的函数实现了__get__方法
1
2
3
def func(): 
pass
func.__get__
<method-wrapper '__get__' of function object at 0x0000022261B5AAF0>
  1. 在类中定义的函数属于绑定方法
1
2
3
class C:
def func(self):
pass
1
2
print(C.func)
print(C().func)
<function C.func at 0x0000022261B5A3A0>
<bound method C.func of <__main__.C object at 0x0000022261B785B0>>

从类中直接读取func对象,得到的是函数,从类的实例中读取func对象,得到的是绑定方法对象. 绑定方法对象的意思就是,通过对象来读取这个函数时,默认把对象传给了这个函数的第一个参数,返回了一个所谓的绑定方法对象. 实际上下面这两个的过程是一样的:

1
2
3
c = C()
print(c.func())
print(C.func(c))
None
None

那么仔细考察上面这个过程的逻辑,实际上就是实现了属性读取的托管. 也就是:

  1. 类中定义的函数是一个描述符

通过类直接访问函数时,函数的__get__方法返回自身的引用;通过实例访问时,函数的__get__方法返回的是绑定方法对象(可以通过instance参数的值是否为None来区分.)