描述符的定义

通常情况下,我们可以认为"假设对象的某个属性被绑定了(__get__, __set__, __delete__)这三个方法中的任意一个方法",那么我们称该属性为"描述符"

class Foo(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

foo = Foo("Wanghw", 18)

我们不能称 foo.name, foo.age 这两个属性为描述符,因为它们都没有绑定上面三个方法。

默认情况下, 对象的属性访问是通过get, set, delete这三个方法访问属性的字典__dict__来实现的。

比如说,a.x会首先查找a.__dict__['x'], 如果没有找到则查找type(a).__dict__['x'], 然后不断的往上查找直到metaclass(不包括metaclass)。

如下代码所示:

class Foo(object):
    country = "China"
    def __init__(self, name, age):
        self.name = name
        self.age = age

foo = Foo("Wanghw", 18)
print(foo.__dict__)    # {'name': 'Wanghw', '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("Wanghw", 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("Wanghw", 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("Wanghw")
print(foo._name)    # "Wanghw"
print(foo.name)    # "Wanghw"

在上面的代码示例中,我们使用foo._name能够找到该属性的值”Wanghw“,我们也可以通过foo.name找到该属性的值”Wanghw“(因为该属性返回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("Wanghw")
print(foo._name)    # "Wanghw"
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 Stock(object):
    def __init__(self, stock):
        self.stock = stock

    def __get__(self, instance, owner):
        return self.stock(instance)


class Foo(object):
    @Stock
    def stock(self):
        return 100 + 100


foo = Foo()
foo.stock

以上代码的执行结果如下:

200

通过结果我们可以看出,与之前的执行结果是一致的。简单修改为更能理解的代码示例,如下所示:

class myproperty(object):
    def __init__(self, stock):
        self.stock = stock

    def __get__(self, instance, owner):
        return self.stock(instance)


class Foo(object):
    @myproperty
    def stock(self):
        return 100 + 100


foo = Foo()
foo.stock

至此,我们实现了自定义的property。