Python 函数

参考书目Python for Data Analysis, 2nd Edition
章节第 3 章 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。

命名空间和作用域

作用域简单理解就是Python程序可以发挥作用的一块区域,通常有两种:全局作用域和局部作用域。描述变量作用域的名称就是命名空间(namespace)。
命名空间提供了一种避免变量名称冲突的方法。各个命名空间是相互独立的,在同一个命名空间内变量名称不能相同,但是在不同的命名空间内,变量名称可以相同。

变量的查找顺序

在一个Python程序中,直接访问一个变量,会从内到外访问所有的作用域直到找到该变量。如果找不到,会放弃查找并抛出NameError异常。

In [5]: a = 1  # a, b是定义在全局作用域的变量,为全局变量
In [6]: b = 2

In [7]: def f():
   ...:     a = 2  # a是定义在局部作用域的变量。与全局变量a名称相同,但是是不同的对象,这是因为在不同的命名空间内变量名称可以相同。
   ...:     print('a='a, 'b='b)

In [8]: f()  # 调用f()函数,最后要打印a,b,会先在f()所在的局部作用域查找a,b,找到a之后打印a,没有找到b接着去全局作用域查找b,找到b之后打印b。
a=2 b=2


In [14]: def f1():
    ...:     print(c)

In [15]: f1() # 调用f1()函数打印c,会先在f1()所在的局部作用域查找,找不到去全局作用域查找,仍为找到会抛出NameError异常。
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Input In [15], in <cell line: 1>()
----> 1 f1()

Input In [14], in f1()
      1 def f1():
----> 2     print(c)

NameError: name 'c' is not defined
变量的生命周期

变量的生命周期取决于变量的作用域。局部变量是在函数被调用时创建,在函数执行完毕之后被销毁。全局变量则在整个模块执行完毕后被销毁。

def func():
	a = []  # 变量a为局部变量,在调用func()之后,首先创建空列表a,在添加5个元素,最后a会在函数退出时被销毁。
	for i in range(5):
		a.append(i)
a = []  # a为全局变量,全局变量既可以在全局命名空间使用,又可以在局部命名空间使用
def func():
	for i in range(5):
		a.append(i)

局部变量和全局变量

定义在函数内部的变量拥有一个局部作用域,称为局部变量;定义在函数外部的变量拥有一个全局作用域,称为全局变量。局部变量只能在其被声明的函数中使用,而全局变量可以在整个程序范围内被访问。

In [1]: total = 0  # total定义在全局作用域,是一个全局变量

In [2]: def sum(num1, num2):
   ...:     total = num1 + num2  # total定义在局部作用域,是一个局部变量
   ...:     print('函数内局部变量:', total)
   ...:     return total

In [3]: sum(1, 3)  # 函数内优先使用局部变量
函数内局部变量: 4
Out[3]: 4

In [4]: total  # 函数外部只能使用全局变量,无法使用局部变量
Out[4]: 0
通过参数传递全局变量

全局变量既可以在全局作用域中使用,也可以在局部作用域中使用,我们可用通过函数的参数传递全局变量。

In [16]: a = 1  # 全局变量

In [17]: def update_a(a):  # 将全局变量a通过参数传递到局部作用域
    ...:     a = a + 1  # 等号右边的a是全局变量a,左边的a是局部变量a
    ...:     print(a)

In [19]: update_a(a) # 调用update_a()函数打印a,会首先在局部作用域查找a,找到局部变量a并打印
2

In [20]: a  # 全局作用域中,只有全局变量a才可以使用
Out[20]: 1
Global关键字修改全局变量

通过向函数中传递参数,只能在局部作用域中访问全局变量,而无法修改全局变量。如果想要修改全局变量,则需要Global关键字。

In [22]: a = 1  # 全局变量a
In [23]: def update_a():
    ...:     global a  # 通过Global关键字说明局部作用域中的a是全局变量a,函数中对a进行的操作都是针对全局变量a的,而不是新声明一个局部变量a。
    ...:     print('修改前a=', a)
    ...:     a = 123
    ...:     print('修改后a=', a)

