> 백엔드 개발 > 파이썬 튜토리얼 > Python 함수의 구현 원리는 무엇입니까

Python 함수의 구현 원리는 무엇입니까

PHPz
풀어 주다: 2023-04-24 16:52:15
앞으로
1762명이 탐색했습니다.

Wedge

함수는 모든 프로그래밍 언어의 기본 요소입니다. 함수는 수행할 여러 작업을 결합할 수 있습니다. 그리고 함수를 호출하면 어떻게 될까요? 예, 함수 실행을 위해서는 스택 프레임이 생성되어야 합니다.

PyFunctionObject

Python의 모든 것은 객체이며 함수도 예외는 아닙니다. 함수는 funcobject.h에 정의된 PyFunctionObject 구조를 통해 하단에 구현됩니다.

typedef struct {
    /* 头部信息,无需多说 */
    PyObject_HEAD
    /* 函数对应的 PyCodeObject 对象
       因为函数也是基于 PyCodeObject 对象构建的 */
    PyObject *func_code;   
    /* 函数的 global 名字空间 */     
    PyObject *func_globals;     
    /* 函数参数的默认值,一个元组或者空 */   
    PyObject *func_defaults;    
    /* 只能通过关键字的方式传递的 "参数" 和 "该参数的默认值" 组成的字典 
       或者空 */   
    PyObject *func_kwdefaults;  
    /* 闭包 */
    PyObject *func_closure;     
    /* 函数的 docstring */
    PyObject *func_doc;  
    /* 函数名 */       
    PyObject *func_name;      
    /* 函数的属性字典,一般为空 */  
    PyObject *func_dict; 
    /* 弱引用列表,对函数的弱引用都会保存在里面 */       
    PyObject *func_weakreflist; 
    /* 函数所在的模块 */
    PyObject *func_module;  
    /* 函数的类型注解 */    
    PyObject *func_annotations; 
    /* 函数的全限定名 */
    PyObject *func_qualname;  
    /* Python 函数在底层也是某个类(PyFunction_Type)的实例对象
       调用时会执行类型对象的 tp_call,在 Python 里面就是 __call__
       但函数比较特殊,它创建出来就是为了调用的,所以不能走通用的 tp_call
       为了优化调用效率,引入了 vectorcall */  
    vectorcallfunc vectorcall;
} PyFunctionObject;
로그인 후 복사

실제로 이 멤버를 가져와 Python에서 어떻게 작동하는지 살펴보겠습니다.

func_code: 함수의 바이트 코드

def foo(a, b, c):
    pass

code = foo.__code__
print(code)  # <code object foo at ......>
print(code.co_varnames)  # (&#39;a&#39;, &#39;b&#39;, &#39;c&#39;)
로그인 후 복사

func_globals: 전역 네임스페이스

def foo(a, b, c):
    pass

name = "古明地觉"
print(foo.__globals__)  # {......, &#39;name&#39;: &#39;古明地觉&#39;}
# 拿到的其实就是外部的 global名字空间
print(foo.__globals__ is globals())  # True
로그인 후 복사

func_defaults: 함수 매개변수의 기본값

def foo(name="古明地觉", age=16):
    pass

# 打印的是默认值
print(foo.__defaults__)  # (&#39;古明地觉&#39;, 16)


def bar():
    pass

# 没有默认值的话,__defaults__ 为 None
print(bar.__defaults__)  # None
로그인 후 복사

fun c_k wdefaults: 키워드를 통해서만 구성된 사전 전달된 "매개변수" 및 "매개변수의 기본값"

def foo(name="古明地觉", age=16):
    pass

# 打印为 None,这是因为虽然有默认值
# 但并不要求必须通过关键字参数的方式传递
print(foo.__kwdefaults__)  # None


def bar(*, name="古明地觉", age=16):
    pass

print(
    bar.__kwdefaults__
)  # {&#39;name&#39;: &#39;古明地觉&#39;, &#39;age&#39;: 16}
로그인 후 복사

앞에는 *가 붙는데, 이는 다음 매개변수가 키워드를 통해 전달되어야 함을 의미합니다. 키워드 매개변수를 통해 전달되지 않으면 *로 수신된 위치 매개변수가 아무리 많아도 이름이나 나이에는 전달될 수 없기 때문입니다.

우리는 *args가 정의되면 함수가 원하는 수의 위치 매개변수를 받을 수 있으며 이러한 매개변수는 튜플 형식으로 args에 저장된다는 것을 알고 있습니다. 하지만 여기서는 필요하지 않습니다. 후속 매개변수가 키워드 매개변수를 통해 전달되어야 하므로 앞에 *만 작성하면 됩니다. 물론 *args를 작성하는 것도 가능합니다.

func_closure: 클로저 객체

def foo():
    name = "古明地觉"
    age = 16

    def bar():
        nonlocal name
        nonlocal age

    return bar

# 查看的是闭包里面使用的外层作用域的变量
# 所以 foo().__closure__ 是一个包含两个元素的元组
print(foo().__closure__) 
"""
(<cell at 0x000001FD1D3B02B0: int object at 0x00007FFDE559D660>,
 <cell at 0x000001FD1D42E310: str object at 0x000001FD1D3DA090>)
"""

