Python 进阶 —— 第一节
# *args
和 **kwargs
我观察到,大部分新的Python程序员都需要花上大量时间理解清楚 *args
和**kwargs
这两个魔法变量。那么它们到底是什么?
首先让我告诉你, 其实并不是必须写成*args
和**kwargs
。 只有变量前面的 *
(星号)才是必须的. 你也可以写成*var
和**vars
。而写成*args
和**kwargs
只是一个通俗的命名约定。 那就让我们先看一下*args
吧。
# *args 的用法
*args
和 **kwargs
主要用于函数定义。 你可以将不定数量的参数传递给一个函数。
这里的不定的意思是:预先并不知道, 函数使用者会传递多少个参数给你, 所以在这个场景下使用这两个关键字。 *args
是用来发送一个非键值对的可变数量的参数列表给一个函数.
这里有个例子帮你理解这个概念:
def test_var_args(f_arg, *argv):
print("first normal arg:", f_arg)
for arg in argv:
print("another arg through *argv:", arg)
test_var_args('yasoob', 'python', 'eggs', 'test')
2
3
4
5
6
这会产生如下输出:
first normal arg: yasoob
another arg through *argv: python
another arg through *argv: eggs
another arg through *argv: test
2
3
4
我希望这解决了你所有的困惑. 那接下来让我们谈谈 **kwargs
。
# **kwargs 的用法
**kwargs
允许你将不定长度的 键值对 , 作为参数传递给一个函数。 如果你想要在一个函数里处理 带名字的参数 , 你应该使用**kwargs
。
这里有个让你上手的例子:
def greet_me(**kwargs):
for key, value in kwargs.items():
print("{0} == {1}".format(key, value))
>>> greet_me(name="yasoob")
name == yasoob
2
3
4
5
6
7
现在你可以看出我们怎样在一个函数里, 处理了一个键值对参数了。
这就是**kwargs
的基础, 而且你可以看出它有多么管用。 接下来让我们谈谈,你怎样使用*args
和 **kwargs
来调用一个参数为列表或者字典的函数。
# 使用 *args
和 **kwargs
来调用函数
那现在我们将看到怎样使用*args
和**kwargs
来调用一个函数。 假设,你有这样一个小函数:
def test_args_kwargs(arg1, arg2, arg3):
print("arg1:", arg1)
print("arg2:", arg2)
print("arg3:", arg3)
2
3
4
你可以使用*args
或**kwargs
来给这个小函数传递参数。 下面是怎样做:
# 首先使用 *args
>>> args = ("two", 3, 5)
>>> test_args_kwargs(*args)
arg1: two
arg2: 3
arg3: 5
# 现在使用 **kwargs:
>>> kwargs = {"arg3": 3, "arg2": "two", "arg1": 5}
>>> test_args_kwargs(**kwargs)
arg1: 5
arg2: two
arg3: 3
2
3
4
5
6
7
8
9
10
11
12
13
# 标准参数与*args、**kwargs
在使用时的顺序
那么如果你想在函数里同时使用所有这三种参数, 顺序是这样的:
# 什么时候使用它们?
这还真的要看你的需求而定。
最常见的用例是在写函数装饰器的时候(会在另一章里讨论)。
此外它也可以用来做猴子补丁(monkey patching)。猴子补丁的意思是在程序运行时(runtime)修改某些代码。 打个比方,你有一个类,里面有个叫get_info
的函数会调用一个API并返回相应的数据。如果我们想测试它,可以把API调用替换成一些测试数据。例如:
import someclass
def get_info(self, *args):
return "Test data"
someclass.get_info = get_info
2
3
4
5
6
我敢肯定你也可以想象到一些其他的用例。
# 调试(Debugging)
利用好调试,能大大提高你捕捉代码Bug的。大部分新人忽略了Python debugger(pdb
)的重要性。 在这个章节我只会告诉你一些重要的命令,你可以从官方文档中学习到更多。
译者注,参考:https://docs.python.org/2/library/pdb.html Or https://docs.python.org/3/library/pdb.html
# 从命令行运行
你可以在命令行使用Python debugger运行一个脚本, 举个例子:
$ python -m pdb my_script.py
这会触发debugger在脚本第一行指令处停止执行。这在脚本很短时会很有帮助。你可以通过(Pdb)模式接着查看变量信息,并且逐行调试。
# 从脚本内部运行
同时,你也可以在脚本内部设置断点,这样就可以在某些特定点查看变量信息和各种执行时信息了。这里将使用pdb.set_trace()
方法来实现。举个例子:
import pdb
def make_bread():
pdb.set_trace()
return "I don't have time"
print(make_bread())
2
3
4
5
6
7
试下保存上面的脚本后运行之。你会在运行时马上进入debugger模式。现在是时候了解下debugger模式下的一些命令了。
# 命令列表:
c
: 继续执行w
: 显示当前正在执行的代码行的上下文信息a
: 打印当前函数的参数列表s
: 执行当前代码行,并停在第一个能停的地方(相当于单步进入)n
: 继续执行到当前函数的下一行,或者当前行直接返回(单步跳过)
单步跳过(n
ext)和单步进入(s
tep)的区别在于, 单步进入会进入当前行调用的函数内部并停在里面, 而单步跳过会(几乎)全速执行完当前行调用的函数,并停在当前函数的下一行。
pdb真的是一个很方便的功能,上面仅列举少量用法,更多的命令强烈推荐你去看官方文档。
# 生成器(Generators)
首先我们要理解迭代器(iterators)。根据维基百科,迭代器是一个让程序员可以遍历一个容器(特别是列表)的对象。然而,一个迭代器在遍历并读取一个容器的数据元素时,并不会执行一个迭代。你可能有点晕了,那我们来个慢动作。换句话说这里有三个部分:
- 可迭代对象(Iterable)
- 迭代器(Iterator)
- 迭代(Iteration)
上面这些部分互相联系。我们会先各个击破来讨论他们,然后再讨论生成器(generators).
# 可迭代对象(Iterable)
Python中任意的对象,只要它定义了可以返回一个迭代器的__iter__
方法,或者定义了可以支持下标索引的__getitem__
方法(这些双下划线方法会在其他章节中全面解释),那么它就是一个可迭代对象。简单说,可迭代对象就是能提供迭代器的任意对象。那迭代器又是什么呢?
# 迭代器(Iterator)
任意对象,只要定义了next
(Python2) 或者__next__
方法,它就是一个迭代器。就这么简单。现在我们来理解迭代(iteration)。
# 迭代(Iteration)
用简单的话讲,它就是从某个地方(比如一个列表)取出一个元素的过程。当我们使用一个循环来遍历某个东西时,这个过程本身就叫迭代。现在既然我们有了这些术语的基本理解,那我们开始理解生成器吧。
# 生成器(Generators)
生成器也是一种迭代器,但是你只能对其迭代一次。这是因为它们并没有把所有的值存在内存中,而是在运行时生成值。你通过遍历来使用它们,要么用一个“for”循环,要么将它们传递给任意可以进行迭代的函数和结构。大多数时候生成器是以函数来实现的。然而,它们并不返回一个值,而是yield
(暂且译作“生出”)一个值。这里有个生成器函数的简单例子:
def generator_function():
for i in range(10):
yield i
for item in generator_function():
print(item)
# Output: 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这个案例并不是非常实用。生成器最佳应用场景是:你不想同一时间将所有计算出来的大量结果集分配到内存当中,特别是结果集里还包含循环。
译者注:这样做会消耗大量资源
许多Python 2里的标准库函数都会返回列表,而Python 3都修改成了返回生成器,因为生成器占用更少的资源。
下面是一个计算斐波那契数列的生成器:
# generator version
def fibon(n):
a = b = 1
for i in range(n):
yield a
a, b = b, a + b
2
3
4
5
6
函数使用方法如下:
for x in fibon(1000000):
print(x)
2
用这种方式,我们可以不用担心它会使用大量资源。然而,之前如果我们这样来实现的话:
def fibon(n):
a = b = 1
result = []
for i in range(n):
result.append(a)
a, b = b, a + b
return result
2
3
4
5
6
7
这也许会在计算很大的输入参数时,用尽所有的资源。我们已经讨论过生成器使用一次迭代,但我们并没有测试过。在测试前你需要再知道一个Python内置函数:next()
。它允许我们获取一个序列的下一个元素。那我们来验证下我们的理解:
def generator_function():
for i in range(3):
yield i
gen = generator_function()
print(next(gen))
# Output: 0
print(next(gen))
# Output: 1
print(next(gen))
# Output: 2
print(next(gen))
# Output: Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# StopIteration
2
3
4
5
6
7
8
9
10
11
12
13
14
15
我们可以看到,在yield
掉所有的值后,next()
触发了一个StopIteration
的异常。基本上这个异常告诉我们,所有的值都已经被yield
完了。你也许会奇怪,为什么我们在使用for
循环时没有这个异常呢?啊哈,答案很简单。for
循环会自动捕捉到这个异常并停止调用next()
。你知不知道Python中一些内置数据类型也支持迭代哦?我们这就去看看:
my_string = "Yasoob"
next(my_string)
# Output: Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: str object is not an iterator
2
3
4
5
好吧,这不是我们预期的。这个异常说那个str
对象不是一个迭代器。对,就是这样!它是一个可迭代对象,而不是一个迭代器。这意味着它支持迭代,但我们不能直接对其进行迭代操作。那我们怎样才能对它实施迭代呢?是时候学习下另一个内置函数,iter
。它将根据一个可迭代对象返回一个迭代器对象。这里是我们如何使用它:
my_string = "Yasoob"
my_iter = iter(my_string)
next(my_iter)
# Output: 'Y'
2
3
4
现在好多啦。我肯定你已经爱上了学习生成器。一定要记住,想要完全掌握这个概念,你只有使用它。确保你按照这个模式,并在生成器对你有意义的任何时候都使用它。你绝对不会失望的!
# Map,Filter 和 Reduce
Map,Filter 和 Reduce 三个函数能为函数式编程提供便利。我们会通过实例一个一个讨论并理解它们。
# Map
Map
会将一个函数映射到一个输入列表的所有元素上。这是它的规范:
规范
map(function_to_apply, list_of_inputs)
大多数时候,我们要把列表中所有元素一个个地传递给一个函数,并收集输出。比方说:
items = [1, 2, 3, 4, 5]
squared = []
for i in items:
squared.append(i**2)
2
3
4
Map
可以让我们用一种简单而漂亮得多的方式来实现。就是这样:
items = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, items))
2
大多数时候,我们使用匿名函数(lambdas)来配合map
, 所以我在上面也是这么做的。 不仅用于一列表的输入, 我们甚至可以用于一列表的函数!
def multiply(x):
return (x*x)
def add(x):
return (x+x)
funcs = [multiply, add]
for i in range(5):
value = map(lambda x: x(i), funcs)
print(list(value))
# 译者注:上面print时,加了list转换,是为了python2/3的兼容性
# 在python2中map直接返回列表,但在python3中返回迭代器
# 因此为了兼容python3, 需要list转换一下
# Output:
# [0, 0]
# [1, 2]
# [4, 4]
# [9, 6]
# [16, 8]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Filter
顾名思义,filter
过滤列表中的元素,并且返回一个由所有符合要求的元素所构成的列表,符合要求
即函数映射到该元素时返回值为True. 这里是一个简短的例子:
number_list = range(-5, 5)
less_than_zero = filter(lambda x: x < 0, number_list)
print(list(less_than_zero))
# 译者注:上面print时,加了list转换,是为了python2/3的兼容性
# 在python2中filter直接返回列表,但在python3中返回迭代器
# 因此为了兼容python3, 需要list转换一下
# Output: [-5, -4, -3, -2, -1]
2
3
4
5
6
7
8
这个filter
类似于一个for
循环,但它是一个内置函数,并且更快。
注意:如果map
和filter
对你来说看起来并不优雅的话,那么你可以看看另外一章:列表/字典/元组推导式。
译者注:大部分情况下推导式的可读性更好
# Reduce
当需要对一个列表进行一些计算并返回结果时,Reduce
是个非常有用的函数。举个例子,当你需要计算一个整数列表的乘积时。
通常在 python 中你可能会使用基本的 for 循环来完成这个任务。
现在我们来试试 reduce:
from functools import reduce
product = reduce( (lambda x, y: x * y), [1, 2, 3, 4] )
# Output: 24
2
3
4
# set
(集合)数据结构
set
(集合)是一个非常有用的数据结构。它与列表(list
)的行为类似,区别在于set
不能包含重复的值。
这在很多情况下非常有用。例如你可能想检查列表中是否包含重复的元素,你有两个选择,第一个需要使用for
循环,就像这样:
some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']
duplicates = []
for value in some_list:
if some_list.count(value) > 1:
if value not in duplicates:
duplicates.append(value)
print(duplicates)
### 输出: ['b', 'n']
2
3
4
5
6
7
8
9
10
但还有一种更简单更优雅的解决方案,那就是使用集合(sets)
,你直接这样做:
some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']
duplicates = set([x for x in some_list if some_list.count(x) > 1])
print(duplicates)
### 输出: set(['b', 'n'])
2
3
4
集合还有一些其它方法,下面我们介绍其中一部分。
# 交集
你可以对比两个集合的交集(两个集合中都有的数据),如下:
valid = set(['yellow', 'red', 'blue', 'green', 'black'])
input_set = set(['red', 'brown'])
print(input_set.intersection(valid))
### 输出: set(['red'])
2
3
4
# 差集
你可以用差集(difference)找出无效的数据,相当于用一个集合减去另一个集合的数据,例如:
valid = set(['yellow', 'red', 'blue', 'green', 'black'])
input_set = set(['red', 'brown'])
print(input_set.difference(valid))
### 输出: set(['brown'])
2
3
4
你也可以用符号来创建集合,如:
a_set = {'red', 'blue', 'green'}
print(type(a_set))
### 输出: <type 'set'>
2
3
集合还有一些其它方法,我会建议访问官方文档并做个快速阅读。
# 三元运算符
三元运算符通常在Python里被称为条件表达式,这些表达式基于真(true)/假(not)的条件判断,在Python 2.4以上才有了三元操作。
下面是一个伪代码和例子:
伪代码:
#如果条件为真,返回真 否则返回假
condition_is_true if condition else condition_is_false
2
例子:
is_fat = True
state = "fat" if is_fat else "not fat"
2
它允许用简单的一行快速判断,而不是使用复杂的多行if
语句。 这在大多数时候非常有用,而且可以使代码简单可维护。
另一个晦涩一点的用法比较少见,它使用了元组,请继续看:
伪代码:
#(返回假,返回真)[真或假]
(if_test_is_false, if_test_is_true)[test]
2
例子:
fat = True
fitness = ("skinny", "fat")[fat]
print("Ali is ", fitness)
#输出: Ali is fat
2
3
4
这之所以能正常工作,是因为在Python中,True等于1,而False等于0,这就相当于在元组中使用0和1来选取数据。
上面的例子没有被广泛使用,而且Python玩家一般不喜欢那样,因为没有Python味儿(Pythonic)。这样的用法很容易把真正的数据与true/false弄混。
另外一个不使用元组条件表达式的缘故是因为在元组中会把两个条件都执行,而 if-else
的条件表达式不会这样。
例如:
condition = True
print(2 if condition else 1/0)
#输出: 2
print((1/0, 2)[condition])
#输出ZeroDivisionError异常
2
3
4
5
6
这是因为在元组中是先建数据,然后用True(1)/False(0)来索引到数据。 而if-else
条件表达式遵循普通的if-else
逻辑树, 因此,如果逻辑中的条件异常,或者是重计算型(计算较久)的情况下,最好尽量避免使用元组条件表达式。