Python基础知识笔记——补充

其实之前已经学过很多python基础了,甚至学了好多遍了,但最近报名了一个线上班,有空会看看一些视频,因此在本文补充一些以前漏学或者老师讲得不错的知识。

目录

Hash模块

简介

Base64

zlib

简介

生成器

魔法方法

__call__

__str__

__repr__

__del__

继承

反射

内置函数

@classmethod

单例模式中的作用

继承时的作用

元类

class创建类的原理(type创建类):

自定义元类

自定义元类来控制类的调用产生自定义对象

type、class、object三者关系

网络

Socket

网络类型

套接字类型(网络协议)

python中创建socket

udp数据收发

TCP

TCP客户端

TCP服务端

TCP服务端循环接收消息

多任务

vim简单操作

串行实例

多线程实例

自定义线程类

global

死锁

进程

python中创建进程

进程与线程的区别:

Process对象使用

进程间不共享全局变量

子进程间通过队列共享数据

Queue对象说明与使用

进程池使用

多任务——协程

迭代器 

可迭代对象

判断对象是否为可迭代对象

判断对象是否为迭代器

可迭代对象和迭代器的区别

next()和iter()

注:

斐波那契数列迭代器

生成器

斐波那契数列生成器

 yield实现简单协程

协程-greenlet

协程-gevent

实例-gevent协程下载图片


pip源

临时修改

pip install -i 源地址 包名

常用源:

豆瓣:Simple Index

阿里: https://mirrors.aliyun.com/pypi/simple/

永久修改

window:在用户主目录(如C:\User\xxx)新建一个pip文件夹,里面新增pip.ini文件

[global]
trusted-host=[主机名]
index-url=[镜像源的url]

Hash模块

简介

hashlib 是一个提供了一些流行的hash(摘要)算法的Python标准库.其中所包括的算法有 md5, sha1, sha224, sha256, sha384, sha512等,所有算法加密对象都是字节,所以str要用encode('utf-8')

特点:

  • 只要传入内容一样,得到的hash值必然一样,可用于文件完整性检验
  • 不能由hash值反解内容,可以把密码做成hash值,从而在网络中传输
  • 只要使用的hash算法不变,无论检验的内容多大,得到的hash值长度是固定的,这样不影响传输。
# 实例1
import hashlib
#创建hash工厂
m = hashlib.md5()
# 在内存里面运送,需要编码成二进制
m.update('test'.encode('utf-8'))
m.update()    # 可以分多次进行加密,如果内存不够的话
# 产出hash值
print(m.hexdigest())

098f6bcd4621d373cade4e832627b4f6

会有撞库风险,比如省份证 生日 弱密码等,只要用同一个hash算法,同样的内容得到的hash值一定一样,因此会撞库

 解决方法

密码前后加盐(中间加盐也可以)

m.update('abc'.encode('utf-8'))
m.update(pwd.encode('utf-8'))
m.update('abc'.encode('utf-8'))

sha后面的数字越大 加密的算法越复杂

m = hashlib.sha256() 

Base64

zlib

简介

对数据进行压缩处理的标准库。

# 实例
def main():
    python_zen = this.s  # 获取Python之禅的Unicode字符串
    com_bytes = zlib.compress(python_zen.encode('utf-8'))  # 编码为UTF-8格式的字节进行压缩
    print(com_bytes)
    decom_bytes = zlib.decompress(com_bytes)  # 将压缩的字节进行解压缩
    print(decom_bytes.decode('utf-8'))  # 将解压缩的字节进行UTF-8解码得到Unicode字符串

生成器

#用生成器求阶层的和
def rank(n):
    res = 1
    for i in range(1, n+1):
        res *= i
        yield res
print(sum(rank(5)))

153        

魔法方法

__call__

实现该方法后,对象可调用(否则不可调用),并且执行的内容即为__call__方法的内容

class Test:
    def __init__(self, name):
        self.name = name

    def __call__(self, *args, **kwargs):
        print(45678913)

t = Test('abc')
t()

45678913

__str__

实现该方法后,打印对象即调用该方法,并打印__str__的返回值

class Test:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f'name:{self.name}'
t = Test('abc')
print(t)

name:abc

__repr__

功能同__str__,当__str__不存在时则会调用该方法,当两个都存在时则调用__str__。

__del__

类似析构函数,对象删除时调用。

继承

来看一个继承的问题

class F:
    def f1(self):
        print('F.f1')
    def f2(self):
        print('F.f2')
        self.f1()