print(foo().__closure__[0].cell_contents)  # 16
print(foo().__closure__[1].cell_contents)  # 古明地觉
로그인 후 복사

참고: 클로저 속성을 보려면 외부 foo가 아닌 내부 함수를 사용합니다.

func_doc: 함수의 독스트링

def foo():
    """
    hi,欢迎来到我的编程教室
    遇见你真好
    """
    pass 

print(foo.__doc__)
"""

    hi,欢迎来到我的编程教室
    遇见你真好
    
"""
로그인 후 복사

func_name: 함수의 이름

def foo(name, age):
    pass

print(foo.__name__)  # foo
로그인 후 복사

물론 함수뿐만 아니라 메소드, 클래스, 모듈에도 고유한 이름이 있습니다.

import numpy as np

print(np.__name__)  # numpy
print(np.ndarray.__name__)  # ndarray
print(np.array([1, 2, 3]).transpose.__name__)  # transpose
로그인 후 복사

func_dict: 함수의 속성 사전

함수도 최하위 수준의 클래스에 의해 인스턴스화되기 때문에 자체 속성 사전을 가질 수 있지만 이 사전은 일반적으로 비어 있습니다.

def foo(name, age):
    pass

print(foo.__dict__)  # {}
로그인 후 복사

물론 몇 가지 트릭도 사용할 수 있습니다.

def foo(name, age):
    return f"name: {name}, age: {age}"

code = """
name, age = "古明地觉", 17

def foo():
    return "satori" 
"""
exec(code, foo.__dict__)

print(foo.name)  # 古明地觉
print(foo.age)  # 17
print(foo.foo())  # satori
print(foo("古明地觉", 17))  # name: 古明地觉, age: 17
로그인 후 복사

따라서 함수라고 부르기는 하지만 특정 유형의 개체에 의해 구현되기도 합니다.

func_weakreflist: 약한 참조 목록

Python은 이 속성을 얻을 수 없습니다. 여기서는 약한 참조에 대해 자세히 논의하지 않습니다.

func_module: 함수가 위치한 모듈

def foo(name, age):
    pass

print(foo.__module__)  # __main__

import pandas as pd
print(
    pd.read_csv.__module__
)  # pandas.io.parsers.readers
from pandas.io.parsers.readers import read_csv
print(read_csv is pd.read_csv)  # True
로그인 후 복사

클래스, 메소드, 코루틴에도 __module__ 속성이 있습니다.

func_annotations: 유형 주석

def foo(name: str, age: int):
    pass

# Python3.5 新增的语法,但只能用于函数参数
# 而在 3.6 的时候,声明变量也可以使用这种方式
# 特别是当 IDE 无法得知返回值类型时,便可通过类型注解的方式告知 IDE
# 这样就又能使用 IDE 的智能提示了
print(foo.__annotations__)  
# {&#39;name&#39;: <class &#39;str&#39;>, &#39;age&#39;: <class &#39;int&#39;>}
로그인 후 복사

func_qualname: 정규화된 이름

def foo():
    pass
print(foo.__name__, foo.__qualname__)  # foo foo


class A:

    def foo(self):
        pass
print(A.foo.__name__, A.foo.__qualname__)  # foo A.foo
로그인 후 복사

완전한 이름이 더 완전해야 합니다.

def foo(name, age):
    pass

# <class &#39;function&#39;> 就是 C 里面的 PyFunction_Type
print(foo.__class__)  # <class &#39;function&#39;>
로그인 후 복사

하지만 이 클래스의 맨 아래 레이어는 우리에게 노출되지 않으며 직접 사용할 수 없습니다. 함수는 def를 통해 생성할 수 있고 유형 개체를 통해 생성할 필요가 없기 때문입니다.

함수는 언제 만들어졌나요?

앞서 이 함수는 최하위 수준의 PyFunctionObject 구조에 의해 구현된다고 언급했습니다. 여기에는 PyCodeObject 객체를 가리키는 func_code 멤버가 있고, 이를 기반으로 함수가 생성됩니다.

PyCodeObject는 코드 조각의 정적 표현이기 때문에 Python 컴파일러가 소스 코드를 컴파일한 후 각 코드 블록(코드 블록)에 대해 단 하나의 PyCodeObject 객체를 생성합니다. 이 객체에는 소스 코드에서 볼 수 있는 정보인 이 코드 블록에 대한 일부 정적 정보가 포함되어 있습니다.

예를 들어 함수에 해당하는 코드 블록에 a = 1과 같은 표현식이 있으면 기호 a, 정수 1 및 이들 사이의 관계는 정적 정보이며 이 정보는 정적으로 저장됩니다.

  • 기호 a는 기호 테이블 co_varnames에 저장됩니다.

  • integer 1은 상수 풀 co_consts에 저장됩니다.

  • 은 둘 사이에 할당문이므로 LOAD_CONST와 STORE_FAST라는 두 가지 명령이 있습니다. , 이는 바이트코드 명령 시퀀스 co_code에 존재합니다.

위 정보는 컴파일 중에 얻을 수 있으므로 PyCodeObject 객체는 컴파일의 결과입니다.

