Day3 PythonWeb全栈课程课堂内容

1. 经典的参数错误

  • 在经典的计算当中,有着许多参数运算的规则。
'''例子1'''
def add(a,b):
    a += b 
    return a
a = 1
b = 2
c = add(a, b)
print(a, b, c) 
'''
结果:a = 1 ; b = 2 ; c = 3
''' 
  • 从例子1中我们发现 a 出现在好几个地方,**def add(a,b) ; return a ; a += b ; a = 1 ; c = add(a,b)**中,这次我们主要看add()函数中的a和a=1的变化情况,从最后的结果可以看出,实参a=1,a的值未发生变化。而在add()的函数当中,a的值发生了改变。我们进一步进行验证add()函数中发生了变化。
def add(a,b):
    a += b 
    print(a) # 3
    return a
a = 1
b = 2
c = add(a, b)
print(a, b, c) # 1, 2, 3
  • 你可从中发现a在add()函数中的值发生了改变,而最后的a值未发生变化,与实参a=1相同。
  • 接下来我们在做几个例子:
'''例子2'''
def add(a,b):
    a += b 
    print(a) # [1, 2, 3, 4]
    return a
a = [1, 2]
b = [3, 4]
c = add(a, b)
print(a, b, c) # [1, 2, 3, 4], [3, 4], [1, 2, 3, 4]
'''例子3'''
def add(a,b):
    a += b 
    print(a) # (1, 2, 3, 4)
    return a
a = (1, 2)
b = (3, 4)
c = add(a, b)
print(a, b, c) # (1, 2), (3, 4), (1, 2, 3, 4)
  • 从例子2中我们惊奇的发现a的值在最后发生了改变,而例子3中a的值未发生改变。这是为什么呢?
  • 原因:因为在Python有两种可变类型,分为不可变类型,和可变类型。
类型具体类型
不可变类型元组,字符串,整数…
可变类型字典,集合,数列…
  • 由于数组为可变类型,而元组为不可变类型,我们不难发现例子2中的a发生变化,而例子3中的a未发生变化。
  • 我们还有不同的解释,获取a的地址,我们依旧使用例子2和例子3。
'''例子2'''
def add(a,b):
    a += b 
    print(id(a)) # 197620424 
    return a
a = [1, 2]
print(id(a)) # 197620424
b = [3, 4]
c = add(a, b)
print(id(a)) # 197620424
'''例子3'''
def add(a,b):
    a += b 
    print(id(a)) # 197450072
    return a
a = (1, 2)
print(id(a)) # 197464584
b = (3, 4) 
c = add(a, b)
print(id(a)) # 197464584
  • 不难发现例子2中a的id地址不变,使得a的值发生变化。而例子3中a的id地址发生改变,add()中a指向197450072地址的值,结束后a又指向197464584,使得a的值不发生变化。
  • 我们再看看下面这个例子。
'''例子4'''
def add(a,b):
    a = a + b 
    print(a) # [1, 2, 3, 4]
    print(id(a)) # 197620744
    return a
a = [1, 2]
print(id(a)) # 197620424
b = [3, 4]
c = add(a, b)
print(a) # [1, 2]
print(id(a)) # 197620424
  • 从例子4中发现,add()函数中,将原来的a += b改成a = a + b,最后a的值发生了未改变,因为add()函数中a的地址发生改变,所以最后a的未发生改变。

总结:

  • 列表为可变类型
    • list += 1,相当于改变list本身。
    • list = list + 1,相当于list是两个变量,id不一致,返回的是,原本的list
  • 元组为不可变类型
    • tuple += 1 也就是重新创建了一个tuple变量,id不一致,返回的是,原本的tuple

