前面在通过讲什么是高阶函数(能够接受函数作为参数传入的函数,或者可以返回函数对象的函数)引出了装饰器的由来和存在的意义。这里对python函数的其他基础概念做个补充和记录。
1. 函数的变量
之前笔记中的例子已经对函数参数传递过程做了总结,提到了怎么调用函数的返回值,怎么实现函数的嵌套,基本概念用法都已经提过,这里只是做个思考和补充。
1.1 局部变量
- 函数内部的定义的变量
- 局部变量只在函数内部生效,不同函数可以拥有同名的局部变量,互不影响(作用域为本函数)
- 局部变量的作用,为了临时保存数据需要在函数中定义变量来进行存储
- 局部变量在函数执行时被创建,函数执行完成后,局部变量会被系统回收
1 | # 局部变量重名互不影响,只作用在当前函数中 |
1.2 全局变量
- 函数外部定义的变量
- 全局变量可以在多个函数中使用(作用域为所有函数)
- 全局变量如果和局部变量重名,只会使用局部变量(就近原则)
- 如果在函数中修改不可变类型全局变量,需要使用global声明
这里有一个很有意思的现象,前面在数据类型里说过,数据可以分为可变数据类型(列表,字典,集合)和不可变数据类型(数字,元组,字符串),而python的所有参数传递都是引用传递而非值传递。因此,对于可变类型的全局变量,在函数中可以被修改;而对于不可变全局对象则无法在函数中直接修改,其本质是修改不可变数据系统会创建一个新的对象(分配一个新的内存地址),然而这个对象名已经被占用了(也就是变量名无法被指向,原来的变量名也没有被收回)。下面举个栗子。
1 | # 不可变类型全局变量在函数内部可以传递使用但是无法直接修改 |
对于global是如何运作,使得python解释器可以将不可变全局变量进行修改的?这点以我的功底还无法解释……暂时只能知道是这么个用法。
顺带一提,还有个嵌套函数对外围函数的不可变变量进行修改,需要用到类似的nonlocal进行声明。
1 | x = 100 |
上面的例子本质上是一样的,对于嵌套函数来说,要修改外围函数的不可变类型的变量(看起来似乎矛盾,不可变的怎么能叫变量呢?前面已经说过,重新赋值造成数字和字符串看起来是“可变的”假象,这里分清两个变分别指什么意思),相当于是上面例子的在函数内修改不可变类型的全局变量(作用域不同,只能说相当于),只不过二者声明的方式不同。
1.3 修改可变全局变量引起的思考
一个很有意思的现象:
1 | a = [1, 2, 3] |
a列表和b列表都是可变全局变量,同一个算法,为什么a在传入函数执行之后没有发生改变呢?
这里需要对可变数据类型做个回顾,可变对象可以对自身内容进行原地修改而不改变存储地址。原地修改画个重点,意思是利用方法比如reverse、sort、append等在原有对象上直接修改。
‘=’ 是赋值语句,将右边的表达式的结果对象,引用绑定到等号左边的变量名上。赋值是创建一个新对象,赋值给目标,返回的也是新对象,引用地址会发生改变。
‘+=’ 是增强赋值语句,对左边的对象进行原地修改,返回值为None,引用地址不变。
看到这里就能明白上面两个看似“同样”的操作为什么会返回不一样的结果,也加深了“可变”与“不可变”的理解。
2. 函数的高级用法
前一篇笔记写的装饰器就是函数的高级用法之一,这里做个完善补充。
2.1 匿名函数
除了用def关键字命名函数这种基础方法之外,还可以使用lambda表达式创建匿名函数。
lambda语法格式如下:
1 | lambda param1,...paramN:expression |
匿名函数的语法比较简洁,能接受任何数量的参数但只能返回一个表达式的值。因为匿名函数比较简洁小巧,也常用在作为参数进行传递。
1 | # 定义匿名函数 |
2.2 嵌套调用
相比来说函数嵌套调用可能算不上是高级用法,不过这里还是补充一下。嵌套调用指一个函数里调用另一个函数,注意和嵌套函数区分。
一个简单的例子:
1 | def func1(): # 定义一个函数 |
2.3 递归函数
递归函数就是在一个函数内部调用自身的函数,本质上是一个循环,循环结束的点就是递归出口。
用阶乘举个最简单的例子:
1 | # 使用迭代实现阶乘算法 |
迭代的方法,从1开始,进入for循环对之前的结果累积乘以 i,直至 n(上例函数被调用了1次)。
递归的方式更为直观,每次通过递减数字的方式递归调用自己(上例函数被调用了10次)。
整体上看递归更简洁明了,但是相比迭代会占用更多内存,运行时间会更长。
递归有最大深度限制,在计算机中,函数名、参数、值类型等,都是存放在栈上的。每进行一次函数调用,就会在栈上加一层,函数返回就减一层,由于栈的大小是有限的,递归次数过多就会导致堆栈溢出。
可以调用sys模块,sys.setrecursionlimit(2000)
将栈的大小调整为2000,sys.getrecursionlimit()
查看当前设置的最大递归深度。这种调整递归深度的方式不是无限大的,我的jupyter在调用递归函数3000次的时候就会直接退出……模块定义和调用方式后一篇笔记再说。
3. 文件操作函数
3.1 open() & close()
函数open()可以打开一个文件,或者创建一个新文件,函数close()可以关闭文件。两者语法如下:
1 | f = open('文件名', '访问模式') |
访问模式 | 说明 |
---|---|
r | 只读方式打开文件,默认模式,打开文件必须存在。 |
w | 写入方式打开文件,已存在的文件会覆盖内容(相当于linux重定向操作符>)。 |
a | 追加方式打开文件,已存在的文件会将内容写到最后(相当于linux重定向操作符>>)。 |
x | 只写方式打开文件,新建一个文件,若文件存在则报错。 |
r+ | 读写方式打开文件,打开文件必须存在。 |
w+ | 读写方式打开文件,已存在的文件会覆盖内容。 |
a+ | 读写方式打开文件,已存在的文件会将内容写到最后。 |
一般用 with open() as 的方式打开文件,这种方式会自动帮我们调用f.close()
3.2 write() & read()
write()向文件写入数据,以w方式访问,如果文件名存在会先清空文件内容,文件名不存在则新建;以a方式访问,如果文件名存在则续写,文件名不存在则新建;以r方式访问则报错。
read()从文件中读取数据,括号里面的参数代表读取的数据长度(字节数),如果不传入参数则读取所有数据。
readline()读取一行,同时会读取一行最后的换行符\n,所以打印出来的时候会多一行空行。
readlines()按照行的方式读取整个文件数据,返回的是一个列表,每行数据是一个元素,同样会读到换行符\n并且显示出来。
需要注意一点,在多次读取的操作中,后一次读取会从上一次读完的位置开始。
1 | with open('test.txt', 'w') as f: # 只写模式创建一个新文件 |
3.3 os模块的文件操作函数
os模块和上面递归函数最后提到的sys模块用的非常多,下篇笔记再详细说明,这里就记一下用法。
这几个函数也非常直观,举个例子就知道分别有什么作用:
1 | import os |