그런데 PyFunctionObject 객체는 언제 생성되었나요? 분명히 이는 런타임 시 Python 코드에 의해 동적으로 생성됩니다. 더 정확하게 말하면 def 문을 실행할 때 가상 머신에 의해 생성됩니다.

가상 머신이 현재 스택 프레임에서 바이트코드를 실행할 때 def 문을 발견한다는 것은 새로운 PyCodeObject 객체가 발견되었음을 의미합니다. 왜냐하면 이 객체는 계층별로 중첩될 수 있기 때문입니다. 따라서 가상 머신은 이 PyCodeObject 객체를 기반으로 해당 PyFunctionObject 객체를 생성한 다음, 함수 이름과 PyFunctionObject 객체(함수 본문)의 키-값 쌍을 현재 로컬 공간에 배치합니다.

PyFunctionObject 객체에서는 관련 정적 정보도 얻어야 하므로 PyCodeObject를 가리키는 func_code 멤버가 있을 것입니다.

除此之外,PyFunctionObject 对象中还包含了一些函数在执行时所必需的动态信息,即上下文信息。比如 func_globals,就是函数在执行时关联的 global 空间,说白了就是在局部变量找不到的时候能够找全局变量,可如果连 global 空间都没有的话,那即便想找也无从下手呀。

而 global 作用域中的符号和值必须在运行时才能确定,所以这部分必须在运行时动态创建,无法静态存储在 PyCodeObject 中,因此要根据 PyCodeObject 对象创建 PyFunctionObject 对象。总之一切的目的,都是为了更好地执行字节码。

我们举个例子:

# 虚拟机从上到下顺序执行字节码
name = "古明地觉"
age = 16

# 啪,很快啊,发现了一个 def 语句
def foo():
    pass

# 出现 def,虚拟机就知道源代码进入一个新的作用域了
# 也就是遇到一个新的 PyCodeObject 对象了
# 而通过 def 可以得知这是创建函数的语句
# 所以会基于 PyCodeObject 创建 PyFunctionObject
# 因此当执行完 def 语句之后,一个函数就创建好了
# 创建完之后,会将函数名和函数体组成键值对,存放在当前的 local 空间中
print(locals()["foo"])
"""
<function foo at 0x7fdc280e6280>
"""
로그인 후 복사

调用的时候,会从 local 空间中取出符号 foo 对应的 PyFunctionObject 对象。然后根据这个 PyFunctionObject 对象创建 PyFrameObject 对象,也就是为函数创建一个栈帧,随后将执行权交给新创建的栈帧,并在新创建的栈帧中执行字节码。

函数是怎么创建的

通过上面的分析,我们知道了函数是虚拟机在遇到 def 语句的时候创建的,并保存在 local 空间中。当我们通过函数名()的方式调用时,会从 local 空间取出和函数名绑定的函数对象,然后执行。

那么问题来了,函数(对象)是怎么创建的呢?或者说虚拟机是如何完成 PyCodeObject 对象到 PyFunctionObject 对象之间的转变呢?显然想了解这其中的奥秘,就必须从字节码入手。

import dis

s = """
name = "satori"
def foo(a, b):
    print(a, b)
    return 123

foo(1, 2)
"""

dis.dis(compile(s, "<...>", "exec"))
로그인 후 복사

源代码很简单,定义一个变量 name 和函数 foo,然后调用函数。显然源代码在编译之后会产生两个 PyCodeObject,一个是模块的,一个是函数 foo 的,我们来看一下。

     # 加载字符串常量 "satori",压入运行时栈
