惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyberwarzone
Cyberwarzone
Vercel News
Vercel News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
aimingoo的专栏
aimingoo的专栏
B
Blog RSS Feed
A
About on SuperTechFans
T
The Blog of Author Tim Ferriss
爱范儿
爱范儿
腾讯CDC
S
SegmentFault 最新的问题
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Hacker News
The Hacker News
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
B
Blog
IT之家
IT之家
Spread Privacy
Spread Privacy
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
C
Cisco Blogs
Recent Announcements
Recent Announcements
H
Hacker News: Front Page
AI
AI
I
InfoQ
H
Heimdal Security Blog
T
Threatpost
Cisco Talos Blog
Cisco Talos Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
I
Intezer
W
WeLiveSecurity
SecWiki News
SecWiki News
MongoDB | Blog
MongoDB | Blog
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
云风的 BLOG
云风的 BLOG
T
Threat Research - Cisco Blogs
V2EX - 技术
V2EX - 技术
N
News and Events Feed by Topic
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
O
OpenAI News
阮一峰的网络日志
阮一峰的网络日志
T
Troy Hunt's Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
雷峰网
雷峰网
T
Tor Project blog
有赞技术团队
有赞技术团队
Schneier on Security
Schneier on Security
Last Week in AI
Last Week in AI

卡瓦邦噶!

服务器高性能网络调优 | 卡瓦邦噶! 为何写作 | 卡瓦邦噶! 读《金阁寺》 | 卡瓦邦噶! 雨季又来 | 卡瓦邦噶! MTU Probe 引起的初始延迟 | 卡瓦邦噶! 3.5 秒的固定延迟问题 | 卡瓦邦噶! 学习网络的一点经验 | 卡瓦邦噶! ARP 问题诊断 | 卡瓦邦噶! 网络断断续续…… | 卡瓦邦噶! Piccolo P2P 镜像分发 | 卡瓦邦噶! 一起看电影 | 卡瓦邦噶! 《征服C指针》 | 卡瓦邦噶! 我的姥姥 | 卡瓦邦噶! Python的哲学 Python 3.5的新特性 学校不教的计算机课 垃圾回收(GC)的三种基本方式 在编程中体验纯粹的快乐 从《美丽新世界》谈自由 在快钱实习 迷人的嗓音和迷人的故事——《Sleepyhead》 Python 的十个自然语言处理工具 记一个愚蠢的bug 一年炉石传说的游戏体验 《以撒的结合:重生》网页版图鉴 分清 C++的指针、引用和数组 笑话三则 自由比皇帝更伟大——《悲惨世界》笔记 Git 10 周年访谈:Linus 讲述背后故事 用 0x3f3f3f3f 设定最大int值的优点 Joel给计算机系学生的建议 一个词法分析器的简单实现 怎样才算健康的生活方式 MacVim 配置攻略 学习培训课程的视频效果好吗? 语言的控制 可悲的大多数 CSS样式思维导图 2014年终总结 奥巴马成为首位写程序的美国总统 青岛老城区的下水道好在哪里? 做优秀 UI 的七个建议(第二部分) 选择爱情的骑士 Git简明教程 Java集合总览 读 《1984》 java问答:终极父类(六)——等待/唤醒和接口 Java 问答:终极父类(五)——toString() Hyperlapse快速视频背后的技术细节 平庸之恶 Java程序员须知的七个日志管理工具 苏州 逛书摊随想 关于考试作弊 你的工作不仅仅是编程 推荐在线学习Java的英文资源 关注女性命运——《千禧年三部曲》 用好你的幻灯片——《演说之禅》 使用ReentrantLock和Lambda表达式让同步更纯净 新手学编程,从哪里开始? 程序员都是工程师吗? 不要学习代码,要学会思考 Java 问答:终极父类(四)——hashCode() Java 问答:终极父类(三)——finalize()和 getClass() 程序员职业之路的选择 我是一名摄影家 写给何小树的城市指南 为什么一些语言会比别的快? Java的常见误区与细节 跟朋友在一起玩游戏 死神永生——读《三体》 五种类型的程序员 创业圣经——读《黑客与画家》 纪念加西亚·马尔克斯 Junit中处理异常的另一种方式:catch-exception Java8采用Martin Fowler的方法创建内部DSL Linux HotSopt虚拟机GC线程的CPU占用率 J2EE概念介绍 如何成为一名黑客 Java 问答:终极父类(二)——equals()方法 为什么我喜欢Java Java 问答:终极父类(一)——clone()方法 七个改变世界的Java项目 java中默认类型转换的小问题 传统与创新 欢迎来到互联网 莫言和马尔克斯——读《生死疲劳》 位运算的妙用 读《人为什么活着》 写博客教会我的事情 中国特色操作系统 2013年总结 《永不妥协》影评 恨不相逢未嫁时——《廊桥遗梦》影评 如何优雅地使用PPT 给明年依然年轻的我们 天才与柱子 黑客守则和黑客精神 Looking for Freedom——《被解救的姜戈》 简洁之道
理解Python对象的属性和描述器
laixintao · 2017-09-05 · via 卡瓦邦噶!

