Python学习教程(六)——抽象之函数

1. 抽象的认识

  当为了实现一个功能而写了一段代码之后,如果在其他模块也需要实现这样的功能,那么我们应该怎么办呢?是重新写一遍相同的代码吗?答案是否定的,真正的程序员不会这么做,他们会让程序抽象一些,把具有能完成某一特定功能的代码封装在一个盒子里,对外提供一个接口(参数列表),封装在盒子里的程序只需要专心做好自己的本职工作而无需关心谁会调用;盒子外面的调用者也无需关心盒子里的具体实现原理,只要知道这个盒子可以完成所需要的特定工作就好;那么这个盒子就是我们本章的重点——函数。只要我们定义好了一个合格的函数,就可以实现代码的复用,而不是哪里要用就在那里复制一个相同的副本。

从本章开始,我们要有面向对象编程思想的准备,从抽象开始,让我们慢慢去理解面向对象的魅力。

2. 创建函数

  函数是可以被调用的(可能带有参数),它执行完特定的功能后返回一个值(并非所有的Python函数都有返回值)。如果要判断函数是否可以被调用,可以使用callable函数。

>>> from math import sqrt
>>> x = 1
>>> y = sqrt
>>> callable(x)
False
>>> callable(y)
True

callable函数在Python3.0中不再可用。需要使用表达式hasattr(func,__call__)代替,后面章节会有更多的解释。

2.1. 函数的定义

  使用def语句——函数定义语句,来定义一个函数。我们来写一个简单的例子:

# coding=UTF-8

#定义了一个名为hello的函数,实现对输入name名字的问候
def hello(name):
    return 'Hello,' + name + '!'

#调用定义的hello函数
print hello('yangliehui')

程序输出:

Hello,yangliehui!

  定义一个函数就是这么简单,1、def关键字;2、给函数起一个能表达出要实现功能含义的名字;3、实现的功能需要哪些参数,把参数列表写在括号里;4、要返回什么内容,return的时候返回;

