Python进阶-自定义函数基础

本文为《爬着学Python》系列第十篇文章。

在实际操作中,可能函数是我们几乎唯一的实现操作的方式,这是因为函数能够构造一个高度集中的变量环境,在合理的设计下,它能使程序思路更加清晰的同时更利于调整与修改。几乎没有哪个程序设计语言会不涉及自定义函数的。

《Python进阶-自定义函数基础》

在上一篇文章中我们留了许多内容说要在本文中介绍,它们是一些和函数参数相关的问题。函数是我们的对操作方式的一种整合,因此我们会通过函数来进行运算或者完成某些功能,这些功能涉及到变量时,我们必须清楚到底发生了哪些事情。废话少说吧。

创建自定义函数

Python的自定义函数格式中规中矩

def func_name(arg1):
    pass

def引导自定义函数名,用括号给出该函数的参数,在冒号后换行通过缩进确定函数体。在格式上和条件判断语句有些相似。

当然,我们从简单的开始讲起,这是Python自定义函数的简单形式。一般能“动手脚”的地方只有三个,一个是def前面可以用装饰器(详见我的另一篇文章Python精进-装饰器与函数对象),一个是函数参数,一个是执行语句。

关于执行语句部分,主要是函数的嵌套以及控制结构的组合,这种内容作为知识讲解没什么意思。大多数人都知道可以这么做,但很多人做不好,是不是因为没学好呢?我觉得不是的,是练少了,积累项目经验以后就会逐渐强化这方面的能力。而装饰器之前专门提前讲过,因此本文的重点会放在函数参数上。之后也会在深入了解Python自定义函数参数设计的基础上去认识如何正确设置函数返回值。

自定义函数的参数

首先我要声明一点,我决定不讲一般意义上的形参(形式参数)和实参(实际参数)的知识。按道理来说,即使Python不严格要求定义函数参数,但这方面的知识有助于理解自定义函数中参数操作的情况,还是应该说明一下的。但是我仔细想了一下在Python编程中不知道这两个概念真的完全没有任何关系,我们可以简单地理解为在定义函数时括号中声明的参数是我们在函数使用中会用到的参数,在调用函数时括号中的变量就是参加函数运算用到的变量。是的,换个名字,参数(用于定义)和变量(用于调用)就足以理解了。

可能完全没有基础的同学看上面一段话还是有些不明白,这很正常,我们还没有讲过函数的调用。没关系再接下来的例子中我们会见到。不过这一节我们重点是看看函数定义时参数有哪些形式。

最普通的参数

最普通的自定义函数参数就是在括号中列出一系列我们要用的参数。

def print_times(_string, _time):
    for i in range(_time):
        print(_string)

print_times('Hello!', 3)

在这个例子中我们定义函数时定义了两个变量,分别是_string_time。这个函数的作用不用我过多说明,函数体只有一个循环语句,目的是重复输出一个字符串。首先要注意的是为什么我要在”string”和“time”前加下划线呢,这是为了防止和其他变量出现冲突。如“string”有可能和内置关键字冲突(其实不会,Python字符串内置关键字是str,这里是为了保险起见),“date”有可能在使用了标准库datetime与其中的方法冲突。为了减少歧义,在定义函数的时候给变量前面加上下划线是比较稳妥的办法。这个技巧是面向对象编程时类设计常用的手段,在自定义函数中一样可以用。在后面的例子中,有时我会用比较长的不太可能冲突的变量就可以不这么做了。

接下来就是函数的作用的问题,我们需要重复输出一个字符串,所以理所当然的情况下我们只需要简单地涉及两个操作对象,一个是要输出的字符串,一个是这个字符串输出的次数。这是比较好理解的。所以我们调用函数的时候,我们给出的字符串是Hello,次数是3,这个函数就会输出Hello三次。

