Python入门 函数
需要去调用一个函数的时候, 是得要知道这个函数的名称, 还有它的具体参数是怎么规定的, 比方说那个用来求绝对值的 abs 函数, 它就接收了一个参数。你可以直接访问该网站的官方页面去查看相关的文档资料, 这些内容都是可以直接获取到的。也可以在交互式命令行通过 help(abs) 查看abs函数的帮助信息。调用那个abs函数的操作是这样的。 abs(100)100 abs(-20)20 abs(12.34)12.34当我们在去调用那个函数的时候。一旦我们传入的参数数量不对。就会出现报错的情况。而且还会特别明白地告诉你具体的错误原因。就是说 abs 这个函数只接受一个参数。但是实际上我们却给出了两个。 abs(1, 2)(most call last):在文件里, 第一行那个地方。: abs() takes one (2 given)在当传入参数的数量被确认是正确的时候, 不过参数类型依然无法被函数所接收的话, 同样也会触发错误的报告动作, 并且还会提供具体的错误提示信息内容出来, 这里所指明的错误状况是表示str属于那种不被接受的参数类型情况。调用函数, 参数为小写字母a。(most call last):在文件里, 第一行那个地方。: bad type for abs(): str那个用来比较的函数, 名字是cmp, 它后面跟着一个括号括起来的x,y, 这个函数整体是需要接受两个参数才成样的, 当那个x如果大于y的时候, 就返回1。 cmp(1, 2)-1对这两个数进行比较, 用2来减1。用比较的这个功能来看一下三和三这两个数之间的关系。内置的那些常用的函数还包含了数据类型转换的功能, 比方说那个int()函数, 它是可以把别的数据类型转换成整数的: int(123)123 int(12.34)12那个叫做 str 的函数, 它的功能呢, 就是把各种各样其他的类型, 全部转换成为 str 这个类型的字符串。 str(123)123把那个数字一点二三转换成为文本字符串。1.23编写函数在中定义一个函数要使用 def 语句依次写出函数名、括号、括号中的参数和冒号:然后在缩进块中编写函数体函数的返回值用 语句返回。我们这里用一个例子来跟大家说明情况, 那个例子就是关于怎么去自己定义一个函数, 这个函数的主要功能是负责计算绝对值的。def (x):if x 0:x else:-x请注意, 函数体里面的那些语句, 在跑的时候, 只要碰到了那个返回的地方了, 那这个函数就算跑完了, 然后就给你返回一个结果。所以说, 函数里面, 你要是用了那种能分情况的好多选择还有能一直循环去做的这种操作的话, 那你就能弄出非常复杂的那个各种各样的逻辑来处理的这个事情。就算里头没写任何东西, 等那个函数跑到底部的时候, 它照样会把结果给扔出来, 只不过是拿 None 这么个玩意儿当结果而已。none是那个意思, 可以把它简成其他的字。返回值会有多个结果。函数是可以返回多个值的呀, 这一点的答案呢是明确给出的那就是肯定的。比如说, 在游戏这个场景下面, 很多时候都需要让某个角色或者对象从一个具体的位置去移动到另一个位置, 如果把这个出发点的坐标数据, 还有需要移动的位移大小以及相关的角度数值都提供清楚的话, 就可以通过计算得出那个新的坐标点具体在哪里了:这个math包里面, 它是提供了sin()函数以及cos()函数的情况所以对于我们的操作来说, 第一步需要去做的, 就是先通过引用的方式, 把它给引进来。math这个函数是用来完成移动动作的, 它需要输入四个值, 这def move(x, y, step, angle):。nx x step * math.cos(angle)ny y - step * math.sin(angle)nx, ny这样一来, 我们就能在同一时间拿到返回的结果了。 x, y move(100, 100, 60, math.pi / 6) print x, y151. 70.0其实, 这不过是一个假象罢了, 因为函数返回的内容依然是单一的数值, 所以情况是: r move(100, 100, 60, math.pi / 6) print r(151.962, 70.0)通过print这个函数来输出返回出来的那个结果, 大家会发现, 原本的返回值其实是一个tuple可是, 在语法方面来说, 如果你返回一个元组的话, 是可以省略掉那个括号的, 同时, 多个变量又能一次性接收好这个元组的东西, 它们就会按照各自的位置去对应着进行赋值的操作, 所以说, 那个函数的能返回多个值这个功能, 它其实就是等于返回了一个元组, 只不过就是写成这样比较方便一点而已。递归函数在函数里面的时候, 是可以调用其他函数的。如果一个函数它自己在内部调用了自身本身, 那么这个函数也就是递归函数。打个比方, 我们去算一算n的阶乘, 也就是n!, 它的写法是1乘以2再乘以3一直乘到最后等于n, 这里我们用函数名fact(n)来把这个过程表示出来, 大家仔细看一眼就会明白:def fact(n):if n1:1n * fact(n - 1)递归函数有着定义方面比较简单, 还有逻辑表现得很清晰这样的优点。从理论的角度来看, 所有的那些可以被称作是递归函数的东西, 其实都是能够去写成循环的形式存在的。不过呢, 循环里面的逻辑清晰度方面, 确实是没有办法能够达到和递归一样的水平, 递归的逻辑还是更加地清晰一些。大家在用递归函数的时候, 得当心别搞出栈溢出这个情况。在计算机里面, 函数调用是靠一种叫作栈的数据结构来弄的。每当你进入一个函数去调它。堆就会加一层叫做栈帧的东西。等到函数跑完又退出来的时候。堆就会减掉一层那个栈帧。因为堆的空间不是什么都可以无限塞下去的。所以, 你要是让递归调用的次数变得太多了。就会引发栈溢出这个问题。你可以去试一下那个计算 fact(10000) 的任务看看。关于函数 move(n, a, b, c) 的具体定义, 它的意思其实很简单, 就是指让咱们把 n 个圆盘, 先从 a 这个地方拿出来, 然后借助着 b 这个中间位置进行一些操作或者暂存, 最终把这些圆盘全都移动到 c 这个目标位置上去。参考代码:def move(n, a, b, c):if n 1:print a, --, cmove(n-1, a, c, b)print a, --, cmove(n-1, b, a, c)执行移动操作, 把第4个元素从位置a转移到位置b, 并且同时涉及位置c。解决递归调用栈溢出的问题, 其方法是通过尾递归优化这个手段。事实上的情况是, 尾递归这种程序和循环的效果是一样的。基于这样的原因, 把循环看成是一种特殊的尾递归函数, 这个观点也是可以的。尾递归是这样的意思, 就是在函数进行返回操作的时候, 去调用它自己本身这一点, 并且相应的代码语句里面不能包含其他的表达式, 正是因为这样一个特点, 编译器或者是解释器就可以对这种尾递归的情况做出优化处理, 于是呢, 不管这个递归被调用多少次数, 其占用的栈帧数量都保持不变, 始终只占用一个, 所以就不会出现因为栈深度过大而导致的栈溢出问题。因为上面的那个fact(n)函数里面的n * fact(n - 1)把乘法表达式给引上去了, 所以它就不是尾递归了。想要把它改成尾递归的方式, 需要额外多一点的代码, 主要的工作是把每一步的乘积传送到递归函数中间去。def fact(n):(n, 1)def (num, ):如果数字是一的话呢, 那就需要去计算那个减一的结果, 同时还得再乘上那个没有指明的数。我们可以看到的是, 那个由逗号分开、左边是num减去1、右边是num乘以某个后面跟着的空格的元组, 其实也就是仅仅代表返回了递归函数本身这一点而已, 因为num减1还有num乘号这些内容, 都会在函数调用动作发生之前就已经被计算清楚了, 所以说并不会对函数调用的过程产生任何影响。fact(5)这个情况, 它对应的就是那个括号里面有5和1的调用, 具体的细节是这样的:结果是, 从位置5指向位置1。输出的是指向位置4, 5。在第三步的时候, 第二十行的那个位置。这表示从第二个位置开始, 一直到第六十个位置。 (1, 120) 120当发生尾递归调用的时候, 要是进行了优化处理的话那么调用栈就不会出现增长的情况, 正因如此, 不管执行了多么多次的调用操作也是不可能导致栈溢出这种错误现象出现的。挺让人遗憾的, 多数编程语言里没有把尾递归当成一回事去优化, 解释器也一样, 没做相应的工作, 于是就算把上面的 fact(n) 函数改成了尾递归的方式, 最后的结果依然是会导致栈溢出。把默认的参数的定义这件事, 给弄好。在定义函数的这一段过程里头, 还能够把默认参数这个东西给设置上。比方说那个自带的int()这个函数, 它实际上是具备两个参数的, 所以我们既可以选择去传递一个参数, 同时也可以选择去传递两个参数: int(123)123 int(123, 8)83在调用 int() 函数的时候, 它的第二个参数是用来指定转换进制的。假设你没有传递这个参数, 那么系统就会默认按照十进制来进行处理, 也就是 base10 的情况。反之, 如果你确实传递了这个参数, 那么程序就会使用你传进去的那个具体数值作为进制标准。可以看得出来, 函数这个默认参数的存在是为了简化调用的过程, 你只需要把那些必须的参数传进去就可以了, 不过在有需要的时候, 你也可以选择把额外的参数给传进去, 以此来覆盖默认的参数的值。我们这里要做的事情, 就是去为一个函数下定义, 这个函数的任务呢, 是把数字 x的 n 次方给算出来。这是一个名为power的函数, 它接收x和n这两个参数。s 1while n 0:n n - 1s s * x我们可以这样来想, 如果在所有的步骤里面, 计算平方这个动作出现的次数是最多的话, 那么我们就把 n 这个数字的默认值设置成 2 就可以了。def power(x, n2):s 1while n 0:n n - 1s s * x这样的话呢, 进行平方这个操作的计算工作就完全不用再费劲去专门传递两个参数了。对power函数进行调用, 同时传入5这个数值。25因为函数那个参数的匹配顺序, 它是按照从左向右这个方向去进行的, 所以就会导致默认参数这东西, 是必须得放在那些必需参数的后面才能被定义好的, 这一点是需要注意明白的。定义一个函数叫做fn1, 它里面带有三个变量参数。这个代码出错的原因是, 在定义fn2函数的时候, 参数b被设定为了默认值。pass首先, 我们要定义一个函数, 这个函数需要接收一个列表作为参数, 然后在这个列表的最后面添加一个表示结尾的元素, 最后把修改过的列表返回出去:def (l):l.(end) l当你进行正常调用的时候, 那个结果看起来是很不错的。 ()1, 2, 3, end (x, y, z变量x, 然后是变量y, 接着是变量z, 最后是表示结束的那个标识end。当你使用默认参数调用时, 在最初的那个阶段结果其实也是正确的: ()可是, 在再一次进行调用的操作时候, 得到的那个结果就非常的不正确了: ()第一个是end, 第二个也是end。 ()end, end, end很多下面把具体的原因是给大家做一下解释的。函数在定义的时候, 默认参数l的值就被计算出来了。默认参数l也是一个变量, 它指向对象。每次调用该函数, 如果改变了l的内容, 则下次调用时, 默认参数的内容就变了。这时候, 它就不再是函数定义时的值了。因此在定义带有默认参数的函数的时候, 大家要心里记清楚这一条关键的内容, 那就是这个所谓的默认参数, 它必须是要指向一个不会改变的对象的。如果你打算要对之前的那个例子做出变更处理, 咱们其实是可以借助于 none 这个不变的物件来把它搞定的。def (lnone):if l is none:l l.(end) l现在这个状况就是, 不管你怎么去调用, 都完全不会有什么样子的任何问题的出现。 () ()人们经常想要问, 为什么要去设计像str还有none这样的一些不可变对象呢? 这里的情况是这样的。因为一个不可变对象一旦就被创建出来了。它内部所存储的那些数据就不可能被修改了。这样子的做, 就可以减少由于对数据进行修改而导致的各种错误情况发生。另外的一点原因是这样的。因为这些对象是不可变的。所以在多任务的环境之下。如果要对这些对象同时进行阅读的话。是完全不需要进行加锁操作的。这样的话, 同时去读取这些数据的时候, 一点问题都不会有。所以我们在编写程序的时候。如果能够去设计一个不可变的对象。那么就请尽量地把这个对象设计成不可变的状态。定义可变参数如果想要使得一个函数的功能得到满足, 也就是这个函数可以去接收那种不固定数量的参数情况, 那么我们是可以采用以下做法的, 那就是去专门定义出一种叫做可变参数的这样的东西出来给你用的。def fn(*args):将参数打印为输出内容。那个可变参数的名字, 它的跟前头是摆着一星号的, 这样子的话, 咱们就可以把那零个参数传进去, 也能把那一个个的参数传进去, 还可以把那多个个的多多的参数也一起给它传进来, 就是这么一个回事。 fn()()调用那个叫做fn的函数, 传入参数a。(a,) fn(a, b)(a, b)调用函数fn, 参数分别是字符a, 字符b和字符c。(a, b, c)因为解释器会把传入的那一些参数, 都组装成一个元组类型的数据, 然后把它传递给那个可变参数的位置, 所以你在函数内部使用的时候呢, 直接把名字叫做args的那个变量, 当成是一个元组去看待就可以了。关键字参数可变参数的这个功能, 允许你往里面传入零个或者任意多个的参数, 这些传进来的可变参数呢, 在程序调用函数的时候, 会自动地被组装成为一个元组形式的数据。另外一点, 关键字参数也支持你传入零个或者任意多个包含有具体参数名称的变量, 这些带名称的关键字参数, 在代码执行的内部环节, 会自然而然地被整理成一种字典格式的结构。def (name, age, **kw):输出姓名这个标签的对应的值, 接着是年龄这个标签的对应的值, 再接着是其他这个标签的对应的值所关联的内容被打印到显示界面上。这个函数里, 除了那个必须要给的参数name还有age之外, 它还会接受那些叫关键字参数的东西kw当中的东西, 大家在调用的时候, 要是只把那些必选的参数传进去也是可以的。 (, 30)这里是一个名叫name的人, 他的年龄是30岁, 其他信息用一个空的字典来表示。这种办法是能够接纳数量上没有限制的, 由各个名目所指定的那些参数, 把它们作为传进来的东西的:结果是, 括号里面写着用小写字母的bob名字, 然后是数字三十和五, 最后面的地方部分是空的。名字是 bob, 年龄是 35其他信息是一个字典, 其中城市的信息是空的, 接着是一组数据, 包括 adam, 四十五岁, 等于号后面跟着字母 m, 还有工作职位的信息是空的。该对象的标识符名为adam, 此人现年45岁, 其附加信息构成一个包含键值映射的特定格式集合, 其中代表性别的字段对应的值为m, 而代表职业身份的字段则是空的。大家可能会关心, 关键字参数到底起到的是什么用作用呢? 它其实可以帮助我们对函数的功能进行进一步的扩展。具体来说, 当我们写这个函数的时候, 我们能够确保自己一定能接收到包括name和age在内的这两个具体的参数在内, 与此同时, 如果那个调用此函数的人非常愿意并且主动提供了比刚才所说的还要更多的其他参数出来, 我们的系统同样是能够把这些额外接收到的数据进行处理的。请大家试着去想象这样一个场景出来, 就是你此刻正在进行一个关于用户注册模块的业务工作。在这个业务逻辑里, 用户名和用户年龄是必须被填写进来的项目要求, 可是除开这两样核心的东西以外, 剩下的所有其他信息选项全部都是可以选择填写或者不填写的可选状态。所以在这种情况下, 如果你选择使用由关键字参数来充当定义该相关函数的主要方式的话, 那么你就完全有能力借此去达成完成整个注册环节所提出的各项需求目标了。命名关键字参数关于那个关键字参数这块事儿, 函数的调用者是可以随便传进去任意多种, 而且数量完全没有限制的关键字参数的。那么至于具体都传进去了哪些东西? 这档子事, 就是需要在函数内部通过一个叫做kw的那个东东来进行检查了。我们还是拿那个括号里的函数例子来说吧, 我们想要检查一下, 是不是有这个城市并且还有工作这两个参数在里面的。def (name, age, **kw):if city in kw: # 有city参数pass在当前这个字典kw里面的情况之下, 如果键名为job的项目存在, 那么这里就会被认定是拥有了一个用于表示工作内容的参数。pass输出姓名这个标签的对应的值, 接着是年龄这个标签的对应的值, 再接着是其他这个标签的对应的值所关联的内容被打印到显示界面上。不过呢, 调用的人还是可以传进来那些没有受到任何限制的关键字参数的。 (jack, 24, city, addr, )若是要对关键字参数的名字实行限制, 那么就可以使用命名关键字参数来进行处理, 打个比方来说, 如果仅打算接收city和job二者作为关键字参数, 那么通过这样的方式所定义的函数会呈现出如下的具体形式。这是一个定义了名为name和age的位置参数, 以及强制使用关键字参数的city和job的函数声明结构。print(name, age, city, job)kw这个部分和关键字参数是不太一样的, 命名关键字参数需要去用一个专门的分隔符*, 然后呢, 在*后面的那些参数会被视为命名关键字参数。这个调用的具体的方式是像下面来展开的。 (jack, 24, city, job)jack 24命名关键字参数是需要传入参数的名称的这种要求和位置参数是不同的。要是没有把参数名传进去, 调用就会报错:这个元组里边的第一个值是叫做杰克的, 然后是24岁, 再后面是两个空字符串。(most call last):file , line 1, in : () takes 2 but 4 were given因为在调用的时候, 缺少了名为city的参数还有名为job的参数, 所以解释程序就会把这里面的4个参数全部看作是位置对应的参数, 但是那个括号表示的函数本身只能够接受2个位置类型的参数。命名关键参数的这一些是可以带有默认值的, 这么做呢能够让那种调用的情况变得更简便一些。定义这个名字, 定义这个年龄, 然后定义这个城市是多少, 如果没给就是空字符串, 然后定义这个工作是哪个。print(name, age, city, job)因为那个用来作为名字的关键字参数的城市, 它是有设定好默认值的, 所以在进行调用的时候, 使用者是可以选择不传递这个关于城市的参数进去的:这是数据 jack, 年龄是二十四, 并且工作方面的信息保持为空的状态。jack 24当你在使用命名关键字参数的时候, 需要特别去留意这一点, *这个符号并不是一个参数, 而是充当了一个特殊的分隔符存在的角色。 假如你把它给遗漏了的话, 那么解释器就没法能够正确地识别出位置参数和命名关键字参数之间的区别了:定义一个包含姓名、年龄、城市和职业这四个要素的函数。这里缺少了一个星号, 所以这个city还有这个job被看成了是位置参数。pass也就是参数的排列组合方式。在中定义函数, 可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数, 这 5 种参数都可以组合使用, 除了可变参数无法和命名关键字参数混合。但是请注意, 参数定义的顺序必须是: 必选参数、默认参数、可变参数 / 命名关键字参数和关键字参数。举个例子来说吧, 就是去定义一个函数, 这个函数里面要包含上面提到过的那么多种参数。def f1(a, b, c0, *args, **kw):print(a , a, b , b, c , c, args , args, kw , kw)def f2(a, b, c0, *, d, **kw):程序执行的显示环节里面会出现变量a的赋值, 以及变量b的赋值, 同时还有变量c的赋值, 再加上变量d的赋值, 最后还会出现关键字参数kw所代表的内容被打印出来供人阅读。当函数被调用的时候, 解释器会自动按照参数的位置信息以及参数名称的方式, 把相对应的那些参数传递到内部去。 f1(1, 2)a 1 b 2 c 0 args () kw {} f1(1, 2, c3)设定变量a等于一, 设定变量b等于二, 设定变量c等于三, 设定元组args为空, 设定字典kw为空, 然后调用函数f1, 并且传入位置参数一、二、三以及参数a和参数b。变量a等于一, 变量b等于二, 变量c等于三。args等于由字符a和字符b组成的元组。kw等于空字典。接着执行f1这一项函数调用, 传入的参数包括数字一、二、三, 以及字符a、b, 同时还有关键字参数x等于九十九。令a 等于 1, b 等于 2, c 等于 3, args 设为元组 (a, b), kw 设为字典 {x: 99}, 接着执行调用 f2(1, 2, d99, extnone)。设变量的值为a等于一, 变量b的值是二, c的值为零, d的值是九十九, kw这个字典里的ext对应的值是none。最让人觉得很神奇的一点是, 你可以通过一个元组以及一个字典这些内容, 去调用上面说到的那些函数。定义一个元组叫做args, 里面放的是1、2、3和4这四个数字。再定义一个字典变量叫做kw, 这个字典里包含两个字段, 一个是带有单引号的d并且对应的值是数字99, 另一个是带有单引号的x并且对应的值是井号符号。先让数值a等于1, 然后让数值b等于2, 接着让数值c等于3, 再把参数args设成一个只包含数字4的元组, 另外把关键字kw设为一个字典, 里面有一个键d对应值99, 还有一个键x对应值井号, 然后再次让参数args变成包含数字1、数字2和数字3的元组, 同时再次让关键字kw变成一个字典, 其中键d对应的值是88, 键x对应的值还是井号最后调用函数f2, 并且解包参数args以及解包关键字kw。a 1 b 2 c 3 d 88 kw {x: #}所以呢, 无论是怎样的函数, 我们总能够用那种类似 func(*args, kw) 的样子去把它给调用起来的, 这就和这个函数的参数到底是怎么定义出来的没有任何关系了。小结该函数具备非常灵活的参数形态, 用户既能够实现简单的调用操作, 又可以传入非常复杂的参数内容。默认的参数是一定要用不可变的对象来进行设定的, 如果使用的是那个可变的对象的话, 那么在程序进行运行的过程当中, 是会出现逻辑上的错误的。你需要关注并且去仔细留意一下, 那个关于定义可变参数, 还有关键字参数的语法规则方面的事情, 具体来讲是这样的:*args这个变量属于可变参数的类型, 当它被使用的时候, args它会接收并容纳一个元组类型的对象。kw这个称呼呢, 代表的是关键字参数, 而kw所接收的那个内容, 实际上是一个字典类型的对象。并且, 在调用这些函数的时候, 具体应该如何去传递那些可变数量的参数, 以及那些关键字形式的参数, 它们的语法究竟是怎么一回事。可变参数这个功能是可以直接传值的, 比如写成func(1, 2, 3)这样的形式, 同时它也支持先把数据组装到一个列表或者元组里面去, 然后再借助于*args这种语法来传入, 比如说写成func(*(1, 2, 3))这种形式。关键字参数这件事, 既能够让人直接地就把那个形式给传进去, 比如说像 func(a1, b2) 这样去用, 同时也是可以先弄出一个 dict 这样的数据结构来, 然后呢, 再通过那种带两个星号的 kw 这种形式把它给传入进去, 就像 func({a: 1, b: 2}) 这模样一样。在使用 *args 和 kw 这种方式的时候, 这是一种约定俗成的用法, 当然用户也可以自己选择其他的参数名称, 但是从规范的角度出发, 还是推荐使用这种大家普遍接受的习惯性写法。所谓命名的关键字参数呢, 它的存在就是为了去限制那些调用者们在调用的时候可以往里面传进去的参数名字都有哪些, 并且呢还可以顺带着提供一些默认值来使用。定义命名关键字参数的时候, 千万别忘了把那个作为分隔符的星号给加上, 如果不加这个分隔符的话, 系统就会把它当成是一种位置参数来进行定义了。