深度解密函数的 local 名字空间(当 Python 中混进一只薛定谔的猫……)

简介: 深度解密函数的 local 名字空间(当 Python 中混进一只薛定谔的猫……)

楔子



上一篇文章我们探究了 Python 函数的实现原理,本次来聊一聊它的局部作用域与名字空间。

我们知道函数的参数和函数内部定义的变量都属于局部变量,均是通过静态的方式访问的。

x = 123
def foo1():
    global x
    a = 1
    b = 2
# co_nlocals 会返回局部变量的个数
# a 和 b 是局部变量,x 是全局变量,因此是 2
print(foo1.__code__.co_nlocals)  # 2
def foo2(a, b):
    pass
print(foo2.__code__.co_nlocals)  # 2
def foo3(a, b):
    a = 1
    b = 2
    c = 3
print(foo3.__code__.co_nlocals)  # 3

无论是参数还是内部新创建的变量,本质上都是局部变量。

按照之前的理解,当访问一个全局变量时,会去访问 global 名字空间(也叫全局名字空间),而这也确实如此。

a73158099325856a902fab4f929ca1c1.png

那么问题来了,当操作函数的局部变量时,是不是也等价于操作其内部的 local 名字空间(也叫局部名字空间)呢?


local 名字空间与局部变量



之前我们说过 Python 变量的访问是有规则的,按照本地闭包全局内置的顺序去查找,也就是 LEGB 规则,所以当然会首当其冲去 local 名字空间里面查找啊。

但不幸的是,在调用函数期间,虚拟机创建栈帧对象时,这个至关重要的 local 名字空间并没有被创建。

//frameobject.c
PyFrameObject* _Py_HOT_FUNCTION
_PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
                     PyObject *globals, PyObject *locals)
{
    //...
        f->f_locals = NULL;
        f->f_trace = NULL;
    //...    
}

局部名字空间由栈帧的 f_locals 属性负责维护,全局名字空间由 f_globals 属性维护。对于模块而言,它的 f_locals 和 f_globals 指向的是同一个字典。

# 通过 locals() 可以拿到局部名字空间
# 通过 globals() 可以拿到全局名字空间
# 模块的局部名字空间和全局名字空间是同一个字典
print(locals() is globals())  # True

但对于函数而言,f_locals 却是 NULL,它都没有局部名字空间。那么问题来了,局部变量到底存储在什么地方呢?我们先来举个例子。

def foo(a, b):
    c = a + b
    print(c)

它的字节码如下:

5c9cf3aa9016622a84f295a275a44723.png

在介绍虚拟机执行字节码的时候我们说过,当函数被调用时,虚拟机会为其创建一个栈帧。栈帧是函数的执行环境,它包含了函数执行时所依赖的全部的上下文,而栈帧内部有一个成员叫 f_localsplus,它是一个数组。

这个数组虽然是一段连续内存,但在逻辑上它被分成了 4 块。

a1496a891248ccf7d20da7f5583afe83.png

其中局部变量便存储在 f_localsplus 的第一份空间中。

而我们看到字节码偏移量为 6 和 10 的两条指令分别是:STORE_FAST 和 LOAD_FAST,这两个指令干的事情如下。

case TARGET(LOAD_FAST): {
    // 基于 GETLOCAL 获取局部变量的值
    PyObject *value = GETLOCAL(oparg);
    //...
    Py_INCREF(value);
    PUSH(value);
    FAST_DISPATCH();
}
case TARGET(STORE_FAST): {
    PREDICTED(STORE_FAST);
    PyObject *value = POP();
    // 基于 SETLOCAL 创建局部变量
    SETLOCAL(oparg, value);
    FAST_DISPATCH();
}
  • LOAD_FAST:基于宏 GETLOCAL 获取一个局部变量;
  • STORE_FAST:基于宏 STORE_FAST 创建一个局部变量;

那么这两个宏具体是怎么做的呢?

b17fd8736f5a522780e75acb161ff49c.png

fastlocals 就是栈帧的 f_localsplus,而函数在编译的时候就知道局部变量的值在 f_localsplus 中的索引,所以通过 GETLOCAL 获取即可。同理 SETLOCAL 则是创建一个局部变量。

所以此时我们对局部变量的藏身之处已经了然于心,它们就存放在栈帧的 f_localsplus 成员中,而之所以没有使用 local 名字空间的原因也很简单。因为函数内部的局部变量有多少,在编译的时候就已经确定了,个数是不会变的。因此编译时就能确定局部变量占用的内存大小,也能确定访问局部变量的字节码指令应该如何访问内存。