class B(F):
    def f1(sel'f):
        print(B.f1')
obj = B()
obj.f2()

这里会输出

F.f2
B.f1

首先obj是B的对象,因为B没有重写父类的f2方法,所以会继承该方法,首先打印'F.f2',接下来再运行self.f1()方法,那这个f1到底是调用谁的呢?因为这里这个obj是B的对象,因此self其实就是obj(obj又是B的对象),因此调用的就是obj的f1,所以打印'B.f1'。

如果B没有重写,那才会打印'F.f1',因为B直接继承了父类的f1()函数呀。

假如现在变成这样,我们在f1前面加上__

class F:
    def __f1(self):
        print('F.f1')
    def f2(self):
        print('F.f2')
        self.__f1()
class B(F):
    def __f1(self):
        print('B.f1')
obj = B()
obj.f2()

此时就会打印:

F.f2
F.f1

__是python中类方法实现的一种方式(也叫伪私有),这时候F的f1其实叫_F__f1,在定义时就已经进行了统一,self.__f1()就是F的专属方法,其实就是self._F__f1(),因此打印的自然就是'F.f1',

当然我们也可以self._B__f1()那就会打印B中得f1()。


反射

通过字符串来反射/映射到对象的属性或方法上,实例:

class T:
    def test1(self):
        print('test1')
    def test2(self):
        print('test2')
    def test3(self):
        print('test3')

while True:
    t = T()
    method = input('调用哪个方法:')
    if hasattr(t, method):
        getattr(t, method)()
————————————输出
调用哪个方法:test1
test1
调用哪个方法:1
调用哪个方法:test2
test2
调用哪个方法:0


内置函数

hasattr(obj,'name'):判断对象是否存在xx属性

getattr(obj,name):获取对象属性

setattr(obj,name):设置对象属性

exec():执行字符串里面的代码,第一个参数是要执行的代码串,第二个参数是全局的字典,第三个参数是局部字典


@classmethod

单例模式中的作用

单例模式:基于某种方法实例化多次得到同一个实例。此时这些实例指向同一个内存,即同一个实例。

定义一个方法为类方法,此时可以通过类名进行调用,一般在单例模式构造同一个类对象时使用。

用法:

class Mysql:
    __instance = None
    def __init__(self, host, port):
        self.host = host
        self.port = port

    @classmethod
    def from_conf(cls):
        if cls.__instance is None:
            cls.__instance = cls('127.0.0.1', '3306')
        return cls.__instance
obj1 = Mysql.from_conf()
print(id(obj1),obj1)
obj2 = Mysql.from_conf()
print(id(obj2),obj2)
obj3 = Mysql.from_conf()
print(id(obj3),obj3)
——————————————————————————————————————————————————
2091650879392 <__main__.Mysql object at 0x000001E7001B8FA0>
2091650879392 <__main__.Mysql object at 0x000001E7001B8FA0>
2091650879392 <__main__.Mysql object at 0x000001E7001B8FA0>

cls就是当前的类。

继承时的作用

此时@classmethod修饰的类方法用来定义多个构造器。在已经写好初始类的情况下,想再给初始类再新添功能,不需要修改初始类,只要在下一个类内部新写一个方法,使用@classmethod装饰即可。如下面例子所示:


# 初始类
class MyDate:
    def __init__(self, year=0, month=0, day=0):
        self.day = day
        self.month = month
        self.year = year

    def print_date(self):
        print(self.day)
        print(self.month)
        print(self.year)

# 新增功能
class StrIntParam(MyDate):
    @classmethod
    def deal_date(cls, string_date):
        # 第一个参数cls,表示调用当前的类名
        year, month, day = map(int, string_date.split('-'))
        after_date = cls(year, month, day)
        # 返回一个初始化后的类
        return after_date

S = StrIntParam.deal_date("2022-1-1")
S.print_date()
——————————————————————
1
1
2022

元类

一切皆对象,元类就是类的类。类也是一个对象,只要是对象都是调用一个类实例化得到的,即Teacher=元类(...),内置的元类即为type。

自定义类的三个关键组成成分

  • 类名
  • 类的基类们object
  • 类的命名空间

看一下type源码的构造方法:

def __init__(cls, what, bases=None, dict=None): # known special case of type.__init__
    """
    type(object_or_name, bases, dict)
    type(object) -> the object's type
    type(name, bases, dict) -> a new type
    # (copied from class doc)
    """
    pass 

class创建类的原理(type创建类):

1、先拿到类名:'xxx'

2、再拿到类的基类们:(object,)

3、然后拿到类的命名空间(执行类体代码,将产生的名字放到类的名称空间也就是一个字典里,使用exec函数实现)

4、调用元类实例化得到自定义的类:Teacher=type('Teacher',(object),{...})

现在不依赖class 创建一个自定义类,实例如下:

# 1、先拿到类名
class_name = 'Teacher'
# 2、拿到类的基类/父类们:(object)
class_bases = (object,)
# 3、拿到类的名称空间 类的属性、方法 也就是类的体代码

class_body = """
HP = 100
def __init__(self, name, age, sex):
    self.name = name
    self.age = age
    self.sex = sex
def run(self):
    print('%s在跑步'%self.name)
"""
class_dic = {}
exec(class_body, {}, class_dic) # exec会把class_body里面的名称空间放到class_dic中
# 4、调用元类实例化得到自定义的类
Teacher = type(class_name, class_bases, class_dic)
print(Teacher)
————————————————————————————
<class '__main__.Teacher'>

自定义元类

必须继承object,自定义元类要自己写。

但凡继承了type的类才能称之为元类,否则只是一个普通类

class Mymeta(type):        # 自定义的元类,必须继承type
    # 接下来参考type的源码写type(class_name, class_bases, class_dic)
    def __init__(self, class_name, class_bases, class_dic):
        print(self)
        print(class_name)
        print(class_bases)
        print(class_dic)

class Teacher(object,metaclass=Mymeta):    # 通过自定义的元类创造类,必须继承object,同时修改metaclass
    test = 'hzt'
    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex
    def run(self):
        print('%s在跑步' % self.name)
——————————————————————————————————————
<class '__main__.Teacher'>
Teacher
(<class 'object'>,)
{'__module__': '__main__', '__qualname__': 'Teacher', 'test': 'hzt', '__init__': <function Teacher.__init__ at 0x000001FB94B5A0D0>, 'run': <function Teacher.run at 0x000001FB94B5A160>}

接下来可以控制类的产生过程! 

  • 文档注释__doc__
  • 类名必须为驼峰体
class Mymeta(type):
    # 接下来参考type的源码写type(class_name, class_bases, class_dic)
    def __init__(self, class_name, class_bases, class_dic):
        if class_name.islower():
            raise TypeError('类名必须为驼峰体!')

class teacher(object,metaclass=Mymeta):
    test = 'hzt'
    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex
    def run(self):
        print('%s在跑步' % self.name)
_____________________________________________
# 此时会报错,因为teacher是全小写的
Traceback (most recent call last):
  File "E:\code\learning\python核心编程\元类.py", line 29, in <module>
    class teacher(object,metaclass=Mymeta):
  File "E:\code\learning\python核心编程\元类.py", line 27, in __init__
    raise TypeError('类名必须为驼峰体!')
TypeError: 类名必须为驼峰体!

class Mymeta(type):
    # 接下来参考type的源码写type(class_name, class_bases, class_dic)
    def __init__(self, class_name, class_bases, class_dic):
        if class_name.islower():
            raise TypeError('类名必须为驼峰体!')
        doc = class_dic.get('__doc__')
        if doc is None or len(doc) == 0 or len(doc.strip('\n ')) == 0:
            raise TypeError('类的体代码必须有文档注释,且不能为空!')

自定义元类来控制类的调用产生自定义对象

teal = Teacher('123',18,'123')

对象的调用调用了类里面的__call__方法。如teal()就是对象的调用。

而类调用实际上是用了元类的__call__方法。如Teacher()就是类的调用。实际上是调用元类的__call__方法。

class Mymeta(type):
    # 接下来参考type的源码写type(class_name, class_bases, class_dic)
    def __init__(self, class_name, class_bases, class_dic):
        print(123)
    def __call__(self, *args, **kwargs):
        # 这个self就是Teacher这个对象
        # 1.先产生一个空对象__new__可以给Teacher这个类创建一个空对象
        tea_obj = self.__new__(self)
        # 2.执行init方法,完成对象的初始属性操作
        self.__init__(tea_obj, *args, **kwargs) # 这个调用的就是Teacher的init方法,而不是Mymeta的init
        # 接下来tea_obj就有name、age、sex这些属性了,因为args中有
        # 3.返回初始化好的那个对象
        return tea_obj

因此元类Mymeta实例化Teacher,调用Teacher()实际上是调用元类的__call__方法

  1. 先产生一个Teacher的空对象tea_obj
  2. Teacher执行__init__方法,完成对象的初始属性操作
  3. 返回初始化好的那个对象

实例:把对象实例化的属性全部变成隐藏属性

也就是Teacher的属性,在元类进行改造,改造属性名成_Teacher__attr。

class Mymeta(type):
    def __call__(self, *args, **kwargs):
        tea_obj = self.__new__(self)
        # 此处还没经过Teacher的构造函数,所以__dict__为空
        self.__init__(tea_obj, *args, **kwargs)
        # 经过构造函数后,属性即增加,所以__dict__就会有属性了
        tea_obj.__dict__ = {f'_{self.__name__}__{k}': tea_obj.__dict__[k] for k in tea_obj.__dict__}
        return tea_obj

class Teacher(object,metaclass=Mymeta):
    test = 'hzt'
    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex
    def run(self):
        print('%s在跑步' % self.name)

tea = Teacher('123',45,'74859')
print(tea)
print(tea.__dict__)
# print(tea.name)

type、class、object三者关系

type创建了所有类,object也是type创建的,同时type也继承了object,而type由自己创建。python一切皆对象,所有对象皆有type创建。type底层是一个指针,自己指向了自己

元类一般用来做反射


网络

局域网的IP:192.168开头

Socket

简称 套接字,是进程间通信的一种方式,与其他进程通信的一个主要不同是:

它能实现不同主机间的进程间通信,网络上的各种服务大多都是基于Socket完成通信的。

根据socket找到对应的IP(主机),再找到对应的端口,再通过socket向指定ip地址和端口号进行数据传输。

网络类型

IPv4:一般用于pc

IPv6:一般用于移动端

套接字类型(网络协议)

UDP

TCP

python中创建socket

import socket
socket.socket(AddressFamily, Type)

AddressFamily指定网络类型

  • socket.AF_INET

Type指定套接字类型

  • socket.SOCK_DGRAM表示UDP
  • socket.SOCK_STREAM表示TCP

实例:创建一个UDP套接字对象

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

关闭套接字对象:s.close(),要记得关闭,否则端口会被一直占用。

udp数据收发

import socket

def accept():
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 绑定本地信息,第一个参数为空str,默认为本地ip地址
    localhost = ('', 7788)
    # 绑定本地信息
    udp_socket.bind(localhost)
    # 在固定端口接收数据,1024代表当前接收的最大字节数
    # 返回一个元祖,第一个元素是接收的数据,第二个元素是元祖,为(发送方的ip、发送方的端口)
    recv_data = udp_socket.recvfrom(1024)
    # 解析数据,直接对收到的数据decode('utf-8')
    print(recv_data)
    udp_socket.close()

def send():
    # 1.创建udp套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    # 2.发送数据 socket.sendto('data',(ip,端口))
    send_data = ''
    # 编码,防止服务端收到的是乱码
    udp_socket.sendto(send_data.encode('utf-8'),('ip','port'))
    # 上面这个发送时IP虽然固定,但是端口随机,下面绑定一个固定窗口

    # 关闭套接字
    udp_socket.close()

绑定端口发送数据,同样用udp_socket.bind(('',7788))

TCP

tcp协议(传输控制协议),是一种面向连接的、可靠的、基于字节流的传输层通信协议。

tcp协议通信三步骤:创建连接、数据传送、终止连接。

特点

  • 面向连接
    • 必须先建立连接才能传输通信
    • 一对一,不适用广播的应用程序
  • 可靠传输
    • 发送的消息接收方一定可以收得到,UDP不可靠
    • 采用发送应答机制,发送的报文必须得到接收方的应答才认为传输成功
    • 超时重传
    • 校验数据包是否完整
    • 流量控制:避免主机发的过快

TCP与UDP的不同

  • 面向连接(确认有创建三方交握,连接已创建才作传输)
  • 有序数据传输
  • 重发丢失数据包
  • 舍弃重复数据包
  • 无差错的数据传输
  • 阻塞/流量控制

TCP客户端

import socket


def main():
    # 1.创建tcp套接字 使用SOCK_STREAM
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 2.连接服务器
    tcp_socket.connect(('192.168.0.197', 7890))

    send_data = 'test'
    # 3.发送数据
    tcp_socket.send(send_data.encode('utf-8'))

    # 4.关闭套接字
    tcp_socket.close()

TCP服务端

import socket


def main():
    # 1.创建一个套接字
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 2.绑定本地信息,固定接收端口
    tcp_socket.bind(('', 7890))
    # 3.将当前套接字的默认状态改为被动监听   TCP默认状态为发送消息而非接收
    # 参数为最大连接数 客户端的数量
    tcp_socket.listen(128)
    # 4.等待tcp客户端连接
    """
    返回一个元祖
        第一个元素为一个新的套接字
        第二个元素为客户端的地址端口
    """
    new_socket, client_address = tcp_socket.accept()
    print(1, client_address)
    # 5.接收消息 参数为最大字节数
    data = new_socket.recv(1024)
    print(client_address)
    print(data.decode('utf-8'))
    # 6.接收消息成功后可以应答客户端消息
    new_socket.send('ok'.encode('utf-8'))
    # 7.关闭套接字 此时有两个套接字 都要关闭 先关闭内层的在关闭外层的
    new_socket.close()
    tcp_socket.close()

 可以在pycharm先启动服务端的代码,再启动客户端发消息,此时服务端的代码也是能收到的。

TCP服务端循环接收消息

def main():
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_socket.bind(('', 7890))
    tcp_socket.listen(128)
    while True:
        new_socket, client_address = tcp_socket.accept()
        print(f'客户端:{client_address}已连接!')
        while True:
            data = new_socket.recv(1024)
            print(f'>>>{data.decode("utf-8")}')
            if not data:
                break
            new_socket.send('ok'.encode('utf-8'))
        new_socket.close()

多任务

vim简单操作

配置vim缩进以及语法高亮(MAC)
首先创建配置文件
vim ~/.vimrc

mac直接在文件中编写
:set tabstop=4        # 设置tab是四个空格,默认两个
:set shiftwidth=4    # 设置换行也是四个
:syntax on            # 高亮
:set nu!            # 设置行号

linux应该是在底行命令模式下输入这些内容
wq保存

让配置文件生效
source ~/.vimrc

多任务:操作系统可以同时运行多个任务

单核CPU执行多任务是操作系统可以同时运行多个任务。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的

串行实例

import time
import threading

def runtime(fun):
    def wrapper(*args, **kwargs):
        start = time.time()
        fun(*args, **kwargs)
        print(f"runtime:{time.time() - start} seconds")
    return wrapper


def sing():
    for i in range(3):
        print(f"singing...{i}")
        time.sleep(1)

def dance():
    for i in range(3):
        print(f"dance...{i}")
        time.sleep(1)

@runtime
def main():
    sing()
    dance()

if __name__ == '__main__':
    main()

——————————————————运行结果——————————————————
singing...0
singing...1
singing...2
dance...0
dance...1
dance...2
runtime:6.0708043575286865 seconds

多线程实例

@runtime
def main():
    # 创建一个线程对象,target指明执行的任务(函数)
    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)
    t1.start()  # 创建线程并运行(前面只是创建对象,子进程在这里才创建)
    t2.start()
    # 运行玩t2.start之后,主线程并不结束,而是卡着没有代码的这个位置,等待子线程全部运行完毕才退出


————————————————输出结果————————————————
singing...0
dance...0
runtime:0.0019979476928710938 seconds
dance...1
singing...1
singing...2dance...

线程一旦执行,其执行顺序是随机的(但是可以去干预)。并且主子线程无关,各运行各的。

查看线程数量:threading.enumerate()

前面说的在start调用之后才创建线程,这里我们可以通过print(threading.enumerate())去验证即可。

自定义线程类

class MyThread(threading.Thread):
    # 线程运行入口是run,因此要重写run,start就会调用run函数,start和run通过魔方方法绑定
    def run(self):
        print(self.__dict__)
        self._target(*self._args, **self._kwargs)
        pass

global

函数中使用global引用全局变量也要分场景,如果变量本身是不可变类型,则需要使用global,如果是可变类型如list,则不需要使用global也能引用。并且只是修改的情况下才要用global。

线程可共享全局变量,通过global进行修改,也可以创建线程是传入参数给args,不过args必须是个元祖。

线程共享给全局变量时会出现资源竞争,如下面例子所示:

nums = 0
def test1(num):
    global nums
    for i in range(num):
        nums += i

def test2(num):
    global nums
    for i in range(num):
        nums += i
def main():
    t1 = threading.Thread(target=test1, args=(10 ** 6, ))
    t2 = threading.Thread(target=test2, args=(10 ** 6, ))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(nums)
————————————————————输出结果——————————————————
671869098264

很明显这里结果错了,正常应该是999999000000

如何解决此问题,加锁,保证事务执行的原子性。

首先来看不加锁的方式

nums = 0
def test1(num):
    global nums
    for i in range(num):
        nums += i
    print(nums)
def test2(num):
    global nums
    for i in range(num):
        nums += i
    print(nums)
@runtime
def main():
    test1(10 ** 7)
    test2(10 ** 7)
    print(nums)
——————————————————————运行结果————————————————————————
49999995000000
99999990000000
99999990000000
runtime:1.36435866355896 seconds

再来看将锁加在for外面

nums = 0
mutex = threading.Lock()        # 声明一个互斥锁,声明时并不上锁,调用acquire()才会上锁,并调用release()解锁
def test1(num):
    global nums
    # 执行到此方法时上锁,如果对象已被上锁,则阻塞直到锁释放
    mutex.acquire()
    for i in range(num):
        nums += i
    mutex.release()
    print(nums)
def test2(num):
    global nums
    mutex.acquire()
    for i in range(num):
        nums += i
    mutex.release()
    print(nums)
@runtime
def main():
    t1 = threading.Thread(target=test1, args=(10 ** 7, ))
    t2 = threading.Thread(target=test2, args=(10 ** 7, ))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(nums)
————————————————————————输出结果————————————————————————————
49999995000000
99999990000000
99999990000000
runtime:1.382345199584961 seconds

再将锁加到num操作那一行

nums = 0
mutex = threading.Lock()        # 声明一个互斥锁,声明时并不上锁,调用acquire()才会上锁,并调用release()解锁
def test1(num):
    global nums
    # 执行到此方法时上锁,如果对象已被上锁,则阻塞直到锁释放
    for i in range(num):
        mutex.acquire()
        nums += i
        mutex.release()
    print(nums)
def test2(num):
    global nums
    for i in range(num):
        mutex.acquire()
        nums += i
        mutex.release()
    print(nums)
@runtime
def main():
    t1 = threading.Thread(target=test1, args=(10 ** 7, ))
    t2 = threading.Thread(target=test2, args=(10 ** 7, ))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(nums)
————————————————————————输出结果———————————————————————————————
92612760587028
99999990000000
99999990000000
runtime:10.302409410476685 seconds

可以发现,串行是最快的!!!没想到吧,这是因为GIL的存在,导致在CPU密集计算的python中,并没有办法同时运行多个线程,相反,由于切换线程更消耗资源,所以导致耗时大大增加。

并且这里发现test1中的nums是错误的,结果是正确的,因为此时锁在num++这里,而不是像实例二一样加在for外边,所以test1先启动运行完,此时nums也在test2中for加了很多次,所以这里的nums肯定是包括了test2的一部分for循环的结果,所以它的值会大于正常的49999995000000。

死锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

死锁实例:

#coding=utf-8
import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()

        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name+'----do1---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()

        # 对mutexA解锁
        mutexA.release()


class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()

        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name+'----do2---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()

        # 对mutexB解锁
        mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

进程

程序:例如xxx.py这是程序,是一个静态的
进程:一个程序运行起来后,代码+用到的资源 称之为进程,它是操作系统分配资源的基本单元。
不仅可以通过线程完成多任务,进程也是可以的

进程的状态 

  • 就绪态:运行的条件都已经具备,正在等在cpu执行
  • 执行态:cpu正在执行其功能
  • 等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态

python中创建进程

import multiprocessing
def test1():
    for i in range(3):
        print(f"test1:{i}",os.getpid(), os.getppid())
        time.sleep(1)


def test2():
    for i in range(3):
        print(f'test2:{i}',os.getpid(), os.getppid())
        time.sleep(1)

def main():
    p1 = multiprocessing.Process(target=test1)
    p2 = multiprocessing.Process(target=test2)
    print(os.getpid(), os.getppid())
    p1.start()
    p2.start()

查看当前进程PID:os.getpid();查看当前进程父进程的PID: os.getppid()

进程默认下执行顺序同线程,也是随机的。此时在linux中也可以通过ps查看进程PID并用kill杀死进程。

进程与线程的区别

  • 线程占用系统资源比进程少,且线程必须在进程中开启,一个进程可以开启多个线程。
  • 进程无法共享全局变量
  • 进程是系统进行资源分配和调度的一个独立单位.
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
  • 线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。

Process对象使用

Process([group [, target [, name [, args [, kwargs]]]]])

  • target:如果传递了函数的引用,可以任务这个子进程就执行这里的代码
  • args:给target指定的函数传递的参数,以元组的方式传递
  • kwargs:给target指定的函数传递命名参数
  • name:给进程设定一个名字,可以不设定
  • group:指定进程组,大多数情况下用不到

Process创建的实例对象的常用方法:

  • start():启动子进程实例(创建子进程)
  • is_alive():判断进程子进程是否还在活着
  • join([timeout]):是否等待子进程执行结束,或等待多少秒
  • terminate():不管任务是否完成,立即终止子进程

Process创建的实例对象的常用属性:

  • name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
  • pid:当前进程的pid(进程号)

进程间不共享全局变量

# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time

nums = [11, 22]

def work1():
    """子进程要执行的代码"""
    print("in process1 pid=%d ,nums=%s" % (os.getpid(), nums))
    for i in range(3):
        nums.append(i)
        time.sleep(1)
        print("in process1 pid=%d ,nums=%s" % (os.getpid(), nums))


def work2():
    """子进程要执行的代码"""
    print("in process2 pid=%d ,nums=%s" % (os.getpid(), nums))

if __name__ == '__main__':
    p1 = Process(target=work1)
    p1.start()
    p1.join()       # 等待子进程1完成后再继续向下执行代码

    p2 = Process(target=work2)
    p2.start()
——————————————————————输出结果——————————————————————
in process1 pid=6936 ,nums=[11, 22]
in process1 pid=6936 ,nums=[11, 22, 0]
in process1 pid=6936 ,nums=[11, 22, 0, 1]
in process1 pid=6936 ,nums=[11, 22, 0, 1, 2]
in process2 pid=2256 ,nums=[11, 22]

可以发现这里两个进程并不共享nums变量

子进程执行原理:

操作系统中如果主进程创建了一个子进程,操作系统会在内存中开辟一个新的内存容量,存储主进程的所有数据(也就是说主进程有1G数据,子进程理论上也是1G)。所以这里两个进程的nums是互不相干的。

子进程间通过队列共享数据

from multiprocessing import Process, Queue

def writer(q):
    data = [1,2,3,4,5]
    # 向队列中写入数据
    for i in data:
        q.put(i)
    print("writer!", q)

def reader(q):
    res = list()
    while True:
        # 从队列中获取数据
        data = q.get()
        res.append(data)
        if q.empty():   # 判断队列是否为空
            break
    print(f"reader result:{res}")
def main():
    # 创建队列(先进先出)
    queue = Queue()
    p1 = Process(target=writer, args=(queue, ))
    p2 = Process(target=reader, args=(queue, ))
    p1.start()
    p2.start()

if __name__ == '__main__':
    main()
——————————————————————————运行结果——————————————————————————
writer! <multiprocessing.queues.Queue object at 0x000001BC2EC1B0D0>
reader result:[1]

这里发现两个问题:

  • 必须在if __name__ == '__main__':中调用main()去启动程序,如果直接用main()启动程序会报错
  • 这个结果有问题!

Queue对象说明与使用

初始化Queue()对象时(例如:q=Queue()),若括号中没有指定最大可接收的消息数量,或数量为负值,那么就代表可接受的消息数量没有上限(直到内存的尽头);

  • Queue.qsize():返回当前队列包含的消息数量;
  • Queue.empty():如果队列为空,返回True,反之False ;
  • Queue.full():如果队列满了,返回True,反之False;
  • Queue.get([block[, timeout]]):获取队列中的一条消息,然后将其从列队中移除,block默认值为True;

1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果为空,此时程序将被阻塞(停在读取状态),直到从消息列队读到消息为止,如果设置了timeout,则会等待timeout秒,若还没读取到任何消息,则抛出"Queue.Empty"异常;
2)如果block值为False,消息列队如果为空,则会立刻抛出"Queue.Empty"异常;

  • Queue.get_nowait():相当Queue.get(False)
  • Queue.put(item,[block[, timeout]]):将item消息写入队列,block默认值为True;

