Python之函数与变量

本节内容


  1. 函数介绍及其作用
  2. 函数的定义与调用
  3. 函数的参数说明
  4. 变量与作用域
  5. 值传递和引用传递

一、函数的介绍及其作用


编程语言中的函数与数学中的函数是有区别的:数学中的函数有参数(输入),就会有相应的结果(输出)。编程语言中的函数有输入,不一定会返回结果。编程语言中的函数其实就是一个用于完成某个特定功能的相关代码的代码段 。那么哪些代码语句应该被整合到一起定义为一个函数呢?这取决于你想让这个函数完成的功能是什么。

为什么要将这个代码段定义成一个函数呢?这其实就是函数的作用。假设我们在编写一个可供用户选择的菜单程序,程序启动时需要打印一遍菜单列表,而且程序运行过程中用户也可以随时打印菜单列表,也就是说打印菜单列表的代码段可能要多次被用到,假设每次打印的菜单列表都是一样的,而且列表很长,那么我们是否应该每次在需要打印菜单的时候重复执行相同的代码呢?那么当我们需要增加或者减少一个菜单项时怎么办呢?显然我们需要在每个打印菜单的代码点都进行修改。如果我们把打印菜单的相关代码拿出来定义为一个函数,又会出现怎样的场景呢?我们只需要在需要打印菜单列表的地方使用这个函数;当需要添加或减少一个菜单项时,只需要修改这个函数中的内容即可,程序的维护和扩展成本大大降低;同时,我们这个程序的代码会更加简洁,而且有条理性更加便于阅读,而不是一坨乱糟糟的让人看着就想重写的东西。当然,如果你要打印的是多级菜单,你可以通过函数的参数或全局变量通知该函数要打印的是几级菜单。总结一下,编程语言中的函数的作用就是实现代码的可重用性,提高代码可维护性、扩展性和可读性

二、函数的定义与调用


1. 函数的定义

高级编程语言通常会提供很多内置的函数来屏蔽底层差异,向上暴露一些通用的接口,比如我们之前用到的print()函数和open()函数。除此之外,我们也可以自定义我们需要的函数。由于函数本身也是程序代码的一部分,因此为了标识出这段代码是一个函数通常需要使用特定的格式或关键字。另外还涉及到参数、方法名称、返回值等相关问题的约束。

Python中定义函数的规则:
  • 函数代码块以def关键字开头,后接函数标识符(函数名称)和圆括号();
  • 函数名称以数字、小写字母和下划线组成并且不能以数字开头;
  • 圆括号中可用于定义可接收的参数;
  • 函数内容以圆括号()之后的冒号换行后起始,并且缩进;
  • 函数的第一行通常用于写一个字符串--函数使用方式、参数说明等文档信息
  • 函数中可以用return关键字返回一个值给函数调用方--return [表达式],如果不写return相当于返回None。

说明: 函数名称可以使用大写字母,但是不符合PEP8规范;另外Python3中函数名可以使用中文,但是还是不要给自己找麻烦为好。另外return语句不一定要写在函数末尾,而可以写在函数体的任意位置。return语句代表着函数的结束,函数在执行过程中只要遇到return语句,就会停止执行并返回结果。

Python中定义函数的语法:
def 函数名称( 参数 ):
    """
    函数使用说明、参数介绍等文档信息
    """
    代码块
    return [表达式]
实例: 写一个求和函数
def add(a, b):
    """
    计算并返回两个数的和
    a: 被加数
    b: 加数
    """
    c = a + b
    return c

通常写成这个样子:

def add(a, b):
    """
    计算并返回两个数的和
    a: 被加数
    b: 加数
    """
    return a + b

2. 函数的调用

Python中函数的调用方式与其他大部分编程语言都一样(其实我目前使用过的编程语言当中,只有shell是个另类;好吧,其实它只是个脚本语言):函数名(参数)

def add(a, b):
    """
    计算并返回两个数的和
    a: 被加数
    b: 加数
    """
    return a + b


sum = add(1, 9)

三、函数的参数说明