def foo(a, b):
    c = a + b
    print(c)
print(
    foo.__code__.co_varnames
)  # ('a', 'b', 'c')

比如符号 c 位于符号表中索引为 2 的位置(编译时就已确定),那么运行时通过 f_localsplus[2] 即可拿到变量 c 对应的值。

这个过程是基于数组索引实现的静态查找,它的效率非常高。而 local 空间是一个字典,虽然字典也是经过高度优化的,但肯定没有静态查找快。

因此,尽管虚拟机为函数实现了 local 空间(初始为 NULL),但是在操作局部变量时却没有使用它,原因就是为了更高的效率。

结论:虽然变量查找的时候是遵循 LEGB 规则,但其实局部变量是静态访问的,不过完全可以按照 LEGB 的方式来理解。


获取 local 名字空间



然后我们再来从 Python 的层面演示一下:

x = 1
def foo():
    globals()["x"] = 2
    
foo()
print(x)  # 2

global 空间全局唯一,在 Python 层面上就是一个字典,可以通过 globals() 拿到。在任何地方操作该字典,都相当于操作全局变量,即使是在函数内部。

因此在执行完 foo() 之后,全局变量 x 就被修改了。但 local 名字空间也是如此吗?我们来看看,通过 locals() 可以拿到 local 名字空间。

def foo():
    x = 1
    locals()["x"] = 2
    print(x)
foo()  # 1

我们按照相同的套路,却并没有成功,这是为什么?原因就是我们刚才解释的那样,函数内部有哪些局部变量在编译时就已经确定好了,查询的时候是从 f_localsplus 中静态查找的,而不是从 local 名字空间中查找。

那么 locals() 会打印出来什么东西呢?

def foo():
    name = "satori"
    print(locals())
    age = 17
    print(locals())
    gender = "female"
    print(locals())
foo()
"""
{'name': 'satori'}
{'name': 'satori', 'age': 17}
{'name': 'satori', 'age': 17, 'gender': 'female'}
"""

我们看到打印 locals() 居然也会显示内部的局部变量,相信聪明如你已经猜到 locals() 是怎么回事了。局部变量不是从局部名字空间里面查找的,所以它初始为空,但当我们执行 locals() 的时候,会动态构建一个字典出来。

首先符号表里面存储了局部变量的符号(或者说名字),f_localsplus 里面存储了局部变量的值,当执行 locals() 的时候,会基于符号表和 f_localsplus 创建一个字典出来。

def foo():
    name = "satori"
    age = 17
    gender = "female"
    print(locals())
# 符号表:保存了函数中创建的局部变量的名字
print(foo.__code__.co_varnames)
"""
('name', 'age', 'gender')
"""
# 调用函数时会创建栈帧,局部变量的值都保存在 f_localsplus 里面
# 并且符号表中变量名的顺序和 f_localsplus 中变量值的顺序是一致的
f_localsplus = ["satori", 17, "female"]
# 这里就用一个列表来模拟了

我们来看一下变量的创建,因为符号 name 位于符号表中索引为 0 的位置,那么执行 name = "satori" 时,就会将 "satori" 也放在 f_localsplus 中索引为 0 的位置。

至于变量 age 和 gender 也是同理,它们的值会放在 f_localsplus 中索引为 1 和 2 的位置。

后续在访问变量的时候,比如访问变量 age,由于它位于符号表中索引为 1 的位置,那么就会通过 f_localsplus[1] 获取它的值,这些符号对应的索引都是在编译阶段确定的。所以在运行时才能实现静态查找,指令 LOAD_FAST 和 STORE_FAST 都是基于索引来静态操作底层数组。

我们用一张图来描述这个过程:

b2d1bbcfbc44596cb066cfa1046af82c.png

符号表负责存储局部变量的名字,f_localsplus 负责存储局部变量的值(里面的元素初始的时候为 NULL),而在给局部变量赋值的时候,本质上就是将值写在了 f_localsplus 中。并且变量名在符号表中的索引,和变量值在 f_localsplus 中的索引是一致的,而操作局部变量本质上就是在操作 f_localsplus 数组。

至于 locals() 或者说局部名字空间,它是基于符号表和 f_localsplus 动态创建的。为了方便我们获取已存在的局部变量,因此在执行 locals() 的时候会临时创建一个字典。

