NumPy笔记(1)—— 多维数组对象ndarray
参考:《利用python进行数据分析》第4章注意,由于本文是jupyter文档转换来的,代码不一定可以直接运行,有些注释是jupyter给出的交互结果,而非运行结果!!文章目录1. 引言1.1 关于NumPy1.2 NumPy的特点1.3 NumPy的主要用途1.4 说明2. ndarray2.1 生成ndarray2.1.1 array方法2.1.2 生成方法概览1. 引言1.1 关于NumPy
- 参考:《利用python进行数据分析》第4章
- 注意,由于本文是jupyter文档转换来的,代码不一定可以直接运行,有些注释是jupyter给出的交互结果,而非运行结果!!
文章目录
1. 引言
1.1 关于NumPy
- Python中的数组计算防暑要追溯到1995年,虽然有许多编程社区开始利用python进行数组编程,但相关类库的生态一直是碎片化的,python自己的内置
array
模块也不是很流行。2005年,NumPy 基于当时的 Numeric 和 Numarray 项目诞生,从此将社区整合到一个框架下。 - NumPy是python数值计算中最重要的基础库,大多数数值计算库都提供了基于NumPy的科学函数功能,NumPy某种程度上成为了各个库之间数据交换的通用语
1.2 NumPy的特点
- 提供了高效多维数组
ndarray
,以及基于它的算数操作和广播功能,灵活简便 - 允许用户进行快速的矩阵运算,而无需编写循环程序
- 允许用户对硬盘中的数据进行读写,并对内存映射文件进行操作
- 提供了线性代数计算、随机数生成、傅里叶变换等功能
- 提供了一套非常易用的C语言API,允许用户使用Python对存量C/C++/Fortran代码库进行封装,并为其提供动态易用的接口
1.3 NumPy的主要用途
- 在数据处理、清洗、构造子集、过滤、变换及其他计算中进行快速向量化运算
- 常见的数组算法,比如
sort
、unique
、set
操作等 - 在数据处理时高效地统计、概述数据
- 进行数据排列及相关的数据操作,比如对异构数据进行
merge
和join
- 使用数组方式来表明条件逻辑,代替 if-elif-else 条件分支的循环
- 对一组数据进行操作(如聚合、变换、函数式操作等)
1.4 说明
- 虽然NumPy提供了一些基础的数值处理方法,但人们通常使用pandas库作为数据统计和分析的基石,NumPy通常被用作一个数据容器,就好像python中的列表或元组一样。
- NumPy的流行主要是因为以下几个原因
- NumPy的设计对于含有大量数组的数据非常有效
- NumPy内部的数据被存储在连续的内存块上,这与python中的其他内建数据结构不同
- NumPy库使用C语言编写,在操作数据内存时,不需要任何数据检查或其他管理操作
- Numpy可以针对全量数组进行复杂计算而避免编写Python循环语句
2. ndarray
ndarray
是NumPy提供的n维数组对象,是快速、灵活的大型数据容器,允许用户使用类似标量的操作语法在整块数据上进行数学计算ndarray
是通用的多维同类数据容器,它包含的所有元素都是相同类型的,每个ndarray
对象都有以下两个属性.shape
:描述数组每一维度的数量dtype
:描述数组的数据类型
2.1 生成ndarray
2.1.1 array方法
.array
方法接受任意的序列型对象(列表、元组、其他ndarray…),生成一个新的包含传递数据的ndarray
对象.array
方法有以下特点- 对于等长嵌套序列,将自动转换为多维数组,否则报警告
- 除非显示地指定,否则自动推断生成数组的数据类型
- 示例
- 自动推断类型
import numpy as np arr = np.array([1,2,3]) print(arr) # [1,2,3] print(arr.dtype) # int32
- 手动设定类型导致类型转换
import numpy as np arr = np.array([1,2,3],dtype = np.float64) # 手动设定类型导致类型转换 print(arr) # [1. 2. 3.] print(arr.dtype) # float64 arr = np.array([1.1,2.2,3.3],dtype = np.int) # 手动设定类型导致类型转换 print(arr) # [1 2 3] print(arr.dtype) # int32
- 转多维数组
arr = np.array([[1,2,3],[4,5,6]]) print(arr.shape) # (2, 3) print(arr.ndim) # 2 这个是维度数量,就是.shape中的元素数量 print(arr) ''' [[1 2 3] [4 5 6]] '''
- 自动推断类型
2.1.2 生成方法概览
- 数组生成方法
方法名 描述 array
将输入数据(列表、元组、数组及其他序列)转换为 ndarray
,如不显式指明数据类型,将自动推断;默认复制所有输入数据asarray
将输入转换为 ndarray
,但若输入已经是ndarray
则不再复制arange
python内置 range
函数的ndarray
版本,返回一个ndarray
ones
根据给定形状和数据类型生成全 1
数组ones_like
根据给定的数组生成一个形状一样的全 1
数组zeros
根据给定形状和数据类型生成全 0
数组zeros_like
根据给定的数组生成一个形状一样的全 0
数组empty
根据给定形状生成一个没有初始化数值的空数组(通常是0,但也可能是一些未初始化的垃圾数值) empty_like
根据给定的数组生成一个形状一样但没有初始化数值的空数组 full
根据给定形状和数据类型生成指定数值的数组 full_like
根据给定的数组生成一个形状一样但内容是指定数值的数组 eye,identity
生成一个 NxN 特征矩阵(对角线位置都是1,其余位置为0) - 除了
.array
方法可能自动推断类型外,其他方法生成的数组,除非显示指定,否则默认为float64
类型 - 示例
In: np.zeros(10) Out: array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) In: np.zeros((3,6)) Out: array([[0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0.]]) In: np.ones_like(np.zeros((3,6))) Out: array([[1., 1., 1., 1., 1., 1.], [1., 1., 1., 1., 1., 1.], [1., 1., 1., 1., 1., 1.]]) In: np.empty((1,2)) Out: array([[-2.95125736e-193, 8.97478633e-100]]) In: np.full((2,2,2),5) Out: array([[[5, 5], [5, 5]], [[5, 5], [5, 5]]]) In: np.arange(10) Out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
2.2 ndarray的数据类型
2.2.1 数据类型一览
- NumPy的数据类型和C语言基本是对应的,这方便了其与C/C++/Fortran等语言进行数据互通
2.2.2 说明
- 数据类型(即
dtype
)是一个特殊的对象,包含了ndarray需要为某种类型数据所申明的内存块信息。这是一种“元数据”,即表示数据的数据 dtype
是ndarray可以和其他系统数据灵活交互的原因- 可以使用
astype
方法显式地转换ndarray的数据类型- 把浮点类型转为整型会截断小数点后部分
- 类型转换失败时抛出
ValueError
(比如string_
转float64
时) astype
方法总是生成一个新数组,即使传入的dtype
与之前一样
- 示例
In: # 浮点型转整型 float_arr = np.array([1.1,2.2,3.3]) print(float_arr.dtype) int_arr = float_arr.astype(np.int) print(int_arr.dtype) print(int_arr) # 小数部分发生截断 Out:float64 int32 [1,2,3] In: # astype方法返回新数组,内存地址改变 print(id(float_arr)) print(id(int_arr)) Out:1736769459424 1736769459184 In: # 整型转浮点型 int_arr = np.arange(10) # int calibers = np.array([1.1,2.2]) # float64 int_arr.astype(calibers.dtype) # 显式转换类型 Out:array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
2.3 NumPy 数组算数
- 数组之所以重要是因为它允许你进行批量操作而无需任何for循环,NumPy称这种特性为
向量化
。 - 任意两个等尺寸数组之间的算数操作都应用了逐元素操作的方式
- 示例
In: arr = np.array([[1,2,3],[4,5,6]],dtype = np.float64) arr Out:array([[1., 2., 3.], [4., 5., 6.]]) In: arr * arr Out:array([[ 1., 4., 9.], [16., 25., 36.]]) In: arr - arr Out:array([[0., 0., 0.], [0., 0., 0.]]) In: 1 / arr Out:array([[1. , 0.5 , 0.33333333], [0.25 , 0.2 , 0.16666667]]) In: arr**0.5 Out:array([[1. , 1.41421356, 1.73205081], [2. , 2.23606798, 2.44948974]]) In: arr2 = np.array([[4,5,6],[1,2,3]],dtype = np.float64) arr > arr2 Out:array([[False, False, False], [ True, True, True]])
2.4 NumPy 索引与切片
- NumPy中主要有三种索引方式:基础索引、布尔索引、神奇索引(花式索引)
- 对切片赋值等价于对切片中所有元素赋值
2.4.1 基础索引
- 数组的基础索引是原数组的 “视图”,切片中的数据并不是原数据的副本,任何对切片的操作都会作用于原数组。这是因为NumPy被设计成适合处理超大数组,如果都是复制数据的话容易引起内存问题
- 如果明确需要数据的拷贝而不是视图,就必须使用
.copy()
方法显式地复制
2.4.1.1 一维数据
- 一维数组的基础索引方法和python列表很类似
- 示例
# 一维数组的索引/切片和python列表很类似 arr = np.arange(10) print(arr) # [0 1 2 3 4 5 6 7 8 9] print(arr[5]) # 5 print(arr[5:8]) # [5 6 7] print(arr[5:]) # [5 6 7 8 9] print(arr[:5]) # [0 1 2 3 4] print(arr[:]) # [0 1 2 3 4 5 6 7 8 9] # 任何对切片的操作都会作用于原数组 arr_slice = arr[5:8] arr_slice[1] = 12 print(arr) # [ 0 1 2 3 4 5 12 7 8 9] arr_slice[:] = 13 print(arr) # [ 0 1 2 3 4 13 13 13 8 9] # 使用.copy()方法获得副本 print(id(arr)) # 1288501881232 print(id(arr.copy())) # 1288501882672 # 对切片赋值等价于对切片中所有元素赋值 arr[:] = 0 print(arr) # [0 0 0 0 0 0 0 0 0 0] # 注意区分切片和ndarray数组 arr = 0 print(arr) # 0
2.4.1.2 多维数组
-
n维数组中每个索引对应一个(n-1)维数组
-
要获取某个元素,可以使用递归索引,也可使用索引列表
-
示例
In: # 二维数组中每个索引对应一个一维数组 arr2d = np.array([[1,2,3], [4,5,6], [7,8,9]]) print(arr2d[1]) Out:array([4, 5, 6]) In:# 三维数组中每个索引对应一个二维数组 arr2d = np.array([[[1,2,3], [4,5,6]], [[7,8,9], [10,11,12]]]) arr2d[1] Out:array([[ 7, 8, 9], [10, 11, 12]]) In: # 使用递归索引或索引列表获取某个元素 print(arr2d[1][1]) print(arr2d[1,1]) Out:[10 11 12] [10 11 12]
# 进行多组切片 In: arr = np.array([[1,2,3],[4,5,6],[7,8,9]]) print(arr,'\n') Out:[[1 2 3] [4 5 6] [7 8 9]] In:print(arr[:2],'\n') Out:[[1 2 3] [4 5 6]] In: print(arr[:2,1:],'\n') Out:[[2 3] [5 6]] In: print(arr[1,:2],'\n') Out:[4,5] In: print(arr[:,:1],'\n') Out:[[1] [4] [7]] In: # 对切片赋值等价于对切片中所有元素赋值 arr[:,:1] = 0 print(arr,'\n') Out:[[0 2 3] [0 5 6] [0 8 9]]
2.4.1.3 批量索引
-
有时我们想从数组中选出某些元素并组成特定的形状,可以用以下方法操作。
-
这本质上属于下面 2.4.3 节的神奇索引
pairs = np.array([0,3,2]) dist = np.array([1,2,3,4,5]) print(dist[pairs]) ''' [1 4 3] '''
pairs = np.array([[[0,0],[0,1],[0,2]], [[1,0],[1,1],[1,2]]]) dist=np.array([[0, 0.1, 0.2, 0.3, 0.4], [0, 0.4, 0.3, 0.2, 0.1], [0, 0.4, 0.3, 0.2, 0.1]]) values = dist[pairs[:,:,0], pairs[:,:,1]] print(values) ''' [[0. 0.1 0.2] [0. 0.4 0.3]] '''
2.4.2 布尔索引
-
布尔索引是使用一个布尔
ndarray
作为另一个ndarray
的索引值,常用于选出符合条件的元素,要求作为索引的布尔值数组长度必须和数组轴索引长度一致 -
通常使用条件语句生成布尔索引
- 使用
!=
或~
运算符实现反选 - 对多个布尔值条件进行联合时,不能使用python语法
and
或or
,要使用按位与&
和按位或|
- 使用
-
布尔索引返回的切片是数据拷贝,操作不会作用于原ndarray数。需要注意的是,虽然修改布尔索引返回的数据拷贝不会影响原始数据,但是如果不用一个变量指向切片,或修改切片的切片,而是直接修改切片整体,就会影响原始数据,这一点我还不清楚为什么,求告知!
-
示例
names = np.array(['Bob','Tom','Alice']) score = np.array([[100,90,90], [85,85,85], [90,80,70]]) print('布尔索引是使用一个布尔ndarray作为另一个ndarray的索引值') print(names == 'Bob','\n') print(score[names == 'Bob'],'\n') print(score[[True,True,False]],'\n') #print('作为索引的布尔值数组长度必须和数组轴索引长度一致') #print(score[[True,True]]) print('布尔索引得到的切片可以继续进行切片') print(score[names == 'Bob',:2],'\n') print('反选') print(score[names != 'Bob']) print(score[~(names == 'Bob')],'\n') print('多条件联合选择') mask = (names == 'Bob') | (names == 'Alice') print(score[mask],'\n') ''' 布尔索引是使用一个布尔ndarray作为另一个ndarray的索引值 [ True False False] [[100 90 90]] [[100 90 90] [ 85 85 85]] 布尔索引得到的切片可以继续进行切片 [[100 90]] 反选 [[85 85 85] [90 80 70]] [[85 85 85] [90 80 70]] 多条件联合选择 [[100 90 90] [ 90 80 70]] '''
# 布尔索引返回的是数据拷贝 arr = np.arange(3) print(arr) # [0 1 2] mask = [True,False,True] # 用一个变量指向切片,修改变量,不影响原始数据 arr1 = arr[mask] arr1 = 10 print(arr) # [0 1 2] # 修改切片的切片,不影响原始数据 arr[mask][:] = 10 print(arr) # [0 1 2] # 直接修改切片整体,影响原始数据(不知道为什么???) arr[mask] = 10 print(arr) # [10 1 10]
2.4.3 神奇索引
- 神奇索引是NumPy的术语,用于描述使用整数列表或数组进行数据索引,常用于选出符合特定顺序的子集
- 传递多个索引数组时,会按顺序递归选择(注意使用
,
分隔),类似基础索引,使用负数索引将从尾部开始选择 - 神奇索引返回的切片是数据拷贝,操作不会作用于原ndarray数组
- 需要注意的是,和布尔索引一样,虽然修改神奇索引返回的数据拷贝不会影响原始数据,但是如果不用一个变量指向切片,或修改切片的切片,而直接修改切片整体,就会影响原始数据,这一点我还不清楚为什么,求告知!
- 示例
arr = np.empty((8,4)) for i in range(8): arr[i] = i print('原数组') print(arr,'\n') print('使用一个整数列表或数组选出特定顺序子集') print(arr[[4,3,0,6]],'\n') print('使用负数索引从尾部开始选择') print(arr[[-1,-5]],'\n') print('索引返回拷贝数据') arr[[-1,-5]][:] = 0 print(arr) ''' 原数组 [[0. 0. 0. 0.] [1. 1. 1. 1.] [2. 2. 2. 2.] [3. 3. 3. 3.] [4. 4. 4. 4.] [5. 5. 5. 5.] [6. 6. 6. 6.] [7. 7. 7. 7.]] 使用一个整数列表或数组选出特定顺序子集 [[4. 4. 4. 4.] [3. 3. 3. 3.] [0. 0. 0. 0.] [6. 6. 6. 6.]] 使用负数索引从尾部开始选择 [[7. 7. 7. 7.] [3. 3. 3. 3.]] 索引返回拷贝数据 [[0. 0. 0. 0.] [1. 1. 1. 1.] [2. 2. 2. 2.] [3. 3. 3. 3.] [4. 4. 4. 4.] [5. 5. 5. 5.] [6. 6. 6. 6.] [7. 7. 7. 7.]] '''
# 传递多个索引数组时,会按顺序递归选择 arr= np.arange(32).reshape(8,4) print('原数组') print(arr,'\n') print('中间数组') print(arr[[1,7,5,2]],'\n') print('递归选择') print(arr[[1,7,5,2],[0,0,0,0]],'\n') # 这样选出了中间数组每个元素中第0号 ''' 原数组 [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11] [12 13 14 15] [16 17 18 19] [20 21 22 23] [24 25 26 27] [28 29 30 31]] 中间数组 [[ 4 5 6 7] [28 29 30 31] [20 21 22 23] [ 8 9 10 11]] 递归选择 [ 4 28 20 8] '''
# 通常使用以下方法选出行列子集形成的矩形区域 print('先选出目标行') print(arr[[1,7,5,2]],'\n') print('再调整列顺序') print(arr[[1,7,5,2]][:,[0,3,1,2]],'\n') ''' 先选出目标行 [[ 4 5 6 7] [28 29 30 31] [20 21 22 23] [ 8 9 10 11]] 再调整列顺序 [[ 4 7 5 6] [28 31 29 30] [20 23 21 22] [ 8 11 9 10]] '''
2.4.4 数组转置和换轴
-
转置是一种特殊的数据重组形式,可以返回底层数据的视图而不需要复制任何内容
-
数组
.T
属性是一个原数组转置后的视图 -
数组
.transpose(*axes)
方法可以按要求对原数组换轴并返回视图- 对于1维数组,无作用
- 对于2维数组,合法参数只有
.transpose((1,0))
,和.transpose((0,1))
,前者返回转置视图(和.T
相同),后者无变化(原先第0轴变为第0轴,原先第1轴变为第1轴)。转置是换轴的一个特例 - 对于n维数组,如果指定了轴参数,则其顺序指示如何排列轴;如果未提给出轴参数,则对于
a.shape =(i[0],i[1],...,i[n-1])
,a.transpose().shape =(i[n-1],i[n-2],...,i[0])
-
数组
.swapaxes(axis1, axis2)
方法交换指定的两个轴,返回新视图 -
示例
import numpy as np arr = np.arange(15).reshape((5,3)) print('原矩阵') print(arr,'\n') print('转置矩阵') print(arr.T,'\n') print(arr.swapaxes(0,1),'\n') print('内积') print(np.dot(arr.T,arr)) ''' 原矩阵 [[ 0 1 2] [ 3 4 5] [ 6 7 8] [ 9 10 11] [12 13 14]] 转置矩阵 [[ 0 3 6 9 12] [ 1 4 7 10 13] [ 2 5 8 11 14]] [[ 0 3 6 9 12] [ 1 4 7 10 13] [ 2 5 8 11 14]] 内积 [[270 300 330] [300 335 370] [330 370 410]] '''
-
二维情况下的换轴就是转置,很好理解,但对于多维情况,就不是那么直观了。不妨把数组索引看作排列组合,换轴前后相同组合的索引对应的数组值相同,即
arr = np.arange(8).reshape((2,2,2)) print('原数组') print(arr,'\n') print('第0个和第1个轴互换,使用transpose方法') print(arr.transpose((1,0,2)),'\n') print('第0个和第1个轴互换,使用swapaxes方法') print(arr.swapaxes(0,1),'\n') print('测试') print(arr[0,1,1],arr.swapaxes(0,1)[1,0,1]) ''' 原数组 [[[0 1] [2 3]] [[4 5] [6 7]]] 第0个和第1个轴互换,使用transpose方法 [[[0 1] [4 5]] [[2 3] [6 7]]] 第0个和第1个轴互换,使用swapaxes方法 [[[0 1] [4 5]] [[2 3] [6 7]]] 测试 3 3 '''
3. 通用函数:快速的逐元素数组函数
- 通用函数,也称为
ufunc
,是一种在ndarray数组中进行逐元素操作的函数。某些简单函数接受一个或多个标量数值,产生一个或多个标量结果。通用函数就是对这些简单函数的向量化封装 - 通用函数分为两类
- 一元通用函数:进行简单的逐元素转换
- 二元通用函数:接受两个数组并返回一个数组作为结果
3.1 一元通用函数
-
总结表
函数名 描述 abs、fabs 逐元素计算整数、浮点数或复数的绝对值 sqrt 计算每个元素的平方根(与arr**0.5相等) square 计算每个元素的平方(与arr**2相等) exp 计算每个元素的自然指数值 e x e^x ex log、log10、log2、log1p 分别对应:自然底数、对数10为底、对数2为底、log(1+x) sign 计算每个元素的符号,整数1;负数-1;0 ceil 每个元素向上取整 floor 每个元素向下取整 rint 元素保留到整数位,并保存dtype modf 分别将数组的小数和整数部分以数组形式返回 isnan 返回数组中的元素是否是NaN,形式为布尔数组 isfinite、isinf 分别返回数组中元素是否有限(非inf,非NaN)、是否是无限的,形式为布尔数组 cos、cosh、sin、sinh、tan、tanh 常规的双曲三角函数 arccos、arccosh、arcsin、arcsinh、arctan、arctanh 反三角函数 logical_not 对数组的元素按位取反(与~arr效果一致) -
部分测试
import numpy as np arr = np.arange(10) print(arr,'\n') print(np.sqrt(arr),'\n') print(np.exp(arr),'\n') print(np.sign(arr),'\n') print(np.cos(arr),'\n') print(np.logical_not(arr),'\n') print(np.modf(arr),'\n') ''' [0 1 2 3 4 5 6 7 8 9] [0. 1. 1.41421356 1.73205081 2. 2.23606798 2.44948974 2.64575131 2.82842712 3. ] [1.00000000e+00 2.71828183e+00 7.38905610e+00 2.00855369e+01 5.45981500e+01 1.48413159e+02 4.03428793e+02 1.09663316e+03 2.98095799e+03 8.10308393e+03] [0 1 1 1 1 1 1 1 1 1] [ 1. 0.54030231 -0.41614684 -0.9899925 -0.65364362 0.28366219 0.96017029 0.75390225 -0.14550003 -0.91113026] [ True False False False False False False False False False] (array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]), array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])) '''
3.2 二元通用函数
- 总结表
函数名 描述 add 将数组相应元素相加 subtract 将数组相应元素相减 multiply 将数组对应元素相乘 divide、floor_divide 除或整除(舍弃余数) power 将第二个数组元素作为第一个数组对应元素的幂次方 maximum、fmax 逐个元素比较,选取其中最大值,fmax忽略NaN minimum、fmin 逐个元素比较,选取其中最小值,fmin忽略NaN mod 按元素的求模计算(即求除法的余数) copysign 第一个数组的符号值改为第二个数组的符号值 greater、greater_equal、less、less_equal、equal、not_equal 逐个元素比较,返回布尔数组 logical_and、logical_or、logical_xor 逐元素进行逻辑运算 - 部分测试
import numpy as np arr = np.arange(10) x = np.arange(10) y = np.arange(9,-1,-1) print(x) print(y,'\n') print('add:',np.add(x,y),'\n') print('substract:',np.subtract(x,y),'\n') print('multiply:',np.multiply(x,y),'\n') print('divide:',np.divide(x,y),'\n') print('floor_divide:',np.floor_divide(x,y),'\n') print('maximum:',np.maximum(x,y),'\n') print('minimum:',np.minimum(x,y),'\n') print('mod:',np.mod(x,y),'\n') print('greater:',np.greater(x,y),'\n') print('logical_and:',np.logical_and(x,y),'\n') ''' [0 1 2 3 4 5 6 7 8 9] [9 8 7 6 5 4 3 2 1 0] add: [9 9 9 9 9 9 9 9 9 9] substract: [-9 -7 -5 -3 -1 1 3 5 7 9] multiply: [ 0 8 14 18 20 20 18 14 8 0] divide: [0. 0.125 0.28571429 0.5 0.8 1.25 2. 3.5 8. inf] floor_divide: [0 0 0 0 0 1 2 3 8 0] maximum: [9 8 7 6 5 5 6 7 8 9] minimum: [0 1 2 3 4 4 3 2 1 0] mod: [0 1 2 3 4 1 0 1 0 0] greater: [False False False False False True True True True True] logical_and: [False True True True True True True True True False] '''
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)