In [25]: update_a()  
修改前a= 1
修改后a= 123

In [26]: a  # 全局变量a被修改了
Out[26]: 123

返回多个值

在Python中,函数可以返回多个值:

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

a, b, c = f()

在上面的例子中,其实函数只返回了一个元组对象,最后将该元组拆包到了各个结果变量中。上面的列子还可以这样写,此时return_value是一个包含3个元素的元组。

return_value = f()

此外,还有一种多值返回方式——返回字典:

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

函数也是对象

在Python中函数也是对象,可以对函数进行一系列的操作。假设有以下的一个字符串数组,希望对其进行一些数据清洗工作并执行一堆转换:

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
sub(pattern, repl, string, count=0, flags=0) 
# pattern:表示要处理的字符串,格式为正则中的模式字符串
# repl:表示要替换的字符串(即匹配到pattern之后替换成repl)
# string:要处理的字符串
# count:可选参数,允许的最大替换次数。默认为0,即所有的匹配都会被替换
# flgs:可选参数

另外还有一种方法:对指定字符串执行的所有操作做成一个列表

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

这种多函数模式可以使我们更容易修改对字符串进行的操作,从而使clean_strings更具有复用性。
还可以将函数用作其他函数的参数,比如内置的map函数,用于在一组数据上应用某个函数:

for x in map(remove_punctuation, states):
	print(x)

匿名(lambda)函数

Python支持一种被称为lambda的函数,该函数由单条语句组成,语句的结果就是函数的返回值。通过lambda关键字来定义。

def short_function(x):
	return x * 2
# 等价于
equiv_anon = lambda x: x * 2

看下面一个简单的例子:

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)
Out[4]: [8, 0, 2, 10, 12]

另一个例子,根据各字符串不重复的字母的数量对其进行排序

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

In [6]: strings.sort(key = lambda x: len(set(list(x))))
In [7]: strings
Out[7]: ['aaaa', 'foo', 'abab', 'bar', 'card']

柯里化(currying):部分参数应用

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

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

通过这个函数派生出一个只有一个参数的函数——add_five:

add_five = lambda y: add_numbers(5, y)

add_numbers的第2个参数被称为“curried”。这里没什么特别花哨的东西,只是通过调用现有的函数生成了一个新函数而已。内置的functools模块的partial函数可以将这个过程简化:

from functools import partial
add_five = partial(add_numbers, 5)
# 5是位置参数,会赋值给add_numbers的第一个参数。如果要赋值给y参数, 可以传递关键字参数y=5

迭代器和生成器

前面介绍的列表、元组、字典和集合等序列都支持for循环遍历存储的元素,这些序列都是可迭代的,因此它们又有一个别称,迭代器。比如说,对字典进行迭代可以得到所有的key:

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

In [2]: for key in some_dict:
   ...:     print(key)

a
b
c

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

In [3]: dict_iterator = iter(some_dict)

In [4]: dict_iterator
Out[4]: <dict_keyiterator at 0x20b62021130>

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

In [6]: tuple(dict_iterator)
Out[6]: ('a', 'b', 'c')

生成器是一类特殊的迭代器,其特殊之处主要表现在:以list的迭代器为例,使用该迭代器迭代数据时,必须将所有的数据都存放在迭代器内;而生成器则不同,可以在迭代的同时生成元素,也就是说,当使用某种算法来得到多个数据时,生成器并不会一次性的生成全部数据,而是在需要的时候才生成。要创建一个生成器,只需要将函数中的return替换为yield即可:

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

和普通函数不同的是,squares()函数的返回值用的是yield关键字而非return关键字,因此此类函数又称为生成器函数,调用生成器函数就会生成一个生成器对象

In [7]: gen = squares()

In [8]: gen
Out[8]: <generator object squares at 0x7fbbd5ab4570>

