Python-Unbound/Bound method object


本篇主要总结Python中绑定方法对象(Bound method object)和未绑定方法对象(Unboud method object)的区别和联系。
主要目的是分清楚这两个极容易混淆的概念,顺便将Python的静态方法,类方法及实例方法加以说明


OK,下面开始

1. 一个方法引发的“血案”

类中所定义的函数称为方法

举例:

>>>class Foo(object):
...        def  foo():
...            print 'call foo'   

然后令人困惑的地方就来了:
当你尝试使用类名.方法名调用函数foo时,会出现如下错误

>>> Foo.foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)

看一下报错信息发现需要一个Foo的实例(instance)来调用,OK,于是调用如下:

>>> Foo().foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes no arguments (1 given)

-.-!!!
估计脾气不好的看到做到这里想要骂街了。
因为从字面上看Foo( ).foo( )并没有传递任何参数,而报错信息却显示(1 given)

在Python中一切皆对象方法是函数,所以我们来仔细查看一下函数对象foo

>>> Foo.foo
<unbound method Foo.foo>
>>> Foo().foo
<bound method Foo.foo of <__main__.Foo object at 0x7ff33b424d50>>

咦~,发现一个有趣的现象:
通过类名Foo获取类函数属性foo时,得到的是unbound method object通过实例Foo()获取类的函数属性foo时,得到的是bound method object
在来看看这两个对象的类型:

>>> type(Foo.foo)
<type 'instancemethod'>
>>> type(Foo().foo)
<type 'instancemethod'>

于是我们产生了更大的疑问:为什么同样是实例方法(instancemethod),获取方式的不同,会导致获得不同的对象呢?

2. bound/unbound method是怎么来的

下面让我们来一层层揭开这个bound/unbound method的面纱。
首先,我们知道,对于类,其属性是存放在__dict__字典中,即:

>>> Foo.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'Foo' objects>, '__module__': '__main__', 'foo': <function foo at 0x7ff33b42a5f0>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None})

在其中我们看到了'foo': <function foo at 0x7ff33b42a5f0>
然后利用字典查看foo

>>> Foo.__dict__['foo']
<function foo at 0x7ff33b42a5f0>

可以看到foo是一个函数对象,根据上一小节最后一个例子的信息,我们发现,foo是有绑定行为的。

在Python中使用描述器有翻译的链接)来表示具有“绑定”行为的对象属性,使用描述器协议方法来控制对具有绑定行为属性的访问,这些描述器协议方法包括:__get__()__set__()__delete__()

根据上面这段难以让人理解的描述,我们可以大胆的猜测,Foo的属性foo是一个描述器,它通过__get__()方法来控制对foo的访问。
根据描述器协议方法descr.__get__(self, obj, type=None) --> value,我们尝试如下:

>>> Foo.__dict__['foo'].__get__(None,Foo)
<unbound method Foo.foo>

于是,我们惊讶的看到这个结果竟然与上一小节看到的结果相同!
这绝不是偶然。
事实上,根据官方文档的描述,调用Foo.foo时,Python会根据查找链从Foo.__dict__['foo']开始,然后查找type(Foo).__dict__['foo'],一路向上查找type(Foo)的所有基类。Foo.foo会被转换为Foo.__dict__['foo'].__get__(None,Foo)
也就是说,我们在代码中使用Foo.foo实际上会被转化成
Foo.__dict__['foo'].__get__(None,Foo)
对于根据描述器协议方法descr.__get__(self, obj, type=None) --> value的参数列表,由于其self参数在这里被赋予了None,所以没有给定实例,因此认为是未绑定(unbound)
(当然这是一种便于理解的描述,其根本机制请移步这里)
那么一个很简单的推理就是:如果self参数给定了实例对象,那么,得到的就是bound method,如下。

>>> Foo.__dict__['foo'].__get__(Foo(),Foo)
<bound method Foo.foo of <__main__.Foo object at 0x7ff33b424d50>>