先来说下形参和实参的概念:

  • 形参:即形式参数,函数定义时指定的可以接受的参数即为形参,比如上面定义的add(a, b)函数中的a和b就是形参;
  • 实参:即实际参数,调用函数时传递给函数参数的实际值即为实参,比如上面都用add(1, 9)函数中的1和9就是实参;

重点需要说下函数的各种不同种类的参数。函数的参数可以分为以下几种:

  • 位置参数
  • 默认参数
  • 关键字参数
  • 可变(长)参数

说明: 这里说的位置参数,其实是指“必选参数”,也就是函数调用时必须要传递的参数,而默认参数是一种有默认值的特殊的位置参数。通常情况下位置参数和默认参数的传递顺序是不能变化的,但是当以指定参数名的方式(如: name='Tom')传递时参数位置是可以变化的。

不同编程语言对以上几种函数参数的支持各不相同,但是位置参数是最基本的参数类型,基本上所有的编程语言都支持。以下是一个常见编程语言的对比表格(Y表示支持,N表示不支持):

可见只有Python支持全部参数类型,而且只有Python支持关键字参数;另外,C、Java和Go都不支持默认参数,其中Java和Go与它们支持的方法重载特性有关(具体可以看下这个帖子),并且它们可以通过方法重载实现默认参数的功能。

下面我们以一个自定义的打印函数来对以上各种参数进行说明:

1. 位置参数

位置参数,顾名思义是和参数的顺序位置和数量有关的。函数调用时,实参的位置和个数要与形参对应,不然会报错。

函数定义:两个位置参数
def my_print(name, age):
    print('NAME: %s' % name)
    print('AGE: %d' % age)
正确调用:参数位置和个数都正确
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
错误调用:参数位置不正确
>>> my_print(18, 'Tom')
NAME: 18
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in my_print
TypeError: %d format: a number is required, not str
错误调用:参数个数不正确
>>> my_print('Tom')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: my_print() missing 1 required positional argument: 'age'

2. 默认参数

默认参数:是指给函数的形参赋一个默认值,它是一个有默认值的位置参数。当调用函数时,如果为该参数传递了实参则该形参取实参的值,如果没有为该参数传递实参则该形参取默认值。

默认参数的应用场景:参数值在大部分情况下是固定/相同的。比如这里打印一个班中学生的姓名和年龄,这个班大部分为同龄人(年龄相同),这时我们就可以给“年龄”这个形参赋一个默认的值。

说明: 默认参数只是一个有默认值的位置参数,因此它还是受到位置参数的限制。默认参数可以避免位置参数的一个限制:传递实参的个数,但是参数位置(顺序)仍然还是要一一对应。另外,默认参数必须放在位置参数后面(自己想想为什么)。

函数定义:两个位置参数,后面一个是默认参数(有默认值)
def my_print(name, age=12):
    print('NAME: %s' % name)
    print('AGE: %d' % age)
正确调用:按照位置参数传值
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18

age取的是函数调用时传递过来的实参

正确调用:不给age形参传值,age将取默认值
>>> my_print('Tom')
NAME: Tom
AGE: 12

函数调用时没有给形参age传值,因此age取的是默认值

错误调用:试图跳过前面的位置参数直接给后面的默认参数传值
>>> my_print(18)
NAME: 18
AGE: 12

可见,我们明明是想传递18给形参age的,结果18被赋给了name,而age仍然取得是默认值。上面已经提到过,默认参数只是可以让我们少传一些参数,但是不能改变参数的位置和顺序。另外,这也说明了默认参数为什么一定要放在后面:因为实参与形参是从前到后一一有序的对应关系,也就是说在给后面参数传值的时候,不论前面的参数是否有默认值,必须要先给前面的参数先赋值。

错误调用:实参个数超过形参个数
>>> my_print('Tom', 18, 'F')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: my_print() takes from 1 to 2 positional arguments but 3 were given

这里要说明的是:默认参数只能相应的减少实参的个数,但是不能增加实参的个数。这个很容易想明白,不做过多解释,只是为下面的可变长(参数)做铺垫。

