一文了解Python错误、异常和文件读写

本文将主要介绍 Python 的语法错误、异常、文件的读取等基础知识。阅读本文预计需要 15 min

1. 前言

错误和异常,以及读取文件,写入文件都是我们经常会遇到的。本文主要内容:

  • 语法错误
  • 异常定义和查看报错信息
  • 异常的处理
  • 抛出异常
  • 自定义异常
  • 文件的读取
  • 读取大文件的方法

2. 语法错误

在 Python 中主要分为两种错误:语法错误(syntax errors) 和异常(exceptions)。
语法错误,又称解析错误(parsing errors),指代码的语法不符合 Python 语法规则,导致“翻译官” Python 解释器无法翻译你的代码给计算机,导致会报错。这很常见,尤其是我们初学编程的时候。

语法错误是代码执行前检测到的错误

举个栗子:

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
                   ^
SyntaxError: invalid syntax

上面就是一个语法错误,学习如何看报错信息。
首先我们看最后一行,SyntaxError: invalid syntax 这句就是告诉我们写的代码出现了 SyntaxError 即语法错误。

继续往上面看,发现 Python 解释器输出了出现语法错误的一行,并且在这句代码有问题的地方(也有可能是这附近,只是在这里检测出来了)用一个箭头 ^ 指示,同时再上面一行输出了文件名 "<stdin>" 和出问题代码所在的行数(line),这里是第一行代码出问题了,而且是在 print()函数附近。

通过这样的顺序,我们就可以快速的定位到错误代码的位置,并且知道是什么错误,从而进行修改。这句代码主要是因为 print()函数前面少了一个冒号(:)。

学会看报错信息,定位错误,对于调试代码非常重要!语法错误相对比较简单,就做这些总结。

3. 异常

除了语法错误,我们更多遇见的是异常(exception)。

3.1 异常的定义

有时候,我们的语法没有任何问题,但是在代码执行的时候,还是可能发生错误。这种在代码运行时检测到的错误成为异常

下面展示一些常见的异常(来源于 Python 官网):

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

我们对比一下语法错误和异常的报错信息,以 10 * (1/0) 的报错信息为例:

  • 异常的最后一行也是告诉我们程序执行过程中遇到了什么错误,异常的类型是什么,这里是 ZeroDivisionError即分母为 0 的异常。
  • 我们在看第一行 Traceback (most recent call last): 这句是告诉我们程序在执行过程中追踪到一个错误,下面开始一步一步追踪定位错误。这在语法错误里面是没有的。因为语法错误是在程序执行前检测到的。官网的说法是以堆栈回溯的形式显示发生异常时的上下文,通常它包含列出源代码行的堆栈回溯,但是不会显示从标准输入中读取的行。
  • 接着一行 File "<stdin>", line 1, in <module> 是告诉我们,文件 "<stdin>" 模块中的第 1 行代码出问题了。从而我们就定位到了代码出错的位置。可以发现,这里没有出现语法错误的箭头。

有时候 Traceback 很长,这时候我们可以先看最后一行,知道错误类型,然后从上往下看报错信息,最终定位到出问题代码的位置(在哪个文件,多少行代码),从而修改代码。

异常的类型很多,这里依次列出了 ZeroDivisionError, NameError 和 TypeError 三种异常,这些异常名称是内置的标识符(identifiers),不是关键字(keywords)。

更多的内置异常(Built-in Exceptions)可以参看官网Built-in Exceptions获取它们的介绍和意义。

3.2 异常的处理

Python 中可以用 try...except 语句来处理异常。这有点像 if...else 条件分支语句。

看这个例子:

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")

这段代码是要求用户一直输入,直到输入一个有效的整数,但允许用户通过 Ctrl + C 中断程序,这时引发的是 KeyboardIterrut 异常。
下面说明一下 try 语句的工作原理:

  1. 首先执行 try 子句(即 try 和 except 之间的所有语句).
  2. 如果没有异常发生,则跳过 except 子句,从而结束 try 语句的执行。
  3. 如果执行 try 语句时发生了异常,则跳过 try 子句剩余的部分。然后,如果异常的类型和 except 关键字后面的异常匹配,则执行相应的 except 子句,然后继续执行 except 后面的代码。
  4. 如果发生的异常和 except 子句中指定的异常不匹配,则将其传递到外部的 try 语句中;如果没有找到处理程序,则他是一个未处理异常,程序将停止并显示相应的信息,如果什么处理都没有,就什么都不显示。

