Python: 函数与方法的区别 以及 Bound Method 和 Unbound Method

公孙胜
• 阅读 27744

函数与方法的区别

随着我们越来越频繁使用Python, 我们难免会接触到类, 接触到类属性和方法.但是很多新手包括我, 不知道方法函数 的区别,这次简单来讨论下, 如果有哪里认识不正确, 希望大神提点指教!
先来看两个定义吧:

function(函数) —— A series of statements which returns some value toa caller. It can also be passed zero or more arguments which may beused in the execution of the body.
method(方法) —— A function which is defined inside a class body. If called as an attribute of an instance of that class, the method will get the instance object as its first argument (which isusually called self).

从上面可以看出, 别的编程语言一样, Function也是包含一个函数头和一个函数体, 也同样支持0到n个形参,而Method则是在function的基础上, 多了一层类的关系, 正因为这一层类, 所以区分了 functionmethod.而这个过程是通过 PyMethod_New实现的

PyObject *
PyMethod_New(PyObject *func, PyObject *self, PyObject *klass)
{
    register PyMethodObject *im;   // 定义方法结构体
    im = free_list;
    if (im != NULL) {
        free_list = (PyMethodObject *)(im->im_self);
        PyObject_INIT(im, &PyMethod_Type);  // 初始化
        numfree--;
    }
    else {
        im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
        if (im == NULL)
            return NULL;
    }
    im->im_weakreflist = NULL;
    Py_INCREF(func);
    
    /* 往下开始通过 func 配置 method*/
    im->im_func = func;
    Py_XINCREF(self);
    im->im_self = self;
    Py_XINCREF(klass);
    im->im_class = klass;
    _PyObject_GC_TRACK(im);
    return (PyObject *)im;

所以本质上, 函数和方法的区别是: 函数是属于 FunctionObject, 而 方法是属 PyMethodObject
简单来看下代码:

def aa(d, na=None, *kasd, **kassd):
    pass
class A(object):
    def f(self):
        return 1
a = A()
print '#### 各自方法描述 ####'
print '## 函数     %s' % aa
print '## 类方法   %s' % A.f
print '## 实例方法 %s' % a.f

输出结果:

#### 各自方法描述 ####
## 函数   <function aa at 0x000000000262AB38>
## 类方法   <unbound method A.f>
## 实例方法 <bound method A.f of <__main__.A object at 0x0000000002633198>>

Bound Method 和 Unbound Method

method 还能再分为 Bound MethodUnbound Method, 他们的差别是什么呢? 差别就是 Bound method 多了一个实例绑定的过程!
A.funbound method, 而 a.fbound method, 从而验证了上面的描述是正确的!

看到这, 我们应该会有个问题:

 方法的绑定, 是什么时候发生的? 又是怎样的发生的?

带着这个问题, 我们继续探讨.很明显, 方法的绑定, 肯定是伴随着class的实例化而发生,我们都知道, 在class里定义方法, 需要显示传入self参数, 因为这个self是代表即将被实例化的对象。
我们需要dis模块来协助我们去观察这个绑定的过程:

[root@iZ23pynfq19Z ~]# cat 33.py
class A(object):
    def f(self):
        return 123
a = A()
print A.f()
print a.f()

## 命令执行 ##
[root@iZ23pynfq19Z ~]# python -m dis 33.py
  1           0 LOAD_CONST               0 ('A')
              3 LOAD_NAME                0 (object)
              6 BUILD_TUPLE              1
              9 LOAD_CONST               1 (<code object A at 0x7fc32f0b5030, file "33.py", line 1>)
             12 MAKE_FUNCTION            0
             15 CALL_FUNCTION            0
             18 BUILD_CLASS         
             19 STORE_NAME               1 (A)

  4          22 LOAD_NAME                1 (A)
             25 CALL_FUNCTION            0
             28 STORE_NAME               2 (a)

  5          31 LOAD_NAME                1 (A)
             34 LOAD_ATTR                3 (f)
             37 CALL_FUNCTION            0
             40 PRINT_ITEM          
             41 PRINT_NEWLINE       

  6          42 LOAD_NAME                2 (a)
             45 LOAD_ATTR                3 (f)
             48 CALL_FUNCTION            0
             51 PRINT_ITEM          
             52 PRINT_NEWLINE       
             53 LOAD_CONST               2 (None)
             56 RETURN_VALUE   
dis输出说明: 第一列是代码的行数, 第二列是指令的偏移量, 第三列是可视化指令, 第四列是参数, 第五列是指令根据参数计算或者查找的结果
咱们可以看到 第4列 和第五列, 分别就是对应: print A.f() 和 print a.f()
他们都是同样的字节码, 都是从所在的codeobject中的co_name取出参数对应的名字, 正因为参数的不同, 所以它们分别取到 A 和 a,下面我们需要来看看 LOAD_ATTR 的作用是什么:
//取自: python2.7/objects/ceval.c
        TARGET(LOAD_ATTR)
        {
            w = GETITEM(names, oparg);  // 从co_name 取出 f
            v = TOP();                  // 将刚才压入栈的 A/a 取出来
            x = PyObject_GetAttr(v, w); // 取得真正的执行函数
            Py_DECREF(v);
            SET_TOP(x);
            if (x != NULL) DISPATCH();
            break;
        }

通过 SET_TOP, 已经将我们需要真正执行的函数压入运行时栈, 接下来就是通过 CALL_FUNCTION 来调用这个函数对象, 继续来看看具体过程:

//取自: python2.7/objects/ceval.c
TARGET(CALL_FUNCTION)
        {
            PyObject **sp;
            PCALL(PCALL_ALL);
            sp = stack_pointer;
#ifdef WITH_TSC
            x = call_function(&sp, oparg, &intr0, &intr1);
#else
            x = call_function(&sp, oparg);  // 细节请往下看
#endif
            stack_pointer = sp;
            PUSH(x);
            if (x != NULL) DISPATCH();
            break;
        }
       
static PyObject *
call_function(PyObject ***pp_stack, int oparg)     
{
    int na = oparg & 0xff;                // 位置参数个数
    int nk = (oparg>>8) & 0xff;           // 关键位置参数的个数
    int n = na + 2 * nk;                  // 总的个数和
    PyObject **pfunc = (*pp_stack) - n - 1;  // 当前栈位置-参数个数,得到函数对象
    PyObject *func = *pfunc;  
    PyObject *x, *w;
    ... // 省略前面细节, 只看关键调用
    if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
            /* optimize access to bound methods */
            PyObject *self = PyMethod_GET_SELF(func);
            PCALL(PCALL_METHOD);
            PCALL(PCALL_BOUND_METHOD);
            Py_INCREF(self);
            func = PyMethod_GET_FUNCTION(func);
            Py_INCREF(func);
            Py_SETREF(*pfunc, self);
            na++;
            n++;
        } else
            Py_INCREF(func);
        READ_TIMESTAMP(*pintr0);
        if (PyFunction_Check(func))
            x = fast_function(func, pp_stack, n, na, nk);
        else
            x = do_call(func, pp_stack, na, nk);
        READ_TIMESTAMP(*pintr1);
        Py_DECREF(func);
}