3. 可变(长)参数

可变(长)参数:顾名思义,是指长度可以改变的参数。通俗点来讲就是,可以传任意个参数(包括0个)。

可变(长)参数的应用场景:通常在写一个需要对外提供服务的方法时,为了避免将来添加或减少什么新的参数使得所有调用该方法的代码点都要进行修改的情况发生,此时就可以用一个可变长的形式参数。

说明: 默认参数允许我们调用函数时,可以少传递一些实参;而可变(长)参数则允许我们调用函数时,可以多传递任意个实参。另外,可变长参数应该定义在默认参数之后,因为调用函数时传递的实参会按照顺序一一赋值给各个形参,如果可变(长)参数定义在前面,那么后面的参数将永远无法取得传递的值。可变(长)参数名称通常用args,且参数名称前要有个"*"号,表示这是一个可变长参数。

函数定义:一个位置参数、一个默认参数、一个可变长参数
def my_print(name, age=12, *args):
    print('NAME: %s' % name)
    print('AGE: %d' % age)
    print(args)

再次强调:位置参数、默认参数、可变长参数在函数定义中的位置不能变。

正确调用:只传递一个实参
>>> my_print('Tom')
NAME: Tom
AGE: 12
()

方法调用时,只传递了一个实参,该实参会按照函数中参数的定义位置赋值给形参name,因此name的值为‘Tom’;而形参age没有接收到实参,但是它有默认值,因此它取的是默认值12;需要注意的是可变参数args也没有接收到传递值,但是打印出来的是一对小括号(),说明args参数在函数内部会被转换成tuple(元祖)类型,当没有接收到实参时便是一个空tuple。

正确调用:传递两个实参
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
()

与值传递一个实参的情况基本相同,只是默认参数接收到了传递值,不再取默认值。

正确调用:传递两个以上的实参

比如,现在需要多接收并打印一个人的性别(F: 表示女,M: 表示男),可以这样用:

>>> my_print('Tom', 18, 'F')
NAME: Tom
AGE: 18
('F',)

比如,现在需要多接收并打印一个人的性别(F: 表示女,M: 表示男)和籍贯信息,可以这样用:

>>> my_print('Tom', 18, 'F', 'Hebei')
NAME: Tom
AGE: 18
('F', 'Hebei')

当然,我们也可以直接将一个tuple或list实例传递给形参args,但是tuple实例前也要加上*号作为前缀:

>>> t = ('F', 'Hebei')
>>> my_print('Tom', 19, *t)
NAME: Tom
AGE: 19
('F', 'Hebei')

你甚至可以将传递给形参name和age的实参也放到要传递的tuple实例中,但是最好不要这样做,因为很容易发生混乱:

>>> t = ('Jerry', 10, 'F', 'Hebei')
>>> my_print(*t)
NAME: Jerry
AGE: 10
('F', 'Hebei')
实际应用说明:

由于args接收到实参之后会被转换成一个tuple(元祖)的实例,而tuple本身是一个序列(有序的队列),因此我们可以通过下标(args[n])来获取相应的实参。但是我们需要在函数使用文档中写明args中各实参的传递顺序及意义,并且在获取args中的元素之前应该对args做非空判断。因此函数的定义及调用结果应该是这样的:

函数定义:

def my_print(name, age=12, *args):
    """
    Usage: my_print(name[, age[, sex[, address]]])
    :param name: 姓名
    :param age: 年龄
    :param args: 性别、籍贯
    :return: None
    """
    print('NAME: %s' % name)
    print('AGE: %d' % age)
    if len(args) >= 1:
        print('SEX: %s' % args[0])
    if len(args) >= 2:
        print('ADDRESS: %s' % args[1])

函数调用及结果:

>>> my_print('Tom')
NAME: Tom
AGE: 12
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
>>> my_print('Tom', 18, 'F')
NAME: Tom
AGE: 18
SEX: F
>>> my_print('Tom', 18, 'F', 'Hebei')
NAME: Tom
AGE: 18
SEX: F
ADDRESS: Hebei
>>> t = ('F', 'Hebei')
>>> my_print('Tom', 19, *t)
NAME: Tom
AGE: 19
SEX: F
ADDRESS: Hebei