对于Python对象的属性是如何读取的,我一直存在一些疑问。对对象的属性赋值,什么时候直接指向一个新的对象,什么时候会直接抛出AttributeError错误,什么时候会通过Descriptor?Python的descriptor是怎么工作的?如果对a.x进行赋值,那么a.x不是应该直接指向一个新的对象吗?但是如果x是一个descriptor实例,为什么不会直接指向新对象而是执行__get__方法?经过一番研究和实验尝试,我大体明白了这个过程。

__getattr__ __getattribute__和__setattr__

对于对象的属性,默认的行为是对对象的字典(即__dict__)进行get set delete操作。比如说,对a.x查找x属性,默认的搜索顺序是a.__dict__[‘x’],然后是type(a).__dict__[‘x’],然后怼type(a)的父类(metaclass除外)继续查找。如果查找不到,就会执行特殊的方法。

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

# -*- coding: utf-8 -*-

class Foo(object):

    class_foo = 'world'

    def __init__(self):

        self.foo = 'hello'

    def __getattr__(self, name):

        print("Foo get attr run..." + self + name)

class Bar(Foo):

    def __getattr__(self, name):

        print("Bar get attr run..." + name)

bar = Bar()

print bar.foo  # hello

print bar.class_foo  # world

__getattr__只有在当对象的属性找不到的时候被调用。

Python

1

2

3

4

5

6

7

   class LazyDB(object):

       def __init__(self):

           self.exists = 5

       def __getattr__(self, name):

           value = Value for %s % name

           setattr(self, name, value)

           return value

Python

1

2

3

4

5

6

7

8

   data = LazyDB()

   print(Before:, data.__dict__)

   print(foo:   , data.foo)

   print(After: , data.__dict__)

   >>>

   Before: {exists: 5}

   foo:    Value for foo

   After:  {exists: 5, foo: Value for foo}

__getattribute__ 每次都会调用这个方法拿到对象的属性,即使对象存在。

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

class ValidatingDB(object):

    def __init__(self):

        self.exists = 5

    def __getattribute__(self, name):

        print("Called __getattribute__(%s)" % name)

        try:

            attr = super().__getattribute__(name)

            print("Call __getattribute__ done")

            return attr

        except AttributeError:

            value = "Value for %s" % name

            setattr(self, name, value)

            return value

data = ValidatingDB()

print("exists:", data.exists)

print("foo:   ", data.foo)

print("foo:   ", data.foo)

输出如下:

1

2

3

4

5

6

Called __getattribute__(exists)

exists: 5

Called __getattribute__(foo)

foo:    Value for foo

Called __getattribute__(foo)

foo:    Value for foo

__setattr__每次在对象设置属性的时候都会调用。

判断对象的属性是否存在用的是内置函数hasattrhasattr是C语言实现的,看了一下源代码,发现自己看不懂。不过搜索顺序和本节开头我说的一样。以后再去研究下源代码吧。

