python-函数传参原理

389 阅读4分钟

一、函数参数形式

位置参数:def f(a,b),调用函数f(a,b), a和b都是位置参数

键参数:def f(a,b),调用函数f(a,b=3),a时位置参数,b是键参数

(函数参数是位置参数还是键参数是由调用函数的实参形式决定的,与定义函数的形参无关)

扩展位置参数:def f(a,b,*lst)*lst是扩展位置参数

扩展键参数:def f(a,b,**key)**key是扩展键参数

二、函数传参方式

def describe_pet(animal_type, pet_name="dawang"):  #位置参数和键参数    
    """显示宠物的信息"""
    print("\nI have a " + animal_type + ".")
    print("My " + animal_type + "'s name is " + pet_name.title() + ".") 

位置实参:describe_pet('hamster', 'harry'),顺序必须一致。

关键字实参: describe_pet(pet_name='harry', animal_type='hamster')

默认值:pet_name参数指定了默认值,函数调用可以不传递该参数:

describe_pet("hamster")或describe_pet(animal_type="hamster")

有默认值的参数一定要放在没有默认值的参数后面。

def make_pizza(*toppings):   #扩展位置参数,参数保存在元组toppings中
    """概述要制作的比萨"""
    print("\nMaking a pizza with the following toppings:")
    for topping in toppings:
        print("- " + topping) 

def build_profile(first, last, **user_info):    #扩展键参数,参数保存在字典user_info中
	"""创建一个字典, 其中包含我们知道的有关用户的一切"""
	profile = {}
	profile['first_name'] = first
	profile['last_name'] = last
	for key, value in user_info.items():
		profile[key] = value
	return profile

传递任意数量的实参:

make_pizza('mushrooms', 'green peppers', 'extra cheese') 

user_profile = build_profile('albert', 'einstein',location='princeton',field='physics')  

如果要让函数接受不同类型的实参, 必须在函数定义中将接纳任意数量实参的形参放在最后。 Python先匹配位置实参或关键字实参, 再将余下的实参都收集到最后一个形参中  。

二、函数传参原理
1、位置参数传参需要按顺序?

因为函数调用时,位置参数从左到右压入运行时栈并按序拷贝到f_localsplus指向的内存区域。在访问函数参数时,是通过索引(偏移位置)访问位置参数,所以参数的顺序十分重要。

2、有默认值的参数一定要放在没有默认值的参数后面,且要连续设置?(函数参数默认值从函数参数列表最右端开始,必须连续设置)

这要从如何函数调用开始后的PyEval_EvalCodeEx函数说起

def f(a=1,b=2):
    print(a+b)

假设有这样的函数定义,可以通过f(),f(2),f(b=3)等方式进行调用。Python虚拟机需要面对判断是否需要设置默认参数,设置多少个默认参数的问题。

根据函数定义,co_argcount=2(函数参数个数),默认参数个数default=2。

(1)f()调用该函数

位置参数个数argcount=na=0,键参数个数kwcount=nk=0。

if(co->co_argcount>0||co->co_flags&(CO_VARARGS|CO_VARKEYWORDS)){
    int i;
    ...
    //n是函数调用传入的位置参数个数,即na,这里是0
    int n=argcount;
    //[1]如果传入的位置参数个数比函数定义的参数个数小,即虚拟机需要设置默认参数
    if(argcount<co->co_argcount){
        //[2]m(无默认值的参数)=函数定义的参数个数-有默认值的参数,无默认值参数必须函数调用时传入,这里m=0
        int m=co->co_argcount-default;
        for(i=argcount;i<m;i++){
            if(GETLOCAL(i)==NULL){
                goto fial;
            }
        }
        //[3]n如果大于m,证明希望替换一些函数定义的有默认值的参数。n-m是函数调用传入得参数中,有多少个参数用于默认位置参数的,这里是0
        if(n>m)
            i=n-m;
        else
            i=0;
        //[4]设置默认位置参数的默认值,这里i=0,default=2,需要设置两个默认参数
        for(;i<default;i++){
            if(GETLOCAL(m+i)==NULL){
                //defs指向函数定义的默认值参数列表
                PyObject *def=defs[i];
                Py_INCREF(def);  
                SETLOCAL(m+i,def);
            }
        }

    }
}

从步骤4可以看到,前面m个参数(无默认值参数)是不需要设置默认值的。需要设置得默认参数个数是default-i个,而且是连续设置的。

整段代码的设计,已经是立在非默认参数在前,默认参数在后且连续的前提上。

(1)f(b=3)调用该函数

位置参数个数argcount=na=0,键参数个数kwcount=nk=1,kws指向函数调用的键参数,kws[0]="b",kws[1]=3。