咱们来捋下调用顺序:

CALL_FUNCTION -> call_function -> 根据函数的类型 -> 执行对应的操作

当程序运行到call_function时, 主要有的函数类型判断有: PyCFunction, PyMethod, PyFunction
在这里, 虚拟机已经判断出func是不属于PyCFunction, 所以将会落入上面源码的判断分支中, 而它将要做的,就是分别通过 PyMethod_GET_SELF, PyMethod_GET_FUNCTION 获得self对象和func函数, 然后通过调用 Py_SETREF(*pfunc, self):

// Py_SETREF 定义如下
#define Py_SETREF(op, op2)                      \
    do {                                        \
        PyObject *_py_tmp = (PyObject *)(op);   \
        (op) = (op2);                           \
        Py_DECREF(_py_tmp);                     \
    } while (0)

可以看出, Py_SETREF是用这个self对象替换了pfunc指向的对象了, 而pfunc在上面已经提及到了, 就是当时压入运行时栈的函数对象. 除了这几步, 还有更重要的就是, na 和 n 都分别自增1
看回上面的 a.f(), 咱们可以知道, 它是不需要参数的, 所以理论上 na,nk和n都是0, 但是因为f是method(方法), 经过上面一系列操作, 它将会传入一个self,而na也会变成1, 又因为*pfunc已经被替换成self, 相应代码:

if (PyFunction_Check(func))
            x = fast_function(func, pp_stack, n, na, nk);
        else
            x = do_call(func, pp_stack, na, nk);

所以它不再进入function的寻常路了, 而是走do_call, 然后就开始真正的调用;
其实这个涉及到Python调用函数的整个过程, 因为比较复杂, 后期找个时间专门谈谈这个

聊到这里, 我们已经大致清楚, 一个method(方法) 在调用时所发生的过程.明白了函数和方法的本质区别, 那么回到主题上 来说下 UnboundBound, 其实这两者差别也不大. 从上面我们得知, 一个方法的创建, 是需要self, 而调用时, 也会使用self,而只有实例化对象, 才有这个self, class是没有的, 所以像下面的执行, 是失败的额

class A(object):
    def f(self):
        return 1
a = A()

print '#### 各自方法等效调用 ####'
print '## 类方法 %s' % A.f()
print '## 实例方法 %s' % a.f()

## 输出结果 ##
#### 各自方法等效调用 ####
Traceback (most recent call last):
  File "C:/Users/Administrator/ZGZN_Admin/ZGZN_Admin/1.py", line 20, in <module>
    print '## 类方法 %s' % A.f()
TypeError: unbound method f() must be called with A instance as first argument (got nothing instead)

错误已经很明显了: 函数未绑定, 必须要将A的实例作为第一个参数
既然它要求第一个参数是 A的实例对象, 那我们就试下修改代码:

class A(object):
    def f(self):
        return 1
a = A()

print '#### 各自方法等效调用 ####'
print '## 类方法 %s' % A.f(a)   #传入A的实例a
print '## 实例方法 %s' % a.f()