和return关键字相比,yield关键字除了返回相应的值,还有一个重要的功能,就是当程序执行完yield语句时会暂时停止。不仅如此,即使调用生成器函数,也只会生成一个生成器对象,而不会执行生成器函数中的代码。
要想执行生成器函数中的代码,可以使用for循环遍历生成器:

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

除了用for循环使生成器函数中的代码执行,还可以直接使用list()函数和tuple()函数将生成器能生成的所有值存放于列表或者元组中

In [8]: list(gen)
Out[8]: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

生成器表达式

另一种更简洁的构造生成器的方法是生成器表达式,这是一种类似于列表、字典和集合推导式的生成器。创建方式为将列表推导式的方括号改为圆括号:

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

In [8]: gen
Out[8]: <generator object <genexpr> at 0x0000020B61F09820>

这个表达式与下面这个生成器函数是完全等价的:

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

生成器表达式还可以代替列表推导式,作为函数的参数:

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

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

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

itertools模块

itertools模块有许多有用的生成器。例如,groupby可以接受一个序列和一个函数,并且根据函数的返回值对序列中的元素进行分组,下面是一个列子:

In [12]: import itertools

In [13]: first_letter = lambda x: x[0]
In [14]: names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']

In [16]: for letter, names in itertools.groupby(names, first_letter):
    ...:     print(letter, list(names))  # names是一个生成器
    ...:
A ['Alan', 'Adam']
W ['Wes', 'Will']
A ['Albert']
S ['Steven']

还有一些其他常用的itertools函数,见下表:

函数说明
combinations(iterable,k)生成一个由iterable中元素所有可能的k元元组组成的序列,不考虑顺序
permutations(iterable,k)生成一个由iterable中元素所有可能的k元元组组成的序列,考虑顺序
In [17]: from itertools import combinations

In [20]: num = (1, 2, 3, 4)
In [21]: for tup in combinations(num, 3):
    ...:     print(tup)

(1, 2, 3)
(1, 2, 4)
(1, 3, 4)
(2, 3, 4)
In [23]: from itertools import permutations

In [26]: num = [1, 2, 3]
In [27]: for tup in permutations(num, 2):
    ...:     print(tup)

(1, 2)
(1, 3)
(2, 1)
(2, 3)
(3, 1)
(3, 2)

错误和异常处理

Python的float函数可以将字符串转换为浮点数,但是输入的不是数值型的字符串时,抛出ValueError异常:

In [28]: float('3.1415')
Out[28]: 3.1415

In [29]: float('something')
---------------------------------------------------------------------------
ValueError                              Traceback (most recent call last)
Input In [29], in <cell line: 1>()
----> 1 float('something')

ValueError: could not convert string to float: 'something'

我们可以使用try-except处理该异常,使其返回输入的值:

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

当try语句抛出异常时,except语句就会执行:

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

当然,float抛出的异常可能不止ValueError一种:

In [33]: float((1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [33], in <cell line: 1>()
----> 1 float((1, 2))

TypeError: float() argument must be a string or a number, not 'tuple'

如果只想处理ValueError异常,可以使用except拦截具体的异常:

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

此时,TypeError类异常就不会被处理

In [36]: attempt_float((1, 2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [36], in <cell line: 1>()
----> 1 attempt_float((1, 2))

Input In [35], in attempt_float(x)
      1 def attempt_float(x):
      2     try:
----> 3         return float(x)
      4     except ValueError:
      5         return x

TypeError: float() argument must be a string or a number, not 'tuple'

except语句之后可以使用元组,拦截多种异常:

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

有一些情况下,我们想无论try部分的代码是否成功,都执行一段代码。可以使用finally语句:

f  = open(path, 'w')

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

这时,无论try语句是否出现异常,文件都会被关闭。
另外,加入else语句可以使只有try语句执行成功时,else语句才会被执行:

f = open(path, 'w')
try:
	write_to_file(f)
except:
	print('Failed')
else:
	print('Succeeded')
finally:
	f.close()

即:except语句在try语句执行不成功时执行,else语句在try语句执行成功时执行,finally语句无论try语句是否执行成功都会被执行。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值