4. 关键字参数

关键字参数:顾名思义,是指调用函数时通过关键字来指定是为哪个形参指定的实参,如name="Tom", age=10。

说明: 这个地方很容易发生思维混淆,所以需要特别说明一下:这里所说的关键字参数可以理解为以key=value的形式传递给函数的实参,注意是实参不是函数定义时声明的形参。而且在函数调用时可以通过关键字参数给函数定义时所声明的位置参数和默认参数传值(但是不能通过关键参数给可变长参数*args传值)。如果想实现像可变长参数那样在函数调用时传递任意个关键字参数给函数,则需要在函数定义时声明一个接受“可变长关键词参数”的形参,该形参名称通常为kwargs,且前面需要带"**"前缀--**kwargs

关键字参数应用场景:关键字参数一方面可以允许函数调用时传递实参的顺序与函数定义时声明形参的顺序不一致,提高灵活性;另一方面,它弥补了可变长参数的不足。想一下,如果想为上面定义了可变长参数的函数只传递“籍贯”参数就必须同时传递“性别”参数;另外还要不断地判断tuple的长度,这是相当不方便的。而关键参数可以通过关键字来判断某个参数是否有传递值并获取该参数的实参值。

函数定义:位置参数、默认参数、可变(长)参数、关键字参数
def my_print(name, age=12, *args, **kwargs):
    print('NAME: %s' % name)
    print('AGE: %d' % age)
    print(args)
    print(kwargs)

正确调用:只传递一个实参

>>> my_print('Tom')
NAME: Tom
AGE: 12
()
{}

方法调用时,只传递了一个实参,该实参会按照函数中参数的定义位置赋值给形参name,因此name的值为‘Tom’;而形参age没有接收到实参,但是它有默认值,因此它取的是默认值12;可变参数args也没有接收到传递值,因此args的值是一个空元组;重点需要注意的是关键字参数kwargs也没有接收到传递值,但是其打印值为一个空字典(dict)实例。

正确调用:传递两个实参
>>> my_print('Tom', 18)
NAME: Tom
AGE: 18
()
{}

与值传递一个实参的情况基本相同,只是默认参数接收到了传递值,不再取默认值。

>>> my_print(age=18, name='Tom')
NAME: Tom
AGE: 18
()
{}

可以不按照形参声明的顺序传递实参

正确调用:传递两个以上的实参

以非key=value的形式传递所有参数:

>>> my_print('Tom', 18, 'F', 'Hebei')
NAME: Tom
AGE: 18
('F', 'Hebei')
{}

可见后面多余的两个实参都传递给了可变长参数args

最后一个addr参数以key=value的形式传递:

>>> my_print('Tom', 18, 'F', addr='Hebei')
NAME: Tom
AGE: 18
('F',)
{'addr': 'Hebei'}
>>>

最后两个参数sex和addr都以key=value的形式传递:

>>> my_print('Tom', 18, sex='F', addr='Hebei')
NAME: Tom
AGE: 18
()
{'sex': 'F', 'addr': 'Hebei'}

由以上两个示例可见,对于除去传递给位置参数和默认参数之外多余的参数,如果是直接以value的形式提供实参,则会被传递给可变长参数args而成为一个元组中的元素;如果是以key=value的形式提供实参,则会被传递给关键字参数kwargs而成为一个字典中的元素。

