• 参考书《Python基础教程(第三版)》—— Magnus Lie Hetland

文章目录

  • ​​一、自定义函数​​
  • ​​1. 判断某个对象x可否作为函数调用​​
  • ​​2. 自定义函数​​
  • ​​二、函数参数​​
  • ​​1. 修改参数​​
  • ​​2. 传参数和传返回值的本质​​
  • ​​3. 参数缺省值和关键字参数​​
  • ​​4. 收集参数​​
  • ​​5. 分配参数​​
  • ​​三、作用域(命名空间)​​
  • ​​1. 什么是作用域​​
  • ​​2. 在函数内访问全局变量​​
  • ​​(1)读全局变量​​
  • ​​(2)写全局变量​​
  • ​​3. 作用域嵌套​​

一、自定义函数

1. 判断某个对象x可否作为函数调用

  • 内置函数​​callable(object):bool​​:返回一个bool值,代表x是不是可被调用的对象
>>> import math
>>> x = math.sqrt
>>> callable(x)
True
>>> y = 1
>>> callable(y)
False

2. 自定义函数

  1. 格式:
def 函数名(形参列表):
函数体
(return 返回值)
  1. 其实不是函数的函数
  • 数学意义上的函数总是返回根据参数计算的结果
  • 在python中,有些函数什么也不返回,这种函数可能无return语句,或者有return但无返回值(这样可以提前结束函数)
  • 在有些语言中,这种称作 “过程”,在python中也叫 “函数”
  • 对于不显式写出返回值的函数,python自动返回None
  • 简单说:所有函数都有返回值,如果没有指定就返回None
  1. 示例
# 定义函数
def hello(name):
return 'Hello,' + name + '!'

#计算斐波那契数列
def fibs(num):
result = [0,1]
for i in range(num-2):
result.append([fib[-2]+fib[-1]])# 递推方式计算
return result


# 调用函数
print(hello('world'))

print(fibs(10)) # 打印[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
print(fibs(5)) # 打印[0, 1, 1, 2, 3]
print(fibs(1)) # 打印[0,1](由于算法设计,在参数小于等于2的时候返回都是这个,需要优化)
  1. 给函数编写文档
  1. 在定义函数的def语句后一行,立即用​​'...'​​形式写一个注释,这称为文档字符串,这个字符串会被作为函数的一部分储存起来
  2. 像这样调用 ​​函数名.__doc__​​ ,返回一个字符串(注意这里前后都是两个下划线)
  3. ​help(函数名)​​在交互式解释器中获取有关函数的信息
def fibs(num):
'lalalala'
result = [0,1]
for i in range(num-2):
result.append(result[-2]+result[-1])
return result

print(fibs.__doc__) #打印lalalala
调用:help(fibs)


打印下面这几个:
Help on function fibs in module __main__:

fibs(num)
lalalala

二、函数参数

1. 修改参数

  1. 字符串、数、元组不可变的,操作形参不会影响实参(对于这些类型来说,传参过程如赋值n = name,实际新建了一个值相同的对象并用形参指向它)。
  • 这种情况下,函数中无法对实参产生影响,如果一定要通过函数修改实参,只有一种妥协的方法:把形参经处理后得到的值作为返回值,在返回后对实参重新赋值
# 尝试用函数修改不可变参数
def try_to_change(n):
n = '123'

name = 'ABC'
try_to_change(name)
print(name) #打印的仍是ABC

# 可以看成下面这种写法:
name = "ABC"
n = name
n = "123"
print(name) #打印ABC

#------------------------------
# 重新赋值,强行利用函数修改实参值
def inc(x):
return x+1

foo = 10
foo = inc(foo)
  1. 列表、字典等可变的,操作形参会影响实参(对于这些类型来说,传参过程如赋值n = name,直接用形参指向实参对象,并没有新建对象)。如果希望函数内操作形参不影响实参,必须创建列表/字典的副本
# 尝试用函数修改可变参数
def try_to_change(n,m):
n[0] = 10
m['Alice'] = '9999'

names = [1,2,3,4,5]
phoneBook = dict([('Alice','1234') , ('Tom','1111')])
try_to_change(names,phoneBook)

print(names) # [10, 2, 3, 4, 5]
print(phoneBook) # {'Tom': '1111', 'Alice': '9999'}

# 可以看成下面这种写法
names = [1,2,3,4,5]
phoneBook = dict([('Alice','1234') , ('Tom','1111')])
n = names
m = phoneBook
names[0] = 10
m['Alice'] = '9999'
print(names)# [10, 2, 3, 4, 5]
print(phoneBook)# {'Tom': '1111', 'Alice': '9999'}

#-------------------------------------------------

# 如果希望函数内操作形参不影响实参,必须创建列表/字典的副本
# - 例如用列表的切片方法返回列表副本
# - 例如用字典的 "从其他字典构造" 或 "深复制" 方法
from copy import deepcopy

def try_to_change(n,m):
n[0] = 10
m['Alice'] = '9999'

names = [1,2,3,4,5]
phoneBook = dict([('Alice','1234') , ('Tom','1111')])
try_to_change(names[:],deepcopy(phoneBook))

print(names) # [1, 2, 3, 4, 5]
print(phoneBook) # {'Tom': '1111', 'Alice': '1234'}