1)如果block使用默认值,且没有设置timeout(单位秒),消息列队如果已经没有空间可写入,此时程序将被阻塞(停在写入状态),直到从消息列队腾出空间为止,如果设置了timeout,则会等待timeout秒,若还没空间,则抛出"Queue.Full"异常;
2)如果block值为False,消息列队如果没有空间可写入,则会立刻抛出"Queue.Full"异常;

  • Queue.put_nowait(item):相当Queue.put(item, False);

队列常用场景:爬虫、消息队列(网站验证码)

进程池使用

from multiprocessing import Pool
import os
import time
import random

def worker(msg):
    # 1.获取时间节点
    start = time.time()
    print(f"worker:{msg}进程号:{os.getpid()}")
    time.sleep(random.random() * 2)
    stop = time.time()
    print(f'{msg}执行完毕,耗时{(stop - start):.2f}')

if __name__ == '__main__':
    # 创建一个进程池,最大有三个进程同时运行
    po = Pool(3)
    for i in range(0, 10):  # 10个任务,此时任务就需要排队了 因为一次只能执行三个,等某一个执行完出池后下一个任务才会进池执行
        po.apply_async(worker, (i,))  # 异步添加任务,不一定按照0123456789的顺序加

    print("start")
    po.close()  # 关闭进程池 不在创建进程 但是已经创建的进程需要进行运行
    po.join()  # 等待进程池的进程执行完毕后关闭代码
    print('end')