但是可能会有疑惑的是,我们有必要自定义一个函数这么做吗?我们直接用一个循环语句不是一样可以完成这样的工作吗?这也就是我们为什么需要自定义函数的问题。

在文章开头我简单讲了一下自定义函数可以把操作进行整合,可以集中变量环境,这里我们仔细说明一下这些话是什么意思。

诚然我们可以通过一个循环语句来完成重复输出字符串的工作。但是,如果我们在程序中需要多次用到这个功能呢,是不是我们每次都要再写一个循环语句呢?情况更糟的是,如果代码写完了好几天以后,我突然想要在每次输出这个字符串以后再输出一个另一个字符串呢?如果我们使用了函数,这时候我们可以把函数改成这样

def print_times(_string, _time, fix_string=None):
    if fix_string is None:
        for i in range(_time):
            print(_string)
    else:
        for i in range(_time):
            print(_string)
            print(fix_string)

或者这样

def print_times(_string, _time, fix_string=None):
    def print_times_former(_string, _time):
        for i in range(_time):
            print(_string)

    if fix_string is not None:
        _string += '\n' + fix_string
    print_times_former(_string, _time)

或者我们可以写一个装饰器(功能会更局限,在此不演示了),总之方法有很多。

注意到我给新参数一个默认值并且使用了一个判断语句,这样原来调用print_times函数的地方不会报错,会像原来一样完成工作(有默认值的参数会在下面介绍)。我们可以去调用了print_times函数的地方加上我们需要使用的函数,它们就可以完成新功能了。

可能你还可以反驳,就算我写了几遍循环,我就去用了循环的地方添上不就行了吗。那好,我的问题是,如果一个文件代码量很大,那么多for语句,你要找出来是重复输出字符串的地方恐怕也挺费劲吧,不小心改到别的循环运行有问题是不是还得回来找?如果用了函数,在任何编辑器中ctrl+F查找print_times结果就一目了然了(在编辑器如VS Code中你只要选中这个字段就能清楚看到,甚至不需要搜索,而且可以复选进行同步修改)。

而且试想一下,这只是一个简单的重复输出字符串的功能而已,如果是更复杂的功能,函数的优势就更明显了。这还是没有返回值的函数,涉及到返回值时,函数的优势非常大,下面我们会谈到。函数还可以在别的文件中引用,而不是直接复制粘贴一大段代码过来。

言归正传,我们来看看最开始的简单的print_times函数是怎么工作的。我们把_string_time作为参数,在函数体的执行语句中定义了一些操作,但是如果我们不调用这个函数,那么什么都不会发生。其实自定义函数就像是一个模板,我们给出要操作的对象的典型(就是参数),在函数体中给出它的操作语句。定义自定义函数的时候它是不会真的对这些参数进行操作的,它只是用来规定我们操作参数的方法。我们定义了一些对这些参数的操作,然后把它打包成一个函数。意思就是,要是以后要对一些变量用这个函数,那么程序就请按这样操作吧。

于是,当我们print_times('Hello!', 3)这样调用print_times函数的时候,程序就会完成我们规定好了的工作。要注意的是,仅仅是print_times的话一般代表这个函数本身,它有可能是函数变量,也有可能是函数对象。而如果函数后面加上括号,在括号里面给出作为参数的变量,print_times('Hello!', 3)就是调用这个函数。这些知识还是参考Python精进-装饰器与函数对象

需要说明的是,函数调用的时候,变量的顺序是要和函数参数定义的时候声明参数的数量相等且顺序一致的。除非我们在给定参数的时候指明参数名,如

print_times(_time=3, _string='Hello!',)

这样即使顺序和参数声明的时候的顺序不一致,解释器也能完成正常完成功能。但是这个方法非常不推荐大家使用,原因在后面会再提。之所以要说函数参数的顺序问题,因为这涉及到其他形式的函数参数,包括有默认值的参数和可选参数。

接下来我们先介绍有默认值的函数参数。

参数的初始值