2. 传参数和传返回值的本质

  1. 假如函数形参n,调用时写入实参name,传参过程可以看作:n = name
  • 如果参数是不可变对象字符串、数、元组:进行值传递,可以理解为根据实参数值创建了相等的对象(不是相同对象)传入函数,实参本身不会受到函数内操作的任何影响。(事实上,仅仅传参数这一步还是传的引用(地址值),但对形参进行赋值时就会创建新对象了)
  • 如果参数是可变对象列表、字典等:进行引用传递,相当于让形参n引用实参name,没有创建新对象,实参会受到函数内操作的影响。(本质是也是传值,不过传的是地址值)
  1. python函数和方法的返回值的方法和传参数的方法相同,也是值传递/引用传递(地址值传递)
# 字典的get方法,如果修改get方法返回的可变对象如列表,原字典也会改变
>>> dct = {1:[1,2,3,4],2:'A'}
>>> x = dct.get(1)
>>> x
[1, 2, 3, 4]
>>> x[0] = 0
>>> dct
{1: [0, 2, 3, 4], 2: 'A'}

3. 参数缺省值和关键字参数

  1. 类似C++,定义函数时可以给参数加上缺省值
def hello(greeting = 'Hello' , name = 'world'):
print("{},{}!".format(greeting,name))

hello() #打印Hello,world!
hello('Greeting') #打印Greeting,world!
  1. 关键字参数:调用函数时,可以指出形参,这样实参就不用按顺序写了
def hello(greeting = 'Hello' , name = 'world'):
print("{},{}!".format(greeting,name))

hello(name='Tom') #打印Hello,Tom!

4. 收集参数

  • 定义函数时利用​​*​​在函数中用形参收集多个非关键字参数列表
  • 定义函数时利用​​**​​在函数中用形参收集多个关键字参数字典
  • 两种收集参数方法(​​*​​和​​**​​)还有普通定义参数方法可以混合使用
  • 详见:Python 中函数的 收集参数 机制
def print_1(x,*y,z):
print(x,y,z)

def print_2(**para):
print(para)

print_1(1,2,3,4,5,6,z = 7) #打印:1 (2, 3, 4, 5, 6) 7
print_2(x = 1,y = 2,z = 3) #打印:{'y': 2, 'x': 1, 'z': 3}

5. 分配参数

  • 调用函数可以用​​*​​把列表作为非关键字参数函数的形参,在函数内部会自动序列解包
  • 调用函数可以用​​**​​把元组作为关键字参数函数的形参,在函数内部会自动序列解包
  • 不要同时使用 “收集参数” 和 “分配参数” 这样做和不加*没有区别
  • 两种分配参数方法(*和**)还有普通写实参的方法可以混合使用
def Hello(greeting = "Hello", name = "Tom"):
print("{},{}!".format(greeting,name))

para = {'name':'Alice','greeting':'Well met'}
Hello(**para) # 打印Well met,Alice!

三、作用域(命名空间)

1. 什么是作用域

  1. 变量可以看作“指向值的名称”,执行x=1,名称x指向1,这几乎和字典一样
  2. 作用域可以看作 “看不见的字典” ,用​​vars()​​可以返回这个 “看不见的字典”
>>> x = 1
>>> scope = vars()
>>> scope['x']
1
>>> scope['x'] += 1
>>> x
2
  1. 注意:不应修改vars()返回的字典,这样的结果是不确定的
  2. 除全局作用域外,每次函数调用都创建一个作用域
# 这里,创建foo函数是重新创建了一个作用域(局部作用域),
# 在这个作用域中创建了一个 "局部变量" x指向42,
# 而全局作用域中 "全局变量" x指向1不被影响
>>> def foo(): x = 42
...
>>> x = 1
>>> foo()
>>> x
1

2. 在函数内访问全局变量

(1)读全局变量

  1. 通常在函数内读取全局变量值没有问题
  2. 但如果函数有局部变量和全部变量同名,全局变量将被 “遮盖”
  3. 可以用​​globals()​​​函数帮忙,这个函数类似​​vars()​​,返回一个包含全局变量的字典(不同于vars()是返回的“看不见的字典”,这是明确的所有全局变量的字典)
  4. 相应的,有​​locals()​​返回一个包含局部变量的字典
>>> def combine(para):
... print(para + globals()['para']) #这里globals()['para']就是取全局变量para的值
...
>>> para = 'berry'
>>> combine('Shrub')
Shrubberry

(2)写全局变量

  1. 在函数内部给变量赋值时,此变量默认为局部变量
  2. 用​​global​​修饰符指出变量是全局的
>>> x = 1
>>> def inc():
... global x
... x = x+1
...
>>> inc()
>>> x
2

3. 作用域嵌套

  1. python允许函数嵌套定义,外面的函数可以返回里面的函数(不是调用)。关键在于,这个返回的函数内部可以访问其定义所在的作用域(即这里的​​multiplier​​函数作用域)
def multiplier(factor):
def multiplyByFactor(number):
return number*factor
return multiplyByFactor
  1. 常利用这种方法,用一个函数创建另一个函数
double = multiplier(2)
print(double(3)) #打印 6
print(double(4)) #打印 8

triple = multiplier(3)
print(triple(4)) #打印 12

multiplier(5)(4) #打印 20
  1. 像​​multiplyByFactor​​这样存储其所在作用域的函数称为 “闭包”
  2. 通常不能给外部作用域内的变量赋值,如果一定要这样做可以用关键字​​nonlocal​​,类似global用法