如下面这个,except 中什么都不处理,所以程序直接结束,什么都没有输出:

>>> try:
...     x = int(input("Please enter a number: "))
...     print("good!")
... except ValueError:
...     # print("Oops!  That was no valid number.  Try again...")
...     pass
...
Please enter a number: a
>>>

一个 try 语句可以有多个 except 子句,以指定不同异常的处理程序,但是最多会执行一个 except 子句,其他的都会跳过。还有一点要注意,except 子句只会处理 try 子句中发生的异常,对于 except 子句中发生的异常是没有不会被 except 子句处理的。换句话说,就是异常处理程序(except 子句)自身发生异常,try 语句中的 except 都无法处理,交给更高一级处理。如:

>>> try:
...     x = int(input("Please enter a number: "))
...     print("good!")
... except ValueError:
...     # print("Oops!  That was no valid number.  Try again...")
...     print(a)
...
Please enter a number: b
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ValueError: invalid literal for int() with base 10: 'b'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
NameError: name 'a' is not defined
>>>

这里可以发现有两个异常,一个是 try 子句引发的 ValueError,还有一个异常处理程序 except 子句中引发的 NameError,这是 try 语句之外的更高一级处理的结果(Python 解释器)。

一个子句也可以将多个异常命名为元组,如:

... except (RuntimeError, TypeError, NameError):
...     pass

注意,Python 的错误也是 class,所有错误类型都继承自 BaseException,所以使用 except 时一定要注意,它不但能捕获该类型的错误,还能把其子类也“一网打尽”,如:

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except B:
        print("B")
    except D:
        print("D")
    except C:
        print("C")


输出结果:
B
B
B

简单说,就是 B 是 Exception 的子类,C 是 B 的子类,D 是 C 的子类,所以 raise 抛出错误的时候(raise 待会会说),B、C、B 三种异常类型都可以被 第一个 except B 子句捕获,这样就大致我们后面的 except 子句永远不会生效。所以要注意书写顺序,先子类,再父类,异常的继承顺序可以看前面给的链接。这里正确的做法是把,except B 作为最后一个 except 子句,这样输出结果就是 B、C、D。

try…except 还有一个好处就是可以跨越多层调用,即不仅可以处理 try 子句遇到的异常,还可以处理 try 子句中调用函数发生的异常,如:main()调用 foo(),foo()调用 bar(),结果 bar()出错了,这时只要 main()捕获了,就可以处理:

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        print('Error:', e)

main()

输出结果:
Error: division by zero

即我们不需要在每个可能出错的地方去捕获错误,只需要在合适的层次去捕获错误就可以了,这样可以减少写 try…except… 的麻烦。

最后的 except 子句可以省略异常名,来作为通配符(匹配剩余所有的异常),但是这种做法要慎重,因为这种做法很容易掩盖真正的编程错误,让你不知道到底发生了什么异常。此外它也还可以用于打印错误消息,然后重新引发异常(这个很常用,也很有用),如:

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise

try…except 语句还有一个可选的 else 子句,在使用时,else 子句必须放在所有的 try 子句后面,如果 try 子句没有引发异常,就会执行,else 子句。else 子句常用在需要向 try 子句添加额外代码的时候。对官网这部分的描述还不是特别理解,这里放一个官网的例子:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

except 子句可以在异常名称后指定一个变量,用于绑定一个异常实例,它的参数存储在 instance.args 中,通常出于方便考虑,异常实例会定义 __str__() 特殊方法,因此可以直接打印参数,而不需要用引用的形式.arg,同时也可以在抛出之前首先实例化异常,并根据需要向其添加任何属性:

try:
    raise Exception('spam', 'eggs')
except Exception as inst:
    print(type(inst))  # the exception instance
    print(inst.args)  # arguments stored in .args
    print(inst)  # __str__ allows args to be printed directly,
    # but may be overridden in exception subclasses
    x, y = inst.args  # unpack args
    print('x =', x)
    print('y =', y)

输出结果:
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

3.3 抛出异常

有时遇到异常,我们不知道怎么处理,我们可以把异常抛出去,交给上面处理,Python 中 raise 语句允许程序员强制发生指定的异常,如:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: HiThere