因此,可以有如下理解:

  1. 当通过类来获取函数属性的时候,得到的是非绑定方法对象
  2. 当通过实例来获取函数属性的时候,得到的是绑定方法对象

3. methods, static method and class method

如果有使用Python方法的经验,那么一定注意过self的使用,请看下面这个例子:

>>> class Foo(object):
...     def foo():
...             print 'call foo'
...     def foo_one(self):
...             print 'call foo_one'
... 
>>> Foo.foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method foo() must be called with Foo instance as first argument (got nothing instead)
>>> Foo().foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes no arguments (1 given)
>>> Foo.foo_one()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unbound method foo_one() must be called with Foo instance as first argument (got nothing instead)
>>> Foo().foo_one()
call foo_one

这个例子定义了两个method:foo()foo_one(self)
可以看到,同样使用类名.方法名()调用时,所报的错误相同。但是在使用实例名.方法名()调用时,foo_one是可以调用成功的。
为什么呢?
原因在于当使用Foo().foo_one()调用时,Python做了如下修改:

>>> Foo.foo_one(Foo())
call foo_one

将实例Foo()作为第一个参数传递进去,因此,函数foo_one(self)调用成功。这也解释了为什么Foo().foo()调用不成功。
因为foo的定义为foo(),当调用Foo().foo()时,Python做了如下修改:

>>> Foo.foo(Foo())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes no arguments (1 given)

传入了一个参数Foo(),所以会出现foo() takes no arguments (1 given)的错误。
我曾经看到有的人把foo()这种参数列表中没有self的方法称为类方法,而把带有self的方法称为实例方法,根据上面的描述可以发现,这种划分是错误的。
那么,Python中有没有类方法呢?
答案是,有。那么如何定义一个类方法呢?

Pyhon 类方法
还是使用上面的例子

>>> class Foo(object):          
...     @classmethod                #定义类方法要点1
...     def foo(cls):               #定义类方法要点2
...             print 'call foo'
... 
>>> Foo.foo()
call foo
>>> Foo().foo()
call foo

定义类方法需要注意两点:1. 添加@classmethod;2. 添加cls参数
这样定义的类方法可以通过类名.方法名()的形式调用,也可以通过实例.方法名()的形式调用。

看到这里会发现,在Python中定义方法,总要带两个参数self或者cls。其中通过self限定的method必须使用实例才能调用。
那么很自然的一个疑问是,能不能定义不包含selfcls的方法呢?像最开始的例子中foo()那样。答案是有的,办法就是加@staticmethod修饰器。
这种被@staticmethod修饰器修饰的方法,称为静态方法

Python 静态方法
除了类方法,还有静态方法,请看下面这个例子:

>>> class Foo(object):
...     @staticmethod
...     def foo():
...             print 'call foo'
... 
>>> Foo.foo()
call foo
>>> Foo().foo()
call foo

静态方法可以通过类名.方法名()实例.方法名()的形式调用。
查看type结果如下:

>>> type(Foo.foo)
<type 'function'>

可以看到,静态方法的类型是function,而类方法的类型是instancemethod

总结

最后来总结一下:
从Python方法定义的角度出发,可以分为三种:
1.第一个参数是self;
2.第一个参数是cls;
3.参数既不含self也不含cls
对于第一种方法,必须通过实例.方法名()类名.方法名(实例)的形式调用;
对于第二种,可以通过实例.方法名()类名.方法名()的形式调用,不能通过类名.方法名(实例)的形式调用;
对于第三种,方法即是普通函数,但是必须通过实例.方法名()类名.方法名()的形式调用,不能通过其他形式调用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,679评论 0 9
  • 从明天开始,是新的一天,又回到了教育行业。 最初上学时,最初得知是学的师范专业,一百个不愿意不甘心,自顾自的把...
    小霏铃子阅读 442评论 0 1
  • 支付宝业务协作协议 您(以下简称“甲方”)与支付宝(中国)网络技术有限公司(以下称“支付宝”或“乙方”)经友好协商...
    山东老李投融建阅读 2,864评论 0 1