所以我们通过 locals() 获取局部名字空间之后,访问里面的局部变量是可以的,只不过此时将静态操作变成了动态操作。

def foo():
    name = "satori"
    # 会从 f_localsplus 中静态查找
    print(name)
    # 先基于已有的变量和值创建一个字典
    # 然后通过字典实现变量的动态查找
    print(locals()["name"])
foo()
"""
satori
satori
"""

两种方式都是可以的,但基于 locals() 来访问,在效率上明显会低一些。

另外基于 locals() 访问一个变量是可以的,但无法创建一个变量。

def foo():
    name = "satori"
    locals()["age"] = 17
    try:
        print(age)
    except NameError as e:
        print(e)
foo()
"""
name 'age' is not defined
"""

局部变量是静态存储在数组里面的,locals() 只是做了一个拷贝而已。往局部名字空间里面添加一个键值对,不等于创建一个局部变量,因为局部变量不是从它这里查找的,因此代码中打印 age 报错了。但如果外部还有一个全局变量 age 的话,那么会打印全局变量 age。

然后再补充一点,我们说全局名字空间在任何地方都是唯一的,而对于函数而言,它内部的局部名字空间也是唯一的。不断调用 locals 多少次,拿到的都是同一个字典。

def foo():
    name = "satori"
    # 执行 locals() 的时候,内部只有一个键值对
    d = locals()
    print(d)  # {'name': 'satori'}
    # 再次获取,此时有两个键值对
    print(
        locals()
    )  # {'name': 'satori', 'd': {...}}
    
    # 但两者的 id 相同,因为一个函数的局部名字空间只有一个
    # 不管调用多少次 locals(),拿到的都是同一个字典
    print(id(d) == id(locals()))  # True
foo()

所以 locals() 和 globals() 都是唯一的,只不过 locals() 是在某个函数内唯一,而 globals() 在所有地方都唯一。

因此局部名字空间初始为 NULL,但在第一次执行 locals() 时,会以符号表中的符号作为 key,f_localsplus 中的值作为 value,创建一个字典作为函数的局部名字空间。而后续再执行 locals() 的时候,由于名字空间已存在,就不会再次创建了,直接基于当前的局部变量对字典进行更新即可。

def foo():
    # 创建一个字典,由于当前还没有定义局部变量,因此是空字典
    print(locals())
    """
    {}
    """
    # 往局部名字空间添加一个键值对
    locals()["a"] = "b"
    print(locals())
    """
    {'a': 'b'}
    """
    # 定义一个局部变量
    name = "satori"
    # 由于局部名字空间已存在,因此不会再次创建
    # 将局部变量的名字作为 key、值作为 value,拷贝到字典中
    print(locals())
    """
    {'a': 'b', 'name': 'satori'}
    """
foo()

注意:虽然局部名字空间里面存在 "a" 这个 key,但 a 这个局部变量是不存在的。


local 名字空间的创建过程



我们已经知道 local 名字空间是怎么创建的了,也熟悉了它的特性,下面通过源码来看一下它的构建过程。

int
PyFrame_FastToLocalsWithError(PyFrameObject *f)
{
    PyObject *locals, *map;
    PyObject **fast;
    PyCodeObject *co;
    Py_ssize_t j;
    Py_ssize_t ncells, nfreevars;
    // 获取局部名字空间
    locals = f->f_locals;
    // 如果为 NULL,那么创建一个新字典,作为名字空间
    // 所以局部名字空间只会创建一次,后续不会再创建
    if (locals == NULL) {
        locals = f->f_locals = PyDict_New();
        if (locals == NULL)
            return -1;
    }
    // 获取 PyCodeObject 对象,然后拿到内部的符号表
    // 符号表(co_varnames)里面保存了函数局部变量的名字
    co = f->f_code;
    map = co->co_varnames;
    // 获取 f_localsplus,它里面保存了局部变量的值
    // 只不过除了局部变量的值之外,还保存了其它的
    fast = f->f_localsplus;
    // 那么 f_localsplus 里面到底有多少个局部变量的值呢?
    // 显然这要基于符号表来判断,co_varnames 里面保存了多少符号
    // f_localsplus 里面就保存了多少个局部变量的值
    j = PyTuple_GET_SIZE(map);
    if (j > co->co_nlocals)
        j = co->co_nlocals;
    // 将 co_varnames 和 f_localsplus 的元素组成键值对,添加到局部名字空间中
    // 相当于 locals.update(zip(co_varnames, f_localsplus))
    if (co->co_nlocals) {
        if (map_to_dict(map, j, locals, fast, 0) < 0)
            return -1;
    }
    // 如果里面有闭包变量的话,也会添加到局部名字空间当中
    ncells = PyTuple_GET_SIZE(co->co_cellvars);
    nfreevars = PyTuple_GET_SIZE(co->co_freevars);
    if (ncells || nfreevars) {
        if (map_to_dict(co->co_cellvars, ncells,
                        locals, fast + co->co_nlocals, 1))
            return -1;
        // ...
    }
    return 0;
}

