![060990d9b00a8599034c9ace8fafdf60.png](https://i-blog.csdnimg.cn/blog_migrate/dc120f91548bc343f898955474afcfb9.jpeg)
在之前的基本功教程中,在做计算的时候已经反复调用了 Numpy 库。这一篇教程我们不调用 Numpy,而是通过 Python 自带的数据结构和函数运算方法,来了解一下如果不用 Numpy 会受到什么样的限制。
阅读这篇教程前,先完成之前的两篇:
多多教Python:Python 基本功: 3. 数据类型zhuanlan.zhihu.com![a7d2a7e6163f60942ef3443ba413fcd0.png](https://i-blog.csdnimg.cn/blog_migrate/f32b232312cdf8fc01a69c9d67870cb9.png)
![642b0a85ae2db800a347f2a947b965fe.png](https://i-blog.csdnimg.cn/blog_migrate/621f3e049ef83a762e9049724916bd6d.jpeg)
教程需求:
- Mac OS (Windows, Linux 会略有不同)
- 安装了 Python 3.0 版本以上, Anaconda
- 阅读了 多多教Python:Python 基本功: 3. 数据类型,多多教Python:Python 基本功: 11. 初学 Pandas 库。
重温列表 List
在 多多教Python:Python 基本功: 3. 数据类型 教程中我们讲到了列表 List。这里我们继续深入,通过列表来学习 Python 中的几个重要的数据结构操作:切片,拼接,广播和生成器。
切片 Slicing
切片在 Python 中的意思是通过索引,来获取或者改变一个数据结构里的数据元素。下面我们打开 Anaconda Jupyter 笔记本来举一些例子:
In[1]:a = list(range(10))
In[2]:a
Out[2]:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [3]:a[:2]
Out[3]:[0, 1]
In [4]:a[2:]
Out[4]:[2, 3, 4, 5, 6, 7, 8, 9]
In [5]:a[-2:]
Out[5]:[8, 9]
In [6]:a[:]
Out[6]:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [7]:a[2:7]
Out[7]:[2, 3, 4, 5, 6]
In [8]:a[1:-1:2]
Out[8]:[1, 3, 5, 7]
In [9]:a[1::2]
Out[9]:[1, 3, 5, 7, 9]
- 通过 range() 函数创建一个列表,参数10代表从0开始的10个连续数字,赋予变量 a。
- 打印 a。
- 这里的索引 [:2] 指从列表的最开始到索引为2结尾,也就是列表里的第3个位置是结尾。这里注意,Python 的第0个位置索引是0,并且结尾的索引是不包阔在回复里的。数学翻译就是:[0, 2),所以回复的是列表里的第一,第二个位置的元素,0 和1。
- 这里的索引 [2:] 指从列表的第2索引一直到列表最后,也就是列表的第2个位置一直到结束,数学翻译是 [2, End]。因为冒号后面没有数字,所以结尾的索引包括在回复内。
- [-2:] 的意思是从倒数第二个索引开始一直到最后。如果从尾开始数的话,因为尾部是没有0的,所以倒数第二个索引就是倒数第二个位置,数学上翻译就是:[-2, End]。
- [:] 如果只有冒号的话意思是从列表开始一直到列表结束,也就是包含了整个列表。
- [2:7] 顾名思义,从第二个索引位置一直到第七个索引位置,但是不包含第七个索引 a[7]=7。
- [1:-1:2] 前两个数字意思是从第一个索引开始一直到最后一个索引结束,这里注意不包含最后一个索引,后面的2代表了步伐: Step Size,也就是每跳一个索引来获取数字。
- [1::2] 这里中间没有了数字,代表了索引从1开始一直到列表结束,所以包含了最后一个索引9,后面的2仍然代表了步伐,所以回复的列表比上一个回复多了最后一个索引位置9。
如果小伙伴们第一次玩切片的话估计已经晕了,大家可以自己建立一个列表玩一玩熟悉熟悉,接下来的相对简单,叫拼接。
拼接 Concat
拼接就是把几个相同的数据结构连接起来,返回一个数据结构。切片是在一个数据结构里进行操作,而拼接是多个数据结构之间的操作,现在我们来看几个例子:
In [1]:b = list(range(10, 20))
In [2]:b
Out[2]:[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
In [3]:a + b
Out [3]:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
In [4]:a += b
Out [4]:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
In [5]:del a[10:]
In [6]:a
Out[6]:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- 创建一个新的列表,从10开始的10个连续数字,赋予变量 b。
- 同样的,从10开始的10个数不包括20,返还10-19。
- 通过 "+" 运算符,就把两个列表连接起来了。
- "+=" 运算符表示 a + b 并且返还的列表重新赋予到 a。
- 如果从 a 中删除一些数值,则通过 "del" 关键字加上索引出来的列表位置。
- 打印删除数值以后的 a, 我们发现10-19 已经背删除了,回归到 0-9。
广播 Broadcasting
广播是指对数据结构里的每一个位元素实施相同的运算,避免了循环。但是运算法则并没有那么的直观,尤其是在高纬度的数据结构中。这里我们简单介绍一下:
In [1]:a * 2
Out[1]:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [2]:'baba' * 100
Out[2]:'babababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababa'
In [3]:a + 1
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-48-ca42ed42e993> in <module>()
----> 1 a + 1
TypeError: can only concatenate list (not "int") to list
- 这里 a * 2 并不是对 a 的每一个元素乘以2,而是把 a 拷贝了一遍然后附加在后面,运算相当于 a + a。
- 在 多多教Python:Python 基本功: 3. 数据类型 中,我们用同样的方法,对一个字符串 * 100, 等于叫了一百遍爸爸。
- 当我们尝试 a + 1的时候,Python 报错的原因是我们没法对 列表和数字进行拼接 (Concat)。那这里就有疑问 ️了,为什么可以把列表乘以2,而不能加一呢?因为 Python 本身对低纬度和高纬度的数据广播支持度不高。这里列表是一维度数据结构,而一个数字是零纬度,两者相加可以产生的结果有:在 a 列表之后加上一个1的数值,或者对每一个 a 元素进行 +1 的运算。
生成器 Generator
如果我们想要对一个数据结构里的每一个元素进行相同的运算,可以借助 Python 的生成器。生成器是一个特殊的迭代器,在每次迭代获取数据时按照特定的规律进行生成。这里两个关键字是迭代和规律:
- 迭代:一个数据结构里把指定出来的元素一个一个获取。
- 规律:就是我们定义的函数方程。
下面来看两个例子:
In [1]:list(i * 2 for i in a)
Out[1]:[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
In [2]:list(map(lambda x: x + 1, a))
Out[2]:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- 通过 for ... in .... 语句,对列表a的元素进行迭代,迭代出来的元素赋予变量 i,然后通过 i * 2 这个规律对元素进行运算,并且保存在一个新创建的列表中。
- 通过 Python 的 map() 函数,实现的功能和上面一段代码一样。map() 是把一个规律(自己定义的函数) 附加到每一个迭代出来的数值上。map() 接受的第一个参数是一个自定义函数,这里是一个匿名函数 lambda,会在之后讲解,第二个参数是一个可以被迭代的数据类,这里是一个列表。回复结果是匿名函数里定义的对 a 的每一个元素进行 +1 运算。
模仿矩阵,列表中的列表
学过高等数学的都知道,高纬度数学运算都是在矩阵中进行的。那在 Python 中如何保存一个矩阵的数据呢?现在我们已经掌握了列表这个结构,并且学习了其中的运算规则,那我们就来尝试一下通过在列表中再加入一个列表 (一纬变二维) 的方法,来创建,模仿一个矩阵:
In [1]:board = [['_'] * 3 for i in range(3)]
In [2]:board
Out[2]:[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
In [3]:board[1][2] = 'X'
In [4]:board
Out[4]:[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
In [5]:weird_board =[['_'] * 3] * 3
In [6]:weird_board
Out[6]:[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
In [7]:weird_board[1][2] = 'X'
In [8]:weird_board
Out[8]:[['_', '_', 'X'], ['_', '_', 'X'], ['_', '_', 'X']]
In [9]:
board = []
for i in range(3):
row = ['_'] * 3
board.append(row)
In [10]:board
Out[10]:[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
In [11]:board[1][2] = 'X'
In [12]:board
Out[12]:
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
这个示例是从 《流畅的Python》一书中摘录并且修改的,这是一本经典的教科书,并且这个例子也很好,下面我们来解释一下:
- 通过 Python 的生成器语法 for ... in ... 来创建了一个列表,赋予变量 board。注意这里没有用 list() 转换成列表而是用了 "[ ]", 效果是一样的。
- 打印 board,会发现这是一个大列表,里面有三个小列表,代表了一个矩阵里面的三行。
- 我们通过多重索引来修改 board 里的一个数值。第一个层索引 [1] 指向第一个位置列表, 注意不是第零位置。第二层索引 [2] 指向第一个位置列表的第二个元素。
- 修改完后的 board, 我们会看到有一个数值改变成了 'X'。
- 现在我们创建另外一个矩阵,这个方法是通过 广播,首先把 ['_'] 只有一个元素的小列表乘以3,也就是加长了2倍,然后把加长了两倍的列表再乘以3,再加长2倍。
- 打印出来,结构就仿佛和之前通过生成器构建的矩阵一样。
- 现在我们同样的方法修改一个数值。
- 结果发现,三个小列表的第二个索引位置的数值都被修改了?!
- 现在我们通过循环的方法,每次循环创建一个小列表,然后把新创建的小列表通过列表函数 append() 添加在大列表内。
- 赋予的 board 变量打印出来,依旧是一样的结构。
- 一样的修改数值。
- 我们会发现,这次修改的效果又正确了。
这里我们会发现,由于 Python 的内存管理机制,当我们对一个列表进行 乘以3 的运算的时候,Python 只是创建了两个列表指针,指向了同一个列表内存地址。所以当我们进行数值修改的时候,一个内存地址的数据会影响到另外两个指针指向的数据,造成了三个列表同时被更改。
这里我们并不需要去深挖 Python 的内存管理,但是需要知道的是通过列表中的列表来制造一个高纬度数据结构是很容易出错的:
In [1]:board_of_board = [b * 3 for b in board]
In [2]:board_of_board
Out [2]:
[['_', '_', '_', '_', '_', '_', '_', '_', '_'],
['_', '_', 'X', '_', '_', 'X', '_', '_', 'X'],
['_', '_', '_', '_', '_', '_', '_', '_', '_']]
看上面这段代码例子,是不是已经完全凌乱了?
救星 Numpy
如果你的代码里出现了上面 weird_board 变量这样的 Bug,Python 自己是不会给你报错的,你必须一行一行的去 Debug,就像多多教Python:Python 基本功: 8. 异常处理 说的:
能够报错的就不是错,真正的错是不会自己报错的。
所以说 Numpy 在这里成了救星。Numpy 库提供的矢量,矩阵结构,和其对切片,拼接,广播和生成器的优化使得我们可以轻松,安全的对高纬数据进行运算。下面我们来体验一下 Numpy 是如何应对上面几个例子的:
In [1]:import numpy as np
In [2]:a = np.array([1, 2, 3, 4])
In [3]:a * 2 # 广播的方式和 Python 自定的列表重复两遍不一样了
Out[3]:array([2, 4, 6, 8])
In [4]:a + 1 # 同样的,可以直接对每一个元素进行广播
Out[4]:array([2, 3, 4, 5])
In [5]:board = np.array([['_'] * 3 for i in range(3)], ndmin=2)
In [6]:board
Out[6]:
array([['_', '_', '_'],
['_', '_', '_'],
['_', '_', '_']], dtype='<U1')
In [7]:board[1][2] = 'X'
In [8]:board
Out[8]:
array([['_', '_', '_'],
['_', '_', 'X'],
['_', '_', '_']], dtype='<U1')
In [9]:board = np.array([['_'] * 3] * 3, ndmin=2) # Numpy 为你处理了错误的语法
In [10]:board[1][2] = 'X' # 仍然有效,不会同时修改三行
In [11]:board
Out[11]:
array([['_', '_', '_'],
['_', '_', 'X'],
['_', '_', '_']], dtype='<U1')
因为这篇不是专门讲 Numpy 库的教程,所以就不一行一行解释了。通过代码注释我们会发现,Numpy 中的广播会给你带来非常直观的高纬度数学运算:对列表中每一个元素直接乘以2,加一。然后在 [9] 中,原本在 Python 中错误的语法,通过 Numpy 的矢量(Array) 转换,你仍然可以直接修改其中某一个数值,而不会因为内存管理而出错。
小结:
这篇教程通过 Python 自带的数据结构,来了解了在高纬度运算时候,我们遇到的瓶颈和困难。而 Numpy 的出现,则很好的解决了这些问题。在下面的教程中,我们会通过 Numpy 的矢量和矩阵两大数据类,来学习这个科学运算库。有兴趣的小伙伴可以继续看两个外部的链接:
比较多示例的 Python 切片教程:
彻底搞懂Python切片操作www.jianshu.com![485072b5e1a64f7defc3f49e7c21158a.png](https://i-blog.csdnimg.cn/blog_migrate/60311405a4e3bc95c1050d4293716ade.jpeg)
一篇知乎很赞的 Numpy 基础详解:
挖掘机小王子:Python之Numpy基础zhuanlan.zhihu.com![35f9522136d11a0cef8d992811639559.png](https://i-blog.csdnimg.cn/blog_migrate/e0d3940d58d49715b66a2d45057c34c8.jpeg)