理解Python中的数据类型Python代码Python代码Python整型不仅仅是一个整型Python列表不仅仅是一个列表Python中的固定类型数组从Python列表创建数组创建数组从头创建数组NumPy标准数据类型numpy数组的基本操作NumPy数组的属性数组索引:获取单个元素数组切片:获取子数组非副本视图的子数组创建数组的副本数组的变形数组拼接和分裂
《Python数据科学手册》读书笔记
要实现高效的数据驱动科学和计算,需要理解数据是如何被存储和操作的。
Python 的用户往往被其易用性所吸引, 其中一个易用之处就在于动态输入。静态类型的语言(如 C 或 Java) 往往需要每一个变量都明确地声明, 而动态类型的语言(例如 Python) 可以跳过这个特殊规定。例如在C 语言中, 你可能会按照如下方式指定一个特殊的操作:
/* C代码 */
int result = 0;
for(int i=0; i<100; i++){
result += i;
}
而在 Python 中, 同等的操作可以按照如下方式实现:
result = 0
for i in range(100):
result += i
注意这里最大的不同之处:在 C 语言中, 每个变量的数据类型被明确地声明;而在 Python 中, 类型是动态推断的。这意味着可以将任何类型的数据指定给任何变量:
x = 4
x = "four
这里已经将 x 变量的内容由整型转变成了字符串, 而同样的操作在 C语言中将会导致编译错误或其他未知的后果:
/* C代码 */
int x = 4;
x = "four"; // 编译失败
这种灵活性是使 Python 和其他动态类型的语言更易用的原因之一。但是这种类型灵活性也指出了一个事实:Python 变量不仅是它们的 值, 还包括了关于值的类型的一些额外信息。
标准的 Python 实现是用 C 语言编写的。这意味着每一个 Python 对象都是一个聪明的伪 C 语言结构体, 该结构体不仅包含其值, 还有其他信息。例如, 当我们在 Python 中定义一个整型, 例如 x = 10000 时, x并不是一个“原生”整型, 而是一个指针, 指向一个 C 语言的复合结构体, 结构体里包含了一些值。查看 Python 3.4 的源代码, 可以发现整型(长整型) 的定义, 如下所示 :
struct _longobject {
long ob_refcnt;
PyTypeObject *ob_type;
size_t ob_size;
long ob_digit[1];
};
Python 3.4 中的一个整型实际上包括 4 个部分。
这意味着与 C 语言这样的编译语言中的整型相比, 在 Python 中存储一个整型会有一些开销。
两者的差异在于, C 语言整型本质上是对应某个内存位置的标签, 里面存储的字节会编码成整型。而 Python 的整型其实是一个指针, 指向包含这个 Python 对象所有信息的某个内存位置, 其中包括可以转换成整型的字节。 由于 Python 的整型结构体里面还包含了大量额外的信息,所以 Python 可以自由、 动态地编码。但是, Python 类型中的这些额外信息也会成为负担, 在多个对象组合的结构体中尤其明显。
设想如果使用一个包含很多 Python 对象的 Python 数据结构, 会发生什么?Python 中的标准可变多元素容器是列表。可以用如下方式创建一个整型值列表:
L = list(range(10))
L
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
type(L[0])
int
或者创建一个字符串列表:
L2 = [str(c) for c in L]
L2
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
type(L2[0])
str
因为 Python 的动态类型特性, 甚至可以创建一个异构的列表:
L3 = [True, "2", 3.0, 4]
[type(item) for item in L3]
[bool, str, float, int]
但是想拥有这种灵活性也是要付出一定代价的:为了获得这些灵活的类型, 列表中的每一项必须包含各自的类型信息、 引用计数和其他信息;也就是说, 每一项都是一个完整的 Python 对象。来看一个特殊的例子, 如果列表中的所有变量都是同一类型的, 那么很多信息都会显得多余,这时将数据存储在固定类型的数组中应该会更高效。
在实现层面, 数组基本上包含一个指向连续数据块的指针。另一方面,Python 列表包含一个指向指针块的指针, 这其中的每一个指针对应一个完整的 Python 对象 。另外, 列表的优势是灵活, 因为每个列表元素是一个包含数据和类型信息的完整结构体,而且列表可以用任意类型的数据填充。固定类型的 NumPy 式数组缺乏这种灵活性, 但是能更有效地存储和操作数据。
Python 提供了几种将数据存储在有效的、 固定类型的数据缓存中的选项。内置的数组(array) 模块 可以用于创建统一类型的密集数组:
import array
L = list(range(10))
A = array.array('i', L)
A
array('i', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
这里的 'i' 是一个数据类型码, 表示数据为整型。更实用的是 NumPy 包中的 ndarray 对象。Python 的数组对象提供了数组型数据的有效存储, 而 NumPy 为该数据加上了高效的操作。稍后将介绍这些操作, 这里先展示几种创建 NumPy 数组的方法。
import numpy as np
# 查看numpy的版本
np.__version__
'1.15.1'
# 获取numpy教程
np?
首先, 可以用 np.array 从 Python 列表创建数组:
np.array([1,2,3,4,5])
array([1, 2, 3, 4, 5])
numpy要求数组必须包含同一类型的数据,如果类型不匹配将会向上转换
np.array([1.2,2,3,4,5])
array([1.2, 2. , 3. , 4. , 5. ])
可以使用dtype改变数组的数据类型
np.array([1,2,3,4,5],dtype='float32')
array([1., 2., 3., 4., 5.], dtype=float32)
不同于 Python 列表, NumPy 数组可以被指定为多维的。以下是用列表的列表初始化多维数组的一种方法:
np.array([range(i,i+3) for i in [2,4,6]])
array([[2, 3, 4],
[4, 5, 6],
[6, 7, 8]])
内层的列表被当作二维数组的行。
面对大型数组的时候, 用 NumPy 内置的方法从头创建数组是一种更高效的方法。以下是几个示例:
# 创建一个所有值为0,长度为10的数组
np.zeros(10,dtype=int)
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
# 创建一个3行5列所有值为1的浮点数组
np.ones((3,5),dtype=float)
array([[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.]])
# 创建一个3行5列的浮点数组,数组的值都是3.14
np.full((3,5),3.14)
array([[3.14, 3.14, 3.14, 3.14, 3.14],
[3.14, 3.14, 3.14, 3.14, 3.14],
[3.14, 3.14, 3.14, 3.14, 3.14]])
# 创建一个数组从0开始到20结束,步长为2
np.arange(0, 20, 2)
array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
# 创建一个有5个元素的数组,5个数均匀分配到0~1之间
np.linspace(0, 1, 5)
array([0. , 0.25, 0.5 , 0.75, 1. ])
# 创建一个3*3的数组,个元素服从0~1上的均匀分布
np.random.random((3, 3))
array([[0.69872258, 0.0299473 , 0.99635603],
[0.25231312, 0.90566931, 0.30884748],
[0.54470709, 0.64877725, 0.48333009]])
# 创建一个3*3的数组,个元素服从均值为0,标准差为1
np.random.normal(0, 1, (3, 3))
array([[ 0.59378053, -0.82101116, 0.28096713],
[ 0.72447884, -0.29206219, 0.86922837],
[-0.82404414, 1.26396153, 0.66129599]])
# 创建一个3*3的整数数组,个元素服从0~10上的均匀分布
np.random.randint(0, 10, (3, 3))
array([[1, 5, 0],
[9, 6, 5],
[1, 5, 4]])
# 创建一个3*3的单位矩阵
np.eye(3)
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
NumPy 数组包含同一类型的值, 因此详细了解这些数据类型及其限制是非常重要的。
数据类型 | 描述 |
---|---|
bool_ | Boolean (True or False) stored as a byte |
int_ | Default integer type (same as C long; normally either int64 or int32) |
intc | Identical to C int (normally int32 or int64) |
intp | Integer used for indexing (same as C ssize_t; normally either int32 or int64) |
int8 | Byte (-128 to 127) |
int16 | Integer (-32768 to 32767) |
int32 | Integer (-2147483648 to 2147483647) |
int64 | Integer (-9223372036854775808 to 9223372036854775807) |
uint8 | Unsigned integer (0 to 255) |
uint16 | Unsigned integer (0 to 65535) |
uint32 | Unsigned integer (0 to 4294967295) |
uint64 | Unsigned integer (0 to 18446744073709551615) |
float_ | Shorthand for float64. |
float16 | Half precision float: sign bit, 5 bits exponent, 10 bits mantissa |
float32 | Single precision float: sign bit, 8 bits exponent, 23 bits mantissa |
float64 | Double precision float: sign bit, 11 bits exponent, 52 bits mantissa |
complex_ | Shorthand for complex128. |
complex64 | Complex number, represented by two 32-bit floats |
complex128 | Complex number, represented by two 64-bit floats |
确定数组的大小、 形状、 存储大小、 数据类型。
获取和设置数组各个元素的值。
在大的数组中获取或设置更小的子数组。
改变给定数组的形状。
将多个数组合并为一个, 以及将一个数组分裂成多个
首先介绍一些有用的数组属性。定义三个随机的数组:一个一维数组、一个二维数组和一个三维数组。将用 NumPy 的随机数生成器设置 一组种子值, 以确保每次程序执行时都可以生成同样的随机数组:
import numpy as np
np.random.seed(0)
x1 = np.random.randint(10, size=6)
x2 = np.random.randint(10, size=(3, 4))
x3 = np.random.randint(10, size=(3, 4, 5))
print("x3 ndim: ", x3.ndim) #数组维度
print("x3 shape:", x3.shape) #数组每个维度的大小
print("x3 size: ", x3.size) #数组的总大小
print("dtype:", x3.dtype) #数组的数据类型
print("itemsize:", x3.itemsize, "bytes") #每个元素的字节大小
print("nbytes:", x3.nbytes, "bytes") #数组总字节大小
x3 ndim: 3
x3 shape: (3, 4, 5)
x3 size: 60
dtype: int32
itemsize: 4 bytes
nbytes: 240 bytes
x1
array([5, 0, 3, 3, 7, 9])
x1[0]
5
x1[-1]
9
x2
array([[3, 5, 2, 4],
[7, 6, 8, 8],
[1, 6, 7, 7]])
x2[0,0]
3
x2[2,-1]
7
# 修改元素
x2[0,0] = 12
x2
array([[12, 5, 2, 4],
[ 7, 6, 8, 8],
[ 1, 6, 7, 7]])
请注意, 和 Python 列表不同, NumPy 数组是固定类型的。这意味着当你试图将一个浮点值插入一个整型数组时, 浮点值会被截短成整型。 并且这种截短是自动完成的, 不会给你提示或警告, 所以需要特别注意这一点!
x1[0] = 3.14
x1
array([3, 0, 3, 3, 7, 9])
x = np.arange(10)
x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# 前5个元素
x[:5]
array([0, 1, 2, 3, 4])
# 索引5之后的元素,包括索引5
x[5:]
array([5, 6, 7, 8, 9])
x[4:7]
array([4, 5, 6])
x[::2]
array([0, 2, 4, 6, 8])
x[1::2]
array([1, 3, 5, 7, 9])
#将所有元素逆序
x[::-1]
array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
x[5::-2]
array([5, 3, 1])
x2
array([[12, 5, 2, 4],
[ 7, 6, 8, 8],
[ 1, 6, 7, 7]])
# 两行,三列
x2[:2, :3]
array([[12, 5, 2],
[ 7, 6, 8]])
# 所有行,前三列
x2[:, :3]
array([[12, 5, 2],
[ 7, 6, 8],
[ 1, 6, 7]])
x2[::-1, ::-1]
array([[ 7, 7, 6, 1],
[ 8, 8, 6, 7],
[ 4, 2, 5, 12]])
# x2的第一列
x2[:,0]
array([12, 7, 1])
# x2的第一行
x2[0,:]
array([12, 5, 2, 4])
# 等价于print(x2[0,:])
x2[0]
array([12, 5, 2, 4])
关于数组切片有一点很重要也非常有用, 那就是数组切片返回的是数组数据的视图, 而不是数值数据的副本。这一点也是 NumPy 数组切片和 Python 列表切片的不同之处:在 Python 列表中, 切片是值的副本。例如此前示例中的那个二维数组:
x2
array([[12, 5, 2, 4],
[ 7, 6, 8, 8],
[ 1, 6, 7, 7]])
x2_sub = x2[:2,:2]
x2_sub
array([[12, 5],
[ 7, 6]])
# 修改子数组
x2_sub[0, 0] = 3.14
x2_sub
array([[3, 5],
[7, 6]])
# 可以发现原始数组也被修改了!
x2
array([[3, 5, 2, 4],
[7, 6, 8, 8],
[1, 6, 7, 7]])
这种默认的处理方式实际上非常有用:它意味着在处理非常大的数据集时, 可以获取或处理这些数据集的片段, 而不用复制底层的数据缓存。
尽管数组视图有一些非常好的特性, 但是在有些时候明确地复制数组里的数据或子数组也是非常有用的。可以很简单地通过 copy()方法实现:
x2_sub_copy = x2[:2, :2].copy()
x2_sub_copy
array([[3, 5],
[7, 6]])
x2_sub_copy[0, 0] = 5.2
x2_sub_copy
array([[5, 5],
[7, 6]])
# 修改子数组,但原始数组没有发生改变
x2
array([[3, 5, 2, 4],
[7, 6, 8, 8],
[1, 6, 7, 7]])
另一个有用的操作类型是数组的变形。数组变形最灵活的实现方式是通过 reshape() 函数来实现。例如, 如果你希望将数字 1~9 放入一个3×3 的矩阵中, 可以采用如下方法:
grid = np.arange(1, 10).reshape((3, 3))
grid
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
请注意, 如果希望该方法可行, 那么原始数组的大小必须和变形后数组的大小一致。如果满足这个条件, reshape 方法将会用到原始数组的一个非副本视图。但实际情况是, 在非连续的数据缓存的情况下, 返回非副本视图往往不可能实现。
另外一个常见的变形模式是将一个一维数组转变为二维的行或列的矩阵。你也可以通过 reshape 方法来实现, 或者更简单地在一个切片操作中利用 newaxis 关键字:
x = np.array([1, 2, 3])
# 通过变形获得行向量
x.reshape((1, 3))
array([[1, 2, 3]])
# 通过newaxis获得行向量
x[np.newaxis, :]
array([[1, 2, 3]])
# 通过变形获得行向量
x.reshape((3, 1))
array([[1],
[2],
[3]])
# 通过newaxis获得行向量
x[:, np.newaxis]
array([[1],
[2],
[3]])
以上所有的操作都是针对单一数组的, 但有时也需要将多个数组合并为一个, 或将一个数组分裂成多个。接下来将详细介绍这些操作。
拼接或连接 NumPy 中的两个数组主要由 np.concatenate、np.vstack 和 np.hstack 例程实现。np.concatenate 将数组元组或数组列表作为第一个参数, 如下所示:
x = np.array([1, 2, 3])
y = np.array([3, 2, 1])
np.concatenate([x, y])
array([1, 2, 3, 3, 2, 1])
z = [99, 99, 99]
np.concatenate([x, y, z])
array([ 1, 2, 3, 3, 2, 1, 99, 99, 99])
grid = np.array([[1, 2, 3],
[4, 5, 6]])
# 沿着第一个轴拼接
np.concatenate([grid, grid])
array([[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[4, 5, 6]])
# 沿着第二个轴拼接
np.concatenate([grid, grid], axis=1)
array([[1, 2, 3, 1, 2, 3],
[4, 5, 6, 4, 5, 6]])
沿着固定维度处理数组时, 使用 np.vstack(垂直栈) 和np.hstack(水平栈) 函数会更简洁:
x = np.array([1, 2, 3])
grid = np.array([[9, 8, 7],
[6, 5, 4]])
# 垂直栈数组
np.vstack([x, grid])
array([[1, 2, 3],
[9, 8, 7],
[6, 5, 4]])
# 水平栈数组
y = np.array([[99],
[99]])
np.hstack([grid, y])
array([[ 9, 8, 7, 99],
[ 6, 5, 4, 99]])
与之类似, np.dstack 将沿着第三个维度拼接数组。
与拼接相反的过程是分裂。分裂可以通过 np.split、 np.hsplit和 np.vsplit 函数来实现。可以向以上函数传递一个索引列表作为参数, 索引列表记录的是分裂点位置:
x = [1, 2, 3, 99, 99, 3, 2, 1]
x1, x2, x3 = np.split(x, [3, 5])# 用索引列表记录分裂点的位置
print(x1, x2, x3)
[1 2 3] [99 99] [3 2 1]
值得注意的是, N 分裂点会得到 N + 1 个子数组。相关的np.hsplit 和 np.vsplit 的用法也类似:
grid = np.arange(16).reshape((4, 4))
grid
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
upper, lower = np.vsplit(grid, [2])
print(upper)
print(lower)
[[0 1 2 3]
[4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]
left, right = np.hsplit(grid, [2])
print(left)
print(right)
[[ 0 1]
[ 4 5]
[ 8 9]
[12 13]]
[[ 2 3]
[ 6 7]
[10 11]
[14 15]]
np.dsplit将沿数组第三个维度进行分裂