第三章-函数与分支(Functions and Branching)
介绍了两个概念:函数和程序流程分支(通常指 if 语句)
3.1-函数
Python 中的函数不同于数学上的函数,是可以在任何时间地点执行、可重复使用的语句的集合。
3.1.1
Python 的函数结构
- 在 Python 中记为:
def F(C): return (9.0/5)*C + 32
- 函数的开头:def 函数名(参数列表):
- 函数体:相对 def 缩进一次;遇到 return 语句直接退出函数,并返回 return 后的值(即返回值);函数的返回值可为任意对象
3.1.2
程序流程的理解
- 可浏览 http://www.pythontutor.com/visualize.html 来观察程序的运行流程
- 首先,选语言,写代码,单击“Visualize Execution”按钮:
- 然后,通过“Firs”、“Back”、“Forward”和“Last”按钮进行观察:
3.1.3
局部变量(Local Variables)和全局变量(Global Variables)
- 函数内定义的变量-局部变量
- 主程序内定义的变量-全局变量
- 局部变量的修改不会影响全局变量
- 函数内若要使用全局变量,则要用 global 变量名 进行定义,如:
a = 20; b = -2.5 # 全局变量 def f1(x): a = 21 # a为一个新的局部变量 return a*x + b print a # 打印 20 def f2(x): global a # 声明此处的a为全局变量 a = 21 # a为全局变量 return a*x + b f1(3); print a # 全局变量a不变,仍为20 f2(3); print a # 全局变量a改变,变为21
- Python 中寻找变量的顺序:局部变量 -> 全局变量 -> Python 中的内建函数(built-in Python functions)
3.1.4
函数可有多个参数
- 在 Python 中记为:
def yfunc(t, v0): g = 9.81 return v0*t - 0.5*g*t**2
- 当时间 t 为 0.1,初速度 v0 为 6 时,主程序中可以这样调用函数:
可用 参数名 = 值 直接传递参数,若不使用 参数名 则 值 的顺序应与参数列表的顺序一致;参数名 = 值 的形式应在只有 值 的形式之后(例如,yfunc(t=0.1, 6) 不合法)y = yfunc(0.1, 6) y = yfunc(0.1, v0=6) y = yfunc(t=0.1, v0=6) y = yfunc(v0=6, t=0.1)
3.1.5
- 在调用函数前,应将函数中的全局变量定义好
3.1.6
- Python 的函数不单单能表达数学中的函数,任何需要重复使用的语句均能写成函数,例如
def makelist(start, stop, inc): value = start result = [] while value <= stop: result.append(value) value = value + inc return result # 返回一个列表 mylist = makelist(0, 100, 0.2) print mylist # 将会打印出 0, 0.2, 0.4, 0.6, ... 99.8, 100
- 其中,函数参数列表中的三个参数 start、stop 和 inc 以及函数内的 value 和 result 均为函数内的局部变量 ,mylist 为全局变量
- Python 自带的 range(start, stop, inc) 的参数只允许整型变量,但是我们写的 makelist(start, stop, inc) 允许浮点型变量
3.1.7
函数可有多个返回值
- 例,同时计算 和 :
def yfunc(t, v0): g = 9.81 y = v0*t - 0.5*g*t**2 dydt = v0 - g*t return y, dydt
- 调用该函数:
position, velocity = yfunc(0.6, 3)
- 实际上,多返回值函数的返回值类型为多元组
3.1.8
给出了一个通过累加计算对数的函数的例子
3.1.9
函数可以没有返回值
- 此时 Python 会在最后一行自动加上 return None
- Python 中 None 意味着 nothing
3.1.10
关键字参数(Keyword Arguments)
- 使用关键字参数,给函数的对应参数赋予默认值:
def somefunc(arg1, arg2, kwarg1=True, kwarg2=0): print arg1, arg2, kwarg1, kwarg2
当不给 kwarg1 和 kwarg2 赋值时,分别默认为 True 和 0
3.1.11
文档字符串(Doc Strings)
- 使用文档字符串,给函数加上说明注释:
def C2F(C): """Convert Celsius degrees (C) to Fahrenheit.""" return (9.0/5)*C + 32 def line(x0, y0, x1, y1): """ Compute the coefficients a and b in the mathematical expression for a straight line y = a*x + b that goes through two points (x0, y0) and (x1, y1). x0, y0: a point on the line (floats). x1, y1: another point on the line (floats). return: coefficients a, b (floats) for the line (y=a*x+b). """ a = (y1 - y0)/float(x1 - x0) b = y0 - a*x0
文档字符串需写在函数开头之后、函数体之前
- 可通过 函数名.__doc__ 读取文档字符串:
>>> print line.__doc__ Compute the coefficients a and b in the mathematical expression for a straight line y = a*x + b that goes through two points (x0, y0) and (x1, y1). x0, y0: a point on the line (floats). x1, y1: another point on the line (floats). return: coefficients a, b (floats) for the line (y=a*x+b).
3.1.12
函数可做函数的参数
- 例如, 的二次导可写为 ,在 Python 中可写为:
def diff2nd(f, x, h=1E-6): r = (f(x-h) - 2*f(x) + f(x+h))/float(h*h) return r
其中,参数 f 即为先前定义的某个函数的函数名
3.1.13
主程序(the Main Program)
- 我们定义函数的那个地方,通常称为主函数;主函数定义的变量,均为全局变量(参见 3.1.3):
from math import * # 在主程序中 def f(x): # 在主程序中 e = exp(-0.1*x) s = sin(6*pi*x) return e*s x = 2 # 在主程序中 y = f(x) # 在主程序中 print 'f(%g)=%g' % (x, y) # 在主程序中
- 以上面的代码为例,主程序的运行流程为:
- 从 math 模块中引入函数(Import functions from the math module)
- 定义函数 f(x)(define a function f(x))
- 定义全局变量 x(define x)
- 调用函数 f 并执行函数体(call f and execute the function body)
- 定义全局变量 y,并赋值为函数 f 的返回值(define y as the value returned from f)
- 打印字符串(print the string.)
- 可以在 3.1.2 提到的网站运行看看
3.1.14
Lambda 函数
- 只有一条语句的函数,可由 lambda 函数直接用一行表示:
""" 只有一条语句的函数 def g(arg1, arg2, arg3, ...): return expression 可用lambda函数直接表示 g = lambda arg1, arg2, arg3, ...: expression """ #例如,以下两式等价 def f(x): return x**2 + 4 f = lambda x: x**2 + 4
- lambda 函数也支持关键字参数,例如:
d2 = diff2nd(lambda t, A=1, a=0.5: -a*2*t*A*exp(-a*t**2), 1.2)
3.2-分支
分段函数 可用 if-else 分支语句表示为:
from math import pi
def f(x):
if 0 <= x <= pi:
value = sin(x)
else:
value = 0
return value
3.2.1
if-else 语句
- 常见的写法:
if 判断条件: <语句块, 当条件为 True 时执行> else: <语句块, 当条件为 False 时执行>
else 可省略
- 用 elif,可加入更多判断;例, 可表示为:
def N(x): if x < 0: return 0.0 elif 0 <= x < 1: return x elif 1 <= x < 2: return 2 - x elif x >= 2: return 0.0
或
def N(x): if 0 <= x < 1: return x elif 1 <= x < 2: return 2 - x else: return 0
3.2.2
用一行表示 if-else
- 形如
if condition: a = value1 else: a = value2
可表示为
a = (value1 if condition else value2)
- 形如
def f(x): return (sin(x) if 0 <= x <= 2*pi else 0)
可表示为
f = lambda x: sin(x) if 0 <= x <= 2*pi else 0
3.3-循环、分支和函数的综合运用(Mixing Loops, Branching, and Functions in Bioinformatics Examples)
DNA 序列由 A、T、G、C 四种碱基(bases)组合而成;以此为例,展示循环、分支和函数的综合运用
3.3.1
举例说明,查看 DNA 中特定碱基出现次数的若干方案
- 方案一:列表迭代法(List iteration)
def count_v1(dna, base): dna = list(dna) # 将类型转为list i = 0 # 计数器 for c in dna: if c == base: i += 1 return i
将 DNA 序列转存为列表,数其中碱基的个数。
- 方案二:字符串迭代法(String iteration)
def count_v2(dna, base): i = 0 # 计数器 for c in dna: if c == base: i += 1 return i
实际上,Python 中包含很多元素的对象(如,string、list、tuple等),都能用 for 循环遍历
- 方案三:索引迭代法(Index iteration)
def count_v3(dna, base): i = 0 # 计数器 for j in range(len(dna)): if dna[j] == base: i += 1 return i
很多语言(如,Fortran、C、Java 等),都能用索引遍历
- 方案四:While 循环法(While loops)
def count_v4(dna, base): i = 0 # 计数器 j = 0 # 计数器 while j < len(dna): if dna[j] == base: i += 1 j += 1 return i
注意缩进
- 方案五:布尔列表求和法(Summing a boolean list)
def count_v5(dna, base): m = [] # 将base与dna中的元素作匹配:若dna[i]==base,则m[i]=True for c in dna: if c == base: m.append(True) else: m.append(False) return sum(m)
实际上,True 值为 1,False 值为 0;将布尔列表求和,结果即为 True 的个数
- 方案六:if 语句单行表示法(Inline if test)
def count_v6(dna, base): m = [] # 将base与dna中的元素作匹配:若dna[i]==base,则m[i]=True for c in dna: m.append(True if c == base else False) return sum(m)
方案方法五的更简练的写法,增加可读性
- 方案七:直接使用布尔值法(Using boolean values directly)
def count_v7(dna, base): m = [] # 将base与dna中的元素作匹配:若dna[i]==base,则m[i]=True for c in dna: m.append(c == base) return sum(m)
c == base 结果非 True 即 False,可直接使用
- 方案八:列表推导法(List comprehensions)
def count_v8(dna, base): m = [c == base for c in dna] return sum(m)
利用 [expr for e in sequence] 直接生成列表;其中,sequence 为已知数列,expr 为与 e 相关的表达式
- 方案九:方案八的简洁表达法
def count_v9(dna, base): return sum([c == base for c in dna])
去掉变量 m,将函数体并为一行
- 方案十:迭代求和法(Using a sum iterator)
def count_v10(dna, base): return sum(c == base for c in dna)
实际上,DNA 序列相当长,列表可能存不下;去掉方法九中的方括号,可避免生成列表
- 方案十一:索引提取法(Extracting indices)
def count_v11(dna, base): return len([i for i in range(len(dna)) if dna[i] == base])
将 dna 中与 base 相同的元素的索引存储在新列表中
- 方案十二:使用 Python 库(Using Python’s library)
def count_v12(dna, base): return dna.count(base)
直接使用 Python 库函数
3.3.2
对以上方案的解题效率进行评估
- 首先,生成用于测试的 DNS 序列
N = 1000000 dna = 'A'*N
此方法,可生成 1000000 个 A 构成的序列,但缺乏随机性;可用 random 库生成随机的序列:
import random alphabet = list(’ATGC’) dna = [random.choice(alphabet) for i in range(N)] dna = ’’.join(dna) # 将列表变为字符串
random.choice(x) 会随机选择列表 x 中的一个元素;但在 Python 2.x 中,range(N) 会生成一个列表,当 N 很大时,列表将无法构建,我们可用 xrange(N) 代替,通过一次生成一个整数来避免生成一个大列表;Pyhon 3.x 中 range,与 2.x 中的 xrange 等价:
import random def generate_string(N, alphabet=’ACGT’): return ’’.join([random.choice(alphabet) for i in xrange(N)]) dna = generate_string(600000)
生成了 600000 长度的随机 DNA 序列
- 计算 CPU 时间
import time ... t0 = time.clock() # 执行其他代码 t1 = time.clock() cpu_time = t1 - t0
time.clock() 返回当前程序的 CPU 时间;若关心实际运行时间(包括读写文件等),可用 time.time()
- 测试每个方案的 CPU 时间
import time functions = [count_v1, count_v2, count_v3, count_v4, count_v5, count_v6, count_v7, count_v8, count_v9, count_v10, count_v11, count_v12] timings = [] # timings[i]保存functions[i]的CPU时间 for function in functions: t0 = time.clock() function(dna, ’A’) t1 = time.clock() cpu_time = t1 - t0 timings.append(cpu_time)
对于 Python 而言,函数也是个对象,因此函数列表和其他列表没有区别
- 利用 zip,同时返回函数名和 CUP 时间
for cpu_time, function in zip(timings, functions): print ’{f:<9s}: {cpu:.2f} s’.format( f=function.func_name, cpu=cpu_time)
- 测试结果显示,在 MacBook Air 11 上的 Ubuntu 操作系统中,使用了 list.append 的方案,耗时是用了函数推导法的方案的两倍;字符串迭代法会再更快一些;而使用了 Python 内建函数 dna.count(base) 的方案,比自己编写的快了30倍!
- 因此,即使是很简单的任务,也要先 Google;因为前人研究出来的解决方案,远比自己拍脑袋想出来的高级
3.3.3
编写测试函数,检验以上方案的正确度
- 假设 dna.count('A') 返回的是标准答案
def test_count_all(): dna = ’ATTTGCGGTCCAAA’ expected = dna.count(’A’) functions = [count_v1, count_v2, count_v3, count_v4, count_v5, count_v6, count_v7, count_v8, count_v9, count_v10, count_v11, count_v12] for f in functions: success = f(dna, ’A’) == expected msg = ’%s failed’ % f.__name__ assert success, msg
其中,f.__name__ 返回函数名
- 测试函数应当遵守如下约定:
- 以 test_ 开头(have a name starting with test_)
- 没有参数(have no arguments)
- 有个名为 success 的布尔变量,通过测试其值为 True,否则为 False(let a boolean variable, say success, be True if a test passes and be False if the test fails)
- 有个名为 msg 的字符串变量,提示错误信息(create a message about what failed, stored in some string, say msg)
- 使用 assert success, msg 的结构,当 success 为 False 时,终止程序并输出 msg 错误信息(use the construction assert success, msg, which will abort the program and write out the error message msg if success is False)
3.4-小结(Summary)
本章所学术语
- 函数(function)
- 方法(method)
- 返回语句(return statement)
- 位置参数(position arguments)
- 关键字参数(key arguments)
- 局部和全局变量(local and global variables)
- 文档字符串(doc strings)
- if、else 和 elif 分支语句(if tests with if, elif, and else (branching))
- None 对象(the None object)
- 测试函数(test functions (for verification))