2    0 LOAD_CONST               0 (&#39;satori&#39;)
     # 将字符串从运行时栈弹出,并使用变量 name 绑定起来
     # 也就是将 "name": "satori" 放到 local 名字空间中
     2 STORE_NAME               0 (name)
     
     # 注意这一步也是 LOAD_CONST,但它加载的是 PyCodeObject 对象
     # 所以 PyCodeObject 对象本质上也是一个常量
3    4 LOAD_CONST               1 (<code object foo at 0x7fb...>)
     # 加载符号 "foo"
     6 LOAD_CONST               2 (&#39;foo&#39;)
     # 将符号 "foo" 和 PyCodeObject 对象从运行时栈弹出
     # 然后创建 PyFunctionObject 对象,并压入运行时栈
     8 MAKE_FUNCTION            0
     # 将上一步创建的函数对象从运行时栈弹出,并用变量 foo 与之绑定起来
     # 后续通过 foo() 即可发起函数调用
    10 STORE_NAME               1 (foo)

     # 函数创建完了,我们调用函数
     # 通过 LOAD_NAME 将 foo 对应的函数对象(指针)压入运行时栈
6   12 LOAD_NAME                1 (foo)
     # 将整数常量(参数)压入运行时栈
    14 LOAD_CONST               3 (1)
    16 LOAD_CONST               4 (2)
     # 将栈里面的参数和函数弹出,发起调用,并将调用的结果(返回值)压入运行时栈
    18 CALL_FUNCTION            2
     # 从栈顶弹出返回值,然后丢弃,因为我们没有用变量接收返回值
     # 如果我们用变量接收了,那么这里的指令就会从 POP_TOP 变成 STORE_NAME
    20 POP_TOP
     # return None
    22 LOAD_CONST               5 (None)
    24 RETURN_VALUE

     # 以上是模块对应的字节码指令,下面是函数 foo 的字节码指令
   Disassembly of <code object foo at 0x7fb......>:
     # 从局部作用域中加载内置变量 print
4    0 LOAD_GLOBAL              0 (print)
     # 从局部作用域中加载局部变量 a
     2 LOAD_FAST                0 (a)
     # 从局部作用域中加载局部变量 b
     4 LOAD_FAST                1 (b)
     # 从运行时栈中将参数和函数依次弹出,发起调用,也就是 print(a, b)
     6 CALL_FUNCTION            2
     # 从栈顶弹出返回值,然后丢弃,因为我们没有接收 print 的返回值
     8 POP_TOP
     # return 123
    10 LOAD_CONST               1 (123)
    12 RETURN_VALUE
로그인 후 복사

上面有一个有趣的现象,就是源代码的行号。之前看到源代码的行号都是从上往下、依次增大的,这很好理解,毕竟一条一条解释嘛。但是这里却发生了变化,先执行了第 6 行,之后再执行第 4 行。

如果是从 Python 层面的函数调用来理解的话,很容易一句话就解释了,因为函数只有在调用的时候才会执行,而调用肯定发生在创建之后。但是从字节码的角度来理解的话,我们发现函数的声明和实现是分离的,是在不同的 PyCodeObject 对象中。

确实如此,虽然函数名和函数体是一个整体,但是虚拟机在实现的时候,却在物理上将它们分离开了。

正所谓函数即变量,我们可以把函数当成普通的变量来处理。函数名就是变量名,它位于模块对应的 PyCodeObject 的符号表中;函数体就是变量指向的值,它是基于一个独立的 PyCodeObject 构建的。

换句话说,在编译时,函数体里面的代码会位于一个新的 PyCodeObject 对象当中,所以函数的声明和实现是分离的。

至此,函数的结构就已经非常清晰了。

Python 함수의 구현 원리는 무엇입니까

所以函数名和函数体是分离的,它们存储在不同的 PyCodeObject 对象当中。

分析完结构之后,重点就要落在 MAKE_FUNCTION 指令上了,我们说当遇到 def foo(a, b) 的时候,就知道要创建函数了。在语法上这是函数的声明语句,但从虚拟机的角度来看这其实是函数对象的创建语句。

所以下面我们就要分析一下这个指令,看看它到底是怎么将一个 PyCodeObject 对象变成一个 PyFunctionObject 对象的。

case TARGET(MAKE_FUNCTION): {
    // 弹出压入运行时栈的函数名
    PyObject *qualname = POP(); 
    // 弹出对应的 PyCodeObject 对象
    PyObject *codeobj = POP();  
    // 创建 PyFunctionObject 对象,需要三个参数
    // 分别是 PyCodeObject 对象、global 名字空间、函数的全限定名
    // 我们看到创建函数的时候将 global 名字空间传递了进去
    // 所以现在我们应该明白为什么函数可以调用 __globals__ 了
    // 当然也明白为什么函数在局部变量找不到的时候可以去找全局变量了
    PyFunctionObject *func = (PyFunctionObject *)
        PyFunction_NewWithQualName(codeobj, f->f_globals, qualname);
    
    // 减少引用计数
    // 如果函数创建失败会返回 NULL,跳转至 error
    Py_DECREF(codeobj);
    Py_DECREF(qualname);
    if (func == NULL) {
        goto error;
    }
    
    // 编译时能够静态检测出函数有没有设置闭包、类型注解等属性
    // 比如设置了闭包,那么 oparg & 0x08 为真
    // 设置了类型注解,那么 oparg & 0x04 为真
    // 如果条件为真,那么进行相关属性设置
    if (oparg & 0x08) {
        assert(PyTuple_CheckExact(TOP()));
        func ->func_closure = POP();
    }
    if (oparg & 0x04) {
        assert(PyDict_CheckExact(TOP()));
        func->func_annotations = POP();
    }
    if (oparg & 0x02) {
        assert(PyDict_CheckExact(TOP()));
        func->func_kwdefaults = POP();
    }
    if (oparg & 0x01) {
        assert(PyTuple_CheckExact(TOP()));
        func->func_defaults = POP();
    }

    // 将创建好的函数对象的指针压入运行时栈
    // 下一个指令 STORE_NAME 会将它从运行时栈弹出
    // 并用变量 foo 和它绑定起来,放入 local 空间中
    PUSH((PyObject *)func);
    DISPATCH();
}
로그인 후 복사

整个步骤很好理解,先通过 LOAD_CONST 将 PyCodeObject 对象和符号 foo 压入栈中。然后执行 MAKE_FUNCTION 的时候,将两者从栈中弹出,再加上当前栈帧对象中维护的 global 名字空间,三者作为参数传入 PyFunction_NewWithQualName 函数中,从而构建出相应的函数对象。

上面的函数比较简单,如果再加上类型注解、以及默认值,会有什么效果呢?

s = """
name = "satori"
def foo(a: int = 1, b: int = 2):
    print(a, b)

foo(1, 2)
"""

import dis
dis.dis(compile(s, "func", "exec"))
로그인 후 복사

这里我们加上了类型注解和默认值,看看它的字节码指令会有什么变化?

0 LOAD_CONST 0 ('satori')
2 STORE_NAME 0 (name)

4 LOAD_CONST 7 ((1, 2))
6 LOAD_NAME 1 (int)
8 LOAD_NAME 1 (int)
10 LOAD_CONST 3 (('a', 'b'))
12 BUILD_CONST_KEY_MAP 2
14 LOAD_CONST 4 ()
16 LOAD_CONST 5 ('foo')
18 MAKE_FUNCTION 5 (defaults, annotations)
......
......

不难发现,在构建函数时会先将默认值以元组的形式压入运行时栈;然后再根据使用了类型注解的参数和类型构建一个字典,并将这个字典压入运行时栈。

后续创建函数的时候,会将默认值保存在 func_defaults 成员中,类型注解对应的字典会保存在 func_annotations 成员中。

def foo(a: int = 1, b: int = 2):
    print(a, b)

print(foo.__defaults__)  
print(foo.__annotations__)
# (1, 2)
# {&#39;a&#39;: <class &#39;int&#39;>, &#39;b&#39;: <class &#39;int&#39;>}
로그인 후 복사

基于类型注解和描述符,我们便可以像静态语言一样,实现函数参数的类型约束。介绍完描述符之后,我们会举例说明。

函数的一些骚操作

我们通过一些骚操作,来更好地理解一下函数。

之前说 是函数的类型对象,而这个类底层没有暴露给我们,但是可以通过曲线救国的方式进行获取。

def f():
    pass

print(type(f))  # <class &#39;function&#39;>
# lambda匿名函数的类型也是 function
print(type(lambda: None))  # <class &#39;function&#39;>
로그인 후 복사

那么下面就来创建函数:

gender = "female"

def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"

# 得到PyCodeObject对象
code = f.__code__
# 根据类function创建函数对象
# 接收三个参数: PyCodeObject对象、名字空间、函数名
new_f = type(f)(code, globals(), "根据 f 创建的 new_f")

# 打印函数名
print(new_f.__name__)  # 根据 f 创建的 new_f

# 调用函数
print(
    new_f("古明地觉", 16)
)  # name: 古明地觉, age: 16, gender: female
로그인 후 복사

是不是很神奇呢?另外我们说函数在访问变量时,显然先从自身的符号表中查找,如果没有再去找全局变量。这是因为,我们在创建函数的时候将 global 名字空间传进去了,如果我们不传递呢?

gender = "female"

def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"

code = f.__code__
try:
    new_f = type(f)(code, None, "根据 f 创建的 new_f")
except TypeError as e:
    print(e)  
    """
    function() argument &#39;globals&#39; must be dict, not None
    """
# 这里告诉我们 function 的第二个参数 globals 必须是一个字典
# 我们传递一个空字典
new_f1 = type(f)(code, {}, "根据 f 创建的 new_f1")
# 打印函数名
print(new_f1.__name__)  # 根据 f 创建的 new_f1

# 调用函数
try:
    print(new_f1("古明地觉", 16))
except NameError as e:
    print(e)  
    """
    name &#39;gender&#39; is not defined
    """

# 我们看到提示 gender 没有定义
로그인 후 복사

因此现在我们又从 Python 的角度理解了一遍,为什么函数能够在局部变量找不到的时候,去找全局变量。原因就在于构建函数的时候,将 global 名字空间交给了函数,使得函数可以在 global 空间进行变量查找,所以它才能够找到全局变量。而我们这里给了一个空字典,那么显然就找不到 gender 这个变量了。

gender = "female"

def f(name, age):
    return f"name: {name}, age: {age}, gender: {gender}"

code = f.__code__
new_f = type(f)(code, {"gender": "少女觉"}, "根据 f 创建的 new_f")

# 我们可以手动传递一个字典进去
# 此时我们传递的字典对于函数来说就是 global 名字空间
# 所以在函数内部找不到某个变量的时候, 就会去我们指定的名字空间中查找
print(new_f("古明地觉", 16)) 
"""
name: 古明地觉, age: 16, gender: 少女觉
"""
# 所以此时的 gender 不再是外部的 "female"
# 而是我们指定的 "少女觉"
로그인 후 복사

此外我们还可以为函数指定默认值:

def f(name, age, gender):
    return f"name: {name}, age: {age}, gender: {gender}"

# 必须接收一个PyTupleObject对象
f.__defaults__ = ("古明地觉", 16, "female")
print(f())
"""
name: 古明地觉, age: 16, gender: female
"""
로그인 후 복사

我们看到函数 f 明明接收三个参数,但是调用时不传递居然也不会报错,原因就在于我们指定了默认值。而默认值可以在定义函数的时候指定,也可以通过 __defaults__ 指定,但很明显我们应该通过前者来指定。

如果你用的是 pycharm,那么会在 f() 这个位置给你飘黄,提示你参数没有传递。但我们知道,由于使用 __defaults__ 已经设置了默认值,所以这里是不会报错的。只不过 pycharm 没有检测到,当然基本上所有的 IDE 都无法做到这一点,毕竟动态语言。

另外 __defaults__ 接收的元组里面的元素个数和参数个数不匹配怎么办?

def f(name, age, gender):
    return f"name: {name}, age: {age}, gender: {gender}"

f.__defaults__ = (15, "female")
print(f("古明地恋"))
"""
name: 古明地恋, age: 15, gender: female
"""
로그인 후 복사

由于元组里面只有两个元素,意味着我们在调用时需要至少传递一个参数,而这个参数会赋值给 name。原因就是在设置默认值的时候是从后往前设置的,也就是 "female" 会给赋值给 gender,15 会赋值给 age。而 name 没有得到默认值,那么它就需要调用者显式传递了。

为啥 Python 在设置默认值是从后往前设置呢?如果从前往后设置的话,会出现什么后果呢?显然此时 15 会赋值给 name,"female" 会赋值给 age,那么函数就等价于如下:

def f(name=15, age="female", gender):
    return f"name: {name}, age: {age}, gender: {gender}"
로그인 후 복사

这样的函数能够通过编译吗?显然是不行的,因为默认参数必须在非默认参数的后面。所以 Python 的这个做法是完全正确的,必须要从后往前进行设置。

另外我们知道默认值的个数是小于等于参数个数的,如果大于会怎么样呢?

def f(name, age, gender):
    return f"name: {name}, age: {age}, gender: {gender}"

f.__defaults__ = ("古明地觉", "古明地恋", 15, "female")
print(f())
"""
name: 古明地恋, age: 15, gender: female
"""
로그인 후 복사

依旧从后往前进行设置,当所有参数都有默认值了,那么就结束了。当然,如果不使用 __defaults__,是不可能出现默认值个数大于参数个数的。

可要是 __defaults__ 指向的元组先结束,那么没有得到默认值的参数就必须由我们来传递了。

最后再来说一下如何深拷贝一个函数。首先如果是你的话,你会怎么拷贝一个函数呢?不出意外的话,你应该会使用 copy 模块。

import copy

def f(a, b):
    return [a, b]

# 但是问题来了,这样能否实现深度拷贝呢?
new_f = copy.deepcopy(f)
f.__defaults__ = (2, 3)
print(new_f())  # [2, 3]
로그인 후 복사

修改 f 的 __defaults__,会对 new_f 产生影响,因此我们并没有实现函数的深度拷贝。事实上,copy 模块无法对函数、方法、回溯栈、栈帧、模块、文件、套接字等类型的实例实现深度拷贝。

那我们应该怎么做呢?

from types import FunctionType

def f(a, b):
    return "result"

# FunctionType 就是函数的类型对象
# 它也是通过 type 得到的
new_f = FunctionType(f.__code__,
                     f.__globals__,
                     f.__name__,
                     f.__defaults__,
                     f.__closure__)
# 显然 function 还可以接收第四个参数和第五个参数
# 分别是函数的默认值和闭包

# 然后别忘记将属性字典也拷贝一份
# 由于函数的属性字典几乎用不上,这里就浅拷贝了
new_f.__dict__.update(f.__dict__)

f.__defaults__ = (2, 3)
print(f.__defaults__)  # (2, 3)
print(new_f.__defaults__)  # None
로그인 후 복사

此时修改 f 不会影响 new_f,当然在拷贝的时候也可以自定义属性。

其实上面实现的深拷贝,本质上就是定义了一个新的函数。由于是两个不同的函数,那么自然就没有联系了。

判断函数都有哪些参数

再来看看如何检测一个函数有哪些参数,首先函数的局部变量(包括参数)在编译时就已经确定,会存在符号表 co_varnames 中。

def f(a, b, /, c, d, *args, e, f, **kwargs):
    g = 1
    h = 2

varnames = f.__code__.co_varnames
print(varnames)
"""
(&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;, &#39;e&#39;, &#39;f&#39;, &#39;args&#39;, &#39;kwargs&#39;, &#39;g&#39;, &#39;h&#39;)
"""
로그인 후 복사

注意:在定义函数的时候,* ** 最多只能出现一次。

显然 a 和 b 必须通过位置参数传递,c 和 d 可以通过位置参数和关键字参数传递,e 和 f 必须通过关键字参数传递。

而从打印的符号表来看,里面的符号是有顺序的。参数永远处于函数内部定义的局部变量的前面,比如 g 和 h 就是函数内部定义的局部变量,所以它在所有参数的后面。

而对于参数,* ** 会位于最后面,其它参数位置不变。所以除了 g 和 h,最后面的就是 args 和 kwargs。

那么接下来我们就可以进行检测了。

def f(a, b, /, c, d, *args, e, f, **kwargs):
    g = 1
    h = 2

varnames = f.__code__.co_varnames

# 1. 寻找必须通过位置参数传递的参数
posonlyargcount = f.__code__.co_posonlyargcount
print(posonlyargcount)  # 2
print(varnames[: posonlyargcount])  # (&#39;a&#39;, &#39;b&#39;)

# 2. 寻找既可以通过位置参数传递、又可以通过关键字参数传递的参数
argcount = f.__code__.co_argcount
print(argcount)  # 4
print(varnames[: 4])  # (&#39;a&#39;, &#39;b&#39;, &#39;c&#39;, &#39;d&#39;)
print(varnames[posonlyargcount: 4])  # (&#39;c&#39;, &#39;d&#39;)

# 3. 寻找必须通过关键字参数传递的参数
kwonlyargcount = f.__code__.co_kwonlyargcount
print(kwonlyargcount)  # 2
print(varnames[argcount: argcount + kwonlyargcount])  # (&#39;e&#39;, &#39;f&#39;)

# 4. 寻找 *args 和 **kwargs
flags = f.__code__.co_flags
# 在介绍 PyCodeObject 的时候,我们说里面有一个 co_flags 成员
# 它是函数的标识,可以对函数类型和参数进行检测
# 如果co_flags和 4 进行按位与之后为真,那么就代表有* args, 否则没有
# 如果co_flags和 8 进行按位与之后为真,那么就代表有 **kwargs, 否则没有
step = argcount + kwonlyargcount
if flags & 0x04:
    print(varnames[step])  # args
    step += 1

if flags & 0x08:
    print(varnames[step])  # kwargs
로그인 후 복사

以上我们检测出了函数都有哪些参数,你也可以将其封装成一个函数,实现代码的复用。

然后需要注意一下 args 和 kwargs,打印的内容主要取决定义时使用的名字。如果定义的时候是 *ARGS 和 **KWARGS,那么这里就会打印 ARGS 和 KWARGS,只不过一般我们都叫做 *args 和 **kwargs。

但如果我们定义的时候不是 *args,只是一个 *,那么它就不是参数了。

def f(a, b, *, c):
    pass
    
# 我们看到此时只有a、b、c
print(f.__code__.co_varnames)  # (&#39;a&#39;, &#39;b&#39;, &#39;c&#39;)

print(f.__code__.co_flags & 0x04)  # 0
print(f.__code__.co_flags & 0x08)  # 0
# 显然此时也都为假
로그인 후 복사

单独的一个 * 只是为了强制要求后面的参数必须通过关键字参数的方式传递。

函数是怎么调用的

到目前为止,我们聊了聊 Python 函数的底层实现,并且还演示了如何通过函数的类型对象自定义一个函数,以及如何获取函数的参数。虽然这在工作中没有太大意义,但是可以让我们深刻理解函数的行为。

下面我来探讨一下函数在底层是怎么调用的,但是在介绍调用之前,我们需要补充一个知识点。

def foo():
    pass

print(type(foo))  
print(type(sum))  
"""
<class &#39;function&#39;>
<class &#39;builtin_function_or_method&#39;>
"""
로그인 후 복사

函数实际上分为两种:

  • 如果是 Python 实现的函数,底层会对应 PyFunctionObject。其类型在 Python 里面是 ,在底层是 PyFunction_Type;

  • 如果是 C 实现的函数,底层会对应 PyCFunctionObject。其类型在 Python 里面是 ,在底层是 PyCFunction_Type;

像内置函数、使用 C 扩展编写的函数,它们都是 PyCFunctionObject。

Python 함수의 구현 원리는 무엇입니까

另外从名字上可以看出 PyCFunctionObject 不仅用于 C 实现的函数,还用于方法。关于方法,我们后续在介绍类的时候细说,这里暂时不做深入讨论。

总之对于 Python 函数和 C 函数,底层在实现的时候将两者分开了,因为 C 函数可以有更快的执行方式。

注意这里说的 C 函数,指的是 C 实现的 Python 函数。像内置函数就是 C 实现的,比如 sum、getattr 等等。

好了,下面来看函数调用的具体细节。

s = """
def foo():
    a, b = 1, 2
    return a + b

foo()
"""

if __name__ == &#39;__main__&#39;:
    import dis
    dis.dis(compile(s, "<...>", "exec"))
로그인 후 복사

还是以一个简单的函数为例,看看它的字节码:

 # 遇见 def 表示构建函数
 # 于是加载 PyCodeObject 对象和函数名 "foo"
 0 LOAD_CONST               0 (<code object foo at 0x7f...>)
 2 LOAD_CONST               1 (&#39;foo&#39;)
 # 构建函数对象,压入运行时栈
 4 MAKE_FUNCTION            0
 # 从栈中弹出函数对象,用变量 foo 保存
 6 STORE_NAME               0 (foo)
 # 将变量 foo 压入运行时栈
 8 LOAD_NAME                0 (foo)
 # 从栈中弹出 foo,执行 foo(),也就是函数调用,这一会要剖析的重点
10 CALL_FUNCTION            0
 # 从栈顶弹出返回值
12 POP_TOP
 # return None
14 LOAD_CONST               2 (None)
16 RETURN_VALUE

Disassembly of <code object foo at 0x7...>:
 # 函数的字节码,因为模块和函数都会对应 PyCodeObject
 # 只不过后者在前者的常量池中
 
 # 加载元组常量 (1, 2)
 0 LOAD_CONST               1 ((1, 2))
 # 解包,将常量压入运行时栈
 2 UNPACK_SEQUENCE          2
 # 再从栈中弹出,分别赋值给 a 和 b 
 4 STORE_FAST               0 (a)
 6 STORE_FAST               1 (b)
 # 加载 a 和 b
 8 LOAD_FAST                0 (a)
10 LOAD_FAST                1 (b)
 # 执行加法运算
12 BINARY_ADD
 # 将相加之和的值返回
14 RETURN_VALUE
로그인 후 복사

相信现在看字节码已经不是什么问题了,然后我们看到调用函数用的是 CALL_FUNCTION 指令,那么这个指令都做了哪些事情呢?

case TARGET(CALL_FUNCTION): {
    PREDICTED(CALL_FUNCTION);
    PyObject **sp, *res;
    // 指向运行时栈的栈顶
    sp = stack_pointer;
    // 调用函数,将返回值赋值给 res
    // tstate 表示线程状态对象
    // &sp 是一个三级指针,oparg 表示指令的操作数
    res = call_function(tstate, &sp, oparg, NULL);
    // 函数执行完毕之后,sp 会指向运行时栈的栈顶
    // 所以再将修改之后的 sp 赋值给 stack_pointer
    stack_pointer = sp;
    // 将 res 压入栈中:*stack_pointer++ = res
    PUSH(res);
    if (res == NULL) {
        goto error;
    }
    DISPATCH();
}
로그인 후 복사

CALL_FUNCTION 这个指令之前提到过,但是函数的核心执行流程是在 call_function 里面,它位于 ceval.c 中,我们来看一下。

Python 함수의 구현 원리는 무엇입니까

Python 함수의 구현 원리는 무엇입니까

Python 함수의 구현 원리는 무엇입니까

因此接下来重点就在 _PyObject_Vectorcall 函数上面,在该函数内部又会调用其它函数,最终会走到 _PyFunction_FastCallDict 这里。

//Objects/call.c
PyObject *
_PyFunction_FastCallDict(PyObject *func, PyObject *const *args, Py_ssize_t nargs,
                         PyObject *kwargs)
{   
    //获取PyCodeObject对象
    PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func); 
    //获取global名字空间
    PyObject *globals = PyFunction_GET_GLOBALS(func);
    //获取默认值
    PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
    //....
      
    //我们观察一下下面的return
    //一个是function_code_fastcall,一个是最后的_PyEval_EvalCodeWithName
    //从名字上能看出来function_code_fastcall是一个快分支
    //但是这个快分支要求函数调用时不能传递关键字参数
    if (co->co_kwonlyargcount == 0 &&
        (kwargs == NULL || PyDict_GET_SIZE(kwargs) == 0) &&
        (co->co_flags & ~PyCF_MASK) == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
    {
        /* Fast paths */
        if (argdefs == NULL && co->co_argcount == nargs) {
            //function_code_fastcall里面逻辑很简单
            //直接抽走当前PyFunctionObject里面PyCodeObject和global名字空间
            //根据PyCodeObject对象直接为其创建一个PyFrameObject对象
            //然后PyEval_EvalFrameEx执行栈帧
            //也就是真正的进入了函数调用,执行函数里面的代码
            return function_code_fastcall(co, args, nargs, globals);
        }
        else if (nargs == 0 && argdefs != NULL
                 && co->co_argcount == PyTuple_GET_SIZE(argdefs)) {
            /* function called with no arguments, but all parameters have
               a default value: use default values as arguments .*/
            args = _PyTuple_ITEMS(argdefs);
            return function_code_fastcall(co, args, PyTuple_GET_SIZE(argdefs),
                                          globals);
        }
    }
  
    //适用于有关键字参数的情况
    nk = (kwargs != NULL) ? PyDict_GET_SIZE(kwargs) : 0;
    //.....
    //调用_PyEval_EvalCodeWithName
    result = _PyEval_EvalCodeWithName((PyObject*)co, globals, (PyObject *)NULL,
                                      args, nargs,
                                      k, k != NULL ? k + 1 : NULL, nk, 2,
                                      d, nd, kwdefs,
                                      closure, name, qualname);
    Py_XDECREF(kwtuple);
    return result;
}
로그인 후 복사

所以函数调用时会有两种方式:

Python 함수의 구현 원리는 무엇입니까

因此我们看到,总共有两条途径,分别针对有无关键字参数。但是最终殊途同归,都会走到 PyEval_EvalFrameEx 那里,然后虚拟机在新的栈帧中执行新的 PyCodeObject。

不过可能有人会好奇,我们之前说过:

  • PyFrameObject 是根据 PyCodeObject 创建的

  • PyFunctionObject 也是根据 PyCodeObject 创建的

那么 PyFrameObject 和 PyFunctionObject 之间有啥关系呢?

如果把 PyCodeObject 比喻成妹子,那么 PyFunctionObject 就是妹子的备胎,PyFrameObject 就是妹子的心上人。

其实在栈帧中执行指令时候,PyFunctionObject 的影响就已经消失了,真正对栈帧产生影响的是PyFunctionObject 里面的 PyCodeObject 对象和 global 名字空间。

也就是说,最终是 PyFrameObject 和 PyCodeObject 两者如胶似漆,跟 PyFunctionObject 之间没有关系,所以 PyFunctionObject 辛苦一场,实际上是为别人做了嫁衣。PyFunctionObject 主要是对 PyCodeObject 和 global 名字空间的一种打包和运输方式。

위 내용은 Python 함수의 구현 원리는 무엇입니까의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:yisu.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