仅用作个人学习记录
Reference:
https://inst.eecs.berkeley.edu/~cs61a/sp21/
https://www.bookstack.cn/read/sicp-py-zh/
回顾上节:
- Values 是可被操纵的数据
- Functions 也是一种值,对数值进行计算的值
- Expressions 表示数值和计算的方式,是得到值的方式
本节,我们将详细介绍函数如何对数值进行操作以及表达式如何表示这些操作。
在 Python 中,函数调用几乎可以完成所有的计算工作,包括赋值。
Python的函数(把函数当作值),可以分配给变量,传递给函数,从函数返回并存储在数据结构中。
func abs(number) func add(left, right)
用 func 表示函数,上述是两个 python 的内建(原生)函数,由 Python 提供。
定义新的函数
def <name>(<formal parameters>): # Header: Name and formal parameters
return <return expression> # Body: Computation performed by function
第二行必须缩进!按照惯例我们应该缩进四个空格,而不是一个Tab。Python 中并不存在{}
,而是用缩进表示不同的代码块。
返回表达式并不是立即求值,它储存为新定义函数的一部分,并且只在函数最终调用时会被求出。
函数的可接受参数的描述,包括形参的数量和名称,叫做函数的签名。
匿名函数
我们常将函数与其名称联系起来,但这种联系是不必要的,我们有匿名函数。Python 使用 lambda 来创建匿名函数。所谓匿名,意即不再使用 def 语句这样标准的形式定义一个函数。
lambda 的主体是一个表达式,而不是一个代码块。仅仅能在 lambda 表达式中封装有限的逻辑进去。
lambda 函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
lambda [arg1 [,arg2,.....argn]]: expression
>>> saxb = lambda a, x, b: a * x + b
>>> print(saxb(1, 2, 3))
5
纯函数
纯函数的输出仅取决于输入参数的值,调用它们时除了返回一个值之外没有其它效果。
非纯函数
除了返回一个值之外,调用非纯函数会产生副作用(Side Effect),这会改变解释器或计算机的一些状态。一个典型的副作用就是在返回值之外生成额外的输出,使用 print 函数。
大多数副作用涉及更改某些变量的值。如n random.randint:
>>> from random import randint
>>> randint(0, 100) # Random number in 0--100.
13
>>> randint(0, 100) # Different result: Something must have changed!
55
两次相同的输入却得到了两个不同的输出,可以推断一定存在某个函数或某个变量记录了调用 random 的次数或者记录了上一次随机生成的数字,即 random 产生了副作用。
调用表达式
表达式是一个递归的概念,可以由更小的表达式或子表达式组成。
调用表达式拥有子表达式:运算符在圆括号之前,圆括号包含逗号分隔的操作数。运算符必须是个函数,操作数可以是任何值。运算符和操作数本身都是表达式(再次递归)。
计算这个调用表达式:
- 计算运算符的结果(称为结果 C),C 必须是函数
- 按照它们出现的顺序计算操作数(将结果值称为 P0 和 P1)
- 将 P0、P1 的实参值代入函数 C
两个例子
嵌套表达式,求值过程本质上是递归的。
mul(add(2, mul(0x10, 0o10)), add(0x3, 5))
注意区分 numeral 和 number,numeral 只表达“形”,是符号,没有量的概念;number 表达“意”,表示量的概念。
表达式从左向右计算,每当求完表达式就有一个完整的函数可以调用,执行函数可以得到一个值。递归的思想。
借助下面的例子可以好好理解返回值与副作用的区别。
>>>print(print(1),print(1))
1 #Side Effect
2 #Side Effect
None None #print function always return None
替换
如何将变量名称、函数名称与对应的数值联系起来?一般有两种解释方法:替换、环境。
首先是替换的概念,用定义代替名称。Python 提供了几种定义名称的方法:赋值、函数定义和参数传递。
def incr(n):
def f(x):
return n + x
return f
incr(5)(6)
def hmmmm(x):
def f(x):
return x
return f
hmmmm(5)(6)
在调用hmmmm时,内部定义(def f)保护了 return x 中的 x 不被替换。
由hmmmm(5) 引起的替换仅替换了hmmmm 主体中自由出现的 x。语句或表达式中自由出现的名称是指未在该语句中定义(绑定)的名称。由于 return 语句中的 x 被定义为 f 的形参,是绑定的,因此不会被替换。
环境
环境是从名称到值的映射。表达式求值所在的环境由帧的序列组成,它们可以表述为一些盒子。每一帧都包含了一些绑定,它们将名称和对应的值关联起来。全局帧只有一个,它包含所有内建函数的名称绑定。
赋值和导入语句会向当前环境的第一个帧添加条目。
def 语句将名称绑定到由定义创建的用户定义函数。
这些环境图示展示了当前环境中的绑定,以及它们所绑定的值(并不是任何帧的一部分)。要注意函数名称是重复出现的,一个在帧中,另一个是函数的一部分。这一重复是有意的,许多不同的名字可能会引用相同函数,但是函数本身只有一个内在名称。但是,在环境中由名称检索值只检查名称绑定。函数的内在名称不在名称检索中起作用。
>>> f = max
>>> f
<built-in function max>
>>> f(1, 2)
2
名称 max 和 f 在全局环境中都绑定到了相同函数上,而 max 才是内在名称。
调用用户定义的函数
from operator import mul
def square(x):
return mul(x,x)
x = -2
square(mul(x, x))
首先在全局环境中计算 square(mul(x, x)) 的子表达式:
从表达式的环境中获取子表达式 x、mul 和 square 的值。进行原始的乘法运算:
要向用户定义的 square 函数传递参数,使用局部环境帧扩展环境,并为 x 提供操作数值。
当我们在这个新的环境中计算mul(x, x) 时,我们得到的 mul 值与之前相同,但 x 是局部值 4,不是全局环境中的2。为了找到名称的含义,循着环境帧链向上寻找并停在第一个具有所需定义的帧处 。
实践指南:名称的选择
下面的准则派生于 Python 的代码风格指南。
- 函数名称应该小写,以下划线分隔。提倡描述性的名称。
- 函数名称通常反映解释器向参数应用的操作(例如
print
、add
、square
),或者结果(例如max
、abs
、sum
)。 - 参数名称应小写,以下划线分隔。提倡单个词的名称。
- 参数名称应该反映参数在函数中的作用,并不仅仅是满足的值的类型。
- 当作用非常明确时,单个字母的参数名称可以接受,但是永远不要使用
l
(小写的L
)和O
(大写的o
),或者I
(大写的i
)来避免和数字混淆。
实践指南:函数的艺术
如何编写良好的函数:
- 每个函数都应该只做一个任务。这个任务可以使用短小的名称来定义,使用一行文本来标识。顺序执行多个任务的函数应该拆分在多个函数中。
- 不要重复劳动(DRY)是软件工程的中心法则。所谓的DRY原则规定多个代码段不应该描述重复的逻辑。反之,逻辑应该只实现一次,指定一个名称,并且多次使用。如果你发现自己在复制粘贴一段代码,你可能发现了一个使用函数抽象的机会。
- 函数应该定义得通常一些,准确来说,平方并不是在 Python 库中,因为它是
pow
函数的一个特例,这个函数计算任何数的任何次方。
文档字符串
函数定义通常包含描述这个函数的文档,叫做文档字符串,它必须在函数体中缩进。文档字符串通常使用三个引号。第一行描述函数的任务。随后的一些行描述参数,并且澄清函数的行为:
>>> def pressure(v, t, n):
"""Compute the pressure in pascals of an ideal gas.
Applies the ideal gas law: http://en.wikipedia.org/wiki/Ideal_gas_law
v -- volume of gas, in cubic meters
t -- absolute temperature in degrees kelvin
n -- particles of gas
"""
k = 1.38e-23 # Boltzmann's constant
return n * k * t / v
当你以函数名称作为参数来调用help
时,你会看到它的文档字符串(按下q
来退出 Python 帮助)。
>>> help(pressure)
编写 Python 程序时,除了最简单的函数之外,都要包含文档字符串。要记住,代码只编写一次,但是会阅读多次。Python 文档包含了文档字符串准则,它在不同的 Python 项目中保持一致。
参数默认值
定义普通函数的结果之一就是额外参数的引入。具有许多参数的函数调用起来非常麻烦,也难以阅读。
在 Python 中,我们可以为函数的参数提供默认值。调用这个函数时,带有默认值的参数是可选的。如果它们没有提供,默认值就会绑定到形式参数的名称上。例如,如果某个应用通常用来计算一摩尔粒子的压强,这个值就可以设为默认:
>>> k_b=1.38e-23 # Boltzmann's constant
>>> def pressure(v, t, n=6.022e23):
"""Compute the pressure in pascals of an ideal gas.
v -- volume of gas, in cubic meters
t -- absolute temperature in degrees kelvin
n -- particles of gas (default: one mole)
"""
return n * k_b * t / v
>>> pressure(1, 273.15)
2269.974834
这里,pressure
的定义接受三个参数,但是在调用表达式中只提供了两个。这种情况下,n
的值通过def
语句的默认值获得(它看起来像对n
的赋值,虽然就像这个讨论暗示的那样,更大程度上它是条件赋值)。
作为准则,用于函数体的大多数数据值应该表示为具名参数的默认值,这样便于查看,以及被函数调用者修改。一些值永远不会改变,就像基本常数k_b
,应该定义在全局帧中。