总结一下,取得一个对象的属性,默认的行为是:

  1. 如果定义了 __getattribute__ 会无条件执行 __getattribute__ 的逻辑
  2. 查找对象的__dict__
  3. 如果没有,就查找对象的class的__dict__,即type(a).__dict__['x']
  4. 如果没有,就查找父类class的__dict__
  5. 如果没有,就执行__getattr__(如果定义了的话)
  6. 否则就抛出AttributeError

对一个对象赋值,默认的行为是:

  1. 如果定义了__set__方法,会通过__setattr__赋值
  2. 否则会更新对象的__dict__

但是,如果对象的属性是一个Descriptor的话,会改变这种默认行为。

Python的Descriptor

对象的属性可以通过方法来定义特殊的行为。下面的代码,Homework.grade可以像普通属性一样使用。

Python

1

2

3

4

5

6

7

8

9

10

11

class Homework(object):

       def __init__(self):

           self._grade = 0

       @property

       def grade(self):

           return self._grade

       @grade.setter

       def grade(self, value):

           if not (0 <= value <= 100):

               raise ValueError(Grade must be between 0 and 100)

               self._grade = value

但是,如果有很多这样的属性,就要定义很多setter和getter方法。于是,就有了可以通用的Descriptor。

Python

1

2

3

4

5

6

7

8

9

10

11

12

class Grade(object):

       def __get__(*args, **kwargs):

           #...

       def __set__(*args, **kwargs):

           #...

class Exam(object):

       # Class attributes

       math_grade = Grade()

       writing_grade = Grade()

       science_grade = Grade()

Descriptor是Python的内置实现,一旦对象的某个属性是一个Descriptor实例,那么这个对象的读取和赋值将会使用Descriptor定义的相关方法。如果对象的__dict__和Descriptor同时有相同名字的,那么Descriptor的行为会优先。

Python

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

# -*- coding: utf-8 -*-

class Descriptor(object):

    def __init__(self, name='x'):

        self.value = 0

        self.name = name

    def __get__(self, obj, type=None):

        print "get call"

        return self.value

    def __set__(self, obj, value):

        print "set call"

        self.value = value

class Foo(object):

    x = Descriptor()

foo = Foo()

print foo.x

foo.x = 200

print foo.x

print foo.__dict__

foo.__dict__['x'] = 500

print foo.__dict__

print foo.x

# --------------

# output

# get call

# 0

# set call

# get call

# 200

# {}

# {'x': 500}

# get call

# 200

实现了__get__()__set__()方法的叫做data descriptor,只定义了__get__()的叫做non-data descriptor(通常用于method,本文后面有相应的解释)。上文提到,data descriptor优先级高于对象的__dict__但是non-data descriptor的优先级低于data descriptor。上面的代码删掉__set__()将会是另一番表现。

Python

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

# -*- coding: utf-8 -*-

class Descriptor(object):

    def __init__(self, name='x'):

        self.value = 0

        self.name = name

    def __get__(self, obj, type=None):

        print "get call"

        return self.value

class Foo(object):

    x = Descriptor()

foo = Foo()

print foo.x

foo.x = 200

print foo.x

print foo.__dict__

foo.__dict__['x'] = 500

print foo.__dict__

print foo.x

# --------------

# output

# get call

# 0

# 200

# {'x': 200}

# {'x': 500}

# 500

如果需要一个“只读”的属性,只需要将__set__()抛出一个AttributeError即可。只定义__set__()也可以称作一个data descriptor。

调用关系

对象和类的调用有所不同。

对象的调用在object.__getattribute__()b.x转换成type(b).__dict__['x'].__get__(b, type(b)),然后引用的顺序和上文提到的那样,首先是data descriptor,然后是对象的属性,然后是non-data descriptor。

对于类的调用,由type.__getattribute__()B.x转换成B.__dict__['x'].__get__(None, B)。Python实现如下:

Python

1

2

3

4

5

6

