爆肝六万字整理的python基础,快速入门python的首选(下)
10 函数
函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。
函数能提高应用的模块性,和代码的重复利用率。你已经知道Python提供了许多内建函数,比如print()。但你也可以自己创建函数,这被叫做用户自定义函数。
10.1 定义函数
语法:
def 函数名(参数列表):
函数体
例:
# 定义一个函数,能够完成打印信息的功能
def printInfo():
print ('------------------------------------')
print (' 人生苦短,我用Python')
print ('------------------------------------')
定义函数的规则:
- 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()。
- 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
- 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
- 函数内容以冒号 : 起始,并且缩进。
- return [表达式] 结束函数,选择性地返回一个值给调用方,不带表达式的 return 相当于返回 None。
10.2 调用函数
定义了函数之后,就相当于有了一个具有某些功能的代码,想要让这些代码能够执行,需要调用它
调用函数很简单的,通过 函数名() 即可完成调用,例:
# 定义完函数后,函数是不会自动执行的,需要调用它才可以
printInfo()
10.3 函数的注释
在函数定义下面使用'''
进行注释,使用help函数可以查看函数的注释。
def test(a1, a2):
'''
用来完成对2个数求和"
:param a1:第一个参数
:param a2:第二个参数
:return:无
'''
print("%d" % (a1 + a2))
print(help(test))
运行结果:
test(a1, a2)
用来完成对2个数求和"
:param a1:第一个参数
:param a2:第二个参数
:return:无
None
10.4 函数的参数
10.4.1 参数的定义
参数分形参、实参
形参:函数定义时括号内的参数
实参:函数调用时括号内的参数
形参相当于变量,实参相当于变量的值。
def add2num(a, b):
c = a + b
print(c)
add2num(11, 22) # 调用带有参数的函数时,需要在小括号中,传递数据
a,b为形参;11,12为实参
形参:
只在被调用时,才分配内存单元。调用结束,立刻释放所分配的内存。
只在函数内部有效。
实参:
可以是:常量、变量、表达式、函数。
进行函数调用时,实参必须是确定的值。
10.4.2 参数的分类
Python的函数除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
1)位置参数
位置形参:函数定义时,从左往右写的参数,比如上面的 a, b
位置实参:函数调用时,从左往右写的参数, 比如上面的 11,12
位置形参定义多少个,调用时位置实参必须写上多少个,多一个少一个都不行。
2)关键参数:
正常情况下,给函数传参数,要按顺序。如果不想按顺序,就用关键参数。
指定参数名的参数,就叫做关键参数。
函数调用时:func(a=1, b=2), 这种指定了参数名的参数,就是关键参数。
调用函数时,关键参数可以和位置参数一起用,但是关键参数必须在位置参数的后面。不然会报错。
例:
def add2num(a, b):
c = a + b
print(c)
#正确的调用方式
add2num(11, b=22) # 调用带有参数的函数时,需要在小括号中,传递数据
add2num(a=11, b=22)
add2num(b=11, a=22)
# 错误的调用方式。
#add2num(22, a=22)
#add2num(b=11,22)
3)默认参数
函数定义时,默认参数必须在位置形参的后面。
函数调用时,指定参数名的参数,叫关键参数。
而在函数定义时,给参数名指定值的时候,这个参数叫做默认参数。
关键参数,和默认参数两个参数写法一样,区别在于:
关键参数是在函数调用时,指定实参的参数名,也可以说指定值的参数名。
默认参数是在函数定义时,指定参数名的值。
定义时,有默认参数的话,调用时,这个实参可以不写。如果实参不写的话,这个形参的参数值是他的默认值。
例:
def add2num(a, b=100):
c = a + b
print(c)
add2num(11, b=22) # 调用带有参数的函数时,需要在小括号中,传递数据
add2num(11)
运行结果:
33
111
4 ) 动态参数:*args **kwargs
*args
针对函数定义时的*:
def func(a, b, *args):
pass
*args会接收函数调用时,传入的多余的位置实参。
*args 是一个元组
例子:
func(1, 2, 3, 4, 5, 6) 函数调用,因为函数定义时,*args前面有形参a, 形参b, *args就接收调用时多余的位置实参
a为1, b为2, *args 为: (3, 4, 5, 6),是一个元组。
针对函数调用时的 *:
func(1, 2, *[1, 2, 3, 4]) == func(1, 2, 1, 2, 3, 4)
函数调用时有*, 就应该立马将*后面的列表,元组,字符串之类的迭代器,打散成位置参数来看。
注意,如果 *后面是一个字典的话,那么打散成的位置参数就是字典的key
*可以看做是for循环。
形参中 *args 只能接收多余的位置实参,成为一个元组。不能接收关键实参。
例:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
nums = [1, 2, 3]
# 调用方式1
print(calc(nums[0], nums[1], nums[2]))
#调用方式2
print(calc(*nums))
运行结果:
14
14
**kwargs:
针对函数定义时,站在形参的角度看 **:
接收多余的关键实参,成为一个字典dict。
字典的key是关键实参的变量名,字典的value是关键实参的值。
将字典交给**后面的变量名,这里是kwargs
站在实参的角度看 ** :
d = {‘x’:1, ‘y’:2, ‘z’:333}
func(**d) # 等价于func(x=1,y=2,z=333)
函数调用时,后面可以接一个字典,然后会把字典打散成关键参数的形式,也就是key=value的形式。
例:
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
#调用方式1
print(person('Michael', 30))
#调用方式2
print(person('Bob', 35, city='Beijing'))
#调用方式3
print(person('Adam', 45, gender='M', job='Engineer'))
运行结果:
name: Michael age: 30 other: {}
None
name: Bob age: 35 other: {'city': 'Beijing'}
None
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
None
5)混合参数时,参数顺序
函数定义时:
从左往右:位置形参 > *args > 默认参数 > **kwargs
位置形参 > 默认参数 > *args > **kwargs 也可以。
因为函数调用时给的实参满足了位置形参、默认参数之后,会把多余的位置实参给args。这样并不会报错。
但是 **kwargs 必须在 *args后面
默认形参必须在位置形参后面
函数调用时:
从左到右:位置实参 > *args > 关键参数 > **kwargs
因为 * args 在函数调用时,会被打散成位置实参,而关键参数必须在位置实参的后面,否则会报错。SyntaxError: positional argument follows keyword argument
*args 必须在 **kwargs后面, 否则会报语法错误:SyntaxError: iterable argument unpacking follows keyword argument unpacking
总结
Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。
默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!
要注意定义可变参数和关键字参数的语法:
*args
是可变参数,args接收的是一个tuple;
**kw
是关键字参数,kw接收的是一个dict。
以及调用函数时如何传入可变参数和关键字参数的语法:
可变参数既可以直接传入:func(1, 2, 3)
,又可以先组装list或tuple,再通过*args
传入:func(*(1, 2, 3))
;
关键字参数既可以直接传入:func(a=1, b=2)
,又可以先组装dict,再通过**kw
传入:func(**{'a': 1, 'b': 2})
。
使用*args
和**kw
是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*
,否则定义的将是位置参数。
10.4.3 参数的传递
Python参数传递采用的肯定是“传对象引用”的方式。这种方式相当于传值和传引用的一种综合。如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值--相当于通过“传引用”来传递对象。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象--相当于通过“传值’来传递对象。
例:
attrList = [0, 1, 2, 3, 4, 5]
print("改变前————————————————————————————————")
print(attrList)
def func(i, attrList):
attrList[i] = attrList[i] * 10
print("改变后————————————————————————————————")
for i in range(3):
func(i, attrList)
print(attrList)
运行结果:
改变前————————————————————————————————
[0, 1, 2, 3, 4, 5]
改变后————————————————————————————————
[0, 10, 20, 3, 4, 5]
可以看到,List发生了改变,如果传入不可变的元素呢?
a = 10
print("改变前————————————————————————————————")
print(a)
def func(c):
print(id(c))
c += 2
print(id(c))
print("func函数的c值:", c)
print(id(a))
func(a)
print("改变后————————————————————————————————")
print(a)
print(id(a))
运行结果:
改变前————————————————————————————————
10
140724141828160
140724141828160
140724141828224
func函数的c值: 12
改变后————————————————————————————————
10
140724141828160
将a变量作为参数传递给了func函数,传递了a的一个引用,把a的地址传递过去了,所以在函数内获取的变量c的地址跟变量a的地址是一样的,但是在函数内,对c进行赋值运算,c的值从10变成了12,实际上10和12所占的内存空间都还是存在的,赋值运算后,c指向12所在的内存。而a仍然指向10所在的内存,所以后面打印a,其值还是10.
10.5 函数的返回值
想要在函数中把结果返回给调用者,需要在函数中使用return,例:
def add2num(a, b):
c = a + b
return c
print(add2num(1, 2))
可以返回多个返回值,例:
def divid(a, b):
shang = a // b
yushu = a % b
return shang, yushu
sh, yu = divid(5, 2)
10.6 局部变量与全局变量
-
局部变量就是定义在一个函数体内部的变量
-
全局变量是定义在外面的变量
例:
a = 1 def f(): b = 2
其中a就是全局变量,而b是局部变量。局部变量只在函数体内部有效,出了函数体,外面是访问不到的,而全局变量则对下面的代码都有效。
全局变量可以直接在函数体内容部使用的,你可以直接访问,但是注意的是,如果对于不可变类型的数据,如果在函数里面进行了赋值操作,则对外面的全局变量不产生影响,因为相当于新建了一个局部变量,只是名字和全局一样,而对于可变类型,如果使用赋值语句,同样对外部不产生影响,但是使用方法的话就会对外部产生影响。
g_b = 3 g_l1 = [1, 2] g_l2 = [1, 2, 3] def t1(): g_b = 2 g_l1 = [] g_l2.append(7) t1() print(g_b, g_l1, g_l2)
运行结果:3 [1, 2] [1, 2, 3, 7]
global关键字
上面说到,如果使用的是赋值语句,在函数内部相当于新建了一个变量,并且重新给了指向,但是有时候我们想把这个变量就是外部的那个全局变量,在赋值操作的时候,就是对全局变量给了重新的指向,这个时候可以通过global关键字表示我在函数里面的这个变量是使用的全局那个。使用方法如下:
g_b = 3
def t1():
global g_b
g_b = 2
t1()
print(g_b)
运行结果:2
这个时候你会发现全局变量g_b也重新指向了,这是因为global g_b表示指定了函数中的g_b就是外面的那个。
10.7 递归函数
如果一个函数在内部调用自身本身,这个函数就是递归函数。例:
def fact(n):
if n == 1:
return 1
return n * fact(n - 1)
print(fact(5))
运行结果:120
可以根据函数定义看到计算过程如下:
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000)。
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
上面的fact(n)函数由于return n * fact(n - 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中,例:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
可以看到,return fact_iter(num - 1, num * product)仅返回递归函数本身,num - 1和num * product在函数调用前就会被计算,不影响函数调用。
fact(5)对应的fact_iter(5, 1)的调用如下:
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。
小结
使用递归函数的优点是逻辑简单清晰,缺点是过深的调用会导致栈溢出。
针对尾递归优化的语言可以通过尾递归防止栈溢出。尾递归事实上和循环是等价的,没有循环语句的编程语言只能通过尾递归实现循环。
Python标准的解释器没有针对尾递归做优化,任何递归函数都存在栈溢出的问题。
10.8 匿名函数
10.8.1 定义
用lambda关键词能创建小型匿名函数。这种函数得名于省略了用def声明函数的标准步骤。
lambda函数的语法只包含一个语句,如下:
lambda [arg1 [,arg2,.....argn]]:expression
如下实例:
sum = lambda a1, a2: a1 * a2
# 调用sum函数
print("Value of total : ", sum(10, 20))
print("Value of total : ", sum(20, 20))
以上实例输出结果:
Value of total : 200
Value of total : 400
Lambda函数能接收任何数量的参数但只能返回一个表达式的值
匿名函数不能直接调用print,因为lambda需要一个表达式
10.8.2 应用场合
例1:自己定义函数作为参数传递
def fun(a, b, opt):
print("a =", a)
print("b =", b)
print("result =", opt(a, b))
fun(1, 2, lambda x, y: x + y)
运行结果:
a = 1
b = 2
result = 3
例2:作为内置函数的参数
想一想,下面的数据如何指定按age或name排序?
stus = [
{"name":"zhangsan", "age":18},
{"name":"lisi", "age":19},
{"name":"wangwu", "age":17}
]
按name排序:
stus.sort(key=lambda x: x['name'])
print(stus)
运行结果:
[{'name': 'lisi', 'age': 19}, {'name': 'wangwu', 'age': 17}, {'name': 'zhangsan', 'age': 18}]
按age排序:
stus.sort(key=lambda x: x['age'])
print(stus)
运行结果:
[{'name': 'wangwu', 'age': 17}, {'name': 'zhangsan', 'age': 18}, {'name': 'lisi', 'age': 19}]
11 文件操作
11.1 打开与关闭
11.1.1 打开文件
在python,使用open函数,可以打开一个已经存在的文件,或者创建一个新文件
open(文件名,访问模式)
示例如下:
f = open('test.txt', 'w')
说明:
访问模式 | 说明 |
---|---|
r | 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。 |
w | 打开一个文件只用于写入。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。 |
a | 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。 |
rb | 以二进制格式打开一个文件用于只读。文件指针将会放在文件的开头。这是默认模式。 |
wb | 以二进制格式打开一个文件只用于写入。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。 |
ab | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。 |
r+ | 打开一个文件用于读写。文件指针将会放在文件的开头。 |
w+ | 打开一个文件用于读写。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。 |
a+ | 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。 |
rb+ | 以二进制格式打开一个文件用于读写。文件指针将会放在文件的开头。 |
wb+ | 以二进制格式打开一个文件用于读写。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。 |
ab+ | 以二进制格式打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。如果该文件不存在,创建新文件用于读写。 |
11.1.2 关闭文件
close( )
示例如下:
# 新建一个文件,文件名为:test.txt
f = open('test.txt', 'w')
# 关闭这个文件
f.close()
11.2 文件的读写
11.2.1 写数据(write)
使用write()可以完成向文件写入数据,例:
f = open('test.txt', 'w')
f.write('hello 大家好, 我是AI浩')
f.close()
运行现象:
注意:
- 如果文件不存在那么创建,如果存在那么就先清空,然后写入数据
11.2.2 读数据(read)
使用read(num)可以从文件中读取数据,num表示要从文件中读取的数据的长度(单位是字节),如果没有传入num,那么就表示读取文件中所有的数据,例:
f = open('test.txt', 'r')
content = f.read(5)
print(content)
print("-"*30)
content = f.read()
print(content)
f.close()
运行结果:
hello
------------------------------
大家好, 我是AI浩
注意:
- 如果open是打开一个文件,那么可以不用谢打开的模式,即只写
open('test.txt')
- 如果使用读了多次,那么后面读取的数据是从上次读完后的位置开始的
11.2.3 读数据(readlines)
就像read没有参数时一样,readlines可以按照行的方式把整个文件中的内容进行一次性读取,并且返回的是一个列表,其中每一行的数据为一个元素
#coding=utf-8
f = open('test.txt', 'r')
content = f.readlines()
print(type(content))
i=1
for temp in content:
print("%d:%s"%(i, temp))
i+=1
f.close()
运行结果:
<class 'list'>
1:hello 大家好, 我是AI浩
11.2.4 读数据(readline)
#coding=utf-8
f = open('test.txt', 'r')
content = f.readline()
print("1:%s"%content)
content = f.readline()
print("2:%s"%content)
f.close()
运行结果:
1:hello 大家好, 我是AI浩
2:asfsifhiudh
11.3 文件的常用操作
11.3.1 获取当前读写的位置
在读写文件的过程中,如果想知道当前的位置,可以使用tell()来获取,例:
# 打开一个已经存在的文件
f = open("test.txt", "r")
str = f.read(3)
print("读取的数据是 : ", str)
# 查找当前位置
position = f.tell()
print("当前文件位置 : ", position)
str = f.read(3)
print("读取的数据是 : ", str)
# 查找当前位置
position = f.tell()
print("当前文件位置 : ", position)
f.close()
运行结果:
读取的数据是 : hel
当前文件位置 : 3
读取的数据是 : lo
当前文件位置 : 6
11.3.2 定位到某个位置
如果在读写文件的过程中,需要从另外一个位置进行操作的话,可以使用seek()
seek(offset, from)有2个参数
- offset:偏移量
- from:方向
- 0:表示文件开头
- 1:表示当前位置
- 2:表示文件末尾
例1:把位置设置为:从文件开头,偏移5个字节
# 打开一个已经存在的文件
f = open("test.txt", "r")
str = f.read(30)
print("读取的数据是 : ", str)
# 查找当前位置
position = f.tell()
print("当前文件位置 : ", position)
# 重新设置位置
f.seek(5, 0)
# 查找当前位置
position = f.tell()
print("当前文件位置 : ", position)
f.close()
例2:把位置设置为:离文件末尾,3字节处
# 打开一个已经存在的文件
f = open("test.txt", "rb")
print("读取的数据是 : ", str)
position = f.tell()
print("当前文件位置 : ", position)
# 重新设置位置
f.seek(-3, 2)
# 读取到的数据为:文件最后3个字节数据
str = f.read()
print("读取的数据是 : ", str)
f.close()
运行结果:
读取的数据是 : <class 'str'>
当前文件位置 : 0
读取的数据是 : b'ddf'
11.3.3 文件重命名
os模块中的rename()可以完成对文件的重命名操作
rename(需要修改的文件名, 新的文件名)
import os
os.rename("test.txt", "test-1.txt")
11.3.4 删除文件
os模块中的remove()可以完成对文件的删除操作
remove(待删除的文件名)
import os
os.remove("test.txt")
11.4 文件夹的相关操作
实际开发中,有时需要用程序的方式对文件夹进行一定的操作,比如创建、删除等
就像对文件操作需要os模块一样,如果要操作文件夹,同样需要os模块
11.4.1 创建文件夹
import os
os.mkdir("aa")
11.4.2 获取当前目录
import os
os.getcwd()
11.4.3 改变默认目录
import os
os.chdir("../")
11.4.4 获取目录列表
import os
os.listdir("./")
11.4.5 删除文件夹
import os
os.rmdir("张三")
11.4.6 检测文件夹是否存在
import os
if not os.path.exists(path):
os.makedirs(path)
11.4.7 创建多级文件夹
import os
os.makedirs(opDir)
- 点赞
- 收藏
- 关注作者
评论(0)