2. 內存与内存管理的简介

  • 何为内存?
    • 从计算机存储器的作用来讲,存储器可以分为主存储器,辅助存储器和缓冲存储器。
    • 其中主存储器,也称主存、内存或可执行存储器,是与 CPU 直接进行信息交换的存储器,它的读写速度相对较快,容量相对较小,通常用来保存进程运行时的程序和相应数据以供 CPU 使用;而辅存不能与 CPU 直接进行信息交换,它的容量较大,读写速度相对较慢。缓冲存储器常用于两个速度不同的部件之间,比如 CPU 和主存之间设置的高速缓冲存储 Cache(也可将内存的概念扩展为主存和高速缓冲存储器的合集)。

在这里插入图片描述

  • 操作系统的内存管理

    • 由于内存的容量有限,很难承载系统以及用户进程所需的全部信息,所以操作系统需要对内存空间进行存储管理。在操作系统层面上内存管理的实现相当复杂,其大致功能包括内存空间的划分与动态分配、回收、扩充,以及存储保护,地址转换等等。
  • 进程内的内存管理

    • 操作系统对各个进程的内存进行管理,同时我们也需要管理我们编写的程序所对应进程内的内存。从可用内存中申请内存并且具有足够内存来进行相关操作,以及在适当的时间释放内存,这些都是我们的程序和系统能够正常运行的前提。

    • 在 C / C++ 中,我们需要手动的进行内存管理,比如 C 语言中通过 mallocfree函数来申请给定字节数的内存以及释放对应的内存。但在 Python 中,我们无须手动进行内存的申请和释放,Python 在内部帮我们完成了大量涉及到内存管理的操作,包括内存分配及垃圾回收。

在这里插入图片描述

3. 内存分配

  • 内存池机制

    • 什么是 PyMalloc ?

    • 在 Python 中,有很多常用的数据结构,包括列表、字典等。比如在列表中,我们不仅可以保存其他不同类型的对象,而且可以非常方便的使用 appendextend 等方法对其进行动态的扩充。针对这些常用对象的一系列操作,会在 Python 中造成内存的频繁分配和释放,同时像 intlist 等 Python 对象的分配和释放通常涉及到的数据量相对较小,因此 Python 在内部引入了内存池机制,实现了小块内存的管理器(称为 PyMalloc )用于提高处理小块内存的效率,这样避免了在底层频繁的 mallocfree 操作对效率带来的影响。

    • 分配策略

    • 在内存分配中,Python 以 512 bytes 为界限对大内存和小内存进行划分,不超过 512 bytes 的内存申请,会通过 PyMalloc 管理器进行处理,超过 512 bytes 的内存申请,则会通过 C 中的 malloc 来进行处理。在管理器的内部,主要包括 blockpoolarena 层级, 其中 block是 Python 内存管理中的最小单元,一个 pool 中包含多个 block,多个 pool 构成一个 arena。 同时,由于内存池机制,Python 并不会将释放的内存立即归还操作系统。

  • 缓冲池机制

    • 在内存池机制的基础之上,Python 为了提高常用对象的创建和释放效率,又进一步对整数、字符串等对象建立了对象缓冲池。比如对于 [-5, 256] 内的小整数,Python 已经在内部创建好了对应的小整数对象的缓冲池。

