前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Python编程导论】第四章- 函数、作用域与抽象

【Python编程导论】第四章- 函数、作用域与抽象

作者头像
Datawhale
发布2019-07-08 10:53:40
8180
发布2019-07-08 10:53:40
举报
文章被收录于专栏:Datawhale专栏

基本概念

4.1 函数与作用域

4.1.1 函数定义

在Python中,按如下形式进行函数定义:

代码语言:javascript
复制
def name of function (list of formal parameters): 
    body of function

(1) def是个保留字,告诉Python要定义一个函数。

(2) 函数名只是个名称,用来引用函数。

(3) 函数名后面括号中的一系列名称是函数的形式参数。使用函数时,形式参数在函数调用时被绑定(和赋值语句一样)到实际参数(通常指代函数调用时的参数)。

(4) 函数体可以是任何一段Python代码。

(5) 函数调用是个表达式,和所有表达式一样,它也有一个值。这个值就是被调用函数返回的值。

代码语言:javascript
复制
例:
#函数定义
def maxVal(x, y): 
    if x > y: 
        return x 
    else: 
        return y
#调用        
maxVal(3, 4)

当函数被调用时,会执行如下过程。

(1) 构成实参的表达式被求值,函数的形参被绑定到求值结果。

(2) 执行点(要执行的下一条指令)从调用点转到函数体的第一条语句。

(3) 执行函数体中的代码,直至遇到return语句。这时,return后面的表达式的值就成为这次函数调用的值;或者没有语句可以继续执行,这时函数返回的值为None;如果return后面没有表达式,这次调用的值也为None。(return语句,只能用在函数体中;执行return语句会结束对函数的调用。)

(4) 这次函数调用的值就是返回值。

(5) 执行点移动到紧跟在这次函数调用后面的代码。

4.1.2 关键字参数和默认值
  • 位置参数:即第一个形参绑定到第一个实参,第二个形参绑定到第二个实参,以此类推。
  • 关键字参数:形参根据名称绑定到实参。(尽管关键字参数可以在实参列表中以任意顺序出现,但将关键字参数放在非关键字参数后面是不合法的。)
  • 关键字参数经常与默认参数值结合使用。默认值允许程序员不指定所有参数即可调用函数。
4.1.3 作用域

每个函数都定义了一个命名空间,也称为作用域。

对“作用域”可以进行如下理解。 (1) 在最顶层,比如shell层,有一个符号表会跟踪记录这一层所有的名称定义和它们当前的绑定。

(2) 调用函数时,会建立一个新的符号表(常称为栈帧)。这个表跟踪记录函数中所有的名称定义(包括形参)和它们当前的绑定。如果函数体内又调用了一个函数,就再建立一个栈帧。

(3) 函数结束时,它的栈帧也随之消失。

如下的示例代码说明了Python的作用域规则:

代码语言:javascript
复制
def f(x): 
    def g(): 
        x = 'abc' 
        print('x =', x) 
    def h(): 
        z = x 
        print('z =', z) 
    x = x + 1 
    print('x =', x) 
    h() 
    g() 
    print('x =', x) 
    return g  


x = 3 
z = f(x) 
print('x =', x) 
print('z =', z) 
z()

与这段代码相关的栈帧历史如图所示:

(1) column1:第一列包含的是函数f之外的名称集合,也就是变量x和z,以及函数名称f。

(2) column2:赋值语句z = f(x)首先使用与x绑定的值调用函数f,对表达式f(x)求值。进入函数f时,会建立一个栈帧。栈帧中的名称是x(形参,并不是调用上下文中的x)、g和h。

(3) column3:在函数f中调用函数h时,会建立另一个栈帧,这个栈帧仅包含局部变量z。(只有一个名称是函数的形参或是被绑定到函数体内一个对象的变量时,才能添加到函数作用域。出现一个没有和函数体内(函数h的内部)任何一个对象绑定的名称(本例中是x)时,解释器会搜索与该函数定义上层作用域相关的栈帧(即与f相关的栈帧)。如果发现这个名称(x),就使用名称绑定的值(4)。如果没有发现,就产生一条错误消息。)