——————————————————————————运行结果————————————————————————————
start
worker:0进程号:10024
worker:1进程号:10904
worker:2进程号:10152
0执行完毕,耗时0.21
worker:3进程号:10024
2执行完毕,耗时0.74
worker:4进程号:10152
1执行完毕,耗时1.07
worker:5进程号:10904
4执行完毕,耗时1.18
worker:6进程号:10152
3执行完毕,耗时1.74
worker:7进程号:10024
6执行完毕,耗时0.11
worker:8进程号:10152
5执行完毕,耗时1.65
worker:9进程号:10904
7执行完毕,耗时1.49
8执行完毕,耗时1.90
9执行完毕,耗时1.62
end

这里要注意为什么多进程时要把代码放到if __main__里面呢?

因为多进程就是启动了多个python解释器,每个解释器都会导入这个脚本。复制一份全局变量和函数给子进程用,有了if __name__后面的代码就不会被import,也就不会被重复执行。否则,这个创建多进程的代码会被import,导致无限递归去创建子进程,最后会报RuntimeError。

多进程踩坑 

不要在主进程创建对象,然后传到子进程去使用这个对象,会出问题。更准确来说应该是不能传递套接字(比如一个数据库的链接对象)到子进程中,因为这类对象并没有在内存中,不是内部数据状态,会报错非序列化异常,但如果是加载到内存中的资源是可以的