4. 垃圾回收机制

  • 垃圾回收机制

    • 介绍

    • 我们知道,Python 程序在运行的时候,需要在内存中开辟出一块空间,用于存放运行时产生的临时变量;计算完成后,再将结果输出到永久性存储器中。如果数据量过大,内存空间管理不善就很容易出现 OOM(out of memory),俗称爆内存,程序可能被操作系统中止。

    • Python 中一切皆对象。因此,你所看到的一切变量,本质上都是对象的一个指针。

    • 那么,怎么知道一个对象,是否永远都不能被调用了呢?

    • 就是当这个对象的引用计数(指针数)为 0 的时候,说明这个对象永不可达,自然它也就成为了垃圾,需要被回收。


  • OS模块

    • 与操作系统交互的库
  • psutil模块

    • 与系统交互的库,能够轻松实现获取系统运行的进程和系统利用率(包括CPU、内存、磁盘、网络等)信息。它主要用来做系统监控,性能分析,进程管理。
  • 通过以下代码检测程序在运行时的内存消耗

    import os # 与操作系统交互
    import psutil # 与新系统交互
    # 工具方法-> 查看当前的程序用的多少内存
    def show_info(start):
        pid = os.getpid() 
        p = psutil.Process(pid)
        info = p.memory_full_info()
        memory = info.uss/1024./1024
        print(f"{start}一共占用{memory:.2f}MB")
    def func():
        show_info("initial")
        a = [i for i in range(1000000)]   
        show_info("created")
    func()
    show_info("finished")
    '''
    initial一共占用27.71MB
    created一共占用66.55MB
    finished一共占用27.96MB 
    a没有指向,被内存释放掉,使最后内存减少。
    '''
    
    • a不被释放的情况
    import os 
    import psutil 
    def show_info(start):
        pid = os.getpid() 
        p = psutil.Process(pid)
        info = p.memory_full_info()
        memory = info.uss/1024./1024
        print(f"{start}一共占用{memory:.2f}MB")
    def func():
        show_info("initial")
        global a
        a = [i for i in range(1000000)]   
        show_info("created")
    func()
    show_info("finished")
    '''
    initial一共占用27.66MB
    created一共占用66.62MB
    finished一共占用66.62MB
    当把a设置成全局变量时,你会发现,最后内存并没有被释放,原因是a被调用,指向了一个值。
    '''
    
    • 另外一种情况也是a被一直使用。
    import os 
    import psutil 
    def show_info(start):
        pid = os.getpid() 
        p = psutil.Process(pid)
        info = p.memory_full_info()
        memory = info.uss/1024./1024
        print(f"{start}一共占用{memory:.2f}MB")
    def func():
        show_info("initial")
        a = [i for i in range(1000000)]   
        show_info("created")
        return a
    a = func()
    show_info("finished")
    '''
    initial一共占用27.54MB
    created一共占用66.50MB
    finished一共占用66.50MB
    返回a 并且a指向返回值,是的内存没有释放,内存使用不变。
    '''
    
    • 而当返回值不指向时,你看:
    import os 
    import psutil 
    def show_info(start):
        pid = os.getpid() 
        p = psutil.Process(pid)
        info = p.memory_full_info()
        memory = info.uss/1024./1024
        print(f"{start}一共占用{memory:.2f}MB")
    def func():
        show_info("initial")
        a = [i for i in range(1000000)]   
        show_info("created")
        return a
    func()
    show_info("finished")
    '''
    initial一共占用27.66MB
    created一共占用66.62MB
    finished一共占用27.77MB
    由于不是调用返回值,并不被指向,内存释放,内存使用下降。
    '''
    

总结

  • a是局部变量时,在返回到函数调用处时,局部变量的引用会注销。这时,列表a所指代对象的引用数为0,Python便会执行垃圾回收,因此之前占用的内存被收回了。

  • a是全局变量的时,即使函数体内代码执行完毕,返回到函数调用处时,对列表a的引用仍然是存在的,所以对象不会被垃圾回收,依然占有大量内存。


5. 引用计数

  • 使用sys模块
import sys

a = []
print(sys.getrefcount(a)) 
# getrefcount() 本身也要占用一次
# 2 表示a被调用2次。
  • 接下来一个例子中
a = [] # 第一次
def func(a): # 第三次
    print(sys.getrefcount(a)) # 第四次
func(a) # 第二次
# 4 表示a被调用4次。
a = [] # 第一次
b = a # 第二次
c = b # 第三次
e = c # 第四次
print(sys.getrefcount(a))  # 第五次
# 5

5. 手动启动启动垃圾回收

  • gc 模块
import os
import psutil
import gc
import sys

def show_info(start):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print(f"{start}一共占用{memory:.2f}MB")

show_info("initial")
a = [i for i in range(1000000)]
show_info("created")

del a
get(sys.getrefcount(a))
'''
    get(sys.getrefcount(a))
NameError: name 'a' is not defined
经过del a 之后,a 被删除,没有被引用
所以不能用sys.getrefcount()来查看使用情况。
'''
  • 选择其他方法
import os
import psutil
import gc
import sys