raise 语句唯一的参数就是要抛出的异常实例,这个实例尽量使用 Python 内置的异常类型,如果传递的是一个异常类,它将通过调用没有参数的构造函数来隐式实例化:raise ValueError # 等价于 'raise ValueError()'

raise 语句不带参数会把当前错误原样抛出。

此外,在 except 中 raise 一个 Exception,还可以把一种类型的异常转化为另一种类型,但尽量别这么干。

3.4 用户自定义异常

用户可以自定义异常,但是自定义异常时需要注意:

  1. 自定义的异常通常应该直接或间接从 Exception 类派生。
  2. 自定义的异常通常保持简单,只提供许多属性,这些属性允许处理程序为异常提取有关错误的信息。
  3. 在创建可能引发多个不同错误的模块时,通常的做法是为该模块定义的异常创建基类,并为不同的错误创建特定异常类的子类。如:
  4. 大多数异常名字都以 Error 结尾,类似于标准异常的命名。
class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.

    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.

    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

3.5 定义清理操作

try 语句还有一个 finally 子句是可选的,用于处理在所有情况下都必须执行的清理操作。finally 子句将作为 try 语句的最后一项任务被执行,无论 try 子句是否发生异常,均会执行,如:

try:
    print(a)
except NameError as e:
    print('NameError: ', e)
finally:
    print('finally...')

结果输出:
NameError:  name 'a' is not defined
finally...

这里有一些 finally 的细节,自己以前没有注意到,看官方文档后发现挺重要的:

  1. 如果在执行 try 子句期间发生了异常,该异常可由一个 except 子句进行捕获处理。 如果异常没有被某个 except 子句所处理,则该异常会在 finally 子句执行之后被重新引发。
  2. 异常也可能在 except 或 else 子句执行期间发生。 同样地,该异常会在 finally 子句执行之后被重新引发。
  3. 如果在执行 try 语句时遇到一个 break, continue 或 return 语句,则 finally 子句将在执行 break, continue 或 return 语句之前被执行。
  4. 如果 finally 子句中包含一个 return 语句,则返回值将来自 finally 子句的某个 return 语句的返回值,而非来自 try 子句的 return 语句的返回值。

以上这些都是强调了 finally 子句一定会被执行,同时它的执行顺序优先于 try 语句无法处理的异常,也优先于 break、return、continue 等控制语句。
看两个官网给的例子:

>>> def bool_return():
...     try:
...         return True
...     finally:
...         return False
...
>>> bool_return()
False

一个更复杂的例子:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)  # try 子句没有异常,else子句执行,最后是finally
result is 2.0
executing finally clause
>>> divide(2, 0)  # 异常被except 捕获,正确处理,finally 在其之后执行
division by zero!
executing finally clause
>>> divide("2", "1")  # 这里except子句无法处理该类异常,所以先执行finally子句,再重新引发异常
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

在实际应用开发中,finally 子句非常有用,它常用于释放外部资源,如文件或者网络连接等。因为及时使用不成功,也可以成功释放这些资源。

3.5 小结

最后列一下 try...except...else...finally 都出现的格式:

try:
    codeblock
except:
    codeblock
else:
    codeblock
finally:
    codeblock
  1. try 语句可以很好地处理一些异常,但是不要滥用,只在关键的位置使用。这个需要多积累经验,看看牛人都是怎么用的。
  2. except 子句可以有多个,但要注意父类和子类异常类型的顺序。
  3. 不要轻易使用 except 后不加异常名称,这会导致异常的类型无法确定,无法更好的定位异常。
  4. 要额外添加到 try 子句的代码最好放到 else 子句中,else 子句是可选的,要使用 else,则 except 子句必须要有。这个需要再看看牛人怎么用,借鉴用法。
  5. finally 子句是可选的,无论如何都会执行,但是要注意如果出现了 try 语句无法处理的异常时,会先执行 finally 子句,再重新引发异常。
  6. 如果在执行 try 语句时遇到一个 break, continue 或 return 语句,则先执行 finally 子句,再执行 break, continue 或 return 语句。
  7. 如果 finally 子句中包含一个 return 语句,则返回值将来自 finally 子句的某个 return 语句的返回值,而非来自 try 子句的 return 语句的返回值。
  8. finally 子句在使用文件、网络连接等资源时非常有用,可以保证我们成功释放资源。

4. 文件