其实参数有默认值的函数我们在上面就见过一个,但是在这里我们先不去管他。我们先来看看这个所谓的参数默认值是什么样的。

def func_defualt(a=3)
    print(a)

func()
func(2)

注意到形式其实很简单,就是在声明函数参数的时候用赋值语句给参数一个初始值。在这样的情况下,我们本来调用函数是需要给出变量作为参数的,但是如果我们的参数有默认值,那么如果我们在调用这个函数时不实例化这个参数,那么程序就会用参数的默认值进行操作。上面的两条调用语句,分别会输出32

接下来要说的,就是刚才我们所说过的参数顺序的问题。直接先说结论,有默认值的参数要放在所有没有默认值的参数后面。这个规定不像之前涉及过的编程习惯问题,这是默认Python解释器规定的出错类型。

>>> def func_default2(a=1,b):
...     print(a, b)
...
  File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
>>>

Python之所以要这样规定,是为了减少程序出错的可能性,是出于safety的考虑。在程序中safety和security是不一样的概念,security一般指程序抵御外部攻击的能力,safety则一般指程序运行的稳定性。

试想一下,如果我们能够用def func(a=1,b):这样的形式定义函数,那么调用这个函数的时候就可能会出现问题。首先,如果你按照顺序给出了所有参数的值,或者虽然打乱顺序但是对应好参数名用变量赋值了,那么你有什么必要给这个参数一个默认值呢?那到了想让参数默认值发挥作用的场景,你也只能把除了有默认值的参数以外的其他参数都对应好参数名用变量赋值,这不仅麻烦而且容易出现纰漏,如果有某个参数没有值,程序就会报错。而且,在实际编程中,函数参数有可能远远不止两个,如果其中一部分有默认值一部分没有,但是顺序又被打乱了,那么调用这个函数将会是非常糟糕的一件事情。所以,为了省去不必要的麻烦,Python解释器将这个按道理来说也是编程习惯的做法变成了强制的规定。

当然,以上一大段都不重要,只要记住一点,有默认值的参数要放在所有没有默认值的参数后面。

另外值得一提的是,一般参数在函数调用时,如果不给出参数名,不能置于有默认值的参数之后

>>> def func_default2(a, b=1):
...     print(a, b)
...
>>> func_default2(b=2, 3)
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
>>>

range函数的练习

知道了上面的概念以后,我们来拿range函数当作练习。由于还没有介绍过生成器,而且我们练习的重点是函数参数的设计,因此我们只需要返回range()对象就行。要求像Python内置的range函数给定参数的规定一样

  1. 当只用一个变量调用这个函数时,这个变量指的是输出的等差数列的终点,如range(5)
  2. 当给定两个变量时,分别指输出的起始值和终点,,如range(2, 5)
  3. 当给定三个变量时,在上一条的基础上第三个变量指输出时的步长,如range(2, 5, -1)

(假定我们调用这个函数时总是用整数或浮点数)

分析一下如何实现这个函数,下面给出我的思路作为参考

  • 一共需要三个参数是显而易见的;
  • 最直观的感受是起始值是要有默认值的,如果不规定从哪里开始,那就从0开始;
  • 步长也是要有默认值的,如果不规定,那么步长是1;
  • 根据有默认值的参数要放在后面的原则,那么最理所当然的参数设计是range_custom(stop, start=0, step=1)
  • 这个方案看上去可行,但是不满足刚才的后面两个要求,如果我们这样用两个变量调用,起始值和终点是反的;
  • 我们加个判断就可以了,如果start用了初始值,那么说明我们调用的时候只给了一个参数,这个时候stop就是终点,如果start被重新赋值了说明给了至少两个参数,那么这时候把stop和start的值调换一下就可以了;
  • 现在这个函数似乎可以满足大多数情况了,但是有一个bug,如果给定参数的时候给的start值就是0怎么办呢?如range_custom(-5, 0)按目前的规则会被翻译成range(0, -5),但是我们的目的却是range(-5, 0)
  • 所以start的初始值不应该是数字而是别的数据类型,为了方便起见,我们把它的初始值赋为None,我们的程序雏形就出来了。