def show_info(start):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print(f"{start}一共占用{memory:.2f}MB")

show_info("initial")
a = [i for i in range(1000000)]
show_info("created")

del a # 删除a对象的引用
gc.collect() #删除没有引用的对象。 手动删除。
show_info("finished")
'''
initial一共占用27.50MB
created一共占用66.47MB
finished一共占用27.62MB
'''
  • 就算不执行gc.collect(),python也会自动帮你释放。

6. 循环引用

  • 引用计数为0的时候,该对象一定会被回收吗?

  • 举个案例:

import gc
import psutil
import os


def show_info(start):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss/1024./1024
    print(f"{start}一共占用{memory:.2f}MB")

def func():
    show_info("initial")
    a = [i for i in range(100000)]
    b = [i for i in range(100000)]
    show_info("after a,b created")
	# 互相引用,出现了引用环
    a.append(b)
    b.append(a)

func()
show_info("finished")
'''
initial一共占用27.65MB
after a,b created一共占用35.91MB
finished一共占用35.91MB
出现一个引用环。python没有自动释放,使得内存最后没有被释放
'''
  • 出现一个引用环。python没有自动释放,使得内存最后没有被释放,所以需要用到手动释放功能,gc.collect()
import gc
import psutil
import os


def show_info(start):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss/1024./1024
    print(f"{start}一共占用{memory:.2f}MB")

def func():
    show_info("initial")
    a = [i for i in range(100000)]
    b = [i for i in range(100000)]
    show_info("after a,b created")
    a.append(b)
    b.append(a)

func()
gc.collect()
show_info("finished")
'''
initial一共占用27.66MB
after a,b created一共占用35.91MB
finished一共占用27.74MB
'''
  • 使用手动删除,使得内存占用下降。
import gc
import psutil
import os


def show_info(start):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss/1024./1024
    print(f"{start}一共占用{memory:.2f}MB")

def func():
    show_info("initial")
    a = [i for i in range(100000)]
    b = [i for i in range(100000)]
    show_info("after a,b created")
    a.append(b)
    b.append(a)
    print(sys.getrefcount(a)) # 3
    print(sys.getrefcount(b)) # 3
	
func()
gc.collect()
show_info("finished")
'''
initial一共占用27.66MB
after a,b created一共占用35.91MB
finished一共占用27.74MB
'''

  • 总而言之,当双向引用的时候,引用计数虽然还在,但我们可以手动拉起回收,进行释放内存。所以,引用次数是垃圾回收的 充分非必要条件。

  • python中解决循环引用的方法

  • 标记清除(Mark and Sweep)和分代回收(Generational)

    • 为了解决引用计数在垃圾回收中无法处理循环引用的问题,Python 引入了标记清除和分代回收来检测和打破循环引用 。相对准确的说,标记清除是追踪回收( Tracing garbage collection )中的一种基础算法,其涉及到两个主要过程,即标记过程和清除过程,在标记过程中将所有可达(reachable)对象进行标记,在清除过程中将所有未标记的对象进行清除。分代回收则是基于标记清除基础上的,一种空间换时间的实现策略。
  • 分代回收

    • 分代回收将 Python 对象划分成 3 代,包括 0、1、2 代。对于新创建的对象,会被放入 0 代。若一个对象在经过一次垃圾回收后没有被清除,则它会被放入下一代中。对于每一代对象来说,都具有触发垃圾回收的相关阈值(收集频率)。关于这个过程的细节,官方文档中给出了比较明确的描述:

垃圾回收器把所有对象分类为三代,取决于对象幸存于多少次垃圾回收。新创建的对象会被放在最年轻代(第 0 代)。如果一个对象幸存于一次垃圾回收,则该对象会被放入下一代。第 2 代是最老的一代,因此这一代的对象幸存于垃圾回收后,仍会留在第 2 代。

