抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

前面在通过讲什么是高阶函数(能够接受函数作为参数传入的函数,或者可以返回函数对象的函数)引出了装饰器的由来和存在的意义。这里对python函数的其他基础概念做个补充和记录。

1. 函数的变量

之前笔记中的例子已经对函数参数传递过程做了总结,提到了怎么调用函数的返回值,怎么实现函数的嵌套,基本概念用法都已经提过,这里只是做个思考和补充。

1.1 局部变量

  • 函数内部的定义的变量
  • 局部变量只在函数内部生效,不同函数可以拥有同名的局部变量,互不影响(作用域为本函数)
  • 局部变量的作用,为了临时保存数据需要在函数中定义变量来进行存储
  • 局部变量在函数执行时被创建,函数执行完成后,局部变量会被系统回收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 局部变量重名互不影响,只作用在当前函数中
def added(a, b):
c = a + b
return c

def connect(a, b):
c = str(a) + str(b)
return c

print(added(4, 5))
print(connect('Phantom', 'Aria'))

'''
运行结果:
9
PhantomAria
'''

1.2 全局变量

  • 函数外部定义的变量
  • 全局变量可以在多个函数中使用(作用域为所有函数)
  • 全局变量如果和局部变量重名,只会使用局部变量(就近原则)
  • 如果在函数中修改不可变类型全局变量,需要使用global声明

这里有一个很有意思的现象,前面在数据类型里说过,数据可以分为可变数据类型(列表,字典,集合)和不可变数据类型(数字,元组,字符串),而python的所有参数传递都是引用传递而非值传递。因此,对于可变类型的全局变量,在函数中可以被修改;而对于不可变全局对象则无法在函数中直接修改,其本质是修改不可变数据系统会创建一个新的对象(分配一个新的内存地址),然而这个对象名已经被占用了(也就是变量名无法被指向,原来的变量名也没有被收回)。下面举个栗子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 不可变类型全局变量在函数内部可以传递使用但是无法直接修改
x = 100
def added():
x = x + 1 # 不可变数据修改,系统会创建新的对象,而变量名x已经是全局变量的变量名,无法成为新的对象的变量名
return x

added()

'''
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
d:\zhuomian\python\test.ipynb Cell 47 in <cell line: 6>()
3 x = x + 1
4 return x
----> 6 added(4)

d:\zhuomian\python\test.ipynb Cell 47 in added(a)
2 def added(a):
----> 3 x = x + 1
4 return x

UnboundLocalError: local variable 'x' referenced before assignment
'''

对于global是如何运作,使得python解释器可以将不可变全局变量进行修改的?这点以我的功底还无法解释……暂时只能知道是这么个用法。

顺带一提,还有个嵌套函数对外围函数的不可变变量进行修改,需要用到类似的nonlocal进行声明。

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
x = 100
def added():
global x # global声明x为全局变量
x = x + 1
print(x)
added()
added()

def A():
y = 200
def B():
nonlocal y # nonlocal声明y为外围函数的变量(不是全局变量!)
y = y + 1
return y
return B
test = A()
print(test())
print(test())

'''
运行结果:
101
102
201
202
'''

上面的例子本质上是一样的,对于嵌套函数来说,要修改外围函数的不可变类型的变量(看起来似乎矛盾,不可变的怎么能叫变量呢?前面已经说过,重新赋值造成数字和字符串看起来是“可变的”假象,这里分清两个变分别指什么意思),相当于是上面例子的在函数内修改不可变类型的全局变量(作用域不同,只能说相当于),只不过二者声明的方式不同

1.3 修改可变全局变量引起的思考

一个很有意思的现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
a = [1, 2, 3]
def add1(ls):
ls = ls + ls

b = [1, 2, 3]
def add2(ls):
ls += ls

add1(a)
print(a)
add2(b)
print(b)

'''
运行结果:
[1, 2, 3]
[1, 2, 3, 1, 2, 3]
'''

a列表和b列表都是可变全局变量,同一个算法,为什么a在传入函数执行之后没有发生改变呢?

这里需要对可变数据类型做个回顾,可变对象可以对自身内容进行原地修改而不改变存储地址。原地修改画个重点,意思是利用方法比如reverse、sort、append等在原有对象上直接修改

  • ‘=’ 是赋值语句,将右边的表达式的结果对象,引用绑定到等号左边的变量名上。赋值是创建一个新对象,赋值给目标,返回的也是新对象,引用地址会发生改变

  • ‘+=’ 是增强赋值语句,对左边的对象进行原地修改,返回值为None,引用地址不变

看到这里就能明白上面两个看似“同样”的操作为什么会返回不一样的结果,也加深了“可变”与“不可变”的理解。

2. 函数的高级用法