而是在各个子进程创建对象。


多任务——协程

迭代器 

迭代是访问集合元素的一种方式。迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。3

可迭代对象

可以通过for in这类语句迭代读取一条数据的对象称为可迭代对象(Iterable).

class Student:
    def __init__(self):
        self.names = list()

    def add(self, name):
        self.names.append(name)

stu = Student()
stu.add('a')
stu.add('b')
stu.add('c')
# 循环打印所有学生名字
for i in stu:
    print(i)
————————————————————————————————————————————————————
TypeError: 'Student' object is not iterable

可以发现这里报错不是可迭代对象,所以无法通过for in循环输出数据。

判断对象是否为可迭代对象

from collections.abc import Iterable
print(isinstance(stu, Iterable))
————————————————————————输出结果——————————————————————————
False

让对象成为可迭代对象,需要实现__iter__方法

但是此时还不能用for in去遍历元素,因为for in遍历的必须要是一个迭代器。

from collections.abc import Iterable
class Student:
    def __init__(self):
        self.names = list()

    def add(self, name):
        self.names.append(name)

    def __iter__(self):
        pass
stu = Student()
stu.add('a')
stu.add('b')
stu.add('c')
# # 循环打印所有学生名字
for i in stu:
    print(i)
print(isinstance(stu, Iterable))
——————————————————————输出结果————————————————————————————
Traceback (most recent call last):
  File "E:\code\learning\python核心编程\协程\可迭代对象.py", line 17, in <module>
    for i in stu:
TypeError: iter() returned non-iterator of type 'NoneType'
True

此时已经是可迭代对象了,但不是迭代器

判断对象是否为迭代器

from collections.abc import Iterator
print(isinstance(stu, Iterator))
————————————————————————————————————————————
False

可迭代对象和迭代器的区别

可迭代对象迭代的过程中,每一次迭代都会返回对象的下一条数据,一直向后读取数据直到迭代了所有数据后结束。在这个过程中,应该有一个"人"去记录每次访问到了第几条数据,以便每次迭代都可以返回下一条数据。因此我们称这个帮助迭代数据的“人”称为"迭代器"。

可迭代对象的本质就是可以向我们提供一个这样的中间“人”即迭代器帮助我们对其进行迭代遍历使用。

可迭代对象通过__iter__方法向我们提供一个迭代器,我们在迭代一个可迭代对象的时候,实际上就是先获取该对象提供的一个迭代器,然后通过这个迭代器来依次获取对象中的每一个数据.

也就是说,一个具备了__iter__方法的对象,就是一个可迭代对象。但还不是迭代器,对象实现了__next__方法后,这个对象才是个迭代器。那此时我们只需在当前对象的__iter__中return self,即对象就是一个迭代器了。

__next__就是用来返回每次for in迭代的数据。