为了判定何时需要进行垃圾回收,垃圾回收器会跟踪上一次回收后,分配和释放的对象的数目。当分配对象的数量减去释放对象的数量大于阈值 threshold0 时,回收器开始进行垃圾回收。起初只有第 0 代会被检查。当上一次第 1 代被检查后,第 0 代被检查的次数多于阈值 threshold1 时,第 1 代也会被检查。相似的, threshold2 设置了触发第 2 代被垃圾回收的第 1 代被垃圾回收的次数。

  • 分代回收的主要目的是降低回收中需要处理的对象数量,提高垃圾回收效率。我们可以使用 gc 模块来执行或优化垃圾回收的相关过程,并获取更多的调试信息,比如通过 gc.get_threshold() 来获取当前的回收阈值;通过 gc.disable() 关闭垃圾回收,这通常在程序中确定不存在循环引用时使用。
  • 另外,一个常见的问题是 CPython 在退出时一定会释放所有内存吗?答案是否定的,当 Python 解释器退出时,会进行内存清理,试图释放每个对象的内存,但不一定会释放所有内存,比如全局命名空间中引用的某些对象、循环引用下 C 扩展库中分配的某些内存都有可能不被释放(一部分相关细节可以通过 gc.garbage 了解)。

7. 调试泄露

  • 对内存分配的浪费。

  • 在Python中通过引用计数和垃圾回收来管理内存,但是在一定情况下也会产生内存泄露

    • 第一是对象被另一个生命周期特别长的对象所引用
    • 第二是循环引用中的对象定义了__del__函数
  • objgraph,一个非常好用的可视化引用关系的包。在这个包中的 show_refs() ,它可以生成清晰的引用关系图。

import objgraph
a = [1,2,3]
b = [4,5,6]

a.append(b)
b.append(a)

objgraph.show_refs(a)
'''
Graph written to C:\Users\ADMINI~1\AppData\Local\Temp\objgraph-8rl1e1f2.dot (7 nodes)
Graph viewer (xdot) and image renderer (dot) not found, not doing anything else
'''
  • 通过objgraph会生成一个后缀名为.dot的文件。普通文件是打不开的,例如用word打开是这个内容
digraph ObjectGraph {
  node[shape=box, style=filled, fillcolor=white];
  o1612344336[fontcolor=red];
  o1612344368[fontcolor=red];
  o1612344400[fontcolor=red];
  o196335752[fontcolor=red];
  o1612344336[label="int\n1"];
  o1612344336[fillcolor="0,0,1"];
  o1612344368[label="int\n2"];
  o1612344368[fillcolor="0,0,1"];
  o1612344400[label="int\n3"];
  o1612344400[fillcolor="0,0,1"];
  o196335752[label="list\n4 items"];
  o196335752[fillcolor="0,0,1"];
  o196335752 -> o1612344496;
  o196335752 -> o1612344464;
  o196335752 -> o1612344432;
  o1612344496[label="int\n6"];
  o1612344496[fillcolor="0,0,0.766667"];
  o1612344464[label="int\n5"];
  o1612344464[fillcolor="0,0,0.766667"];
  o1612344432[label="int\n4"];
  o1612344432[fillcolor="0,0,0.766667"];
}

在这里插入图片描述

import objgraph
a = [1,2,3]
b = [4,5,6]

a.append(b)
b.append(a)

objgraph.show_refs([a])
'''
Graph written to C:\Users\ADMINI~1\AppData\Local\Temp\objgraph-nlqf7rua.dot (8 nodes)
Graph viewer (xdot) and image renderer (dot) not found, not doing anything else
'''

在这里插入图片描述

import objgraph
a = [1,2,3]
b = [4,5,6]

a.append(b)
b.append(a)

objgraph.show_refs([b])

在这里插入图片描述


  • 使用 objgraph 工具查看。

8. 代码调试

  • 必须使用的方法
a = 1
b = 2
def demo():
    1/0