2.2. 文档化参数

  如果想要给函数写文档,让其他使用该函数的人能理解的话,可以加入注释(#开头),这种方式我们在上一节定义函数时已经用到了。另外一个方式就是直接写上一个字符串。这类字符串可能在其他地方会非常有用,比如在def语句后面。如果在函数的开头写下字符串,它会做诶函数的一部分进行存储,这称为文档字符串

def hello(name):
    '名为hello的函数,实现对输入name名字的问候'
    return 'Hello,' + name + '!'

print hello.__doc__

程序输出:

名为hello的函数,实现对输入name名字的问候

  定义语句下面的一行字符串就是文档字符串,如果要访问文档字符串可以使用函数名.__doc__的方式。任何的函数都可以这种方式,比如一些内建函数:

import math
print math.sqrt.__doc__

程序输出:

sqrt(x)

Return the square root of x.

__doc__是函数的属性,属性中的双下划线表示它是个特殊属性。详细的内容会在后面的章节中涉及到。

  我们也可以使用内建的help函数得到关于函数,包括它的文档字符串的信息:

import math
help(math.sqrt)

程序输出:

Help on built-in function sqrt in module math:

sqrt(...)
    sqrt(x)

    Return the square root of x.

2.3. 无返回值的函数

  正常情况下,定义一个函数后总要在执行最后返回一个值,但是Python允许创建一个没有返回值的函数,即没有return语句,或虽然有return语句但是不返回任何值。

def void_func(name):
    print 'Hello,' + name + " !"

void_func('yangliehui')

程序输出:

Hello,yangliehui !

  上面定义了一个没有return语句的函数。

def void_func(name):
    print 'Hello,' + name + " !"
    return 
    print 'Hi ,' + name + " !"

x = void_func('yangliehui')
print x

程序输出:

Hello,yangliehui !
None

  上面定义了一个有return语句,但是没有return任何值的函数,我们可以看到return后面的语句被阻止执行了,并且由于没有返回任何值,所以看到返回值打印出了None。

3. 参数

3.1 形参与实参

def function_name(形参1,形参2):
    do_somethings()

function_name(实参1,实参2)

  以上面的代码为例,def语句中函数后面的变量通常叫做函数的形参,而调用函数的时候提供的值成为实参,有时我们也会把实参称为“值”。

3.2. 参数传递

  函数通过它的参数获得一系列值。那么这些值能改变吗?如果改变了又会怎样?我们先看一下例子:

def try_to_change(n):
    n = 'zhangsan'

name = 'yangliehui'
try_to_change(name)
print name

程序输出:

yangliehui

  从上面的程序可以看到,当函数体内n的值改变后,变量name的值并没有变化。这也就是说形参只是一个变量而已,在函数内为这个变量赋值不会改变外部变量的值。并且函数定义的形参的作用域只会在函数体内起作用,我们成为“局部变量”。
  为了验证上面的例子,我们尝试用数字再试一下:

def try_to_change(n):
    n = 2

number = 1
try_to_change(number)
print number

程序输出:

1

  可以看到,当参数为数字时,同样没有改变外部参数的值。所以我们看,不管是字符串还是数字,甚至是元组,由于它们的值都是不可变的,所以在函数体内,我们只能用新的值覆盖,无法对传入的值进行原地修改。
  下面我们看一个可变的值,例如列表,因为列表可变,那么我们尝试在函数体内对传入的值进行原地修改,看看会是什么结果。

def try_to_change(n):
    n[0] = 0

num = [1,2,3]
try_to_change(num)
print num

程序输出:

[0, 2, 3]

  在本例中,我们看到外部的参数被改变了。这和之前的例子不太一样,我们看一下下面已经学过的例子就好理解了。

num = [1,2,3]
n = num
n[0] = 0
print num

程序输出:

[0, 2, 3]

  总结一下上面的现象,我们知道函数名后面括号里的形参实际是一个定义的局部变量,我们看一个局部变量和传入的变量是什么关系:

num = [1,2,3]
def try_to_change(n):
    print num is n

try_to_change(num)

程序输出:

True

同样的,当num的值为数字、元组、字符串时,程序输入的都是True。这说明在一开始实参和形参两个不同的引用指向了同一个地址(值)。即——num--->[1,2,3]<---n,所以当实际的值是一个可变类型时,通过n原地改变了值,同样也能反映在num上,即num--->[0,2,3]<---n
  而当实际传入的值是一个不可变类型,不能支持原地修改值时,比如调用函数的一开始num--->(1,2,3)<---n,随后在函数内我们只能做赋新值的操作,即num--->(1,2,3) n --->(0,2,3),这时num和n不再指向同一个地址(值),所有当n重新赋值后并不会反映到num上。
  了解了上面总结的原理,我们也就自然理解了上面的各种现象。那么我们现在还有一个问题,我们怎样让可变类型的值也像不可变类型的值那样,在函数体内改变值而不影响外部的参数值呢?答案就是像不可变类型值那样,对函数定义的形参重新赋值,而不是原地修改。

num = [1,2,3]
def try_to_change(n):
    n = [0,2,3]
    print num is n

try_to_change(num)
print num

程序输出:

False
[1, 2, 3]

  我们看到,虽然列表是可以原地修改的可变类型值,但是由于在函数体内进行了重新赋值,所有外部的num并没有受到影响。
  假如函数体内即要使用外部传入的值,又不希望影响外部的参数值时怎么办呢?我们可以利用前面所学的deepcopy函数(关于copy和deepcopy的区别可以复习前面学习的内容),copy一份与外部变量一样的值重新赋值给局部参数变量。

import copy
num = [1,2,3]
def try_to_change(n):
    n = copy.deepcopy(n)
    print num is n
    n[0] = 0

try_to_change(num)
print num

程序输出:

False
[1, 2, 3]

  我们看到,copy以后,num和n指向的不再是同一个地址,返回了False。即——num--->[1,2,3] n--->[1,2,3],这时我们在操作n的值时,并没有影响到外部的num的值。

3.3. 关键字参数与默认值

  定义函数时的形参的位置至关重要,当我们在调用函数时,顺序不对将导致程序运行错误。如:

def hello(greeting ,name):
    print '%s , %s !' % (greeting,name)

hello('Hello', 'world')
hello('world', 'Hello')

程序输出:

Hello , world !
world , Hello !

  
  我们可以看到,调用函数时,参数的顺序不同,输出的结果也不同。但有时候当参数较多,我们不便于记参数顺序时,可以通过提供参数的名字实现对应。

def hello(greeting ,name):
    print '%s , %s !' % (greeting,name)

hello(greeting='Hello', name='world')
hello(name='world', greeting='Hello')

程序输出:

Hello , world !
Hello , world !

  这样参数的顺序就不会影响函数的正常执行。但参数名和值一定要对应的上。这种使用参数名提供的参数叫做关键字参数
  关键字参数可以用在定义函数的时候使用,可以在函数中给参数提供默认值。

def hello(greeting='hello' ,name='world'):
    print '%s , %s !' % (greeting,name)

hello()
hello('Greetings')
hello('Greetings', 'universe')
hello(name='yangliehui')

程序输出:

hello , world !
Greetings , world !
Greetings , universe !
hello , yangliehui !

  我们看到,在定义函数时定义了默认值,在调用函数时可以不写参数,或只写其中的一个参数。除此之外,位置参数和关键字参数还可以混合使用。把位置参数放在前面就可以了(如果不这么做,解释器不知道它们到底是谁)。


def hello(name,greeting='Hello',punctuation='!'):
    print '%s, %s%s' % (greeting,name,punctuation)

hello('Mars')

hello('Mars','Howdy')

hello('Mars','Howdy','...')

hello('Mars',punctuation='.')

hello('Mars',greeting='Top of the morning to ya')

hello()

程序输出:

Hello, Mars!
Howdy, Mars!
Howdy, Mars...
Hello, Mars.
Top of the morning to ya, Mars!
    hello()
TypeError: hello() takes at least 1 argument (0 given)

  当直接调用hello()函数时,由于第一个参数没有默认值,调用时也没有指定参数值,所有报错了。

3.4. 可变参数

  有时候让用户调用函数时传入任意个数的参数,我们可以这样定义:

def print_params(*params):
    print params

print_params()
print_params('hello')
print_params(1,2,3)
print_params('a','b','c')
print_params(1,2,3,'a','b','c')

程序输出:

()
('hello',)
(1, 2, 3)
('a', 'b', 'c')
(1, 2, 3, 'a', 'b', 'c')

  上面的例子中,我们定义函数时只定义了一个参数,但是前面多了一个*号,通过输出结果我们可以看出,这种写法表示,这个函数可以输入任意数量的参数值,其结果以元组的形式打印出来,同样我们也可以和普通参数一起使用,这里不再举例。
  这里要注意,这种用法不能使用关键字参数:

def print_params(*params):
    print params

print_params(something='a')

程序输出:

    print_params(something='a')
TypeError: print_params() got an unexpected keyword argument 'something'

  运行上面的程序会发现程序报错了。那么有没有能实现上述所说的,可以使用关键字参数的写法呢?我们稍微改一下上面的代码:


def print_params(**params):
    print params

print_params(something='a')

程序输出:

{'something': 'a'}

  程序正常运行,并且输出了一个字典,对比上一个例子,一个微小的差别是定义函数的参数前面多了一个*号,即可实现关键字参数的可变参数,并且以字典的形式收集了传入的参数。
  把上面的所有情况联合使用一下试试:

def print_params(x,y,z=3,*pospar,**keypar):
    print x,y,z
    print pospar
    print keypar

print_params(1,2,3,4,5,6,7,foo=1,bar=2)

程序输出:

1 2 3
(4, 5, 6, 7)
{'foo': 1, 'bar': 2}

  上面的例子联合使用了各种参数形式,1,2,3对应了x,y,z,4,5,6对应了pospar,foo=1,bar=2这种关键字参数对应了keypar。

练习:观察print_params(1,2) 语句,会输出什么?

3.5. 可变参数的逆过程

  上面一节中,通过对函数定义可变参数,可以实现把调用时传入的任意个数的参数,收集成为元组或字典。这一节我们要说的是这个过程的逆向参数,即我们调用函数时传入的是一个已经收集好的元组或字典,看函数如何和进行拆分出每一个参数。

def add_num(x,y):
    print x + y

num = (1,2)
add_num(*num)

程序输出:

3

  通过上面简单的例子,我们看到了,当我们传入一个元组(被收集好的参数)时,函数进行了拆分,自动匹配到了x和y两个变量上,实现这一功能的还是星号,只是这个星号写在了调用函数的地方。这种情况要注意以下,元组中的元素数量要和函数的参数数量一致,比如上面的例子,如果改为num = (1,2,3)程序就会报错,因为1对应x,2对应y,3则无处对应。
  同样双星号也是类似的用法。比如:

def hello(greeting,name):
    print greeting,name

params = {'greeting':'Hello','name':'world'}

hello(**params)

程序输出:

Hello world

  上面就是双星号的例子,很好理解,对比上一节的例子,在双星号收集参数时,greeting='Hello',name='world'被收集为了字典{'greeting':'Hello','name':'world'}。那么这个逆过程其实就是把字典{'greeting':'Hello','name':'world'}拆分为greeting='Hello',name='world'并调用hello函数的过程,那么由此我们也可以断定,params参数定义的字典的两个键的名字必须一个是greeting另一个是name,否则就会报错。另外,字典也必须只能有这两对键值对,多了也会报错。

def hello(**p):
    print p['greeting'],p['name']

params = {'greeting':'Hello','name':'world'}

hello(**params)

  上面的代码在定义函数和调用函数都使用了双星号,我们可以这样理解,hello(**params)时是把字典拆分为了greeting='Hello',name='world',而def hello(**p)时,又把已经拆分的参数进行了收集,转换为了字典。上面的例子只是为了学习这个知识点,在实际开发过程中,如果能明确参数的数量,我们还是尽量避免这种用法。

4. 作用域

  究竟什么是变量?你可以把它们看作是值的名字。在执行x=1赋值语句后,名称x引用到值1。这就像用字典一样。键引用值,当然,变量所对应的值用的是个“不可见”的字典。实际上这么说已经很接近真实的情况了。内建的vars函数可以返回这个字典:

x = 1
vars_scope = vars()
print vars_scope['x']
vars_scope['x'] += 1
print x

程序输出:

1
2

一般来说,vars所返回的字典是不能修改的,可能会得不到想要的结果。

  这类“不可见字典”叫做命名空间作用域。那么到底有多少个命名空间呢?除了全局作用域外,每个函数调用都会创建一个新的作用域:

def foo():
    x = 42

x = 1
foo()
print x

  上面的例子中,函数foo内的x变量只会作用于函数体内,外部打印x时,并不会受函数体内的x的干扰。但是反过来,函数体内可以访问外部的全局变量,如下:

def foo():
    print x

x = 1
foo()

程序输出

1

  函数内部读取全局变量不是问题,但是当局部变量或者参数的名字和想要访问的全局变量名相同的话,就不能直接访问了。全局变量会被局部变量屏蔽。如果在这种情况下,依然想要访问全局变量的话,可以使用globals函数获取全局变量值,该函数的近亲是vars函数,它可以返回全局变量的字典(locals返回的是局部变量的字典),如下面的例子:

x = 1
def foo():
    x = 2
    print x , globals()['x']

foo()

程序输出:

2 1

  接下来讨论重绑定全局变量,如果在函数内将值赋予一个变量,它会自动成为局部变量,除非告知python将其声明为全局变量。那么怎么才能告诉Python这是一个全局变量呢?

x = 1
def change_globle():
    global x
    x += 1

change_globle()
print x

程序输出:

2

  发现函数内的x被重新绑定为了全局变量,在函数外部,我们看到了值的变化。但是我们要避免这种用法,它会让程序变的非常混乱。

5. 函数嵌套(选学)

  Python的函数时可以嵌套的,也就是说可以将一个函数放在另一个函数的里面,比如:

def foo():
    def bar():
        print "Hello world!"
    bar()

  嵌套一般来说并不是那么有用,但是它有一个突出的应用,例如需要用一个函数”创建“另一个函数。例如:

def multiplier(factory):
    def multiplyByFactory(number):
        return number * factory
    return multiplyByFactory

  一个函数位于另一个函数里面,外层函数返回里面的函数。也就是说函数本身被返回了,但是并没有被调用。重要的是返回的函数还可以访问它的定义所在的作用域。换句话说,它“带着”它的环境(和相关的局部变量)返回了。
  每次调用外层函数,它内部的函数都被重新绑定,factory变量每次都有一个新的值。由于Python的嵌套作用域,来自(multiplier的)外部作用域的这个变量,稍后会被内层函数访问。例如:

double = multiplier(2)
print double(5)

triple = multiplier(3)
print triple(3)

print multiplier(5)(4)

程序输出:

10
9
20

  类似multiplyByFactory函数存储子封闭作用域的行为叫做闭包。

外部作用域的变量一般来说是不能进行重新绑定的。但是Python3.0中,nonlocal关键字被引用。它和global关键字的使用方式类似,可以让用户对外部作用域(并非是全局作用域)的变量进行赋值。

6. 递归调用

  所谓递归就是函数可以在函数体内调用自己,本章的内容其实就是这一句话就讲完了。但是如果加以妥善利用又能发挥出极大的用处。它更多的属于算法的范畴,因为任何编程语言都可以有递归调用,由于每个人对递归算法的掌握程度不同,所以不太好讲。但是我们仍然可以利用Python这门语言去初步认识一下递归调用,这里不做算法的深入讲解,如果感兴趣可以单独深入学习一下递归算法。在此仅举一个例子体验一下递归调用:
  假设想要计算n的阶乘,n的阶乘定义为: n×(n1)×(n2)×...×1

def factorial(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

print factorial(4)

程序输出:

24

  上面的例子中在函数体内调用自身,但是我们发现,递归总是要有一个明确的结束的标志的,比如当n==1时return数字1,结束递归,否则会造成无限循环。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值