Python 函数(八):函数、参数与返回值

Python 函数(八):函数、参数与返回值 相信大家看了前几章python教程能明显发现这些代码基本上都是从上往下执行的。前面那些案例我主要是用来模拟演示的在实际的项目开发过程中这种写法既低效又繁琐尤其是在需要重复执行同一套逻辑时更是显而易见。举个例子leader让你开发商品折扣的计算功能如果反复遇到同样的计算逻辑你就得把每一个计算公式都写一遍。这么做一方面效率低下另一方面如果你后期想修改折扣规则的时候容易漏改某一处。但如果你把这套逻辑写成函数以后遇到同样的业务场景就可以直接调用函数。那我们今天这一章要讲解的内容就是函数的定义和调用以及参数与返回值的常见写法下面我们开始吧。一、为什么要使用函数先看一段没有使用函数的代码price1200final_price1price1*0.8print(final_price1)price2350final_price2price2*0.8print(final_price2)两次计算使用的是同一套规则只是商品价格不同。可以把计算过程放进一个函数defcalculate_discount(price):final_priceprice*0.8returnfinal_priceprint(calculate_discount(200))print(calculate_discount(350))这样写以后折扣规则只保留一份。如果折扣从八折改成九折只需要修改函数里面的计算。函数还有一个作用就是给一段逻辑起名字。看到calculate_discount()基本就能知道这段代码在计算折扣不需要每次都重新阅读里面的细节。二、定义和调用函数Python 使用def定义函数defsay_hello():print(Hello)这一段代码由几个部分组成def函数名():函数体def表示开始定义一个函数。say_hello是函数名。函数名后面要写一对圆括号()。行尾的冒号:不能省略。函数体需要缩进。定义函数并不会立即执行函数体。需要使用函数名加圆括号进行调用say_hello()完整代码是defsay_hello():print(Hello)say_hello()say_hello()函数调用了两次因此会打印两次Hello。三、函数的执行过程程序执行到函数定义时会记住这个函数不过不会进入函数体。执行到函数调用时才会进入函数内部defshow_message():print(正在执行函数)print(调用之前)show_message()print(调用之后)输出顺序为调用之前 正在执行函数 调用之后调用结束以后程序会回到原来的位置继续执行函数调用后面的代码。函数通常要先定义再调用。下面的代码会出现NameErrorsay_hello()defsay_hello():print(Hello)Python 从上到下执行。调用say_hello()时后面的函数定义还没有执行所以此时找不到这个名字。四、参数和实参函数如果只能执行一套固定内容用途会比较有限。参数可以让调用者把不同的数据传入函数defgreet(name):print(fHello{name})greet(Ivan)greet(Tom)定义函数时圆括号中的name叫作形参。调用函数时传入的Ivan和Tom叫作实参。可以简单理解为形参是函数内部准备接收数据的变量实参是调用时真正交给函数的数据defgreet(name):# name 是形参print(name)greet(Ivan)# Ivan 是实参很多时候大家也会把它们都称为参数只要知道它们分别出现在定义和调用阶段即可。五、多个参数函数可以接收多个参数参数之间使用逗号分隔defadd(a,b):resultabprint(result)add(10,20)调用时传入的实参数量要与形参数量对应。下面少传了一个参数会出现TypeErroradd(10)多传参数同样会报错add(10,20,30)除非函数使用了后面要讲的不定长参数否则参数数量不能随意增加或减少。六、位置参数按照参数在函数定义中的位置传值叫作位置参数defintroduce(name,age,city):print(f姓名{name})print(f年龄{age})print(f城市{city})introduce(Ivan,20,杭州)调用时第一个实参交给name第二个交给age第三个交给city。位置写错以后Python 不一定会报错但是数据的含义可能已经变了introduce(20,杭州,Ivan)这段代码可以运行不过name得到的是20age得到的是杭州。所以使用位置参数时需要记住参数顺序。七、关键字参数调用函数时也可以明确写出参数名introduce(nameIvan,age20,city杭州)这种写法叫作关键字参数。因为参数名称已经写清楚所以顺序可以调整introduce(city杭州,nameIvan,age20)关键字参数更长一些不过参数较多或者某个布尔值、数字的含义不明显时会更容易读懂defcreate_video(width,height,fps,muted):print(width,height,fps,muted)create_video(width1920,height1080,fps30,mutedTrue)如果只写成create_video(1920, 1080, 30, True)最后的True代表什么就不太直观。位置参数和关键字参数可以一起使用但是位置参数必须写在关键字参数前面introduce(Ivan,age20,city杭州)下面的写法会出现语法错误introduce(nameIvan,20,杭州)八、默认参数有些参数在大多数时候使用同一个值可以在定义函数时设置默认值defgreet(name,messageHello):print(f{message}{name})greet(Ivan)greet(Tom,早上好)第一次调用没有传入message所以使用默认值Hello。第二次传入了新的内容默认值会被覆盖。必填参数要放在默认参数前面defgreet(name,messageHello):print(message,name)下面的顺序不正确defgreet(messageHello,name):print(message,name)因为调用时无法确定只传入一个位置参数时它应该交给谁Python 会直接提示语法错误。默认参数适合处理真正具有常用默认值的设置例如页码、排序方式和是否显示某个选项defsearch(keyword,page1,page_size20):print(keyword,page,page_size)search(Python)search(Python,page2)九、不要使用可变对象作为默认值默认参数还有一个很容易踩到的问题。下面的写法看起来像是每次调用都创建一个空列表defadd_item(item,items[]):items.append(item)returnitemsprint(add_item(Python))print(add_item(Java))实际结果为[Python] [Python, Java]默认值是在函数定义时创建的不是每次调用时重新创建。同一个列表会被后续调用继续使用。通常改成None再在函数内部创建新列表defadd_item(item,itemsNone):ifitemsisNone:items[]items.append(item)returnitems字典和集合也是可变对象不建议直接作为默认参数。字符串、数字、布尔值、None这类不可变值通常没有这个问题。十、使用*args接收多个位置参数如果函数需要接收的参数数量不固定可以在参数名前加一个星号*defcalculate_total(*prices):total0forpriceinprices:totalpricereturntotalprint(calculate_total(20,35,18))print(calculate_total(10,20,30,40,50))这里的prices是一个元组。调用时传入多少个位置参数它就保存多少个defshow_args(*args):print(args)print(type(args))show_args(1,2,3)输出结果为(1, 2, 3) class tupleargs只是常用名称真正让它接收多个参数的是前面的*。不过实际编写代码时通常还是沿用args这样别人一眼就能看懂。普通参数可以和*args一起出现defprint_scores(title,*scores):print(title)forscoreinscores:print(score)print_scores(第一次考试,86,92,78)第一个实参交给title剩余的位置参数都放入scores。十一、使用**kwargs接收多个关键字参数两个星号**可以接收数量不固定的关键字参数defcreate_user(**info):print(info)create_user(nameIvan,age20,city杭州)这里的info是一个字典{name: Ivan, age: 20, city: 杭州}调用时写下的参数名会成为字典的键传入的数据会成为对应的值。kwargs同样只是常用名称真正起作用的是**defshow_info(**kwargs):forkey,valueinkwargs.items():print(key,value)*args保存额外的位置参数得到元组**kwargs保存额外的关键字参数得到字典。十二、不同参数的排列顺序普通参数、默认参数、*args和**kwargs可以放在同一个函数中defshow_data(name,age18,*args,**kwargs):print(name,name)print(age,age)print(args,args)print(kwargs,kwargs)show_data(Ivan,20,Python,Java,city杭州,activeTrue,)结果为name Ivan age 20 args (Python, Java) kwargs {city: 杭州, active: True}基础阶段可以先记住常用顺序普通参数、带默认值的参数、*args、**kwargs。参数写得太多以后调用和维护都会变得麻烦。如果一组数据本身属于同一个对象例如用户信息很多时候直接传入一个字典会更合适没有必要为了展示参数写法把函数设计得特别复杂。十三、使用return返回结果前面的部分函数直接使用print()输出结果但是输出和返回并不是一回事defadd(a,b):print(ab)这个函数会把结果打印到终端不过调用者拿不到计算结果继续使用。改成returndefadd(a,b):returnab resultadd(10,20)print(result)return会把结果交回函数调用的位置所以可以把它保存到变量中也可以参与后续计算totaladd(10,20)*2print(total)打印适合把信息显示出来返回值适合让程序继续使用结果。实际函数中经常需要返回数据不应该为了看到结果就把所有内容都写成print()。十四、return会结束函数程序执行到return以后会立即离开函数后面的代码不会再执行defcheck_age(age):ifage0:return年龄不能小于 0return年龄有效print(check_age(-1))print(check_age(20))这种写法经常用来提前处理不符合要求的数据defdivide(a,b):ifb0:returnNonereturna/b当b等于0时函数返回None并结束不会继续执行除法。下面的第二个print()永远不会执行deftest():return100print(这一行不会执行)十五、函数没有写return会怎样如果函数没有写return或者只写了一个没有返回值的returnPython 会默认返回Nonedefshow_message():print(Hello)resultshow_message()print(result)输出结果为Hello None这也是为什么一个只负责打印内容的函数把调用结果保存到变量以后会得到None。下面两种写法的返回结果相同deffunc1():passdeffunc2():return它们都会返回None。十六、返回多个值一个函数可以在return后面写多个值defcalculate(a,b):totalab differencea-breturntotal,difference resultcalculate(10,3)print(result)输出结果为(13, 7)看起来像是返回了两个独立的值其实 Python 会把它们放进一个元组。调用时可以直接解包total,differencecalculate(10,3)print(total)print(difference)左侧变量的数量要与返回值数量对应否则会出现ValueError。如果返回内容比较多而且每项都有明确含义使用字典往往更清楚defanalyze_scores(scores):return{count:len(scores),max:max(scores),min:min(scores),average:sum(scores)/len(scores),}十七、参数传递的是对象调用函数时形参会引用传入的对象。对于数字、字符串和元组这类不可变对象函数内部重新赋值不会修改外面的变量defchange_number(number):number100value10change_number(value)print(value)结果仍然是10。列表、字典和集合是可变对象。如果函数直接修改对象中的内容外部也能看到变化defadd_skill(skills):skills.append(Python)user_skills[Java]add_skill(user_skills)print(user_skills)输出结果为[Java, Python]如果不希望修改原列表可以在函数内部创建副本并返回defadd_skill(skills):new_skillsskills.copy()new_skills.append(Python)returnnew_skills这一部分和前面学过的可变对象有关。更完整的对象引用、浅拷贝和深拷贝会在后面的章节中单独讲。十八、函数文档字符串函数的第一行可以写一段文档字符串用来说明函数做什么、接收什么参数以及返回什么内容defcalculate_discount(price,rate0.8):根据原价和折扣率计算折后价格。returnprice*rate文档字符串使用三引号通常放在函数体的第一行。可以通过help()查看help(calculate_discount)简单函数的说明不需要写得很长不过函数要提供给别人使用或者参数含义不够直观时文档字符串会比较有用。十九、函数命名函数名通常使用小写字母和下划线尽量表达它实际完成的操作defcalculate_total():passdefget_user_info():passdefsave_file():pass像f1()、do_it()这类名字虽然可以运行但是很难从调用位置判断函数的用途。函数应该尽量完成一件相对明确的事情。一个函数里同时读取文件、处理数据、连接数据库、发送消息后面修改和测试都会比较麻烦。不过也没有必要把每一两行代码都拆成函数还是要看这段逻辑是否会重复使用以及单独命名以后是不是更清楚。二十、综合示例计算订单价格下面写一个订单价格函数。它接收商品价格列表、折扣率和运费返回订单的各项金额defcalculate_order(prices,discount1.0,shipping_fee0):计算订单原价、折扣金额和最终价格。ifnotprices:return{subtotal:0,discount_amount:0,shipping_fee:shipping_fee,final_price:shipping_fee,}subtotalsum(prices)discounted_pricesubtotal*discount discount_amountsubtotal-discounted_price final_pricediscounted_priceshipping_feereturn{subtotal:subtotal,discount_amount:discount_amount,shipping_fee:shipping_fee,final_price:final_price,}ordercalculate_order(prices[99,128,56],discount0.9,shipping_fee10,)print(商品原价,order[subtotal])print(优惠金额,order[discount_amount])print(运费,order[shipping_fee])print(最终价格,order[final_price])这里把prices作为必填参数因为没有商品价格就无法正常计算。discount和shipping_fee有常用默认值所以设置成默认参数。函数返回字典而不是直接打印结果。这样调用者可以决定如何使用数据可以打印到终端也可以显示在页面上或者继续参与其他计算。空列表会走前面的return直接返回一份金额为零的结果后面的计算不会继续执行。二十一、常见错误1. 定义函数以后忘记调用defsay_hello():print(Hello)这里只完成了定义。如果希望它执行还要写say_hello()。2. 调用函数时忘记圆括号resultsay_hello这行代码取得的是函数本身没有调用函数。调用要写成say_hello()。3. 参数数量不一致defadd(a,b):returnab add(10)函数需要两个参数只传入一个会出现TypeError。4. 把print()当成返回值defadd(a,b):print(ab)resultadd(10,20)print(result)函数打印了30但是没有返回结果因此result是None。5. 在return后面继续写要执行的代码return会立即结束函数。确实需要执行的内容应该放在return前面。6. 使用列表作为默认参数可变默认值会被多次调用共同使用。需要空列表时通常把默认值写成None再在函数内部创建列表。二十二、练习练习一计算矩形面积编写一个函数接收矩形的长度和宽度返回矩形面积。练习二判断奇偶数编写一个函数接收一个整数。偶数返回True奇数返回False。练习三计算平均分使用*args接收数量不固定的成绩返回平均分。没有传入成绩时返回None。练习四创建用户信息使用**kwargs接收用户姓名、年龄和城市返回一个用户信息字典。练习五订单价格在综合示例的基础上增加满减规则例如订单原价达到300时再减20并把满减金额放进返回结果。函数可以把一段会重复使用的逻辑单独保存起来参数负责把数据传入函数return负责把处理结果交回调用位置。下一章我们就来讲变量作用域、匿名函数和高阶函数。