if(co->co_argcount>0||co->co_flags&(CO_VARARGS|CO_VARKEYWORDS)){
    int i;
    int n=argcount;
    ...
    //[1]遍历函数调用传入的所有键参数
    for(i=0;i<kwcount;i++){
        PyObject *keyword=kws[2*i];  //键参数的键,这里是"b"
        PyObject *value=kws[2*i+!];  //键参数的值,这里是3
        int j;
        //[2]遍历函数定义的所有参数(保存在变量表co->co_varnames中),查找和传入的键参数键keyword相同的默认参数,这里是b
        for(j=0;j<co-co_argcount;j++){
            PyObject *nm=PyTuple_GET_ITEM(co->co_varnames,j);
            int cmp=PyObject_RichCompareBool(keyword,nm,Py_EQ);
            if(cmp>0)
                break;
            else if(cmp<0)
                goto faill
        }
        //[3]如果keyword不在变量名表,这里是(a,b)
        if(j>=co-co_argcount){...}  
        //[4]如果keyword在变量名表
        else{
            if(GETLOCAL(j)!=NULL){
                goto fail;
            }
            Py_INCREF(value);   
            //设置"b"的键值是3           
            SETLOCAL(j,value);
        }
    }
    ...
    //[4]设置默认位置参数的默认值,这里i=0,default=2,需要设置两个默认参数
    for(;i<default;i++){
        if(GETLOCAL(m+i)==NULL){
            //defs指向函数定义的默认值参数列表
            PyObject *def=defs[i];
            Py_INCREF(def);  
            SETLOCAL(m+i,def);
        }
    }
}

3、如果要让函数接受不同类型的实参, 必须在函数定义中将接纳任意数量实参的形参(扩展位置参数和扩展键参数)放在最后?

def f(value,*list,**key)

根据函数定义,co_argcount=1(函数参数个数),在python内部,lst是有PyTupleObject实现的局部变量,**key由PyDictObject实现的局部变量,不计入co_argcount中。如果由lst形式的位置参数,co_flag会设置CO_VARARGS标识符;而如果有**key形式的参数,co_flag会设置CO_VARKEYWORDS标识符。

假设以f(-1,1,2,a=3,b=4)的方式调用函数

位置参数个数argcount=na=3,键参数个数kwcount=nk=2。args指向函数调用的位置参数(-1,1,2)。先来看看扩展位置参数的访问;

if(co->co_argcount>0||co->co_flags&(CO_VARARGS|CO_VARKEYWORDS)){
    int i;
    int n=argcount;
    //这里argcount=3,co->co_argcount=1
    if(argcount>co->co_argcount){
        n=co_co_argcount;
    } 
    //[1]设置位置参数值,n是函数定义的位置参数个数
    for(i=0;i<n;i++){
        x=args[i];
        SETLOCAL(i,x);
    }  
    if(co->co_flags&CO_VARARGS){
        //[2]u是扩展位置参数个数
        u=PyTuple_New(argcount-n);
        SETLOCAL(co->co_argcount,u);
        //[3]将扩展位置参数放入到PyTupleObject中
        for(i=n;i<argcount;i++){
            x=arg[i];
            PyTuple_SET_ITEM(u,i-n,x);
        }
    }
}

在[3]处,扩展位置参数一股脑的塞进了PyTupleObject对象中。在[2]处,这个存放扩展位置参数的元素放入了f_localsplus,且在位置参数后面。而后面的扩展键参数,又会放在PyTupleObject对象后面,如下:

if(co->co_argcount>0||co->co_flags&(CO_VARARGS|CO_VARKEYWORDS)){
    int i=0;
    int n=argcount;
    PyObject *kwdict=NULL;
    ...
    //[1]创建PyDictObject对象,放在PyTupleObject之后
    if(co->co_flags&CO_VARKEYWORDS){
        kwdict=PyDict_New();
        i=co->co_argcount;
        if(co->co_flags&CO_VARARGS)
            i++;
        SETLOCAL(i,kwdict);
    }
    //[2]遍历函数调用传入的所有键参数,确定函数的def语句中是否出现了见参数的keyword
    for(i=0;i<kwcount;i++){
        PyObject *keyword=kws[2*i]; 
        PyObject *value=kws[2*i+!]; 
        int j;
        //[2]遍历函数定义的所有参数(保存在变量表co->co_varnames中),查找和传入的键参数键keyword相同的默认参数,这里是b
        for(j=0;j<co-co_argcount;j++){
            PyObject *nm=PyTuple_GET_ITEM(co->co_varnames,j);
            int cmp=PyObject_RichCompareBool(keyword,nm,Py_EQ);
            if(cmp>0)
                break;
            else if(cmp<0)
                goto faill
        }
        //[3]如果keyword不在变量名表
        if(j>=co-co_argcount){
            PyDict_SetItem(kwdict,keyword,value);
        }else{
        ...
        }

}

f(-1,1,2,a=3,b=4)调用函数后,PyFrameObject对象中f_localsplus的结构如下