读写文件是最常见的 IO 操作,在磁盘上读写文件的功能是由操作系统提供的,所以读写文件就是请求操作系统打开一个文件对象(file object),这通常描述为文件描述符,然后通过操作系统提供的接口从这个文件对象中读取数据,或者写入数数据。有点像,我们告诉操作系统我们要做什么,操作系统再去帮我们完成。

4.1 文件的读写

Python 中 open() 函数用来读写文件,用法和 C 是兼容的。它返回的是一个文件对象(file object)。open()函数有好几个参数,最常用的是 open(filename, mode=‘r’, encoding=None, errors=None)。

  • filename 代表文件的名字,需要我们传入一个包含文件名的字符串,这是必传参数。
  • mode 也是需要我们传入一个字符串,告诉函数,以什么样的方式打开文件,默认情况下是文件只能读取('r')。还有其他几种方式,待会列出来。
  • encoding 是使用文件时的编码或解码的格式,这只用于文本文件,不能用于二进制文件,默认模式和平台有关,因此有时读文本文件,我们会指定 encoding='utf-8'
  • errors 是用于编码错误的,如果一个文件存在多种编码,这个时候,我们可以指定 errors=‘ignor’ 来忽略错误,这会造成部分数据丢失。

open() 函数默认是打开文本文件(text file),打开的方式主要有一些几种:

mode描述
‘r’默认情况,只读模式
‘w’只写模式,注意存在的同名文件会被删除
‘a’追加写模式,任何写入的数据都会自动添加到文件末尾
‘x’只写模式,推荐,存在同名文件会提示报错
‘+’上面的几种模式加上’+’,都变成可读可写,如:r+,w+,a+等

默认打开的是文本文件,如果我们要打开图片、视频等二进制文件时,就需要用二进制模式,模式和上面类似,只是加一个 ‘b’ 就可以了。

mode描述
‘rb’默认情况,只读模式
‘wb’只写模式,注意存在的同名文件会被删除
‘ab’追加写模式,任何写入的数据都会自动添加到文件末尾
‘xb’只写模式,推荐,存在同名文件会提示报错
‘+’上面的几种模式加上’+’,都变成可读可写,如:rb+,wb+,ab+等

举个栗子:

# 我在代码当前目录建了一个b.txt文件
f = open('b.txt', mode='r')  # 打开文件
for line in f.readlines():
    print(line, end='')
f.close()  # 关闭文件

输出结果:
Java
Python
C
C++

如果文件 b.txt 不存在,会得到一个 FileNotFoundError 异常,文件对象的方法,我们待会讲。
注意,我们打开文件后,一定要记得关闭,不然文件对象会一直占用操作系统资源。所以这里使用 try…finally 来完成是非常好的。

try:
    f = open('b.txt', mode='r')
    for line in f.readlines():
        print(line, end='')
finally:
    if f:
        f.close()

这样写很繁琐,Python 提供了更加优雅的方式帮助我们打开文件,会在我们使用结束文件,或者处理文件时发生异常,都能自动关闭文件,这也是 Python 官方推荐的方式。

with open('b.txt', mode='r') as f:
    for line in f.readlines():
        print(line, end='')

输出结果:
Java
Python
C
C++

发现 with 关键字的写法更加优雅,简洁,极其推荐。

接下来将简单介绍一些文件对象的方法。

4.2 文件对象的方法

Python 内置了很多文件对象的方法帮助我们对文件进行操作。下面列举一些常用的方法,更多的方法,大家可以使用 help(file object)进行查看,如 help(f),我们就可以看到我们可以对文件对象 f,进行哪些操作。

  • close()用于关闭文件。使用了 with 这个就可以忽略了。

  • read(self, size=-1, /),用于按大小读取文件的大小,如果不传入 size 这个仅限位置参数,则默认读取整个文件。

  • readline(self, size=-1, /),用于按行读取文件,每次读取文件的一行。

  • readlines(self, hint=-1, /),用于按行读取文件,hint 仅限位置参数用于指定要读取的行数,如果不指定,默认读取文件的全部行数,并放入一个列表中返回。

  • write(self, text, /),写入文本,并返回写入的文本字符数,即文本的长度。

  • writelines(self, lines, /),写入文本,不过是写入多行文本,需要我们传入一个列表。不过需要注意,每一行行末的换行符\n需要我们自己添加。

  • seek(),指定文件指针在文件中的位置,seek(0) 代表将文件指针指向文件开头,seek(n)代表将文件指针指向第 n 个字符。

  • tell(),告诉我们当前文件指针的位置。

