写点什么

Python 函数基础回顾

作者:timerring
  • 2023-05-04
    山东
  • 本文字数:6134 字

    阅读完需:约 20 分钟

文章和代码等已经归档至【Github 仓库:https://github.com/timerring/dive-into-AI 】或者公众号【AIShareLab】回复 python 数据分析 也可获取。

函数

函数使用def关键字声明,用return关键字返回值:


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


函数可以有一些位置参数(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 关键字。因为全局变量一般是用于存放系统的某些状态的。如果你发现自己用了很多,那可能就说明得要来点儿面向对象编程了(即使用类)。

返回多个值

Python 的一个功能是:函数可以返回多个值。


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
复制代码


结果如下所示:


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: # 中间经过clean_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 GeorgiaGeorgiageorgiaFlOrIdasouth   carolinaWest virginia
复制代码

匿名(lambda)函数

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


def short_function(x):    return x * 2# lambda后的x就是指自变量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 *2for 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]: stringsOut[179]: ['aaaa', 'foo', 'abab', 'bar', 'card']
复制代码


笔记:lambda 函数之所以会被称为匿名函数,与 def 声明的函数不同,原因之一就是这种函数对象本身是没有提供名称__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 partialadd_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)abc
复制代码


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


In [182]: dict_iterator = iter(some_dict)
In [183]: dict_iteratorOut[183]: <dict_keyiterator at 0x7fbbd5a9f908>
复制代码


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


In [184]: list(dict_iterator)Out[184]: ['a', 'b', 'c']
复制代码


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


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]: genOut[187]: <generator object squares at 0x7fbbd5ab4570>
复制代码


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


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

生成器表达式

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


In [189]: gen = (x ** 2 for x in range(100))
In [190]: genOut[190]: <generator object <genexpr> at 0x7fbbd5ab29e8>
复制代码


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


def _make_gen():    for x in range(100):        yield x ** 2gen = _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 generatorA ['Alan', 'Adam']W ['Wes', 'Will']A ['Albert']S ['Steven']
复制代码


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


错误和异常处理

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


In [197]: float('1.2345')Out[197]: 1.2345
In [198]: float('something')---------------------------------------------------------------------------ValueError Traceback (most recent call last)<ipython-input-198-439904410854> in <module>()----> 1 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))---------------------------------------------------------------------------TypeError                                 Traceback (most recent call last)<ipython-input-202-842079ebb635> in <module>()----> 1 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))---------------------------------------------------------------------------TypeError                                 Traceback (most recent call last)<ipython-input-204-9bdfd730cead> in <module>()----> 1 attempt_float((1, 2))<ipython-input-203-3e06b8379b6b> in attempt_float(x)      1 def attempt_float(x):      2     try:----> 3         return float(x)      4     except ValueError:      5         return xTypeError: 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 <module>()     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 进行事后调试。

发布于: 刚刚阅读数: 4
用户头像

timerring

关注

公众号【AIShareLab】 2022-07-14 加入

他日若遂凌云志

评论

发布
暂无评论
Python函数基础回顾_Python_timerring_InfoQ写作社区