next()和iter()

iter()会返回一个可迭代对象的迭代器,其实就是调用了对象的__iter__方法。

next()可以从迭代器中获取下一条数据,其实就是调用了迭代器的__next__方法。

注:

并不是只有for in能接受可迭代对象,类似list(),tuple()都能接受可迭代对象并且转换成相应的对象类型。

斐波那契数列迭代器

class Fib:
    def __init__(self, n):
        self.n = n
        self.current = 0    # 当前计数器,表示循环到第几个元素了
        self.a = 0
        self.b = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < self.n:
            self.res = self.a
            self.a, self.b = self.b, self.a + self.b
            self.current += 1
            return self.res
        else:
            raise StopIteration


print(list(Fib(10)))
————————————————————运行结果——————————————————————
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

生成器

在使用生成器实现的方式中,我们将原本在迭代器__next__方法中实现的基本逻辑放到一个函数中来实现,但是将每次迭代返回数值的return换成了yield,此时新定义的函数便不再是函数,而是一个生成器了。简单来说:只要在def中有yield关键字的 就称为 生成器

生成器理解:生成一个值的模板。

利用迭代器,我们可以在每次迭代获取数据(通过next()方法)时按照特定的规律进行生成。但是我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合next()函数进行迭代使用,我们可以采用更简便的语法,即生成器(generator)。生成器是一类特殊的迭代器。

py3创建生成器实例:(x for x in range(10)),此时这个对象就是一个生成器(generator)而不是一个元祖,只能通过for去遍历。