## 结果 ##
#### 各自方法等效调用 ####
## 类方法 1
## 实例方法 1

可以看出来, BoundUnbound判断的依据就是, 当方法真正执行时, 有没有传入实例, A.f(a) 和 a.f() 用法的区别只是在于, 第一种需要人为传入实例才能调用, 而第二种, 是虚拟机帮我们做好了传入实例的动作, 不用我们那么麻烦而已, 两种方法本质上是等价的
欢迎各位大神指点交流,转载请注明来源: https://segmentfault.com/a/11...

点赞
收藏
评论区
推荐文章
CuterCorley CuterCorley
4年前
商业数据分析从入门到入职(8)Python模块、文件IO和面向对象
前言本文先介绍了Python中程序、模块和包的基本使用,并在此基础上介绍了Python标准库。然后详细介绍了Python中的文件IO操作,包括文本文件、二进制文件的读写和其他IO操作。最后介绍了面向对象,包括类的定义、继承的使用、鸭子类型和魔法方法。一、程序、模块和包1.自定义模块和包之前我们使用的.ipynb文件都不是纯Python文件,
Stella981 Stella981
3年前
Python import与from import使用及区别介绍
Python程序可以调用一组基本的函数(即内建函数),比如print()、input()和len()等函数。接下来通过本文给大家介绍Pythonimport与fromimport使用及区别介绍,感兴趣的朋友一起看看吧下面介绍下Pythonimport与fromimport使用,具体内容如下所示:Python程序可以调用一组基本的函数(即内建函
Wesley13 Wesley13
3年前
Java类和对象
一、类类是封装对象的属性和行为的载体,在Java语言中对象的属性以成员变量的形式存在,而对象的方法以成员方法的形式存在。1\.类的构造方法构造方法是一个与类同名的方法,对象的创建就是通过构造方法完成的,构造方法分为有参构造方法和无参构造方法,区别就在于有没有参数。说这么多概念是不是感觉有点麻木,直接看下面的例子吧。pub
Stella981 Stella981
3年前
Python中的@函数装饰器到底是什么?
在解释@函数装饰器之前,先说一下,类中的类方法和静态方法。在Python中完全支持定义类方法、静态方法。这两种方法很相似,Python它们都使用类来调用(ps:用对象调用也可以)。区别在于:Python会自动绑定类方法的第一个参数,类方法的第一个参数会自动绑定到类本身;但对于静态方法则不会自动绑定。类方法用@classmethod
Stella981 Stella981
3年前
Python3中的super()函数详解
关于Python3中的super()函数我们都知道,在Python3中子类在继承父类的时候,当子类中的方法与父类中的方法重名时,子类中的方法会覆盖父类中的方法,那么,如果我们想实现同时调用父类和子类中的同名方法,就需要使用到super()这个函数,用法为super().函数名()下面是一个例子:
Stella981 Stella981
3年前
JavaScript中的类定义和继承实现
ES5中因为没有class关键字,所以创建类的方式是通过构造函数来定义的。我将一步步的用代码演示如何慢慢用原生的语法实现JS的类的定义和继承。希望大家喜欢。废话不多说,我们来看原生JavaScript定义类的方法。1\.最简单的类//类的构造函数functionPerson(){this.nam
Stella981 Stella981
3年前
Scala基础
1\.介绍柯里化(currying,以逻辑学家HaskellBrooksCurry的名字命名)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数作为参数的函数。在Scala中方法和函数有细微的差别,通常编译器会自动完成方法到函数的转换。如果想了解Scala方法和函数的具体区别,请参考博文S
Stella981 Stella981
3年前
PythonStudy——魔法函数 Magic Methods
魔法函数python中以双下划线开始和结束的函数(不可自己定义)为魔法函数调用类实例化的对象的方法时自动调用魔法函数(感觉不需要显示调用的函数都叫)在自己定义的类中,可以实现之前的内置函数,比如下面比较元素sorted时用It函数(lt(self,other):判断self对象是否小于other对象;)
Wesley13 Wesley13
3年前
C# 代理用法
delegate到底是什么东西C语言总学过吧,如果你学得不像我那么差的话,函数指针总用过吧,就算没用过总听说过吧,嗯,大胆的告诉你,你完全可以把delegate理解成C中的函数指针,它允许你传递一个类A的方法m给另一个类B的对象,使得类B的对象能够调用这个方法m,说白了就是可以把方法当作参数传递。不过delegate和函数指针还是有点区别的,dele
Stella981 Stella981
3年前
Python中函数和方法的区别
1、函数要手动传self,方法不用传self2、如果是一个函数,用类名去调用,如果是一个方法,用对象去调用 举例说明:classFoo(object):def__init__(self):self.name"haiyan"deffunc(self):
小万哥 小万哥
1年前
C++ 类方法解析:内外定义、参数、访问控制与静态方法详解
C类方法类方法,也称为成员函数,是属于类的函数。它们用于操作或查询类数据,并封装在类定义中。类方法可以分为两种类型:类内定义方法:直接在类定义内部声明和定义方法。类外定义方法:在类定义内部声明方法,并在类外部单独定义方法。类内定义方法在类定义内部可以直