demo()
c = 3
'''
	1 / 0
ZeroDivisionError: division by zero
'''
  • 会出现代码错误如何调试。

    1. 使用print方法定位。
    a = 1
    b = 2
    print(1)
    def demo():
        1/0
    print(2)
    demo()
    print(3)
    c = 3
    '''
    1
    2
    Traceback (most recent call last):
      File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/代码调试.py", line 18, in <module>
        demo()
      File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/代码调试.py", line 16, in demo
        1/0
    ZeroDivisionError: division by zero
    表面在demo()的时候出现了问题,Traceback追踪信息。
    '''
    
    • 说明def demo()中有错误,然后慢慢调试。我们发现这种调试比较慢。
  • 断点调试,使用pycharm软件,的Debug’代码调试’

在这里插入图片描述

  • 按F8按钮。

在这里插入图片描述

  • 出现闪电 标志显示错误地方。

  • 使用pdb模块

    def func():
        print("Hello World")
    a = 1
    b = 2
    import pdb
    pdb.set_trace()
    func()
    c = 3
    

在这里插入图片描述

  • 参考对应的官方文档:https://docs.python.org/3/library/pdb.html#module-pdb

  • 使用cProfile 进行性能分析

    • 除了要对程序进行调试,性能分析也是每个开发者的必备技能。
    • 日常工作中,我们常常会遇到这样的问题:在线上,我发现产品的某个功能模块效率低下,延迟高,占用的资源多,但却不知道是哪里出了问题。
    • 这时,对代码进行 profile 就显得异常重要了。
    • 这里所谓的 profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。
    '''计算斐波拉契数列,运用递归思想'''
    def fib(n):
        if n == 0:
            return 0
        elif n == 1:
            return 1
        else:
            return fib(n-1) + fib(n-2)
    
    def fib_seq(n):
        res = []
        if n > 0:
            res.extend(fib_seq(n-1))
        res.append(fib(n))
        return res
    
    fib_seq(30)
    fib_seq(3000)
    '''
    Traceback (most recent call last):
      File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 20, in <module>
        fib_seq(3000)
      File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 17, in fib_seq
        res.extend(fib_seq(n-1))
      File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 17, in fib_seq
        res.extend(fib_seq(n-1))
      File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 17, in fib_seq
        res.extend(fib_seq(n-1))
      [Previous line repeated 994 more times]
      File "D:/Python/venv/Python-web全栈开发课堂练习文件/Day3/性能分析.py", line 16, in fib_seq
        if n > 0:
    RecursionError: maximum recursion depth exceeded in comparison
    超过python递归深度。
    '''
    
    • python最大递归深度为998,且最大递归深度是可以修改的。

    • cProfile模块

    def fib(n):
        if n == 0:
            return 0
        elif n == 1:
            return 1
        else:
            return fib(n-1) + fib(n-2)
    
    def fib_seq(n):
        res = []
        if n > 0:
            res.extend(fib_seq(n-1))
        res.append(fib(n))
        return res
    import cProfile
    cProfile.run('fib_seq(30)')
    '''
             7049218 function calls (96 primitive calls) in 2.765 seconds
    
       Ordered by: standard name
    
       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            1    0.000    0.000    2.764    2.764 <string>:1(<module>)
            1    0.000    0.000    2.765    2.765 {built-in method builtins.exec}
           31    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
            1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
           30    0.000    0.000    0.000    0.000 {method 'extend' of 'list' objects}
         31/1    0.000    0.000    2.764    2.764 性能分析.py:14(fib_seq)
    7049123/31    2.764    0.000    2.764    0.089 性能分析.py:6(fib)
    
    '''
    

在这里插入图片描述

  • 参数介绍
    • ncalls:函数被调用的次数。如果这一列有两个值,就表示有递归调用,第二个值是原生调用次数,第一个值是总调用次数。
    • tottime:函数内部消耗的总时间。(可以帮助优化)
    • percall:是tottime除以ncalls,一个函数每次调用平均消耗时间。
    • cumtime:之前所有子函数消费时间的累计和。
    • filename:lineno(function):被分析函数所在文件名、行号、函数名。
def add(n):
    if n == 0:
        return 1
    else:
    	return n * add(n-1)
import cProfile
cProfile.run('add(30)')

在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值