def range_custom(stop, start=None, step=1):
    if start is None:
        return range(stop)
    return range(stop, start, step)

现在这个程序已经满足我们的要求了,但是看上去不太舒服,可以改成

def range_custom(start, stop=None, step=1):
    if stop is None:
        return range(start)
    return range(start, stop, step)

现在这个函数的参数顺序在逻辑上更好理解一些,可以说基本上满足我们的要求了。当然,本例只是为了说明参数的顺序问题,并不是为了实现range函数。事实上Python的range函数还包括参数实例化,生成器等知识,在后面我们应该还有机会再接触它。

可选参数

说到可选参数,可能有的人见过,却也不明白到底是什么意思,它一般是这样出现的

def func_option(*args):
    return args

注意到我们声明函数的时候在参数名前加了个*星号,这是声明可选参数的方法。那么可选参数到底有什么用呢?

可选参数的作用是用元组把所有多余的变量收集起来,这个元组的名字就是这个可选参数名。在上例func_option中我们可以用任意多个变量调用它,比如a = func_option(1, 2, 3)那么a就会是元组(1, 2, 3)。关于为什么是元组而不是列表,我们在上一篇Python进阶-简单数据结构中说过,元组在Python中往往是比列表更优先考虑使用的数据结构,具体原因在本文靠后深入自定义函数参数部分会讨论。

我们刚才说可选参数会收集多余的变量。我这么说是有原因的。

>>> def func_option(a, *args, c=2):
...     return args
...
>>> func_option2(1)
()
>>> func_option2(1, 2)
(2,)
>>> func_option2(1, 2, 3)
(2, 3)

注意到我们的*args把除了给普通参数的第一个变量以外的值都放进了元组中。这样做导致了一个,问题在于我们的有默认值的参数如果不给定参数名地调用的话,就永远只能用默认值了。而且如果我们在调用函数时不把有默认值的参数放在最后面程序还会报错。

>>> func_option2(c=1, 2, 3)
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument

那么有没有好的办法能规避这个问题呢?我们可以试试把可选参数放在有默认值的参数后面。

>>> def func_option3(a, c=2, *args):
...     return args
...
>>> func_option3(1)
()
>>> func_option3(1, 2)
()
>>> func_option3(1, 2, 3)
(3,)
>>> func_option2(c=1, 2, 3)
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument

那么这种形式的函数能不能解决之前的问题呢。看上去不行,不过我们知道了,调用函数的时候,要尽量把有默认值的参数放在靠后
的位置赋予变量。那么这两种我们到底该用哪个方法呢?在实际操作中,我们倾向于将可选参数放在有默认值的参数之后,而且如果参数较多,我们倾向于调用函数时都会所有变量都加上参数名。而且实际操作中,其实可选参数用得不那么多,相对来说,另一种可选参数其实用得更多。这种可选参数
的形式一般是这样

def func_optionkw(**kwargs):
    return args

在这种情况下,关键字可选参数都是作为键值对保存在参数名的的字典中。也就是说,在调用函数时,在满足一般参数以后,变量都应该以赋值语句的形式给出,等号左边作为键右边作为值。如果不这样做,就会报错了。