代码有略微的删减,可以看到源码的实现逻辑和我们之前分析的是一样的。

  • 变量 map 是符号表 co_varnames,保存了局部变量的名字;
  • 变量 fast 是 f_localsplus,保存了局部变量的值
  • 变量 j 是局部变量的个数
  • 变量 locals 是局部名字空间

然后将它们作为参数,传递给 map_to_dict 函数进行调用。该函数内部会进行遍历,按照顺序将变量名和变量值依次添加到局部名字空间中。

但 map_to_dict 函数有一处细节非常之关键:

0130f8e96b4d13623c26062d3b7f1b3f.png

当变量值为 NULL 时,则说明在获取名字空间时,该变量还没有被赋值。要是此时变量已经在局部名字空间中,那么会将它从名字空间中删掉。这一处非常关键,在介绍 exec 的时候你就会明白。

以上就是 local 名字空间的获取过程在源码层面的体现。


local 名字空间与 exec 函数



我们再来搭配 exec 关键字,结果会更加明显。首先 exec 函数可以将一段字符串当成代码来执行,并将执行结果体现在当前的名字空间中。

def foo():
    print(locals())  # {}
    exec("x = 1")
    print(locals())  # {'x': 1}
    try:
        print(x)
    except NameError as e:
        print(e)  # name 'x' is not defined
        
foo()

尽管 locals() 变了,但是依旧访问不到 x,因为虚拟机并不知道 exec("x = 1") 是创建一个局部变量,它只知道这是一个函数调用。

事实上 exec 会作为一个独立的编译单元来执行,并且有自己的作用域。

所以 exec("x = 1") 执行完之后,效果就是改变了局部名字空间,里面多了一个 "x": 1 键值对。但关键的是,局部变量 x 不是从局部名字空间中查找的,exec 终究还是错付了人。

由于函数 foo 对应的 PyCodeObject 对象的符号表中并没有 x 这个符号,所以报错了。

补充:exec 默认影响的是 local 名字空间,如果在执行时发现 local 名字空间为 NULL,那么会自动创建一个。所以调用 exec 也可以创建名字空间(当它为 NULL 时)。

exec("x = 1")
print(x)  # 1

如果放在模块里面是可以的,因为模块的 local 名字空间和 global 名字空间指向同一个字典,所以 global 名字空间会多一个 key 为 "x" 的键值对。而全局变量是从 global 名字空间中查找的,所以这里没有问题。

def foo():
    # 此时 exec 影响的是全局名字空间
    exec("x = 123", globals())
    # 这里不会报错, 但此时的 x 不是局部变量, 而是全局变量
    print(x)
foo()
print(x)
"""
123
123
"""

可以给 exec 指定要影响的名字空间,代码中 exec 影响的是全局名字空间,打印的 x 也是全局变量。


变量名冲突的问题



我们说 exec 的执行效果会体现在 local 名字空间中,但是需要考虑变量名冲突的问题。举个例子

def foo():
    exec("x = 1")
    print(locals()["x"])
foo()
"""
1
"""
def bar():
    exec("x = 1")
    print(locals()["x"])
    x = 123
bar()
"""
Traceback (most recent call last):
  File .....
    bar()
  File .....
    print(locals()["x"])
KeyError: 'x'
"""

这是什么情况?函数 bar 只是多了一行赋值语句,为啥就报错了呢?要想搞懂这个问题,首先要明确两点:

1)函数的局部变量在编译的时候已经确定,并存储在对应的 PyCodeObject 对象的符号表 (co_varnames) 中,这是由语法规则所决定的;

2)函数内的局部变量在其整个作用域范围内都是可见的;

对于 foo 函数来说,exec 执行完之后相当于往 local 名字空间中添加一个键值对,这没有问题。对于 bar 函数而言也是如此,在执行完 exec("x = 1") 之后,local 名字空间也会存在 "x": 1 这个键值对,但我们下面执行 locals() 的时候又把字典更新了。