纳尼?你还想试试其他传参方式?看看下面有没有你想要的
>>> t=('Jerry', 19, 'F', 'Hebei')
>>> my_print(*t)
NAME: Jerry
AGE: 19
('F', 'Hebei')
{}
>>> d={'name':'Tom', 'age':18, 'sex':'F', 'addr':'Hebei'}
>>> my_print(**d)
NAME: Tom
AGE: 18
()
{'sex': 'F', 'addr': 'Hebei'}
>>> d={'sex':'F', 'addr':'Hebei'}
>>> my_print(age=18, name='Tom', **d)
NAME: Tom
AGE: 18
()
{'sex': 'F', 'addr': 'Hebei'}
>>> t=('Tom', 18, 'abc')
>>> d={'sex':'F', 'addr':'Hebei'}
>>> my_print(*t, **d)
NAME: Tom
AGE: 18
('abc',)
{'sex': 'F', 'addr': 'Hebei'}
>>> my_print(name='Tom', 18, sex='F', addr='Hebei')
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument

4. 总结

关于Python中的函数参数说了这么多,我觉得很多必要来个总结:

  • Python中函数的参数有4种:位置参数、默认参数(有默认值的位置参数)、可变(长)参数、关键字参数(特殊的、优化过的可变长参数);
  • 无论是函数定义时声明形参,还是函数调用时传递实参,都必须按照上面的顺序进行(允许只包含一种或几种不同种类的参数);简单点来说就是,调用函数时key=value形式的关键参数必须在value形式的参数后面;
  • Python函数调用时,传递的实参会对应的传递给相应的形参,同一个形参接收到的实参不能多也不能少;

四、变量与作用域


1. 变量的概念

变量由两部分组成:变量名 和 变量值。变量值是存放在内存中的,变量的数据类型决定了其值在内存中的存放方式;变量名只是一个指向变量值所在内存空间地址的引用而已。

Python中的变量回收机制:
  • 变量的值存放在内存空间中
  • 内存空间是有地址的(门牌号)
  • 变量名是对其变量值所在内存空间地址的引用
  • Python解释器对内存地址引用次数是有记数的
  • Python解释器会定期将引用次数为0的内存地址清空--释放

函数名也是变量,函数体就是这个变量的值:
calc = lambda x: x*3

2. 作用域的概念

Python的作用域一共有4种,分别是:

  • L (Local) 局部作用域
  • E (Enclosing) 闭包函数外的函数中
  • G (Global) 全局作用域
  • B (Built-in) 内建作用域

查找变量的值时以 L --> E --> G -->B 的顺序进行查找,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找。

说明: Python除了def/class/lambda 外,其他如: if/elif/else/ try/except for/while并不能改变其作用域。定义在它们之内的变量,外部还是可以访问。

关于Python变量作用域的问题,可以参考这篇文档:http://www.jianshu.com/p/3bb277c2935c

3. 全局变量与局部变量

一个程序中的变量是有作用域(scope)的,作用域的大小会限制变量可访问的范围。Python中的变量根据作用域范围的大小不同可以分为:全局变量和局部变量。顾名思义,全局变量表示变量在全局范围内都可以被访问,而局部变量只能在一个很小的范围内生效。这就好比国家主席与各省的省长:在全国范围内国家主席都是同一个人,因此国家主席就是个全局变量;而各省的省长只能在某个省内生效,河北省省长是一个人,河南省省长又是另外一个人,因此省长就是个局部变量。对于Python编程语言而言,定义在一个函数内部的变量就是一个局部变量,局部变量只能在其被声明的函数内访问;定义在函数外部的变量就是全局变量,全局变量可以在整个程序范围内访问。

来看个示例:

#!/usr/bin/env python
# -*- encoding:utf-8 -*-

name = 'Tom'

def func1():
    age = 10
    print(name)
    print(age)

def func2():
    sex = 'F'
    print(name)
    print(sex)

print(name)
func1()
func2()

输出结果:

Tom
Tom
10
Tom
F

上面的示例中,name是一个全局变量,因此它在程序的任何地方都可以被访问;而func1函数中的age变量和func2函数中的sex变量都是局部变量,因此它们只能在各自定义的函数中被访问。

问题1:如果在函数内定义一个与全局变量同名的变量会怎样?
#!/usr/bin/env python
# -*- encoding:utf-8 -*-

name = 'Tom'

def func3():
    name = 'Jerry'
    print(name)

print(name)
func3()
print(name)

输出结果:

Tom
Jerry
Tom