(4) column4:函数h返回后,与这次对h的调用相关的栈帧就会消失(从栈的顶端弹出)(注意,不能从栈的中间移除帧,只能移除最近添加的帧。正是因为它具有这种“后入先出”的性质,所以我们称之为栈)

(5) column5:调用函数g,一个包含g中局部变量x的栈帧被添加进来。

(6) column6:函数g返回后,这个帧被弹出。

(7) column7:函数f返回后,包含函数f相关名称的栈帧被弹出。

只要在函数体内任何地方有对象与名称进行绑定(即使在名称作为赋值语句左侧项之前,就已经出现在某个表达式中),就认为这个名称是函数的局部变量。

看下面的代码:

代码语言:javascript
复制
def f(): 
    print(x) 
def g(): 
    print(x) #UnboundLocalError: local variable 'x' referenced before assignment
    x = 1  


x = 3 
f() 
x = 3 
g()

在函数g中,执行到print语句时,会产生信息是因为:print语句后面的赋值语句使x成为函数g中的局部变量,执行print语句时还没有被赋值。

4.2 规范

三引号之间的文本在Python中称为文档字符串。按照惯例,Python程序员使用文档字符串提供函数的规范。可以使用内置函数help(function)访问这些字符串。如果在编辑器中输入function(,会显示形参列表。

函数的规范定义了函数编写者与使用者之间的约定。我们将函数使用者称为客户。可以认为约定包括以下两部分:

(1) 假设:客户使用函数时必须满足的前提条件,通常是对实参的限制。它几乎总是限定每个参数可以接受的变量类型,偶尔对一个或多个参数的取值添加限制条件。

(2) 保证:调用方法满足条件时,函数应当实现的功能。

函数是一种创建基本程序元素的方式。我们非常乐于像内置函数一样使用求根函数和很多其他复杂操作,就像使用内置函数max和abs一样。函数通过分解和抽象的功能,大大提高了编程的便捷性。

(1) 分解实现了程序结构化。

(2) 抽象隐藏了细节。它允许我们将一段代码当作黑箱使用。

4.3 递归

一般情况下,递归定义包括两部分。

(1) 至少有一种基本情形可以直接得出某种特定情形的结果

(2) 还至少有一种递归情形(或称归纳情形)定义了该问题在其他情形下的结果,其他情形通常是同样问题的简化版本。

代码语言:javascript
复制
世界上最简单的递归定义可能是自然数的阶乘函数(在数学中一般使用!表示)。
经典的归纳定义是:
    1! = 1 
    (n +1)! = (n + 1) * n! 
    第一个等式定义了基本情形。
    第二个等式在前一个数的阶乘的基础上定义了所有自然数的阶乘——除基本情形外。


#阶乘的迭代实现
def factI(n):
    """假设n是正整数
        返回n!"""
    result = 1
    while n>1:
        result = result * n
        n -= 1
    return result 

#阶乘的递归实现
def factR(n):
    """假设n是正整数
        返回n!"""
    if n == 1:
        return n
    else:
        return n*factR(n - 1)
4.3.1 斐波那契数列

斐波那契数列是另一个经常使用递归方式定义的常用数学函数。“他们像兔子一样繁殖”经常用来形容人口增长过快。1202年,意大利数学家比萨的列奥纳多(也称为斐波那契)得出了一个公式,用来计算兔子的繁殖情况。尽管在我们看来,他的假设有些不太现实。 假设一对新生的兔子被放到兔栏中(更坏的情况是放到野外),一只是公兔,一只是母兔。再假设兔子在一个月大时就可以交配(令人惊奇的是,有些品种确实可以),并有一个月的妊娠期(令人惊奇的是,有些品种确实如此)。最后,假设这些神话般的兔子永远不死,并且母兔从第二个月之后每月都能产下一对小兔(一公一母)。那么6个月后,会有多少只母兔? 母兔数量的增长可以很自然地使用以下递推公式描述: females(0) = 1 females(1) = 1 females(n + 2) = females(n+1) + females(n)

代码语言:javascript
复制
#斐波那契数列的递归实现
def fib(n): 
    """假定n是正整数
        返回第n个斐波那契数""" 
    if n == 0 or n == 1: 
        return 1 
    else:  
        return fib(n-1) + fib(n-2)

斐波那契数列的定义与阶乘的递归定义有些不同。

(1) 它有两种基本情形,而不是一种。一般来说,只要需要,我们可以有任意多种基本情形。

(2) 在递归情形中,有两个递归调用,而不是一个。同样,如果需要,可以有任意多个调用。

4.3.2 回文

递归也经常用于很多与数值无关的问题中。下面代码中包含了一个函数isPalindrome,可以检查一个字符串在顺读和倒读时是否一样。

代码语言:javascript
复制
#回文检测
def isPalindrome(s): 
    """假设s是字符串
        如果s是回文字符串则返回True,否则返回False。
        忽略标点符号、空格和大小写。""" 

    #辅助函数toChars将所有字母转换为小写,并且移除了所有非字母字符。
    def toChars(s): 
        s = s.lower() 
        letters = '' 
        for c in s: 
            if c in 'abcdefghijklmnopqrstuvwxyz': 
                letters = letters + c 
            return letters 

    #辅助函数isPal使用递归完成实际的工作。
    def isPal(s): 
        if len(s) <= 1: 
            return True 
        else: 
            return s[0] == s[-1] and isPal(s[1:-1]) #从左到右进行求值。除非第一个合取项取值为True,否则第二个合取项不被求值。

    return isPal(toChars(s))

这种对isPalindrome的实现是分治策略的典型例子。这种解决问题的原则就是,将一个困难问题分解成一组子问题逐个解决。分解出来的子问题具有以下特性: (1) 子问题比初始问题更容易解决; (2) 子问题的解决方案可以组合起来解决初始问题。

本例中,我们将初始问题分解为一个更简单的情形(检查一个更短的字符串是否是回文字符串)和一个我们可以解决的简单情形(比较单个字符),然后使用and将这两个问题的解组合起来。

4.4 全局变量

如果试着使用一个非常大的数调用函数fib,那么你可能会发现函数需要运行很长一段时间。假设我们想知道究竟进行了多少次递归调用,可以添加一些代码计算调用次数。这时就要使用全局变量。

代码语言:javascript
复制
def fib(x): 
    """假设x是正整数
        返回第x个斐波那契数""" 
    global numFibCalls #使用全局变量
    numFibCalls += 1 
    if x == 0 or x == 1: 
        return 1 
    else: 
        return fib(x-1) + fib(x-2) 


def testFib(n): 
    for i in range(n+1): 
        global numFibCalls 
        numFibCalls = 0 
        print('fib of', i, '=', fib(i)) 
        print('fib called', numFibCalls, 'times.')

每个函数中,global numFibCalls这行代码都会告诉Python,名称numFibCalls是定义在代码所在函数外层的模块作用域中的,而不是在代码所在函数的作用域中的。如果我们没有包括global numFibCalls这行代码,那么名称numFibCalls就会被认为是函数fib和testFib的局部变量。函数testFib每次调用fib时,都将numFibCalls绑定到0。每次进入函数fib时,fib都会增加numFibCalls的值。

4.5 模块

模块就是一个包含Python定义和语句的.py文件。

例如,我们可以创建一个包含下面代码的circle.py文件。

代码语言:javascript
复制
#一些关于圆与球的代码
pi = 3.14159 

def area(radius): 
    return pi*(radius**2) 

def circumference(radius): 
    return 2*pi*radius 

def sphereSurface(radius): 
    return 4.0*area(radius) 

def sphereVolume(radius): 
    return (4.0/3.0)*pi*(radius**3)

程序可以通过import语句访问一个模块。如下面的代码:

代码语言:javascript
复制
import circle 
pi = 3 
print(pi) 
print(circle.pi) 
print(circle.area(3)) 
print(circle.circumference(3)) 
print(circle.sphereSurface(3))


会输出:
3 
3.14159 
28.27431 
18.849539999999998 
113.09724

运行import M语句后,会将模块M绑定到import语句所在的作用域中。因此,在导入上下文中,我们使用点标记法表示引用的名称是定义在导入模块中的。

还有一种import语句的变种,允许导入程序不需使用模块名称即可访问定义在被导入模块中的名称。执行语句from M import *会将M中定义的所有对象绑定到当前作用域,而不是M本身。例如,以下代码:

代码语言:javascript
复制
from circle import * 
print(pi) 
print(circle.pi) 


会先输出3.14159
然后产生一条错误消息:NameError: name 'circle' is not defined

正如我们所见,模块可以包含可执行的语句,也可以包含函数定义。通常,这些语句用来对模块进行初始化。基于这个原因,模块中的语句仅在模块第一次被导入程序时才执行。而且,一个模块在每个解释器会话中只能被导入一次。如果你启动了shell,导入一个模块,然后修改这个模块中的内容,那么解释器仍然会继续使用这个模块的初始版本。这在调试程序时会引起令人困惑的状况。你疑惑不解时,可以启动一个新的shell。

4.6 文件

每种操作系统(如Windows和MAC OS)都通过自己的文件系统创建和使用文件。Python通过文件句柄处理文件,实现了操作系统的独立性。

下面代码可以打开一个文件,使用write方法向文件写入两行数据,然后关闭文件。(程序使用完文件后,请一定记得关闭文件,否则写入的内容可能部分或全部丢失。)

代码语言:javascript
复制
nameHandle = open('kids', 'w') #如果不想覆盖原来的内容,可以使用参数'a'用追加(不使用可写方式)方式打开文件。
for i in range(2): 
    name = input('Enter name: ') 
    nameHandle.write(name + '\n') 
nameHandle.close()

我们还可以以只读(read)方式打开文件,然后输出其中的内容。(因为Python将文件看成是行的序列,所以可以使用for语句遍历文件内容)

代码语言:javascript
复制
nameHandle = open('kids', 'r') 
for line in nameHandle: 
    print(line) #输出结果之间有一个空行,因为每次输出到文件行尾的'\n'时,都会开始一个新行。可以使用print line[:-1]避免输出空行。
nameHandle.close()

常用的文件操作:

open(fn, 'w'):fn是一个表示文件名的字符串。创建一个文件用来写入数据,返回文件句柄。

open(fn, 'r'):fn是一个表示文件名的字符串。打开一个已有文件读取数据,返回文件句柄。

open(fn, 'a'):fn是一个表示文件名的字符串。打开一个已有文件用来追加数据,返回文件句柄。

fh.read():返回一个字符串,其中包含与文件句柄fh相关的文件中的内容。

fh.readline():返回与文件句柄fh相关的文件中的下一行。

fh.readlines():返回一个列表,列表中的每个元素都是与文件句柄fh相关的文件中的一行。

fh.write(s):将字符串s写入与文件句柄fh相关的文件末尾。

fh.writeLines(S):S是个字符串序列。将S中的每个元素作为一个单独的行写入与文件句柄fh相关的文件。

fh.close():关闭与文件句柄fh相关的文件。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Datawhale 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基本概念
    • 4.1 函数与作用域
      • 4.1.1 函数定义
      • 4.1.2 关键字参数和默认值
      • 4.1.3 作用域
    • 4.2 规范
      • 4.3 递归
        • 4.3.1 斐波那契数列
        • 4.3.2 回文
      • 4.4 全局变量
        • 4.5 模块
          • 4.6 文件
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档