爆肝六万字整理的python基础,快速入门python的首选(下)

举报
AI浩 发表于 2022/01/18 06:33:28 2022/01/18
【摘要】 10 函数函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。函数能提高应用的模块性,和代码的重复利用率。你已经知道Python提供了许多内建函数,比如print()。但你也可以自己创建函数,这被叫做用户自定义函数。 10.1 定义函数语法:def 函数名(参数列表): 函数体例:# 定义一个函数,能够完成打印信息的功能def printInfo(): print...

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()

运行现象:

image-20210831144255382

注意:

  • 如果文件不存在那么创建,如果存在那么就先清空,然后写入数据

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 大家好, 我是AI2: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)
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。