通过上面两个示例的输出结果我们可以得出这样的结论:

  • 函数内引用一个变量时,会先查找该函数内部是否存在这样一个局部变量,如果存在则直接引用该局部变量,否则将查找并引用全局变量;
  • 对局部变量的赋值并不会对全局变量产生什么影响,因为它们本来就是两个不相关的变量。
问题2:如果想在上面示例中的函数内部为全局变量重新赋值怎么办?

可以在函数内部通过global关键字声明该局部变量就是全局变量:

#!/usr/bin/env python
# -*- encoding:utf-8 -*-

name = 'Tom'

def func4():
    global name
    name = 'Jerry'
    print(name)

print(name)
func4()
print(name)

输出结果:

Tom
Jerry
Jerry

可见全局name的值的确被func4函数内部的操作改变了。

问题3:能不能将全局变量通过传参的方式传递给函数,然后在函数内部对全局变量做修改呢?

变量值的改变通常有两种方式:(1) 重新赋值 (2) 改变原有值。要想在函数内部通过重新赋值来改变全局变量的值,则只能通过上面介绍的使用global关键字来完成,通过传参是无法实现的。而要想在函数内部改变全局变量的原有值的属性就要看该参数是值传递还是引用传递了,如果是引用传递则可以在函数内部对全局变量的值进行修改,如果是值传递则不可以实现。具体请看下面的分析。

五、值传递与引用传递


这个话题在几乎所有的编程语言中都会涉及,之所以把它放到最后是因为觉得这个问题对于编程新手来说比较难理解。与 “值传递与引用传递” 相似的概念是 “值拷贝与引用拷贝”。前者主要是指函数调用时传递参数的时候,后者是指把一个变量赋值给其他变量或其他一些专门的拷贝操作(如深拷贝和浅拷贝)的时候。

这里我们需要先来说明下定义变量的过程是怎样的。首先,我们应该知道变量的值是保存在内存中的;以name='Tom'为例,定义变量name的过程是这样的:

  1. 在内存中分配一块内存空间;
  2. 将变量的值(字符串“Tom”)存放到这块内存空间;
  3. 将这块内存空间的地址(门牌号)赋值给变量name;

也就是说变量保存的不是真实的值,而是存放真实值的内存空间的地址。

“值拷贝”和“值传递”比较好理解,就是直接把变量的值在内存中再复制一份;也就是说会分配并占用新的内存空间,因此变量指向的内存空间是新的,与之前的变量及其指向的内存空间没有什么关联了。而“引用拷贝”和“引用传递”仅仅是把变量对内存空间地址的引用复制了一份,也就是说两个变量指向的是同一个内存空间,因此对一个变量的值的修改会影响其他指向这个相同内存空间的变量的值。实际上,向函数传递参数时传递的也是实参的“值拷贝或引用拷贝”。

因此当我们判断一个变量是否被修改时,只需要搞明白该变量所指向的内存地址以及该内存地址对应的内存空间中的值是否发生了改变即可。

示例1:
name1 = 'Tom'
name2 = name1
name2 = 'Jerry'

print('name1: %s' % name1)
print('name2: %s' % name2)

思考:name1被改变了吗?

分析下上面操作的过程:

  • 定义变量name1:在内存中开辟一块空间,将字符串'Tom'保存到该内存空间,然后name1指向该内存空间的地址;

  • 定义变量name2,并将name1赋值给它:实际上就是让name2也指向name1所指定的内存空间;

  • 为变量name2重新赋一个新值:在内存中开辟一块新的空间,将字符串‘Jerry’保存到该内存空间,然后name2指向该内存空间的地址;

name1指向的内存地址发生改变了吗?-- 没有,因为name1并没有被重新进行赋值操作。

name1所指向的内存空间中的内容改变了吗? -- 没有,并没有对它做什么,并且字符串本就是个常量,是不可能被改变的。

So, 答案已经有了,name1并没有被改变,因此输出结果是:

name1: Tom
name2: Jerry
示例2:
num1 = 10
num2 = num1
num2 += 1