因为局部变量可以在函数的任意位置创建,或者修改,所以每一次执行 locals() 的时候,都会遍历符号表和 f_localsplus,组成键值对将原来的字典更新一遍。

在 bar 函数里面有一行 x  = 123,所以知道函数里面存在局部变量 x,符号表里面也会有 "x" 这个符号,这是在编译时就确定的。但我们是在 x = 123 之前调用的 locals,所以此时符号 x 在 f_localsplus 中对应的值还是一个 NULL,没有指向一个合法的 PyObject。换句话说就是,知道里面存在局部变量 x,但是还没有来得及赋值。

然后在更新名字空间的时候,如果发现值是个 NULL,那么就把名字空间中该变量对应的键值对给删掉。

8d30507d91d7a05baa0d3aa3edc43655.png

所以 bar 函数执行 locals()["x"] 的时候,会先获取名字空间,原本里面是有 "x": 1 这个键值对的。但因为赋值语句 x = 123 的存在,导致符号表里面存在 "x" 这个符号,但执行 locals() 的时候又尚未完成赋值,所以值为 NULL,于是又把这个键值对给删掉了。所以执行 locals()["x"] 的时候,出现了 KeyError。

因为局部名字空间体现的是局部变量的值,而调用 locals 的时候,局部变量 x 还没有被创建。所以 locals() 里面不应该存在 key 为 "x" 的键值对,于是会将它删除。

我们将名字空间打印一下:

def foo():
    # 创建局部名字空间,并写入键值对 "x": 1
    # 此时名字空间为 {"x": 1}
    exec("x = 1")
    # 获取名字空间,会进行更新
    # 但当前不存在局部变量,所以名字空间仍是 {"x": 1}
    print(locals())
def bar():
    # 创建局部名字空间,并写入键值对 "x": 1
    # 此时名字空间为 {"x": 1}
    exec("x = 1")
    # 获取名字空间,会进行更新
    # 由于里面存在局部变量 x,但尚未赋值
    # 于是将字典中 key 为 "x" 的键值对给删掉
    # 所以名字空间变成了 {}
    print(locals())
    x = 123
foo()  # {'x': 1}
bar()  # {}

上面代码中,局部变量的创建发生在 exec 之后,如果发生在 exec 之前也是相同的结果。

def foo():
    exec("x = 2")
    print(locals())
foo()  # {'x': 2}
def bar():
    x = 1
    exec("x = 2")
    print(locals())
bar()  # {'x': 1}

exec("x = 2") 执行之后,名字空间也变成了 {"x": 2}。但每次调用 locals,都会对字典进行更新,所以在 bar 函数里面获取名字空间的时候,又把 "x" 对应的 value 给更新回来了。

当然这是在变量冲突的情况下,会保存真实存在的局部变量的值。但如果不冲突,比如 bar 函数里面是 exec("y = 2"),那么 locals() 里面就会存在两个键值对。但只有 x 才是真正的局部变量,而 y 则不是。

将 exec("x = 2") 换成 locals()["x"] = 2 也是一样的效果,它们都是往局部名字空间中添加一个键值对,但不会创建一个局部变量。


薛定谔的猫



当 Python 中混进一只薛定谔的猫……,这是猫哥在 19 年更新的一篇文章,里面探讨的内容我们本文的主题是重叠的。猫哥在文章中举了几个疑惑重重的例子,看看用上面学到的内容能不能合理地解释。

# 例 0
def foo():
    exec('y = 1 + 1')
    z = locals()['y']
    print(z)
foo()
# 输出:2
# 例 1
def foo():
    exec('y = 1 + 1')
    y = locals()['y']
    print(y)
foo()
# 报错:KeyError: 'y'

以上是猫哥文章中举的示例,首先例 0 很简单,因为 exec 影响了所在的局部名字空间,里面存在 "y": 2 这个键值对。至于里面的变量 z 则不影响,因为我们获取的是 "y" 这个 key 对应的 value。

但例 1 则不同,因为 Python 在语法解析的时候发现了 y  = ... 这样的赋值语句,那么它在编译的时候就知道函数里面存在 y 这个局部变量,并写入符号表中。既然符号表中存在,那么调用 locals 的时候就会对它进行更新。

而对 y 赋值是发生在调用 locals 之后,所以在调用 locals 的时候,y 的值还是一个 NULL,也就是变量还没有赋值。所以会将名字空间中的 "y": 2 这个键值对给删掉,于是报出 KeyError 错误。

