GVKun编程网logo

描述符作为python中的实例属性(描述符 python)

25

如果您想了解描述符作为python中的实例属性和描述符python的知识,那么本篇文章将是您的不二之选。我们将深入剖析描述符作为python中的实例属性的各个方面,并为您解答描述符python的疑在这

如果您想了解描述符作为python中的实例属性描述符 python的知识,那么本篇文章将是您的不二之选。我们将深入剖析描述符作为python中的实例属性的各个方面,并为您解答描述符 python的疑在这篇文章中,我们将为您介绍描述符作为python中的实例属性的相关知识,同时也会详细的解释描述符 python的运用方法,并给出实际的案例分析,希望能帮助到您!

本文目录一览:

描述符作为python中的实例属性(描述符 python)

描述符作为python中的实例属性(描述符 python)

描述符对象需要存在于类中,而不是存在于实例中

因为那是实现的方式__getattribute__

一个简单的例子。考虑一个描述符:

class Prop(object):    def __get__(self, obj, objtype=None):        if obj is None:            return self        return obj._value * obj._multiplier    def __set__(self, obj, value):        if obj is None:            return self        obj._value = valueclass Obj(object):    val = Prop()    def __init__(self):        self._value = 1        self._multiplier = 0

考虑每个obj具有多个Prop的情况:我将需要使用唯一的名称来标识值和乘数(如此处。具有每个实例的描述符对象将允许将_multiplier(和_value)存储在描述符本身中,从而简化了几样东西。

我知道以前也曾提出过类似的问题,但我没有找到真正的解释:

  1. 为什么以这种方式设计Python?
  2. 建议使用哪种方式存储描述符需要的信息,但按实例存储?

答案1

小编典典

只有在类而不是实例上定义时,大量高级功能才起作用。例如,所有特殊方法。除了使代码评估更有效之外,这还使实例和类型之间的分隔变得清晰,否则分隔将容易崩溃(因为所有类型都是对象)。

我不确定这是如何推荐的,但是您可以在实例中存储从描述符实例到属性值的映射:

class Prop(object):     def __get__(self, obj, objtype=None):        if obj is None:            return self        return obj._value * obj._multiplier[self]    def __set__(self, obj, value):        if obj is None:            return self        obj._value = valueclass Obj(object):    val = Prop()    def __init__(self):        self._value = 1        self._multiplier = {Obj.val: 0}

与其他两个建议的选项相比,它具有明显的优势:

  1. 每个实例的类破坏了面向对象并增加了内存使用;
  2. 覆盖__getattribute__是低效的(因为所有属性访问都必须通过覆盖的特殊方法进行访问)并且脆弱。

或者,您可以使用proxy属性:

class PerInstancePropertyProxy(object):    def __init__(self, prop):        self.prop = prop    def __get__(self, instance, owner):        if instance is None:            return self        return instance.__dict__[self.prop].__get__(instance, owner)    def __set__(self, instance, value):        instance.__dict__[self.prop].__set__(instance, value)class Prop(object):    def __init__(self, value, multiplier):        self.value = value        self.multiplier = multiplier    def __get__(self, instance, owner):        if instance is None:            return self        return self.value * self.multiplier    def __set__(self, instance, value):        self.value = valueclass Obj(object):    val = PerInstancePropertyProxy(''val'')    def __init__(self):        self.__dict__[''val''] = Prop(1.0, 10.0)    def prop(self, attr_name):        return self.__dict__[attr_name]

Python - 类属性和实例属性

Python - 类属性和实例属性

一、实例属性

首先说说差异化最小的实例属性。从代码中直视它们的异同。

JAVA实例属性:

class Book{
	private String name;
    public Book(String name){
        this.name=name;
    }
	public String getName(){
		return this.name;
	}
}

Python实例属性:

class Book:
	def getName(self):
		return self.name

	def __init__(self,name=''unnamed''):
        self.name=name

除了语法层面的区别,语义层面是没有任何差异的。这里所描述的“差异”一词,表达的是这样的语境“我明白JAVA中的实例属性,所以Python中的实例属性,我只需要了解语法的区别,就能明白Python的实例属性”

显然,就实例属性来说,二者的区别只在于语法层面。

二、类属性

JAVA代码如下:

class Book{
	public static String paper="宣纸";
}
//为了书写方便,省略main。
Book book=new Book();
System.out.println(book.paper);
Book.paper="竹简";
System.out.println(book.paper);
book.paper="宣纸";
System.out.println(book.paper+"|"+Book.paper);

类属性可以通过类名以及实例名来访问,上述代码通过类名方式修改类属性,查看实例名访问时类属性的变化情况。代码执行结果如下:

>java Book
宣纸
竹简
宣纸|宣纸

通过结果看,JAVA在处理类名和实例名使用类属性时完全一致,尽管类名和实例名不是一个逻辑层次的概念,所以才会有尴尬的提示——建议使用类名来访问类属性。

这种处理方式会让人觉得“很随意”,但是的确降低了程序员的理解成本。

Python代码如下:

class Book:
	paper=u''宣纸''

book=Book()
print book.paper
Book.paper=u''竹简''
print book.paper
book.paper=u''宣纸''
print book.paper,Book.paper

运行结果如下:

>python book.py
宣纸
竹简
宣纸 竹简

对比JAVA的输出,最大的区别在于——实例名操作了类属性之后,结果出现天壤之别。

为什么会是这样?

  1. python的属性查找规则造成了,实例名也可以访问类属性的假象。当调用book.paper时,python会向上查找属性,也就是说最终显示出来的是Book.paper
  2. 通过实例名设置一个不存在的属性时,python会动态创建这个属性。这个属性既然是真实存在的,那么就和Book.paper没有任何关系,各自存储各自的数据,所以不一样的变化就出现了。

这么“诡异”的现象把理由讲出来后,你会发现这是一种很“自然”的处理方式。代价当然是增加了理解的难度。

通过这个“好玩”的东东,衍生了一个设计模式——Borg,根据上述的说明,它非常容易理解。

三、Borg设计模式

代码如下(摘自github):

class Borg(object):
    __state_pool = {}
    def __init__(self):
        self.__dict__ = self.__state_pool #这是秘密的关键,所有实例的所有属性将全部共享。
        self.state = ''Init''
    def __str__(self):
        return self.state
class YourBorg(Borg):
    pass

if __name__ == ''__main__'':
    rm1 = Borg()
    rm2 = Borg()
    rm1.state = ''Idle''
    rm2.state = ''Running''
    print(''rm1: {0}''.format(rm1))
    print(''rm2: {0}''.format(rm2))
    rm2.state = ''Zombie''
    print(''rm1: {0}''.format(rm1))
    print(''rm2: {0}''.format(rm2))
    print(''rm1 id: {0}''.format(id(rm1)))
    print(''rm2 id: {0}''.format(id(rm2)))
    rm3 = YourBorg()
    print(''rm1: {0}''.format(rm1))
    print(''rm2: {0}''.format(rm2))
    print(''rm3: {0}''.format(rm3))

执行效果如你所想:

>python 01.py
rm1: Running
rm2: Running
rm1: Zombie
rm2: Zombie
rm1 id: 31013576
rm2 id: 40287424
rm1: Init
rm2: Init
rm3: Init

Borg实现了所有实例的所有属性的共享——单态池!我想这应该是非常贴切的一个名字。

为任意一个实例添加一个属性,其他所有实例都可以共享访问。

复制代码,修改后试试效果。

python – 更新类的实例属性

python – 更新类的实例属性

我一直在谷歌搜索这个主题,我没有找到一个普遍接受的方式来实现我的目标.

假设我们有以下课程:

import numpy as np
class MyClass:

    def __init__(self,x):
        self.x = x
        self.length = x.size

    def append(self,data):
        self.x = np.append(self.x,data)

和x应该是一个numpy数组!如果我跑

A = MyClass(x=np.arange(10))
print(A.x)
print(A.length)

我明白了

[0 1 2 3 4 5 6 7 8 9]和10.到目前为止一切顺利.但是如果我使用append方法

A.append(np.arange(5))

我得到[0 1 2 3 4 5 6 7 8 9 0 1 2 3 4]和10.这也是预期的,因为实例属性长度是在实例化A期间设置的.现在我不确定什么是最pythonic的方式更新实例属性是.例如,我可以再次运行__init__:

A.__init__(A.x)

然后长度属性将具有正确的值,但在其他一些帖子中,我发现这有点不受欢迎.另一个解决方案是直接更新append方法中的length属性,但我有点想避免这种情况,因为我不想忘记在某个时候更新属性.有更多pythonic方法更新此类的长度属性吗?

解决方法

不要更新它,只需在 a getter需要时阅读它:

class MyClass:
    ...

    @property
    def length(self):
        return self.x.size

Python 中的属性访问与描述符

Python 中的属性访问与描述符

在Python中,对于一个对象的属性访问,我们一般采用的是点(.)属性运算符进行操作。例如,有一个类实例对象foo,它有一个name属性,那便可以使用foo.name对此属性进行访问。一般而言,点(.)属性运算符比较直观,也是我们经常碰到的一种属性访问方式。然而,在点(.)属性运算符的背后却是别有洞天,值得我们对对象的属性访问进行探讨。

在进行对象属性访问的分析之前,我们需要先了解一下对象怎么表示其属性。为了便于说明,本文以新式类为例。有关新式类和旧式类的区别,大家可以查看Python官方文档。

对象的属性

Python中,“一切皆对象”。我们可以给对象设置各种属性。先来看一个简单的例子:

Python
class Animal(object): run = True class Dog(Animal): fly = False def __init__(self, age): self.age = age def sound(self): return "wang wang~"
1
2
3
4
5
6
7
8
class Animal(object):
    run = True
class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    def sound(self):
        return "wang wang~"

上面的例子中,我们定义了两个类。类Animal定义了一个属性run;类Dog继承自Animal,定义了一个属性fly和两个函数。接下来,我们实例化一个对象。对象的属性可以从特殊属性__dict__中查看。

Python
# 实例化一个对象dog >>> dog = Dog(1) # 查看dog对象的属性 >>> dog.__dict__ {''age'': 1} # 查看类Dog的属性 >>> Dog.__dict__ dict_proxy({''__doc__'': None, ''__init__'': <function __main__.__init__>, ''__module__'': ''__main__'', ''fly'': False, ''sound'': <function __main__.sound>}) # 查看类Animal的属性 >>> Animal.__dict__ dict_proxy({''__dict__'': <attribute ''__dict__'' of ''Animal'' objects>, ''__doc__'': None, ''__module__'': ''__main__'', ''__weakref__'': <attribute ''__weakref__'' of ''Animal'' objects>, ''run'': True})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 实例化一个对象dog
>>> dog = Dog(1)
# 查看dog对象的属性
>>> dog.__dict__
{''age'': 1}
# 查看类Dog的属性
>>> Dog.__dict__
dict_proxy({''__doc__'': None,
            ''__init__'': <function __main__.__init__>,
            ''__module__'': ''__main__'',
            ''fly'': False,
            ''sound'': <function __main__.sound>})
# 查看类Animal的属性
>>> Animal.__dict__
dict_proxy({''__dict__'': <attribute ''__dict__'' of ''Animal'' objects>,
            ''__doc__'': None,
            ''__module__'': ''__main__'',
            ''__weakref__'': <attribute ''__weakref__'' of ''Animal'' objects>,
            ''run'': True})

由上面的例子可以看出:属性在哪个对象上定义,便会出现在哪个对象的__dict__中。例如:

  • Animal定义了一个属性run,那这个run属性便只会出现在类Animal__dict__中,而不会出现在其子类中。
  • Dog定义了一个属性fly和两个函数,那这些属性和方法便会出现在类Dog__dict__中,同时它们也不会出现在实例的__dict__中。
  • 实例对象dog__dict__中只出现了一个属性age,这是在初始化实例对象的时候添加的,它没有父类的属性和方法。
  • 由此可知:Python中对象的属性具有 “层次性”,属性在哪个对象上定义,便会出现在哪个对象的__dict__中。

在这里我们首先了解的是属性值会存储在对象的__dict__中,查找也会在对象的__dict__中进行查找的。至于Python对象进行属性访问时,会按照怎样的规则来查找属性值呢?这个问题在后文中进行讨论。

对象属性访问与特殊方法__getattribute__

正如前面所述,Python的属性访问方式很直观,使用点属性运算符。在新式类中,对对象属性的访问,都会调用特殊方法__getattribute____getattribute__允许我们在访问对象属性时自定义访问行为,但是使用它特别要小心无限递归的问题。

还是以上面的情景为例:

Python
class Animal(object): run = True class Dog(Animal): fly = False def __init__(self, age): self.age = age # 重写__getattribute__。需要注意的是重写的方法中不能 # 使用对象的点运算符访问属性,否则使用点运算符访问属性时, # 会再次调用__getattribute__。这样就会陷入无限递归。 # 可以使用super()方法避免这个问题。 def __getattribute__(self, key): print "calling __getattribute__\n" return super(Dog, self).__getattribute__(key) def sound(self): return "wang wang~"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal(object):
    run = True
class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    # 重写__getattribute__。需要注意的是重写的方法中不能
    # 使用对象的点运算符访问属性,否则使用点运算符访问属性时,
    # 会再次调用__getattribute__。这样就会陷入无限递归。
    # 可以使用super()方法避免这个问题。
    def __getattribute__(self, key):
        print  "calling __getattribute__\n"
        return super(Dog, self).__getattribute__(key)
    def sound(self):
        return "wang wang~"

上面的例子中我们重写了__getattribute__方法。注意我们使用了super()方法来避免无限循环问题。下面我们实例化一个对象来说明访问对象属性时__getattribute__的特性。

Python
# 实例化对象dog >>> dog = Dog(1) # 访问dog对象的age属性 >>> dog.age calling __getattribute__ 1 # 访问dog对象的fly属性 >>> dog.fly calling __getattribute__ False # 访问dog对象的run属性 >>> dog.run calling __getattribute__ True # 访问dog对象的sound方法 >>> dog.sound calling __getattribute__ <bound method Dog.sound of <__main__.Dog object at 0x0000000005A90668>>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 实例化对象dog
>>> dog = Dog(1)
# 访问dog对象的age属性
>>> dog.age
calling __getattribute__
1
# 访问dog对象的fly属性
>>> dog.fly
calling __getattribute__
False
# 访问dog对象的run属性
>>> dog.run
calling __getattribute__
True
# 访问dog对象的sound方法
>>> dog.sound
calling __getattribute__
<bound method Dog.sound of <__main__.Dog object at 0x0000000005A90668>>

由上面的验证可知,__getattribute__是实例对象查找属性或方法的入口。实例对象访问属性或方法时都需要调用到__getattribute__,之后才会根据一定的规则在各个__dict__中查找相应的属性值或方法对象,若没有找到则会调用__getattr__(后面会介绍到)。__getattribute__是Python中的一个内置方法,关于其底层实现可以查看相关官方文档,后面将要介绍的属性访问规则就是依赖于__getattribute__的。

对象属性控制

在继续介绍后面相关内容之前,让我们先来了解一下Python中和对象属性控制相关的相关方法。

  • __getattr__(self, name)__getattr__可以用来在当用户试图访问一个根本不存在(或者暂时不存在)的属性时,来定义类的行为。前面讲到过,当__getattribute__方法找不到属性时,最终会调用__getattr__方法。它可以用于捕捉错误的以及灵活地处理AttributeError。只有当试图访问不存在的属性时它才会被调用。
  • __setattr__(self, name, value)__setattr__方法允许你自定义某个属性的赋值行为,不管这个属性存在与否,都可以对任意属性的任何变化都定义自己的规则。关于__setattr__有两点需要说明:第一,使用它时必须小心,不能写成类似self.name = “Tom”这样的形式,因为这样的赋值语句会调用__setattr__方法,这样会让其陷入无限递归;第二,你必须区分 对象属性类属性 这两个概念。后面的例子中会对此进行解释。
  • __delattr__(self, name)__delattr__用于处理删除属性时的行为。和__setattr__方法要注意无限递归的问题,重写该方法时不要有类似del self.name的写法。

还是以上面的例子进行说明,不过在这里我们要重写三个属性控制方法。

Python
class Animal(object): run = True class Dog(Animal): fly = False def __init__(self, age): self.age = age def __getattr__(self, name): print "calling __getattr__\n" if name == ''adult'': return True if self.age >= 2 else False else: raise AttributeError def __setattr__(self, name, value): print "calling __setattr__" super(Dog, self).__setattr__(name, value) def __delattr__(self, name): print "calling __delattr__" super(Dog, self).__delattr__(name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal(object):
    run = True
class Dog(Animal):
    fly = False
    def __init__(self, age):
        self.age = age
    def __getattr__(self, name):
        print "calling __getattr__\n"
        if name == ''adult'':
            return True if self.age >= 2 else False
        else:
            raise AttributeError
    def __setattr__(self, name, value):
        print "calling __setattr__"
        super(Dog, self).__setattr__(name, value)
    def __delattr__(self, name):
        print "calling __delattr__"
        super(Dog, self).__delattr__(name)

以下进行验证。首先是__getattr__:

Python
# 创建实例对象dog >>> dog = Dog(1) calling __setattr__ # 检查一下dog和Dog的__dict__ >>> dog.__dict__ {''age'': 1} >>> Dog.__dict__ dict_proxy({''__delattr__'': <function __main__.__delattr__>, ''__doc__'': None, ''__getattr__'': <function __main__.__getattr__>, ''__init__'': <function __main__.__init__>, ''__module__'': ''__main__'', ''__setattr__'': <function __main__.__setattr__>, ''fly'': False}) # 获取dog的age属性 >>> dog.age 1 # 获取dog的adult属性。 # 由于__getattribute__没有找到相应的属性,所以调用__getattr__。 >>> dog.adult calling __getattr__ False # 调用一个不存在的属性name,__getattr__捕获AttributeError错误 >>> dog.name calling __getattr__ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 10, in __getattr__ AttributeError
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
# 创建实例对象dog
>>> dog = Dog(1)
calling __setattr__
# 检查一下dog和Dog的__dict__
>>> dog.__dict__
{''age'': 1}
>>> Dog.__dict__
dict_proxy({''__delattr__'': <function __main__.__delattr__>,
            ''__doc__'': None,
            ''__getattr__'': <function __main__.__getattr__>,
            ''__init__'': <function __main__.__init__>,
            ''__module__'': ''__main__'',
            ''__setattr__'': <function __main__.__setattr__>,
            ''fly'': False})
# 获取dog的age属性
>>> dog.age
1
# 获取dog的adult属性。
# 由于__getattribute__没有找到相应的属性,所以调用__getattr__。
>>> dog.adult
calling __getattr__
False
# 调用一个不存在的属性name,__getattr__捕获AttributeError错误
>>> dog.name
calling __getattr__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in __getattr__
AttributeError

可以看到,属性访问时,当访问一个不存在的属性时触发__getattr__,它会对访问行为进行控制。接下来是__setattr__

Python
# 给dog.age赋值,会调用__setattr__方法 >>> dog.age = 2 calling __setattr__ >>> dog.age 2 # 先调用dog.fly时会返回False,这时因为Dog类属性中有fly属性; # 之后再给dog.fly赋值,触发__setattr__方法。 >>> dog.fly False >>> dog.fly = True calling __setattr__ # 再次查看dog.fly的值以及dog和Dog的__dict__; # 可以看出对dog对象进行赋值,会在dog对象的__dict__中添加了一条对象属性; # 然而,Dog类属性没有发生变化 # 注意:dog对象和Dog类中都有fly属性,访问时会选择哪个呢? >>> dog.fly True >>> dog.__dict__ {''age'': 2, ''fly'': True} >>> Dog.__dict__ dict_proxy({''__delattr__'': <function __main__.__delattr__>, ''__doc__'': None, ''__getattr__'': <function __main__.__getattr__>, ''__init__'': <function __main__.__init__>, ''__module__'': ''__main__'', ''__setattr__'': <function __main__.__setattr__>, ''fly'': False})
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
# 给dog.age赋值,会调用__setattr__方法
>>> dog.age = 2
calling __setattr__
>>> dog.age
2
# 先调用dog.fly时会返回False,这时因为Dog类属性中有fly属性;
# 之后再给dog.fly赋值,触发__setattr__方法。
>>> dog.fly
False
>>> dog.fly = True
calling __setattr__
# 再次查看dog.fly的值以及dog和Dog的__dict__;
# 可以看出对dog对象进行赋值,会在dog对象的__dict__中添加了一条对象属性;
# 然而,Dog类属性没有发生变化
# 注意:dog对象和Dog类中都有fly属性,访问时会选择哪个呢?
>>> dog.fly
True
>>> dog.__dict__
{''age'': 2, ''fly'': True}
>>> Dog.__dict__
dict_proxy({''__delattr__'': <function __main__.__delattr__>,
            ''__doc__'': None,
            ''__getattr__'': <function __main__.__getattr__>,
            ''__init__'': <function __main__.__init__>,
            ''__module__'': ''__main__'',
            ''__setattr__'': <function __main__.__setattr__>,
            ''fly'': False})

实例对象的__setattr__方法可以定义属性的赋值行为,不管属性是否存在。当属性存在时,它会改变其值;当属性不存在时,它会添加一个对象属性信息到对象的__dict__中,然而这并不改变类的属性。从上面的例子可以看出来。

最后,看一下__delattr__

Python
# 由于上面的例子中我们为dog设置了fly属性,现在删除它触发__delattr__方法 >>> del dog.fly calling __delattr__ # 再次查看dog对象的__dict__,发现和fly属性相关的信息被删除 >>> dog.__dict__ {''age'': 2}
1
2
3
4
5
6
# 由于上面的例子中我们为dog设置了fly属性,现在删除它触发__delattr__方法
>>> del dog.fly
calling __delattr__
# 再次查看dog对象的__dict__,发现和fly属性相关的信息被删除
>>> dog.__dict__
{''age'': 2}

描述符

描述符是Python 2.2 版本中引进来的新概念。描述符一般用于实现对象系统的底层功能, 包括绑定和非绑定方法、类方法、静态方法特特性等。关于描述符的概念,官方并没有明确的定义,可以在网上查阅相关资料。这里我从自己的认识谈一些想法,如有不当之处还请包涵。

在前面我们了解了对象属性访问和行为控制的一些特殊方法,例如__getattribute____getattr____setattr____delattr__。以我的理解来看,这些方法应当具有属性的”普适性”,可以用于属性查找、设置、删除的一般方法,也就是说所有的属性都可以使用这些方法实现属性的查找、设置、删除等操作。但是,这并不能很好地实现对某个具体属性的访问控制行为。例如,上例中假如要实现dog.age属性的类型设置(只能是整数),如果单单去修改__setattr__方法满足它,那这个方法便有可能不能支持其他的属性设置。

在类中设置属性的控制行为不能很好地解决问题,Python给出的方案是:__getattribute____getattr____setattr____delattr__等方法用来实现属性查找、设置、删除的一般逻辑,而对属性的控制行为就由属性对象来控制。这里单独抽离出来一个属性对象,在属性对象中定义这个属性的查找、设置、删除行为。这个属性对象就是描述符。

描述符对象一般是作为其他类对象的属性而存在。在其内部定义了三个方法用来实现属性对象的查找、设置、删除行为。这三个方法分别是:

  • get(self, instance, owner):定义当试图取出描述符的值时的行为。
  • set(self, instance, value):定义当描述符的值改变时的行为。
  • delete(self, instance):定义当描述符的值被删除时的行为。

其中:instance为把描述符对象作为属性的对象实例;
owner为instance的类对象。

以下以官方的一个例子进行说明:

Python
class RevealAccess(object): def __init__(self, initval=None, name=''var''): self.val = initval self.name = name def __get__(self, obj, objtype): print ''Retrieving'', self.name return self.val def __set__(self, obj, val): print ''Updating'', self.name self.val = val class MyClass(object): x = RevealAccess(10, ''var "x"'') y = 5
1
2
3
4
5
6
7
8
9
10
11
12
13
class RevealAccess(object):
    def __init__(self, initval=None, name=''var''):
        self.val = initval
        self.name = name
    def __get__(self, obj, objtype):
        print ''Retrieving'', self.name
        return self.val
    def __set__(self, obj, val):
        print ''Updating'', self.name
        self.val = val
class MyClass(object):
    x = RevealAccess(10, ''var "x"'')
    y = 5

以上定义了两个类。其中RevealAccess类的实例是作为MyClass类属性x的值存在的。而且RevealAccess类定义了__get____set__方法,它是一个描述符对象。注意,描述符对象的__get____set__方法中使用了诸如self.valself.val = val等语句,这些语句会调用__getattribute____setattr__等方法,这也说明了__getattribute____setattr__等方法在控制访问对象属性上的一般性(一般性是指对于所有属性它们的控制行为一致),以及__get____set__等方法在控制访问对象属性上的特殊性(特殊性是指它针对某个特定属性可以定义不同的行为)。

以下进行验证:

Python
# 创建Myclass类的实例m >>> m = MyClass() # 查看m和MyClass的__dict__ >>> m.__dict__ {} >>> MyClass.__dict__ dict_proxy({''__dict__'': <attribute ''__dict__'' of ''MyClass'' objects>, ''__doc__'': None, ''__module__'': ''__main__'', ''__weakref__'': <attribute ''__weakref__'' of ''MyClass'' objects>, ''x'': <__main__.RevealAccess at 0x5130080>, ''y'': 5}) # 访问m.x。会先触发__getattribute__方法 # 由于x属性的值是一个描述符,会触发它的__get__方法 >>> m.x Retrieving var "x" 10 # 设置m.x的值。对描述符进行赋值,会触发它的__set__方法 # 在__set__方法中还会触发__setattr__方法(self.val = val) >>> m.x = 20 Updating var "x" # 再次访问m.x >>> m.x Retrieving var "x" 20 # 查看m和MyClass的__dict__,发现这与对描述符赋值之前一样。 # 这一点与一般属性的赋值不同,可参考上述的__setattr__方法。 # 之所以前后没有发生变化,是因为变化体现在描述符对象上, # 而不是实例对象m和类MyClass上。 >>> m.__dict__ {} >>> MyClass.__dict__ dict_proxy({''__dict__'': <attribute ''__dict__'' of ''MyClass'' objects>, ''__doc__'': None, ''__module__'': ''__main__'', ''__weakref__'': <attribute ''__weakref__'' of ''MyClass'' objects>, ''x'': <__main__.RevealAccess at 0x5130080>, ''y'': 5})
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
31
32
33
34
35
36
37
38
# 创建Myclass类的实例m
>>> m = MyClass()
# 查看m和MyClass的__dict__
>>> m.__dict__
{}
>>> MyClass.__dict__
dict_proxy({''__dict__'': <attribute ''__dict__'' of ''MyClass'' objects>,
            ''__doc__'': None,
            ''__module__'': ''__main__'',
            ''__weakref__'': <attribute ''__weakref__'' of ''MyClass'' objects>,
            ''x'': <__main__.RevealAccess at 0x5130080>,
            ''y'': 5})
# 访问m.x。会先触发__getattribute__方法
# 由于x属性的值是一个描述符,会触发它的__get__方法
>>> m.x
Retrieving var "x"
10
# 设置m.x的值。对描述符进行赋值,会触发它的__set__方法
# 在__set__方法中还会触发__setattr__方法(self.val = val)
>>> m.x = 20
Updating var "x"
# 再次访问m.x
>>> m.x
Retrieving var "x"
20
# 查看m和MyClass的__dict__,发现这与对描述符赋值之前一样。
# 这一点与一般属性的赋值不同,可参考上述的__setattr__方法。
# 之所以前后没有发生变化,是因为变化体现在描述符对象上,
# 而不是实例对象m和类MyClass上。
>>> m.__dict__
{}
>>> MyClass.__dict__
dict_proxy({''__dict__'': <attribute ''__dict__'' of ''MyClass'' objects>,
            ''__doc__'': None,
            ''__module__'': ''__main__'',
            ''__weakref__'': <attribute ''__weakref__'' of ''MyClass'' objects>,
            ''x'': <__main__.RevealAccess at 0x5130080>,
            ''y'': 5})

上面的例子对描述符进行了一定的解释,不过对描述符还需要更进一步的探讨和分析,这个工作先留待以后继续进行。

最后,还需要注意一点:描述符有数据描述符和非数据描述符之分。

  • 只要至少实现__get____set____delete__方法中的一个就可以认为是描述符;
  • 只实现__get__方法的对象是非数据描述符,意味着在初始化之后它们只能被读取;
  • 同时实现__get____set__的对象是数据描述符,意味着这种属性是可读写的。

属性访问的优先规则

在以上的讨论中,我们一直回避着一个问题,那就是属性访问时的优先规则。我们了解到,属性一般都在__dict__中存储,但是在访问属性时,在对象属性、类属型、基类属性中以怎样的规则来查询属性呢?以下对Python中属性访问的规则进行分析。

由上述的分析可知,属性访问的入口点是__getattribute__方法。它的实现中定义了Python中属性访问的优先规则。Python官方文档中对__getattribute__的底层实现有相关的介绍,本文暂时只是讨论属性查找的规则,相关规则可见下图:

Python属性查找

上图是查找b.x这样一个属性的过程。在这里要对此图进行简单的介绍:

  1. 查找属性的第一步是搜索基类列表,即type(b).__mro__,直到找到该属性的第一个定义,并将该属性的值赋值给descr
  2. 判断descr的类型。它的类型可分为数据描述符、非数据描述符、普通属性、未找到等类型。若descr为数据描述符,则调用desc.__get__(b, type(b)),并将结果返回,结束执行。否则进行下一步;
  3. 如果descr为非数据描述符、普通属性、未找到等类型,则查找实例b的实例属性,即b.__dict__。如果找到,则将结果返回,结束执行。否则进行下一步;
  4. 如果在b.__dict__未找到相关属性,则重新回到descr值的判断上。
    • descr为非数据描述符,则调用desc.__get__(b, type(b)),并将结果返回,结束执行;
    • descr为普通属性,直接返回结果并结束执行;
    • descr为空(未找到),则最终抛出 AttributeError 异常,结束查找。

python 属性描述符和属性查找过程

python 属性描述符和属性查找过程

属性描述符和属性查找过程

# 属性描述符对象:
# 只要实现 __get__ __set__ __delete__ 这3个当中的任何一个方法,这个对象就是一个属性描述符的对象
# 通过属性描述符,可以控制赋值时的一些行为

import numbers


class IntField():
    # 数据属性描述符
    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        # 检查参数是否是int类型
        if not isinstance(value, numbers.Integral):
            raise ValueError("int value need")

        if value < 0:
            raise ValueError("positive value need")

        # 如果value值是int,需要将这个值保存起来
        # 此时不能将值保存在instance里,比如 instance.age = value,如果这样保存,此instance又会调用IntField的 __set__方法,
        # 这样就陷入死循环,这里将值保存在IntField class里,所以是将值放在self里
        # 因为是将值放在了 IntField类里,所以__get__时,需要将它的值 return回去
        self.value = value

    def __delete__(self, instance):
        pass


class NonDataField:
    # 只实现一个 __get__ 方法,称为: 非数据属性描述符
    def __get__(self, instance, owner):
        return self.value

"""
属性查找过程
如果user是某个类的实例,那么user.age(以及等价的getattr(user, ''age''))首先调用__getattribute__。
如果类定义了__getattr__方法,那么在__getattribute__抛出AttributeError的时候就会调用到__getattr__ ,
而对于描述符(__get__)的调用,则是发生在__getattribute__内部的。

user = User(),那么user.age 顺序如下:
(1) 如果"age"是出现在User或其基类的__dict__中, 且age是data descriptor(数据属性描述符), 那么调用其__get__方法,否则
(2) 如果"age"出现在obj(这里指user对象)的__dict__中, 那么直接返回obj.__dict__["ages"], 否则
(3) 如果"age"出现在User或其基类的__dict__中
    (3.1) 如果age是non-data descriptor(非数据属性描述符),那么调用其__get__方法,否则
    (3.2) 返回__dict__["age"]
(4) 如果User有__getattr__方法,调用__getattr__方法,否则
(5) 抛出AttributeError 
"""


class User:
    age = IntField()


if __name__ == ''__main__'':
    user = User()
    user.age = 30  # 当对user对象设置age属性时,它会调用IntField类的 __set__ 方法, 所以当需要对设置的属性值做些检查时,可以把逻辑写在__set__方法中
    print(user.age)

    # user.age = "abc"  # 会抛出异常
    # print(user.age)

关于描述符作为python中的实例属性描述符 python的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于Python - 类属性和实例属性、python – 更新类的实例属性、Python 中的属性访问与描述符、python 属性描述符和属性查找过程的相关知识,请在本站寻找。

本文标签: