3.2 函数

函数是Python中最主要也是最重要的代码组织和复用手段。作为最重要的原则,如果你要重复使用相同或非常类似的代码,就需要写一个函数。通过给函数起一个名字,还可以提高代码的可读性。
函数使用def关键字声明,用return关键字返回值:

def my_function(x, y, z=1.5):
    if z > 1:
        return z * (x + y)
    else:
        return z / (x + y)

同时拥有多条return语句也是可以的。如果到达函数末尾时没有遇到任何一条return语句,则返回None。
函数可以有一些位置参数(positional)和一些关键字参数(keyword)。关键字参数通常用于指定默认值或可选参数。在上面的函数中,x和y是位置参数,而z则是关键字参数。也就是说,该函数可以用下面这两种方式进行调用:

my_function(5, 6, z=0.7)
my_function(3.14, 7, 3.5)
my_function(10, 20)

函数参数的主要限制在于:关键字参数必须位于位置参数(如果有的话)之后。你可以任何顺序指定关键字参数。也就是说,不用死记硬背函数参数的顺序,只要记得它们的名字即可。

笔记:也可以用关键字传递位置参数。前面的例子,也可以写为:

my_function(x=5, y=6, z=7)
my_function(y=6, x=5, z=7)

这种写法可以提高可读性。

命名空间、作用域,和局部函数
函数可以访问两种不同作用域中的变量:全局(global)和局部(local)。Python有一种更科学的用于描述变量作用域的名称,即命名空间(namespace)。任何在函数中赋值的变量默认都是被分配到局部命名空间(local namespace)中的。局部命名空间是在函数被调用时创建的,函数参数会立即填入该命名空间。在函数执行完毕之后,局部命名空间就会被销毁(会有一些例外的情况,具体请参见后面介绍闭包的那一节)。先看下面这个函数:

def func():
    a = []
    for i in range(5):
        a.append(i)

调用func()之后,首先会创建空列表a,然后添加5个元素,最后a会在该函数退出的时候被销毁。假如我们像下面这样定义a:

a = []
def func():
    for i in range(5):
        a.append(i)

虽然可以在函数中对全局变量进行赋值操作,但是那些变量必须用global关键字声明成全局的才行:

In [168]: a = None

In [169]: def bind_a_variable():
   .....:     global a
   .....:     a = []
   .....: bind_a_variable()
   .....:

In [170]: print(a)
[]

注意:我们一般不建议大家频繁使用global关键字。因为全局变量一般是用于存放系统的某些状态的。如果你发现自己用了很多,那可能需要使用面向对象编程了(类)。

返回多个值
下面是一个简单的例子:

def f():
    a = 5
    b = 6
    c = 7
    return a, b, c

a, b, c = f()

在数据分析和其他科学计算应用中,你会发现自己常常这么干。该函数其实只返回了一个对象,也就是一个元祖,最后该元祖会被拆包到各个结果变量中。在上面的例子中,我们还可以这样写:

return_value = f()

这里的return_value将会是一个含有3个返回值的三元元祖。此外,还有一种非常具有吸引力的多值返回方式——返回字典:

def f():
    a = 5
    b = 6
    c = 7
    return {'a' : a, 'b' : b, 'c' : c}

取决于工作内容,第二种方法可能很有用。
函数也是对象
由于Python函数都是对象,因此,在其他语言中较难表达的一些设计思想在Python中就要简单很多了。假设我们有下面这样一个字符串数组,希望对其进行一些数据清洗工作并执行一堆转换:

In [171]: states = ['   Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
   .....:           'south   carolina##', 'West virginia?']

为得到一组能用于分析的格式统一的字符串,需要做很多事情:去除空白符、删除各种标点符号、正确的大小写格式等等。做法之一是使用内建的字符串方法和正则表达式re模块:

import re

def clean_strings(strings):
    result = []
    for value in strings:
        value = value.strip()
        value = re.sub('[!#?]', '', value)
        value = value.title()
        result.append(value)
    return result

备注:
strip()方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列。注意:该方法只能删除开头或是结尾的字符,不能删除中间部分的字符。

re.sub(pattern, repl, string, count=0, flags=0)
pattern:表示正则表达式中的模式字符串;
repl:被替换的字符串(既可以是字符串,也可以是函数);
string:要被处理的,要被替换的字符串;
count:匹配的次数, 默认是全部替换
flags:具体用处不详

title()方法返回"标题化"的字符串,就是说所有单词都是以大写开始,其余字母均为小写。

结果如下所示:

In [173]: clean_strings(states)
Out[173]: 
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

其实还有另外一种不错的方法:将需要在一组给定字符串上执行的所有运算做成一个列表:

def remove_punctuation(value):
    return re.sub('[!#?]', '', value)

clean_ops = [str.strip, remove_punctuation, str.title]

def clean_strings(strings, ops):
    result = []
    for value in strings:
        for function in ops:
            value = function(value)
        result.append(value)
    return result

然后就可以使用自定义函数这样处理了:

In [175]: clean_strings(states, clean_ops)
Out[175]: 
['Alabama',
 'Georgia',
 'Georgia',
 'Georgia',
 'Florida',
 'South   Carolina',
 'West Virginia']

这种多函数模式使你能在很高的层次上轻松修改字符串的转换方式。此时的clean_strings也更具有复用性~
还可以将函数用作其他函数的参数,比如内置的map函数,它用于在一组数据上应用一个函数:

In [176]: for x in map(remove_punctuation, states):
   .....:     print(x)
Alabama 
Georgia
Georgia
georgia
FlOrIda
south   carolina
West virginia

匿名(lambda)函数
Python支持一种匿名函数。它仅由单条语句组成,该语句的结果就是返回值。它是通过lambda关键字定义的,这个关键字没有别的含义,仅仅表示“我们正在声明的是一个匿名函数”。

def short_function(x):
    return x * 2

equiv_anon = lambda x: x * 2

我们在数据分析中经常使用到lambda函数,因为你会发现很多数据转换函数都是以函数作为参数的。直接传入lambda函数比编写完整函数声明要少输入很多字,甚至比将lambda函数赋值给一个变量还要少输入很多字。看看下面这个例子吧:

def apply_to_list(some_list, f):
    return [f(x) for x in some_list]

ints = [4, 0, 1, 5, 6]
apply_to_list(ints, lambda x: x * 2)

虽然你也可以直接编写[x *2 for x in ints],但是这里我们可以非常轻松地传入一个自定义运算给apply_to_list函数。
再来看另外一个例子,假设有一组字符串,你想要根据各个字符串包含的不同字母的数量对其进行排序:

In [177]: strings = ['foo', 'card', 'bar', 'aaaa', 'abab']

这里,我们可以传入一个lambda函数到列表的sort方法:

In [178]: strings.sort(key=lambda x: len(set(list(x))))

In [179]: strings
Out[179]: ['aaaa', 'foo', 'abab', 'bar', 'card']

笔记:lambda函数之所以被称为匿名函数,原因之一是这种函数对象本身没有提供名称name属性。

柯里化:部分参数应用
柯里化(currying)是一个有趣的计算机科学术语,它指的是通过“部分参数应用”(partial argument application)从现有函数派生出新函数的技术。例如,假设我们有一个执行两数相加的简单函数:

def add_numbers(x, y):
    return x + y

通过这个函数,我们可以派生出一个新的只有一个参数的函数——add_five,它用于对其参数加5:

add_five = lambda y: add_numbers(5, y)

add_numbers的第二个参数称为“柯里化的”(curried)。这里没有什么特别花哨的东西,因为我们其实就只是定义了一个可调用现有函数的新函数而已,内置的functools模块可以用partial函数将此过程简化:

from functools import partial
add_five = partial(add_numbers, 5)

生成器
能以一种一致的方式对序列进行迭代(比如列表中的对象或文件中的行)是Python的一个重要特点。这是通过一种叫做迭代器协议(iterator protocol,它是一种使对象可迭代的通用方式)的方式实现的,一个原生的使对象可迭代的方法。比如说,对字典进行迭代可以得到其所有的键:

In [180]: some_dict = {'a': 1, 'b': 2, 'c': 3}

In [181]: for key in some_dict:
   .....:     print(key)
a
b
c

当你编写for key in some_dict时,Python解释器首先会尝试从some_dict创建一个迭代器:

In [182]: dict_iterator = iter(some_dict)

In [183]: dict_iterator
Out[183]: 

迭代器是一种特殊对象,它可以在诸如for循环之类的上下文中向Python解释器输送对象。大部分能接受列表之类的对象的方法也都可以接受任何可迭代对象。比如min、max、sum等内置方法以及list、tuple等类型构造器:

In [184]: list(dict_iterator)
Out[184]: ['a', 'b', 'c']

生成器(generator)是构造新的可迭代对象的一种简单方式。一般的函数执行之后只会返回单个值,而生成器则是以延迟的方式返回一个值序列,即每返回一个值之后暂停,直到下一个值被请求时再继续。要创建一个生成器,只需将函数中的return替换成yelid即可:

def squares(n=10):
    print('Generating squares from 1 to {0}'.format(n ** 2))
    for i in range(1, n + 1):
        yield i ** 2

调用该生成器时,没有任何代码会被立即执行:

In [186]: gen = squares()

In [187]: gen
Out[187]: 

直到你从该生成器中请求元素时,它才会开始执行其代码:

In [188]: for x in gen:
   .....:     print(x, end=' ')
Generating squares from 1 to 100
1 4 9 16 25 36 49 64 81 100

生成器表达式
另外一种更加简洁的构造生成器的方法是使用生成器表达式(generator expression)。这是一种类似于列表、字典、集合推导式的生成器。其创建方式为,把列表推导式两端的方括号改为圆括号:

In [189]: gen = (x ** 2 for x in range(100))

In [190]: gen
Out[190]:  at 0x7fbbd5ab29e8>

它跟下面这个冗长的生成器是完全等价的:

def _make_gen():
    for x in range(100):
        yield x ** 2
gen = _make_gen()

生成器表达式也可以取代列表推导式,作为函数参数:

In [191]: sum(x ** 2 for x in range(100))
Out[191]: 328350

In [192]: dict((i, i **2) for i in range(5))
Out[192]: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

itertools模块
标准库itertools模块中有一组用于许多常见数据算法的生成器。例如,groupby可以接受任何序列和一个函数。塔根据函数的返回值对序列中的连续元素进行分组。下面是一个例子:

In [193]: import itertools

In [194]: first_letter = lambda x: x[0]

In [195]: names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']

In [196]: for letter, names in itertools.groupby(names, first_letter):
   .....:     print(letter, list(names)) # names is a generator
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']

表3-2中列出了经常使用的itertools函数,建议参阅Python官方文档,进一步学习。

表3-2 常用的itertools函数

错误和异常处理
优雅地处理Python的错误和异常是构建程序的重要部分。在数据分析中,许多函数只用于部分输入。例如,Python的float函数可以将字符串转换成浮点数,但是输入有误时,有ValueError错误:

In [197]: float('1.2345')
Out[197]: 1.2345

In [198]: float('something')
---------------------------------------------------------------------------
File "", line 1, in 
    float('something')
ValueError: could not convert string to float: 'something'

假如想优雅地处理float的错误,让它返回输入值。我们可以写一个函数,在try、except中调用float:

def attempt_float(x):
    try:
        return float(x)
    except:
        return x

当float(x)抛出异常时,才会执行except部分:

In [200]: attempt_float('1.2345')
Out[200]: 1.2345

In [201]: attempt_float('something')
Out[201]: 'something'

你可能注意到float抛出的异常不仅是ValueError:

In [202]: float((1, 2))
---------------------------------------------------------------------------
  File "", line 1, in 
    float((1, 2))
TypeError: float() argument must be a string or a number, not 'tuple'

你可能只想处理ValueError,TypeError错误(输入不是字符串或数值)可能是合理的bug。可以写一个异常类型:

def attempt_float(x):
    try:
        return float(x)
    except ValueError:
        return x

然后有:

In [204]: attempt_float((1, 2))
---------------------------------------------------------------------------
  File "", line 3, in attempt_float
    return float(x)
TypeError: float() argument must be a string or a number, not 'tuple'

可以用元祖包含多个异常:

def attempt_float(x):
    try:
        return float(x)
    except (TypeError, ValueError):
        return x

某些情况下,你可能不想抑制异常,你想无论try部分的代码是否成功,都执行一段代码。可以使用finally:

f = open(path, 'w')

try:
    write_to_file(f)
finally:
    f.close()

这里,文件处理f总会被关闭。相似的,你可以用else让只在try部分成功的情况下,才执行代码:

f = open(path, 'w')

try:
    write_to_file(f)
except:
    print('Failed')
else:
    print('Succeeded')
finally:
    f.close()

IPython的异常
如果是在%run一个脚本或一条语句时抛出异常,IPython默认会打印完整的调用栈(traceback),在栈的每个点都会有几行上下文:

In [10]: %run examples/ipython_bug.py
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/home/wesm/code/pydata-book/examples/ipython_bug.py in ()
     13     throws_an_exception()
     14
---> 15 calling_things()

/home/wesm/code/pydata-book/examples/ipython_bug.py in calling_things()
     11 def calling_things():
     12     works_fine()
---> 13     throws_an_exception()
     14
     15 calling_things()

/home/wesm/code/pydata-book/examples/ipython_bug.py in throws_an_exception()
      7     a = 5
      8     b = 6
----> 9     assert(a + b == 10)
     10
     11 def calling_things():

AssertionError:

自身就带有文本是相对于Python标准解释器的极大优点。你可以用魔术命令%xmode,从Plain(与Python标准解释器相同)到Verbose(带有函数的参数值)控制文本显示的数量。后面可以看到,发生错误之后,(用%debug或%pdb magics)可以进入stack进行事后调试。

你可能感兴趣的:(3.2 函数)