Python中的描述符
描述符的定义:
通常情况下,我们可以认为"假设对象的某个属性被绑定了(get, set, delete)这三个方法中的任意一个方法",那么我们称该属性为"描述符"
class Foo(object):
def init(self, name, age):
self.name = name
self.age = age
foo = Foo("pizza", 18)
我们不能称 foo.name, foo.age 这两个属性为描述符,因为它们都没有绑定上面三个方法。
默认情况下, 对象的属性访问是通过get, set, delete这三个方法访问属性的字典__dict__来实现的。
如下代码所示:
class Foo(object):
country = "China"
def __init__(self, name, age):
self.name = name
self.age = age
foo = Foo("pizza", 18)
print(foo.__dict__) # {'name': 'pizza', 'age': 18}
print(type(foo).__dict__) # {'__module__': '__main__', 'country': 'China', '__init__': <function Foo.__init__ at 0x103802488>, '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None}
上面的代码中,如果print(foo.name)或者print(foo.age)或查找foo.__dict__
, 如果print(foo.country)则会查找type(foo)既Foo.__dict__
。
如果查找过程中遇到描述符,那么Python解释器就会用描述符中的方法来替代查找顺序,到底是先查找对象的__dict__还是描述符,取决于描述符类型,我们将在下面的小节中演示。
描述符协议
descr.__get__(self, obj, type=None) --> value
descr.__set__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
如果定义了以上三个方法中的任意一个,那么,我们就可以认为该对象是一个描述符对象,它会覆盖对象属性的查找顺序。
如下代码所示:
class Bar(object):
def __get__(self, instance, owner):
print("__get__")
def __set__(self, instance, value):
print("__set__")
def __delete__(self, instance, value):
print("__delete__")
class Foo(object):
bar = Bar()
foo = Foo()
以上代码中,foo的bar属性就被认为是一个描述符。
上文提到了描述符类型,描述符分为,Data Descriptor和Non-data Descriptor。
如果一个对象定义了__get__()和__set__()这两个方法,那么我们认为该对象是一个Data Descriptor。如果只定义了__get__()方法,那就是Non-data Descriptor,如下代码所示:
Data Descriptor
class Bar(object):
def __get__(self, instance, owner):
print("get")
def __set__(self, instance, value):
print("__set__")
def __delete__(self, instance, value):
print("__delete__")
class Foo(object):
bar = Bar()
foo = Foo()
以上代码中,foo的bar属性就被认为是一个描述符,而且是Data Descriptor。
Non-data Descriptor
class Bar(object):
def __get__(self, instance, owner):
print("__get__")
class Foo(object):
bar = Bar()
foo = Foo()
以上代码中,foo的bar属性就被认为是一个描述符,而且是Non-data Descriptor。
Data and non-data descriptors的不同点在于访问对象属性的方式。
如果对象的字典__dict__
中有一个跟Data Descriptor同名的属性,那么,Data Descriptor会覆盖__dict__
的查找,如下代码所示:
class Bar(object):
def __get__(self, instance, owner):
print("__get__")
def __set__(self, obj, value):
print("__set__")
class Foo(object):
bar = Bar()
def __init__(self, name, age):
self.name = name
self.age = age
self.bar = "bar"
foo = Foo("pizza", 18)
foo.bar # __get__
以上代码中,foo对象的bar属性查找会执行对象的__get__
方法。因为,Data Descriptor会覆盖__dict__
的查找。
如果对象的字典__dict__
中有一个跟Non-data Descriptor同名的属性,那么,对象的__dict__
查找会覆盖Non-data Descriptor,如下代码所示:
class Bar(object):
def __get__(self, instance, owner):
print("__get__")
class Foo(object):
bar = Bar()
def __init__(self, name, age):
self.name = name
self.age = age
self.bar = "bar"
foo = Foo("pizza", 18)
foo.bar # "bar"
以上代码中,foo对象的bar属性查找会打印“bar”,因为,对象的__dict__
查找会覆盖Non-data Descriptor。
Python中默认的property
在Python面向对象的设计中,有一个非常重要的知识点,叫做property,它的实现方式有多种,我们通过下面的代码演示其中一种:
class Foo(object):
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
foo = Foo("Pizza")
print(foo._name) # "Pizza"
print(foo.name) # "Pizza"
在上面的代码示例中,我们使用foo._name能够找到该属性的值”Pizza“,我们也可以通过foo.name找到该属性的值”Pizza“(因为该属性返回self._name
)。
我们通过在Foo类中定义一个name方法,然后通过默认的装饰器property实现访问方法时,不进行常用的函数调用方式。
在这个过程中,存在一个疑问,既然希望通过属性的方式访问对象的方法,且返回值就是某个属性,那么为什么不直接在__init__
里面定义一个属性。
其实,属性的更多的时候,是动态的获取某个值,并保留属性的访问方式,而不是简单的返回已存在的属性的值,比如:
class Foo(object):
def __init__(self, name):
self._name = name
@property
def stock(self):
return 100 + 100
foo = Foo("Pizza")
print(foo._name) # "Pizza"
print(foo.stock) # 200
我们在前面的章节中提到过,Python中的property,static method,class method的实现都依赖于descriptor的机制来实现。
那么,接下来,我们来自定义一个property。
使用Descriptor自定义property
从上一小节中,我们可以看到,将类中的方法修改为一个property,就是利用了装饰器@property。我们知道装饰器语法糖@decorator,等价于 func = decorator(func),如下代码所示:
class Foo(object):
def stock(self):
return 100 + 100
print(stock) # <function Foo.stock at 0x101a57048>
class Foo(object):
@property
def stock(self):
return 100 + 100
print(stock) # <property object at 0x103811c78>
上面的代码示例中,print(stock)的打印结果是<property object at 0x103811c78>
和<function Foo.stock at 0x101a57048>
,既,在stock方法上面加上@property之后,stock这个方法变为了property的对象,与第一个print(stock)的<function Foo.stock at 0x101a57048>
不同。
接下来,我们的目的是通过descriptor来实现自定义property。
在实现自定义property之前,我们先假设有一个类,如下代码所示:
class Foo(object):
def stock(self):
return 100 + 100
foo = Foo()
foo.stock
我们已知如下几点:
- 装饰器语法糖 @property等价于 stock = property(stock);
- 描述符是一个类的实例化对象,如 bar = Bar(),然后在Bar这个类中定义了
__get__, __set__, __delete__
;
我们的目的是,通过类似属性访问的方式(foo.stock)而非方法调用的方式(foo.stock()),获得返回值200。首选,我们通过描述符的方式,来实现简单的属性访问,如下代码所示:
class Stock(object):
def __get__(self, instance, owner):
print("__get__")
return 100 + 100
class Foo(object):
stock = Stock()
foo = Foo()
foo.stock
此时,通过访问foo.stock会先打印__get__
, 然后显示200。那么,如果我们将stock变为类中的一个方法呢?如下代码所示:
class Stock(object):
def __get__(self, instance, owner):
print("__get__")
return 100 + 100
class Foo(object):
def stock(self):
print("stock")
如果能将stock方法变为一个descriptor,那么我们就可以通过foo.stock访问该descriptor的__get__
方法,然后获取其返回值,既,200。
我们知道,将属性变为descriptor,直接通过给该属性绑定__get__
方法即可,如:stock = Stock(),但是,如何利用装饰器语法糖呢?我们知道,装饰器语法糖@Stock等价于stock = Stock(stock),因此,我们需要,在Stock这个类中定义一个__init__
方法,并定义一个形参来接收Stock类实例化时传入的stock函数,如下代码所示:
class Stock(object):
def __init__(self, stock):
self.stock = stock
def __get__(self, instance, owner):
print("__get__")
return 100 + 100
class Foo(object):
@Stock # stock = Stock(stock)
def stock(self):
print("stock")
通过以上,代码,我们就将Foo类中的stock方法,成功的变成了一个descriptor,接下来我们可以通过Foo类的实例化对象来访问stock方法,并且使用普通的属性调用方法,因为此时Foo类中的stock方法已经是一个descriptor了。
foo = Foo()
foo.stock
以上代码会先打印__get__
, 然后显示200。
事实上,细心的同学会发现,如果采用这种实现方式,我们实现了自定义的property,但是,与官方正版的property还有差距,这个差距在于,访问foo.stock的时候,Foo类中的stock并没有被执行,而正版的property中的属性是被执行了的,也就是说,我们最后需要获取的值,是直接从该属性中计算来获得的,如下代码所示:
class Foo(object):
@property
def stock(self):
return 100 + 100
foo = Foo()
foo.stock
在上面的代码示例中,我们通过foo.stock获取到的结果200,是通过Foo类中的stock这个方法来计算获得的。如果我希望在自定义的property中也采用同样的方式,该如何做呢?
我们知道,在定义__get__
方法时,它接受三个参数,第一个self表示descriptor,下面我们,分别print第二个和第三个参数,看看它们分别表示什么:
class Stock(object):
def __init__(self, stock):
self.stock = stock
def __get__(self, instance, owner):
print("__get__")
print("instance: ", instance)
print("owner:", owner)
return 100 + 100
class Foo(object):
@Stock
def stock(self):
print("stock")
foo = Foo()
foo.stock
以上代码的执行结果如下:
__get__
instance: <__main__.Foo object at 0x106c2b9b0>
owner: <class '__main__.Foo'>
200
从以上代码的执行结果可以看出,instance和owner这两个形参,分别被传入了foo和Foo这两个对象,一个是Foo类的实例化对象,一个是Foo类本身,那么,我们是否可以使用foo或者Foo在__get__
方法中,调用stock呢?
答案是否定的,因为此时的stock已经是一个descriptor了,如果在__get__
方法中调用,那么就进入死循环了,一直重复的执行__get__
方法。
最原始的那个Foo类中的stock方法,在进行@Stock时,被传入了Stock类中的__init__
方法进行初始化,因此,此时我们只能通过如下代码示例中使用的方式,进行调用:
class Bar(object):
def __init__(self, stock):
self.stock = stock
def __get__(self, instance, owner):
return sef.stock(instance)
class Foo(object):
@Stock
def stock(self):
return 100 + 100
foo = Foo()
foo.stock
以上代码的执行结果如下:
__get__
instance: <__main__.Foo object at 0x106c2b9b0>
owner: <class '__main__.Foo'>
200
通过结果我们可以看出,至此,我们实现了自定义的property。