def __getattribute__(self, key):

    "Emulate type_getattro() in Objects/typeobject.c"

    v = object.__getattribute__(self, key)

    if hasattr(v, '__get__'):

        return v.__get__(None, self)

    return v

需要注意的一点是,Descriptor默认是由__getattribute__()调用的,如果覆盖__getattribute__()将会使Descriptor失效。

Function,ClassMethod和StaticMethod

看起来这和本文内容没有什么关系,但其实Python中对象和函数的绑定,其原理就是Descriptor。

在Python中,方法(method)和函数(function)并没有实质的区别,只不过method的第一个参数是对象(或者类)。Class的__dict__中把method当做function一样存储,第一个参数预留出来作为self。为了支持方法调用,function默认有一个__get__()实现。也就是说,所有的function都是non-data descriptor,返回bound method(对象调用)或unbound method(类调用)。用纯Python实现,如下。

Python

1

2

3

4

5

class Function(object):

    . . .

    def __get__(self, obj, objtype=None):

        "Simulate func_descr_get() in Objects/funcobject.c"

        return types.MethodType(self, obj, objtype)

Python

1

2

3

4

5

6

7

8

9

10

11

>>> class D(object):

...     def f(self, x):

...         return x

...

>>> d = D()

>>> D.__dict__['f']  # Stored internally as a function

<function f at 0x00C45070>

>>> D.f              # Get from a class becomes an unbound method

<unbound method D.f>

>>> d.f              # Get from an instance becomes a bound method

<bound method D.f of <__main__.D object at 0x00B18C90>>

bound和unbound method虽然表现为两种不同的类型,但是在C源代码里,是同一种实现。如果第一个参数im_self是NULL,就是unbound method,如果im_self有值,那么就是bound method。

总结:Non-data descriptor提供了将函数绑定成方法的作用。Non-data descriptor将obj.f(*args)转化成f(obj, *args),klass.f(*args)转化成f(*args)。如下表。

Transformation Called from an Object Called from a Class
function f(obj, *args) f(*args)
staticmethod f(*args) f(*args)
classmethod f(type(obj), *args) f(klass, *args)

可以看到,staticmethod并没有什么转化,和function几乎没有什么差别。因为staticmethod的推荐用法就是将逻辑相关,但是数据不相关的functions打包组织起来。通过函数调用、对象调用、方法调用都没有什么区别。staticmethod的纯python实现如下。

Python

1

2

3

4

5

6

7

8

class StaticMethod(object):

    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):

        self.f = f

    def __get__(self, obj, objtype=None):

        return self.f

classmethod用于那些适合通过类调用的函数,例如工厂函数等。与类自身的数据有关系,但是和实际的对象没有关系。例如,Dict类将可迭代的对象生成字典,默认值为None。

Python

1

2

3

4

5

6

7

8

9

10

11

12

class Dict(object):

    . . .

    def fromkeys(klass, iterable, value=None):

        "Emulate dict_fromkeys() in Objects/dictobject.c"

        d = klass()

        for key in iterable:

            d[key] = value

        return d

    fromkeys = classmethod(fromkeys)

>>> Dict.fromkeys('abracadabra')

{'a': None, 'r': None, 'b': None, 'c': None, 'd': None}

classmethod的纯Python实现如下。

Python

1

2

3

4

5

6

7

8

9

10

11

12

class ClassMethod(object):

    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):

        self.f = f

    def __get__(self, obj, klass=None):

        if klass is None:

            klass = type(obj)

        def newfunc(*args):

            return self.f(klass, *args)

        return newfunc

最后的话

一开始只是对对象的属性有些疑问,查来查去发现还是官方文档最靠谱。然后认识了Descriptor,最后发现这并不是少见的trick,而是Python中的最常见对象——function时时刻刻都在用它。从官方文档中能学到不少东西呢。另外看似平常、普通的东西背后,可能蕴含了非常智慧和简洁的设计。

相关阅读

  1. hasattr的陷阱
  2. Effective Python: Item 31
  3. Descriptor HowTo Guide