再来看看猫哥文章的例 2:

# 例 2
def foo():
    y = 1 + 1
    y = locals()['y']
    print(y)
foo()
# 2

locals() 是对真实存在的局部变量的一个拷贝,在调用 locals 之前 y 就已经创建好了。符号表里面有 "y",f_localsplus 里面有一个数值 2,所以调用 locals() 的时候,会得到 {"y": 2},因此函数执行正常。

猫哥文章的例 3:

# 例3
def foo():
    exec('y = 1 + 1')
    boc = locals()
    y = boc['y']
    print(y)
foo()
# KeyError: 'y'

这个例3 和例1 是一样的,只不过用变量 boc 将局部名字空间保存起来了。执行 exec 的时候,会创建局部名字空间,写入键值对 "y": 2

但调用 locals 的时候,发现函数内部存在局部变量 y 并且还尚未赋值,于是又会将 "y": 2 这个键值对给删掉,因此 boc 变成了一个空字典。

在执行 y = boc["y"] 的时候会出现 KeyError。

猫哥文章的例 4:

# 例4
def foo():
    boc = locals()
    exec('y = 1 + 1')
    y = boc['y']
    print(y)
foo()
# 2

显然在调用 locals 的时候,会返回一个空字典,因为此时的局部变量都还没有赋值。但需要注意的是:boc 已经指向了局部名字空间(字典),而局部名字空间在一个函数里面也是唯一的。

所以 exec("y = 1 + 1") 执行之后,会往局部名字空间里面写入一个键值对,而变量 boc 指向的字典也会发生改变,因为是同一个字典,所以程序正常执行。

猫哥文章的例 5:

# 例5
def foo():
    boc = locals()
    exec('y = 1 + 1')
    print(locals())
    y = boc['y']
    print(y)
foo()
# {'boc': {...}} 
# KeyError: 'y'

首先在执行 boc = locals() 之后,boc 会指向一个空字典,然后 exec 函数执行之后会往字典里面写入一个键值对 "y": 2。如果在 exec 执行之后,直接执行 y = boc["y"],那么代码是没有问题的,但问题是执行之前插入了一个 print(locals())

我们说过,当调用 locals 的时候,会对名字空间进行更新,然后返回更新之后的名字空间。由于函数内部存在 y = ... 这样的赋值语句,所以符号表中就存在 "y" 这个符号,于是会进行更新。但更新的时候,发现 y 还没有被赋值,于是又将字典中的键值对 "y": 2 给删掉了。

由于局部名字空间只有一份,所以 boc 指向的字典也会发生改变,换句话说在 print(locals()) 之后,boc 就指向了一个空字典,因此出现 KeyError。


小结



以上我们就探讨了 local 名字空间相关的内容,它是真实存在的局部变量的一个拷贝,每当我们调用一次 locals,都会拷贝一次。

然后函数的局部变量都是静态存储的,编译时就已经确定,无法在运行时动态添加。我们往局部名字空间里面添加键值对,并不等价于创建局部变量。

相关文章
|
2天前
|
大数据 测试技术 数据库
【Python篇】Python 函数综合指南——从基础到高阶
【Python篇】Python 函数综合指南——从基础到高阶
7 2
|
2天前
|
存储 自然语言处理 Python
解密 Python 的作用域和名字空间
解密 Python 的作用域和名字空间
7 1
|
11天前
|
Python
python函数进阶
python函数进阶
|
10天前
|
安全 Python
Python量化炒股的获取数据函数—get_industry()
Python量化炒股的获取数据函数—get_industry()
20 3
|
11天前
|
Python
Python sorted() 函数和sort()函数对比分析
Python sorted() 函数和sort()函数对比分析
|
10天前
|
Python
Python量化炒股的获取数据函数—get_security_info()
Python量化炒股的获取数据函数—get_security_info()
21 1
|
1天前
|
Python
从零到一:构建Python异步编程思维,掌握协程与异步函数
从零到一:构建Python异步编程思维,掌握协程与异步函数
7 0
|
2天前
|
存储 IDE 编译器
解密 Python 函数的实现原理
解密 Python 函数的实现原理
7 0
|
10天前
|
Python
Python量化炒股的获取数据函数— get_billboard_list()
Python量化炒股的获取数据函数— get_billboard_list()
21 0
|
10天前
|
安全 数据库 数据格式
Python量化炒股的获取数据函数—get_fundamentals()
Python量化炒股的获取数据函数—get_fundamentals()
25 0