前言
这篇文章内容产生背景是我在学习python过程中的一些个人理解,主要是对python语言的辨识度较高的知识点即语法中比较特殊的地方进行考究与归纳。
适合供已经掌握某一门编程语言或有一定编程知识背景的同学,想快速了解python的朋友来一起学习并分享交流。
P.S.前面基本都是总结现有知识,从第7点“高阶函数”开始属于“灰色地带”,即会带有很多我个人提出的观点以及奇特的思考与理解方式的成分,同时它们也是我下一步想要研究的方向,若有兴趣,欢迎交流讨论,同时欢迎指正!
学习资料:
《python学习手册》(第4版)
《Introduction to the Theory of Computation》
廖海峰python教程
知乎
Google
Content
1. 数据类型-动态语言
什么是动态语言,就是变量本身的数据类型是不固定的,可变的语言。与之相反的就是静态语言,比如JAVA、Objective-C。
声明一个Objective-C的变量为整型,那么之后无论你用任何操作,都不能改变它的数据类型(除非你直接改变它的声明的类型)。
|
|
而python是一种动态语言,因此python没有类型声明,即创建对象的时候你不需要花时间去预先定义它的数据类型,你使用的运行的表达式和语法会帮你决定这个对象的类型。因此在python中,你可以这样:
运行结果:
可以发现a的数据类型在执行a=b后发生了改变。
那么你可能会认为这样可能会有危险,会出现乱套了的情况,因此这就要求,一旦对象创建之后,它就应该和它对应的操作集合绑定了,例如只可以对字符串进行字符串相关操作,对列表进行列表相关操作。
总结上面两点:
- python是动态类型的,它自动地追踪你的类型而不是声明代码。
- python也是强类型语言,你只能对一个对象进行适合该类型的有效的操作。
这里我还想补充的就是,实际中python中的某个类型的操作集合会比想象的要多,比如对于字符串来讲,是可以对两个字符串使用“+”来进行合并的。很多我们在其他语言中不被允许的操作,在python都是合法的,例如重访嵌套:在一个字典中嵌套一个字典或列表,并对嵌入的字典或列表进行扩展,嵌入的字典或列表是字典包含的一部分独立内存,它可以自由地增加或减少。关于对象的内存部署会在之后讨论。
因此,这也就是为什么python使用起来会觉得非常灵活的原因吧。
2. list和tuple
- list
list是一种有序集合,是python内置的一种数据类型,可以随时添加和删除其中的元素。
- 设置一个list
|
|
2.获取list元素的个数
3.用索引来访问list中每一个位置的元素
|
|
要确保索引不要越界。
还可以用负数来表示倒序的索引
4.追加到list到末尾
|
|
5.把元素插入到指定位置
|
|
6.删除list末尾的元素
|
|
7.替换list中某个元素
8.list元素的数据类型可以不同
9.list的元素也可以是list
就是说可以在list里嵌套一个list,并且可以对嵌入的list进行操作,也就是刚才提到的重访嵌套。
访问嵌入的list中的元素
tuple
tuple和list非常相似,只是tuple是一旦初始化后它的元素就不可以被修改的。它没有append(),insert()之类的方法。1>>> students('Tom','Bob','Lucy')tuple使用括号来表示,然而在写代码进行计算的时候括号又可能是数学公式中的小括号
比如: t = (1),这时候t代表1这个数,而不会是一个只有一个元素的tuple。
正确表示只有一个元素的tuple应该是这样:带上逗号123>>> t = (1,)>>> t(1,)还有一个需要注意的地方就是,tuple的元素是不能被修改的,但是当tuple嵌套了list作为元素的时候,这个list的元素还是可以被修改的。
例如:在test.py里定义一个tuple和一个list12t = ([1, 2], 3, 4, 5)l = [6, 7]交互式运行:
1.不能对tuple添加元素123456>>> test.t([1, 2], 3, 4, 5)>>> test.t.insert(4)Traceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: 'tuple' object has no attribute 'insert'2.不能改变tuple中的list这个元素
这里尝试把tuple中的第一个list元素替换成另一个list l,失败。1234>>> test.t[0] = test.lTraceback (most recent call last):File "<stdin>", line 1, in <module>TypeError: 'tuple' object does not support item assignment3.可以改变tuple中的list中的元素
12345>>> test.t[0][0]1>>> test.t[0][0] = 3>>> test.t[0][0]3
使用tuple的好处就在于因为它是不可变的,因此代码会更安全。
3. dict和set
- dict
python内置的字典dict,在其他语言中也被称作map,就是使用键-值(key-value)存储数据,便于查找。dict用一对花括号表示,一个键值对用冒号区分键与值:
|
|
一个key只能对应一个value,重复对一个key放入value会把之前的value覆盖掉。
小提示,如果实际情况是想要同一个key对应多个value,那不妨把这个key对应的value设置成一个list就好了,把想要存放的这些多个数值作为这个list中的元素。
然而必须要注意的一点是,dict中的key必须是不可变对象,例如python中的字符串、整型都是不可变对象,因此可以放心作为key来使用,但是绝对不能把key设置成一个可变对象list。这是因为dict中会根据key来计算value存储的位置。如果key是一个可变的对象,那么当这个key发生改变,用原来的查找算法的计算结果就不同,那dict的查找机制就乱套了,就会出错。这种通过key计算位置的算法便是哈希算法(Hash)。
另外,访问一个不存在的key是会报错的。
以上其实和一般的字典都差不多。
- set
set其实就是我们数学中学到的集合的概念,它的一个重要特性就是没有重复的元素。也可以理解为它是一个存放key的集合,但不存储value。可以重复添加元素,但它会自动过滤掉重复的元素。123>>> s = set([2,3,4,5])>>> sset([2,3,4,5])
既然set是一组key的集合,那么自然这些key同样也就不可以是可变的,这和dict是一样的。
4.不可变对象
list是一个可变对象,而str(字符串)是一个不可变对象。那么是否意味着我们就不能对一个str进行更改的操作呢?实际上,我们是可以用replace()这样的方法去引用str对象所指向的内容来进行更改操作,然而这个对象本身却不会改变。
例子:
执行完replace()方法的时候,确实得到了一个改变后的句子,然而再访问a对象时,会发现它没有变,这就是不可变对象。实际上我们在对它进行更改操作的时候,可以理解为是把对象a复制为一个新对象b,然后更改操作是对b对象所指的内容进行更改。
5. 切片
切片(Slice)是python中非常常用的一个操作。它用于对一个list或者tupe取其一部分的元素。
切片操作符使用非常简单,就是一个取制定索引的符号:[:]
[0:3]表示取前3个元素
以此类推,[1:3]就是从第一个元素(第一个不算)开始取两个元素,[2:3]就是取第三个元素。
又根据python支持用L[-1]表示倒序的第一个,那么它同样也可以支持倒序切片:
L[-2:]表示从倒数第二个开始(包括倒数第二个)到最后一个。
L[-2:-1]表示从倒数第二开始到倒数第一个(不包括倒数第一个)
根据以上可以发现切片操作符是一个左闭右开的取值范围。
那么我很好奇[-2:0]的情况会是如何:
返回的是一个空列表,这个切片操作失败了
这就告诉我们,切片是不支持“循环”列表的,也就是索引的头尾不会相接,即不能从倒数跨到正数。另外,切片符中前面的数不能大于后面的数,既索引总是从左向右的。
切片符还支持定义隔多少个元素取一个,只需要再定义个“切片步长”就好了,例如[::2]就代表在整个列表中,下一元素总取从这一元素开始(不包括这个元素)的第二个元素。
那么L[:]其实就是默认slice step为1,相当于L[::1]。
切片同样也可以用在tuple和字符串上。
用切片的主要好处就是省去了很多像遍历列表这样繁琐的循环。
6. 生成器
讲生成器之前,我先大概说一下python的列表生成式。
列表生成式
列表生成式List Comprehensions,用于按照自定义的规则来创建一个列表。
例如生成一定范围的数字:
这个range只能用于integer的范围。
又根据这个基础,我们可以生成某个定义域下的x*x的列表:
又比如,在两个变量各自的定义域内,生成某种函数下两个变量的函数值的列表:
举两个字符串各个字符全排列的例子:
另外for循环中也可以有多个变量,当然这个循环器需要能够同时迭代多个变量。
例如dict中的iteriems()可以同时迭代key和value:
还可以加入if判断
生成器
列表生成式是按照一定规则生成一个列表,那么如果我们只是需要定义这个规则而不需要一次就把这个列表创建出来的时候呢?这时候我们就要用到python的生成器。
生成器generator,就是一种实现一边循环一遍计算的机制。
将列表生成式的方括号[]改为(),即可以创建一个生成器。
那么这个生成器要怎么用呢?首先我们可以用它的next()方法来generate列表中的每个元素:
然而事实上我们会用for循环来迭代它。
生成器还被用于处理一些复杂的推算规则,以著名的斐波那契数列为例:后一个数字为前两个数字相加之和
这个函数能够打印出斐波那契数列的第max个数
如果我把print b放进while循环,那么这个函数就会一次性打印出斐波那契数列的前max个数。
那么其实这个fib函数已经非常像一个生成器了,那如果我不想要一次性打印出来,想把它变成一个生成器,那就只需要这样:
把print b 改为 yield b。
这就创建了一个按照斐波那契数列规则,最多执行到第五个数的生成器:
这里的yield可以当作为一个“闸”的效果,即生成器执行一次next(),它就会输出一次,并且停在这里。下一次执行会从这里开始。
通过我的实验发现,一个生成器创建之后,它的循环周期就只有一次,不能重新开始,除非你重新创建一个。也就是说,如果我一次没有循环完这个生成器,那么下一次它还会从它上次循环停止的地方继续开始。如果最后我循环完了这个生成器,那么这个生成器的周期就结束了。
7. 高阶函数
高阶函数的英文是Higher-oder function。简单来说,高阶函数的概念就是把函数泛化为普通的参数,既函数能够作为参数被传入。
深入理解,主要总结为以下几点:
1.函数的结果可以赋值给变量。
|
|
2.函数本身可以赋值给变量,并且通过这个变量可以调用这个函数
|
|
3.函数名是指向函数的变量
这就是说,在python中,函数名可以完全当作一个普通的变量来使用。
可以给函数名这个变量赋值试试:
|
|
因为这个函数名被重新赋值成了一个整型变量10,abs这个变量就不代表这个函数,也就不能被调用了。
4.函数作为参数
当一个函数的参数中包含一个函数的时候,这种函数就被称之为高阶函数。
其实用数学里的表达就是:f(g(x)),f函数的参数又是一个g函数。来感受一下“阶”的概念:例如对这个二阶函数求导,就是二阶求导。
一个最简单的高阶函数:
用数学表达式去理解,例如:g(x, y, f)= f(x)+f(y)
这个函数g可以理解为是:
假设我们把这个函数掏空,剩下的这个函数的骨架实际上就是一个“产生某种关系”的方法。这个方法组合了更多的方法以完成更庞大的任务。
根据不同的分支计算方法与产生关系方法,高阶函数就会发挥出不同的效果,而其潜在的无穷能力也正是无数计算机科学家、数学家为之着迷的原因所在吧。
8. 函数式编程
像这种高度抽象的编程范式,我们就把它称作函数式编程。
Python对函数式编程提供部分支持。但由于Python允许使用变量,因此,Python不是纯函数式编程语言。
满足函数式编程的条件中还有一条,那就是允许函数返回一个函数。
实际上我认为,这与是否满足“函数可以作为参数”并不是独立开的。
因为当函数A可以作为函数B的参数传入时,是否需要返回一个函数只取决于B函数的骨架,也就是“产生某种关系“的方法。如果这种关系需要通过返回函数来产生(维持),那么它就是如此这般的一类特殊的高阶函数,仅此而已,并不代表它是一个独立的概念。
- 思考:return是不是可以理解成一个函数呢?那么返回一个函数是不是也就是把函数作为参数(或者构成了一个表达式)传给了return这个“函数”呢?
9. 递归
揭开这这种特殊的高阶函数的面纱,没错,它就是 ”递归“。
假如说某个问题是一个能够通过方法迭代的关系来解决的过程,也就是我们可以把它分解成多个可重复解决的小问题,那么我们就只需要找到解决这一种小问题的方法,然后对这个方法进行递归调用,就可以解决这整一个问题。
递归的执行过程可以理解为,当停止条件存在且为A时,从问题的起点开始计算n次,n即代表达到停止条件所需要迭代方法f的次数:
g(f,A)= f(f(f(f(f(f(f…(A)…)))))
发现递归函数其实就是一种高阶函数,这个函数对各层计算得到的“参数”的组合方式就是“重复调用直到结束条件满足为止”。