>>> func_optionkw(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: t2() takes 0 positional arguments but 1 was given

需要说明的是,一个自定义函数只能有一个可选参数,同时也可以有至多一个关键字参数。其中关键字参数应该放在普通可选参数之后。

现在我们来总结一下函数参数顺序一般规律:

  • 一般参数放在最前面
  • 可选参数放在最后面
  • 关键字可选参数放在一般可选参数后面
  • 函数调用时尽量把有默认值的参数对应的变量放在靠后的位置
  • 如果参数比较多,调用函数时,最好所有变量都指明参数名

以上这些,有的是为了防止函数定义时出错,有的是为了防止函数调用时出错,总之,应该养成良好的编程习惯。

自定义函数的返回值

我们使用自定义函数集成对变量的操作,那么我们如何获得变量操作的结果呢?一般来说有两种,一种是对变量进行操作使其本身变化,这种行为是极不提倡的,这是不利于上面提到过的safety的,因为通过函数操作变量会带来不确定性,在下一部分我们会详细介绍;还有一种就是用变量当作运算的初始值,最后返回运算的结果。在上面的例子中,我们一般都是后面这种方法定义函数。

需要说明的是,这个返回值说是运算的结果,其实类型非常宽容。它可以是经过操数值运算后的一个数据,他也可以是列表元组等数据结构,它可以是个函数,它还可以是调用某个函数后用其返回值当作自己的返回值,总之返回值非常灵活。

那么我们刚才说的通过函数对变量本身进行操作的方法需不需要返回值呢?一般来说是不需要的,在C语言中,我们习惯性会对这种函数设置一个return 0这是为了检测是否函数正常运行,在Python中我们当然也可以这么做。虽然我说这种方法不安全,不常用,但是几乎每个C语言都会都会用到这个方法,这个方法一般用在main()函数中。关于编程范式的知识在这里就不展开讲了,我就只顺便简单讲讲Python中的main()函数一般长什么样子。

if __name__ = '__main__':
    pass

不管见过没见过,这个结构都是Python编程中非常普遍的方法。这个结构的功能是,如果该.py文件不是被其他文件import引用,就执行pass部分的语句。这就相当于Python的main()函数。如果我们直接执行Python文件,那么执行的就是这些语句。如果采用了这种结构,那么这个文件中的其他部分要么是静态变量,要么就是定义好了的函数。我们通过这个结构来调用一系列集成过的自定义函数来完成某种复杂的功能。

深入自定义函数参数

在这个部分中,我们会重点讲一下关于Python可变对象和不可变对象在函数中需要注意的地方。这个知识点几乎是面试必考内容,因为它体现了一个Python使用者对Python数据类型的理解以及函数设计方面的认识

可变和不可变

首先我们要介绍一下到底什么是可变对象什么是不可变对象。在之前即使介绍数据结构我也没有展开来讲,为的就是现在和函数参数一起进行说明。我们就拿列表和元组举例,这是我们之前讲过的典型的可变和不可变对象。

首先是列表:

>>> list_sample = [1, 2, 3]
>>> list_sample_mirror = list_sample
>>> id(list_sample)    # id函数用来查看变量在内存中的地址
1372626593864
>>> id(ist_sample_mirror)
1372626593864
>>> list_sample[1] = 5
>>> id(list_sample)
1372626593864
>>> list_sample[1] += [4]
>>> id(list_sample)
1372626593864
>>> print(list_sample_mirror)
[1, 5, 3, 4]

注意到我们可以更改列表的值,更改列表的值以后,本来和它初值相等的另一个列表也被改变了。出现这种现象的原因在于,由于Python的引用语义,变量赋值往往会指向一个内存中的最终对象,如果存在就直接引用。那么对于可变对象来说,改变它的值,就是对内存中的那个对象进行修改,因此其他引用这个对象的变量也受到“牵连”了。

那我们再来看元组又是什么情况呢:

>>> tuple_sample = (1, 2, 3)
>>> tuple_sample_mirror = tuple_sample
>>> id(tuple_sample)
2473662073160
>>> id(tuple_sample_mirror)
2473662073160
>>> tuple_sample[1] = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> tuple_sample += (4, 5)
>>> tuple_sample
(1, 2, 3, 4, 5)
>>> id(tuple_sample)
2473625127928
>>> tuple_sample_mirror
(1, 2, 3)
>>> id(tuple_sample_mirror)
2473662073160

可以看到一样是引用同一个内存对象,但是元组不允许改变其中的元素。不过好在元组也支持连接操作,但是和列表有什么区别呢,我们看到,连接后的元组其实已经不是原来那个元组了,其实Python按照要求的操作重新创建了一个元组将其赋值给这个变量。而另一个引用原来的元组的变量没有受到任何影响。Python通过限制操作来控制元组的稳定性。

这种情况下,通过赋值得来的tuple_sample_mirror就更加“safe”,它的值会保持在我们的意料之中。

需要说明的是,在函数中,这些事情一样会发生。

列表

def func_mutable(list_a):
    list_a += [1]
    print(list_a)

a = [0]

func_mutable(a)     # 输出[0, 1]
print(a)            # 输出[0, 1]
func_mutable(a)     # 输出[0, 1, 1]
print(a)            # 输出[0, 1, 1]

元组

def func_immutable(tuple_a):
    tuple_a += (1,)
    print(tuple_a)

a = (0,)

func_mutable(a)     # 输出(0, 1)
print(a)            # 输出(0,)
func_mutable(a)     # 输出(0, 1)
print(a)            # 输出(0,)

以上其实就是可变对象和不可变对象的区别。需要注意的是,可变对象有些操作也是不改变这个对象的,如索引操作。而不可变对象只要不对变量重新赋值,那么原来的变量永远不会变。

Python中另外一些数据类型几乎都是不可变的,如字符串和数字以及布尔值还有None。由于可变和不可变带来的相关操作细节非常多。比如说为什么在判断None的时候优先使用is None而不去判断==None,因为所有None都是用的同一个对象,判断时只需要查找内存地址看是不是引用同一个地址,而不用去看地址里面的内容是不是一致了。

可变对象作为函数参数

现在我们回到函数的问题上来,即可变对象作为函数参数的操作处理。我们先看一个例子:

def func_mutable(list_a=[]):
    list_a += [1]
    print(list_a)

func_mutable()
func_mutable()

注意到这个函数只有一个有默认值的参数,这个参数的默认值是一个空列表。那么实际操作中,会有什么样的问题出现呢?问题就在于,我们两次调用这个函数的输出是不一样的。两次分别是[1][1, 1]这是不合常理的。我们又没有改变参数的默认值,为什么函数执行结果还能不一样呢?原因就在于我们的参数默认值是个可变对象。

我们在Python精进-装饰器与函数对象中先把函数比作了列表,后来修正成为了元组。那学过简单数据结构以后,我今天要给出新的类比了,自定义函数其实更像是不可变字典。字典和不可变这两个概念都已经介绍过,那么合在一起理解起来应该难度也不大。Python的自定义函数有许多内置方法来保存运行所需要的信息,就像是用键值对保存信息的字典,不仅如此,它的键和值分别都是不可变对象。Python自定义函数用来保存参数默认值的内置方法是__defaults__,我们可以直接调用它来查看函数参数的默认值。那么我们就来试一下。

def func_mutable(list_a=[], tuple_b=()):
    list_a += [1]
    tuple_b += (1,)
    print(list_a, tuple_b)

print(func_mutable.__defaults__)
func_mutable()
print(func_mutable.__defaults__)
func_mutable()
print(func_mutable.__defaults__)

执行这个文件的输出结果是这样的:

([], ())
[1] (1, )
([1], ())
[1, 1] (1, )
([1, 1], ())

可以清楚地看到,Python是用元组来保存参数默认值信息的。当元组中的可变对象被操作以后,元组保留了操作结果。同样进行了操作,tuple_b却没有改变默认值,而且它的输出结果和我们设想的一样,两次都是同样的输出结果。

通过以上的对比我们不难看出,列表是不适合作为函数参数使用的,至少,不应该有默认值。如果一定要用有默认值的列表当作参数,有没有办法同时又能保证参数默认值一直是空列表不会变呢?方法是有的。

def func_mutable(list_a=[]):
    list_exec = list_a
    list_exec += [1]
    print(list_a)

这样做行不行呢?我们在函数体内新声明一个变量来复制列表的值,对这个新变量而不是列表本身进行操作可不可以?通过前面的讲解我们知道,这样做是自欺欺人的。

而且,我刚才还有一点故意没说。tuple_b += (1,)这个操作在我们之前的试验中,虽然元组自身不会变,但是变量会被重新赋值,那么为什么__defaults__里面保存的不是这个新元组呢?其实,Python函数在调用是,相当于自动实例化了参数,即使你不用list_exec = list_a,程序也是这样做的,程序运行的时候操作对象是list_exec而不是list_a。之所以看上去像是直接对参数进行操作,那是为了方便学习者理解,但程序底层会使用更加安全的方式去执行。这也是为什么不要用可变对象当默认值,因为这样的话,程序执行时,就真的相当于对参数本身进行操作了。

这也是为什么面试的时候老是考这样的问题,因为如果你能理解这里面的区别,那么说明对Python的运算特点算是有一定的了解了。我们言归正传,除了刚才自欺欺人的办法,有没有真正有效的方法呢?方法是有的。

def func_mutable(list_a=[]):
    list_exec = list_a.copy()
    list_exec += [1]
    print(list_a)

或者

def func_mutable(list_a=[]):
    list_exec = list(list_a)
    list_exec += [1]
    print(list_a)

这两种办法都能解决刚才的问题,都能保证正确的输出结果。那么到底该选哪个方法,可以看个人取舍,我倾向于推荐第一种方法。但是第二种方法也有好处,它不仅可以用在列表上,用在元组上也是可以的,而且会使我们的操作非常灵活。

那么我们再回头看一下,我们刚才说Python会自动进行类似list_exec = list_a这样的处理,那么它为什么不用list_exec = list_a.copy()呢?一方面,这种办法浪费内存,而且运行起来效率要比前者低,另一方面,这样其实也限制了很多的操作。如果我们对自己有信心,那么利用元组保存列表的形式来构建类似可变元组的方法其实是非常有用的。而且这样做保留了用函数改变列表的可能性,简单程序如果面向过程开发往往是最直接最高效的。

但是,我还是要重申,一般来说

  • 尽量不要用列表当作变量传入函数中,尤其不要依赖默认值;
  • 如果一定要用列表变量当函数参数,那么在函数中尽量不要涉及修改列表的操作
  • 如果一定要在函数内部进行修改列表的操作,那么最好用安全的办法复制列表;
  • 如果是真的要用函数来改变列表,那么一定要有清晰的思路,程序非常简单而且是临时代码

(以上这些对字典一样适用)

其中第二点是最关键的。我们需要辨别对可变对象的哪些操作是不会改变列表的,哪些是只访问这个列表的而不进行修改的。这些都是为了能够提高代码复用时的稳定性。

装饰器和函数对象

这个就不展开来讲了,跳转本专题另一篇文章Python精进-装饰器与函数对象

最后的废话

本文和上一篇Python进阶-简单数据结构一样,字数真的很多。因为即使我只讲一些简单用法,而且我的确在这么做,但是还是有非常多的内容。不过已经是Python进阶部分了,多了解一些技术细节也是应该的,但是我还要强调一次,编程重在练习。以上这些用过的简单例子,大可以用命令行尝试一下看看输出结果加深印象。
我一开始的想法是争取日更,但像现在这样的一篇1W字日更显然是不现实的。我也只能晚上睡觉前有空就多少写一点,争取周更。
下一篇计划把之前最开始的一篇环境配置修补一下,补充说明一下Linux环境下Python的配置问题以及远程连接的问题。

    原文作者:SyPy
    原文地址: https://www.jianshu.com/p/c535704938eb
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