print('num1: %d' % num1)
print('num2: %d' % num2)

与示例1过程相似,只是+=操作也是一个赋值的过程,其他不再做过多解释。
输出结果:

num1: 10
num2: 11
示例3:
list1 = ['Tom', 'Jerry', 'Peter', 'Lily']
list2 = list1
list2.pop(0)

print('list1: %s' % list1)
print('list2: %s' % list2)

思考: list1被改变了吗?

分析上面操作的过程:

  • 定义变量list1:在内存中开辟一块空间,将列表 ['Tom', 'Jerry', 'Peter', 'Lily'] 保存到该内存空间(列表在内存中的保存没这么简单,此处只是为了便于理解),然后list1指向该内存空间的地址;
  • 定义变量list2,并将list1赋值给它:实际上就是让list2也指向list1所指定的内存空间;
  • 移除list2中的一个元素,就是从list2指向的内存地址所对应的内存空间中的内容中移除一个元素;

list1指向的内存地址发生改变了吗?-- 没有,因为list1并没有被重新进行赋值操作。

list1所指向的内存空间中的内容改变了吗? -- 是的,因为list1和list2指向的是同一个内存地址,通过list2修改了该内存地址中的内容后就相当于修改了list1。

So, 答案已经有了,list1被改变了,因此输出结果是:

list1: ['Jerry', 'Peter', 'Lily']
list2: ['Jerry', 'Peter', 'Lily']
示例4:

其实函数参数的传递过程也是类似的,比如:

num1 = 10
name1 = 'Tom'
list1 = ['Tom', 'Jerry', 'Peter', 'Lily']

def fun1(num2, name2, list2):
    num2 += 1
    name2 = 'Jerry'
    list2.pop(0)
    print('num2: %d' % num2)
    print('name2: %s' % name2)
    print('list2: %s' % list2)


fun1(num1, name1, list1)
print('num1: %d' % num1)
print('name1: %s' % name1)
print('list1: %s' % list1)

为了跟上面的示例做对比,我故意把func1函数中的形参的名称写为num2、name2和list2,实际上他们可以为任意有意义的名称。

输出结果:

num2: 11
name2: Jerry
list2: ['Jerry', 'Peter', 'Lily']
num1: 10
name1: Tom
list1: ['Jerry', 'Peter', 'Lily']
那么Python中变量拷贝是值拷贝还是引用拷贝呢?Python中的参数传递是值传递还是引用传递呢?

其实这是相同的问题,因为上面说过了:参数传递的过程实际上就像先拷贝,然后将拷贝传递给形参。如果是值拷贝,那么调用函数传参时就是值传递;如果是引用拷贝,那么调用函数传参时就是引用(内存地址)传递。其实通过上面的示例,我们大概可以猜测到对于列表类型的变量貌似是引用传递,但是数字和字符串类型的变量是值传递还是引用传递呢?Python中的参数的传递都是引用传递,关于这个问题我们可以通过Python内置的一个id()函数来进行验证。id()函数会返回指定变量所指向的内存地址,如果是引用传递,那么实参和被赋值后的形参所指向的内存地址肯定是相同的。事实上,确实如此,如下所示:

num1 = 10
name1 = 'Tom'
list1 = ['Tom', 'Jerry', 'Peter', 'Lily']

def fun1(num2, name2, list2):
    print(id(num2), id(name2), id(list2))

print(id(num1), id(name1), id(list1))
fun1(num1, name1, list1)

输出结果:

1828586224 1856648389328 1856648385800
1828586224 1856648389328 1856648385800

实参和形参的内存地址一致,说明Python中的参数传递确实是“引用传递”。

这篇文章写了很久,想说的东西太多。有时候手放到键盘上放了许久,却不知从何写起。算是对知识点的梳理,也希望对他人有所帮助。Python中关于函数的其它内容,如:函数递归、匿名函数、嵌套函数、高阶函数等,之后再讲。

问题交流群:666948590

posted @ 2016-12-21 15:22  云游道士  阅读(4115)  评论(2编辑  收藏  举报