前一篇笔记写的装饰器就是函数的高级用法之一,这里做个完善补充。

2.1 匿名函数

除了用def关键字命名函数这种基础方法之外,还可以使用lambda表达式创建匿名函数。

lambda语法格式如下:

1
lambda param1,...paramN:expression

匿名函数的语法比较简洁,能接受任何数量的参数但只能返回一个表达式的值。因为匿名函数比较简洁小巧,也常用在作为参数进行传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 定义匿名函数
func1 = lambda x, y : x + y
result = func1(1, 2)
print("匿名函数func1执行结果:",result)

# 匿名函数作为参数传递
def func2(x, y, opt):
print('函数func2执行结果为:', opt(x, y))

func2(4, 5, lambda x, y : x + y)

'''
运行结果:
匿名函数func1执行结果: 3
函数func2执行结果为: 9
'''

2.2 嵌套调用

相比来说函数嵌套调用可能算不上是高级用法,不过这里还是补充一下。嵌套调用指一个函数里调用另一个函数,注意和嵌套函数区分

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def func1():        # 定义一个函数
print('第一个函数输出Phantom')

def func2(): # 定义第二个函数
func1() # 在第二个函数里调用第一个函数功能
print('第二个函数输出Aria')

func2() # 执行一个函数,实际上两个函数都执行了一遍

'''
运行结果:
第一个函数输出Phantom
第二个函数输出Aria
'''

2.3 递归函数

递归函数就是在一个函数内部调用自身的函数,本质上是一个循环,循环结束的点就是递归出口。

用阶乘举个最简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用迭代实现阶乘算法
def factorial(n):
result = 1
for i in range(2, n +1):
result *= i
i += 1
return result

# 使用递归实现阶乘算法
def factorial_1(n):
if n == 1:
return 1
else:
return n * factorial_1(n - 1)

print(factorial(10))
print(factorial_1(10))

'''
运行结果:
3628800
3628800
'''
  • 迭代的方法,从1开始,进入for循环对之前的结果累积乘以 i,直至 n(上例函数被调用了1次)。

  • 递归的方式更为直观,每次通过递减数字的方式递归调用自己(上例函数被调用了10次)。

整体上看递归更简洁明了,但是相比迭代会占用更多内存,运行时间会更长

递归有最大深度限制,在计算机中,函数名、参数、值类型等,都是存放在栈上的。每进行一次函数调用,就会在栈上加一层,函数返回就减一层,由于栈的大小是有限的,递归次数过多就会导致堆栈溢出

可以调用sys模块,sys.setrecursionlimit(2000)将栈的大小调整为2000,sys.getrecursionlimit()查看当前设置的最大递归深度。这种调整递归深度的方式不是无限大的,我的jupyter在调用递归函数3000次的时候就会直接退出……模块定义和调用方式后一篇笔记再说。

3. 文件操作函数

3.1 open() & close()

函数open()可以打开一个文件,或者创建一个新文件,函数close()可以关闭文件。两者语法如下:

1
2
3
4
5
f = open('文件名', '访问模式')
f.close() # 注意最后一定要有close()

with open('文件名', '访问模式') as f: # 自动调用close()
f.方法()
访问模式 说明
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
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
with open('test.txt', 'w') as f:    # 只写模式创建一个新文件
f.write('My name is Phantom. \nI am Aria.')

with open('test.txt', 'a') as f: # 追加模式进行续写
f.write('\nWell, it\'s been so long.')
'''
生成的test.txt内容: # 实际前面两行末尾都有换行符
My name is Phantom.
I am Aria.
Well, it's been so long.
'''

with open('test.txt', 'r') as f: # 只读方式打开文件
line = f.read(1) # read读取第一个字节
print(line)
line = f.readline() # readline读取第一个字节后的第一行,因为读取了换行符,所以运行结果多一行空行
print(line)
line = f.readlines() # readlines读取接下来的两行,每行数据为一个元素,返回一个列表
print(line)

'''
运行结果:
M
y name is Phantom.

['I am Aria.\n', "Well, it's been so long."]
'''

3.3 os模块的文件操作函数

os模块和上面递归函数最后提到的sys模块用的非常多,下篇笔记再详细说明,这里就记一下用法。

这几个函数也非常直观,举个例子就知道分别有什么作用:

1
2
3
4
5
6
7
import os

os.rename('test.txt', 'TEST.txt') # 文件重命名
os.remove('TEST.txt') # 文件删除
os.mkdir('./test') # 创建文件夹,文件夹存在的话会报错,且只能创建一级目录
os.rmdir('./test') # 删除文件夹
os.makedirs('./TEST/TEST1/TEST2') #递归的方式创建多级目录

欢迎小伙伴们留言评论~