而[x for x in range(10)]称列表推导式,其并不是一个生成器,该对象是一个list。

  • yield的作用
    • 保存当前运行状态(断点),然后暂停执行,即将生成器(函数)挂起
    • 将yield关键字后面表达式的值作为返回值返回,此时可以理解为起到了return的作用
  • 可以使用next()函数让生成器从断点处继续执行,即唤醒生成器(函数)
  • Python3中的生成器可以使用return返回最终运行的返回值,而Python2中的生成器不允许使用return返回一个返回值(即可以使用return从生成器中退出,但return后不能有任何表达式

斐波那契数列生成器

def fib(n):
    current = 0
    num1, num2 = 0, 1
    while current < n:
        # 将函数变成生成器
        yield num1
        num1, num2 = num2, num1 + num2
        current += 1

print(type(fib(10)))
print(list(fib(10)))
————————————————————输出结果————————————————————
<class 'generator'>
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

使用for输出也是调用next()去迭代,每次next()会运行从函数开始到yield前面的代码段,运行完yield返回一个值后函数会暂停在这个位置,yield后面的代码段会在下次迭代时运行。

 yield实现简单协程

协程,又称微线程,纤程。英文名Coroutine。

协程是python个中另外一种实现多任务的方式,只不过比线程更小占用更小执行单元(理解为需要的资源)。 为啥说它是一个执行单元,因为它自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。
通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定。

协程和线程差异

  • 在实现多任务时, 线程切换从系统层面远不止保存和恢复 CPU上下文这么简单。 操作系统为了程序运行的高效性每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作。 所以线程的切换非常耗性能。但是协程的切换只是单纯的操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
import time
def test1():
    while True:
        print('test1-----')
        time.sleep(1)
        yield
def test2():
    while True:
        print('test2-----')
        time.sleep(1)
        yield
def main():
    t1 = test1()
    t2 = test2()
    while True:
        next(t1)
        next(t2)
main()
——————————————
test1-----
test2-----
test1-----
test2-----

似乎并没有啥用啊?输出test1后仍然会等待1s才输出test2,输出test2后也会等待1s才输出test1

协程-greenlet

pip install greenlet

greenlet基于yield,传入得参数会将其变成生成器。

from greenlet import greenlet
import time

def test1():
    while True:
        print("---A--")
        gr2.switch()    # 执行到此处时切换到test2
        time.sleep(3)

def test2():
    while True:
        print("---B--")
        gr1.switch()    # 执行到此处时切换到test1
        time.sleep(3)

# 创建两个协程
gr1 = greenlet(test1)
gr2 = greenlet(test2)

#切换到gr1中运行,先执行gr1即test1
gr1.switch()
————————————————输出结果—————————————————
---A--
---B--
---A--
---B--
---A--
---B--

似乎还是没啥用,首先执行test1,然后切换到test2,切换是很快的,test2又切换回test1,此时test1还要sleep(3),所以好像没啥用(伪多任务,本质还是单线程)。

协程-gevent

greenlet虽然已实现协程,但仍然得人工切换。因此使用能够自动切换任务的gevent模块。

原理:当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行

由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。

安装:pip install gevent

import gevent

def f(n):
    for i in range(n):
        # 获取当前对象的引用
        print(gevent.getcurrent(), i)

# 创建一个协程对象,f就是函数的引用,5就f的参数
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()   # 启动
g2.join()
g3.join()
——————————————————————————————————————
<Greenlet at 0x28bc486de10: f(5)> 0
<Greenlet at 0x28bc486de10: f(5)> 1
<Greenlet at 0x28bc486de10: f(5)> 2
<Greenlet at 0x28bc486de10: f(5)> 3
<Greenlet at 0x28bc486de10: f(5)> 4
<Greenlet at 0x28bc6bba040: f(5)> 0
<Greenlet at 0x28bc6bba040: f(5)> 1
<Greenlet at 0x28bc6bba040: f(5)> 2
<Greenlet at 0x28bc6bba040: f(5)> 3
<Greenlet at 0x28bc6bba040: f(5)> 4
<Greenlet at 0x28bc6bba150: f(5)> 0
<Greenlet at 0x28bc6bba150: f(5)> 1
<Greenlet at 0x28bc6bba150: f(5)> 2
<Greenlet at 0x28bc6bba150: f(5)> 3
<Greenlet at 0x28bc6bba150: f(5)> 4

可以发现这里是顺序执行的,并不是多任务,按照g1、g2、g3的顺序执行

如果实现协程让其切换任务勒,在函数中使用gevent.sleep()进行休眠,此时才会切换。

import gevent

def f(n):
    for i in range(n):
        # 获取当前对象的引用
        print(gevent.getcurrent(), i)
        gevent.sleep(1)    # 必须调用gevent的内置延时方法

# 创建一个协程对象,f就是函数的引用,5就f的参数
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()   # 启动
g2.join()
g3.join()
————————————————————————————————————————————
<Greenlet at 0x27fdee7de10: f(5)> 0
<Greenlet at 0x27fe110a040: f(5)> 0
<Greenlet at 0x27fe110a150: f(5)> 0
<Greenlet at 0x27fdee7de10: f(5)> 1
<Greenlet at 0x27fe110a040: f(5)> 1
<Greenlet at 0x27fe110a150: f(5)> 1
<Greenlet at 0x27fdee7de10: f(5)> 2
<Greenlet at 0x27fe110a040: f(5)> 2
<Greenlet at 0x27fe110a150: f(5)> 2
<Greenlet at 0x27fdee7de10: f(5)> 3
<Greenlet at 0x27fe110a040: f(5)> 3
<Greenlet at 0x27fe110a150: f(5)> 3
<Greenlet at 0x27fdee7de10: f(5)> 4
<Greenlet at 0x27fe110a040: f(5)> 4
<Greenlet at 0x27fe110a150: f(5)> 4

也是伪协程,单线程,只是切换快。

如果一定要用其他的延时方法或者IO等待怎么办?

打补丁,使用gevent.monkey.patch_all()

import gevent
import time
from gevent import monkey
monkey.patch_all()    # 打补丁,猴子补丁

def f(n):
    for i in range(n):
        # 获取当前对象的引用
        print(gevent.getcurrent(), i)
        time.sleep(1)

# 创建一个协程对象,f就是函数的引用,5就f的参数
g1 = gevent.spawn(f, 2)
g2 = gevent.spawn(f, 2)
g3 = gevent.spawn(f, 2)
g1.join()   # 启动
g2.join()
g3.join()
____________________________________________
<Greenlet at 0x232a3e0abf0: f(2)> 0
<Greenlet at 0x232a3e0ad00: f(2)> 0
<Greenlet at 0x232a3e0ae10: f(2)> 0
<Greenlet at 0x232a3e0abf0: f(2)> 1
<Greenlet at 0x232a3e0ad00: f(2)> 1
<Greenlet at 0x232a3e0ae10: f(2)> 1

实例-gevent协程下载图片

import urllib.request
import gevent
from gevent import monkey

monkey.patch_all()


def downloader(img_name, img_url):
	req = urllib.request.urlopen(img_url)

	img_content = req.read()

	with open(img_name, "wb") as f:
		f.write(img_content)

def main():
	gevent.joinall([
	        gevent.spawn(downloader, "3.jpg", "https://rpic.douyucdn.cn/appCovers/2017/09/22/1760931_20170922133718_big.jpg"),
	        gevent.spawn(downloader, "4.jpg", "https://rpic.douyucdn.cn/appCovers/2017/09/17/2308890_20170917232900_big.jpg")
	])


if __name__ == '__main__':
	main()

协程本质是一个生成器,而生成器是一个特殊的迭代器,生成器可以通过yield进行任务暂停,从而切换任务。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值