下面我们就测试一下:
我们先在当前目录新建一个 d.txt 文件里面内容如下:

C
Python
Java

测试如下:

>>> f = open('d.txt', mode='r+')  # 读写方式打开d.txt
>>> f.tell()  # 告诉我当前文件指针位置在0,即开头
0
>>> f.readline()  # 读取返回文件的一行
'C\n'
>>> f.readline()  # 读取返回文件的下一行
'Python\n'
>>> f.tell()  # 告诉我当前文件指针位置在11,即读到了11个字符位置
11
>>> f.seek(0)  # 将文件指针移到开头
0
>>> f.tell()  # 可以看到文件指针回到了开头位置
0
>>> f.readlines(2)  # readlines()读取两行,返回一个列表
['C\n', 'Python\n']
>>> f.readlines()  # 读取剩余文件的全部行,返回一个列表
['Java']
>>> f.readline()  # 文件读完了,继续读会得到一个空字符串
''
>>> f.tell()  # 文件指针在 15
15
>>> f.seek(0)  # 文件指针置于文件开始处
0
>>> f.read(11)  # read(11)指定读取文件11个字符
'C\nPython\nJa'
>>> f.read()  # 未指定参数,读取文件剩余的全部内容
'va'
>>> f.seek(0)  # 将文件指针移到开头
0
>>> f.read() # 一次性读取全部文件内容到内存
'C\nPython\nJava'
>>> f.close()  # 关闭文件

好了,看明白上面的,我们再简单看一个例子:

lines = ['C\n', 'Python\n', 'Java\n']
with open('d.txt', mode='w+') as f:
    f.writelines(lines)  # 写入多行文件
    f.seek(0)  # 文件指针置于开头,从头开始读取
    for line in f:  # 一行一行输出文件
        print(line, end='')

结果输出:
C
Python
Java

理解每一个方法,恰当的使用它们,可以让我们的代码更加高效。

4.3 大文件的读取

文件比较小,我们内存够大,所以方法选择上没那么重要,但是如果我们的文件非常大, 比如 8 个 G,这时如果你用 f.read()去读取,或者用 f.readlines(),默认读取整个文件,那么很可能你会因为内存不足,系统崩溃。
所以怎么读取大文件呢?

其实解决思路很简单,就是我们不要一次性都读入,把文件拆分,比如一行一行读入,或者一次读入 1024 个字符。

方法一(官网推荐):

with open('d.txt') as f:
    for line in f:
        print(line, end='')

输出结果:
C
Python
Java

这种情况适合一行一行读入,但是如果一行很大,通常视频图片时,比如一行有 1G 呢,这时候可以指定大小读入:

with open('d.txt') as f:
    while True:
        part = f.read(4)  # 每次读取4个字符
        if part:
            print(part, end='')
        else:
            break

输出结果:
C
Python
Java

这里我们可以封装成一个函数,方便我们更加灵活的调用和配置:

def read_part(file_path, size=1024, encoding="utf-8"):
    with open(file_path, mode='r', encoding=encoding) as f:
        while True:
            part = f.read(size)
            if part:
                yield part
            else:
                return None

file_path = r'd.txt'  # r 代表原始字符串
size = 2 # 每次读取指定大小的内容到内存
encoding = 'utf-8'

for part in read_part(file_path=file_path, size=size, encoding=encoding):
    print(part, end='')

按大小读取可以灵活控制一次读取的 size,在速度上较按行读取有优势,适用于一些大的二进制文件,比如读取一些大的视频或者图片等。
按行读取在处理一些文本的时候感觉更加便利,按行读更容易对文本进行处理。

当然我们也可以每次读入指定的行数,这里就不实现了。

到这里,我们基本把 Python 的基础部分总结完了。

5. 巨人的肩膀

  1. Errors and Exceptions
  2. Reading and Writing Files

推荐阅读:

  1. 编程小白安装Python开发环境及PyCharm的基本用法
  2. 一文了解Python基础知识
  3. 一文了解Python数据结构
  4. 一文了解Python流程控制
  5. 一文了解Python函数
  6. 一文了解Python部分高级特性
  7. 一文了解Python的模块和包
  8. 一文了解 Python 中的命名空间和作用域
  9. 一文了解Python面向对象

后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技,扫描下方二维码或者搜索每日学一技关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!
每日学一技

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值