一文了解Python部分高级特性

本部分主要介绍 Python 的部分高级特性,包括切片、迭代器、推导式、生成器、匿名函数、装饰器等。阅读本文预计需要 15 min。

一文了解Python部分高级特性

    • 1. 前言
    • 2. 切片
    • 3. 迭代、可迭代对象、迭代器
    • 4. 推导式
      • 4.1 列表推导式
      • 4.2 集合推导式
      • 4.3 字典推导式
    • 5. 生成器和生成器表达式
      • 5.1 生成器
      • 5.2 生成器表达式
    • 6. 匿名函数
    • 7. 装饰器
    • 8. 巨人的肩膀

1. 前言

Python 非常灵活强大,跟它具有一些特性有关,如匿名函数、列表推导式、迭代器、装饰器等。本文主要简单介绍:

  • 切片
  • 迭代、可迭代对象、迭代器
  • 推导式(列表推导式、集合推导式、字典推导式)
  • 生成器和生成器表达式
  • 匿名函数
  • 装饰器

2. 切片

切片(slice)在 Python 中非常强大,可以轻松对字符串、列表和元组进行切割,完成拷贝。注意切片是浅拷贝,关于浅拷贝和深拷贝留作以后讨论。

切片的语法是:obj[start: end: step]

  • obj 是支持切片的对象,如:列表、字符串、元组等。
  • start 是开始切的索引位置,索引是从 0 开始标记的。start 可以省略,默认值是 0.
  • end 是切片结束的位置,实际上切不到 obj[end]。end 也可以省略,默认值是对象的长度。
  • step 是切片的步长,也可以省略,默认值是 1。

对字符串、列表、元组进行切片。

>>> word = "Python"
>>> word[:]
'Python'
>>> word[1:3]
'yt'
>>> word[::2]
'Pto'
>>> ls = [1, 2, 3, 4, 5, 6]
>>> ls[::2]
[1, 3, 5]
>>> t = (1, 2, 3, 4, 5, 6)
>>> t[2:6]
(3, 4, 5, 6)

切片也支持负数,使用 obj[::-1] 可以轻松实现翻转,如把列表翻转:

>>> ls = [1, 2, 3, 4, 5, 6, 7]
>>> ls[::-1]
[7, 6, 5, 4, 3, 2, 1]

obj[end] 是取不到的。

>>> ls = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> ls[1:9]
[2, 3, 4, 5, 6, 7, 8, 9]

可以看到 ls[1:9], ls[1],即 2 可以取到,ls[9],即 10 是取不到的。

3. 迭代、可迭代对象、迭代器

迭代(iteration):迭代是一种操作,可以理解为遍历,如用 for 循环遍历列表或者元组。
可迭代对象(iterable object):可以用 for 循环迭代的对象。如列表、元组、字符串、字典、集合等。
到目前为止,可以看到大多数容器对象都可以使用 for 语句:

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

我们还可以用 collections 模块中的 Iterable 类型判断一个对象是否是可迭代对象。

>>> from collections import Iterable
>>> isinstance([1, 2], Iterable)
True
>>> isinstance((1, 2), Iterable)
True
>>> isinstance('abc', Iterable)
True

迭代器(iterator):是遵循迭代器协议的可迭代对象就称为迭代器。迭代器协议机制是:for 语句会在容器对象上调用 iter()。 该函数返回一个定义了 __next__() 方法的迭代器对象,此方法将逐一访问容器中的元素。 当元素用尽时,__next__() 将引发 StopIteration 异常来通知终止 for 循环。你可以使用 next() 内置函数来调用 __next__() 方法。
通俗理解就是:能被 next()函数调用并不断返回下一个值的对象成为迭代器。

迭代器的使用非常普遍并使得 Python 成为一个统一的整体。下面这个例子显示了迭代器的运作方式:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "", line 1, in <module>
    next(it)
StopIteration

我们可以使用 isinstance()方法判断一个对象是否是 Iterator 对象。

>>> from collections import Iterator
>>> ls = [1, 2, 3, 4]
>>> isinstance(ls, Iterator)
False
>>> isinstance(iter(ls), Iterator)
True
>>> t = (1, 2, 3, 4, 5)
>>> isinstance(t, Iterator)
False
>>> isinstance(iter(t), Iterator)
True
>>> s = 'abcde'
>>> isinstance(s, Iterator)
False
>>> isinstance(iter(s), Iterator)
True

通过上面的例子我们可以看到,列表、元组、字符串等是可迭代对象,但是不是迭代器。可以使用 iter()函数,轻松把列表、元组、字符串等转为迭代器。

为什么列表、元组、字符串等不是迭代器呢?
因为 Python 的 Iterator 对象表示的是一个数据流,迭代器可以被 next()函数不断调用并返回下一个数据,直到没有数据时抛出 StopIteration 错误。我们可以把这个数据流看成一个有序序列,但是我们却不能提前知道序列的长度,只能不断通过 next()函数实现按需计算下一个数据,因此 Iterator 的计算是惰性的,只有在需要返回下一个数据时它才会计算。

所以 Iterator 可以表示一个无限大的数据流,如全部整数,但是列表等容器由于内存空间限制,用于不可能存储全体整数。

小结:

  1. 迭代(iteration):迭代是一种操作,用 for 循环遍历。
  2. 可迭代对象(iterable object):可以用 for 循环迭代的对象。
  3. 迭代器(iterator):可以作用于 next()函数的可迭代对象,它们是一个惰性计算序列。
  4. 可以用 iter()函数把 list、str、tuple、dict 转为迭代器。

4. 推导式

推导式(comprehension)是 Python 非常重要的一个特性,提供了更加简单的创建列表、集合、字典的方式。其中列表推导式(list comprehensions)是用的最多的。

4.1 列表推导式

列表推导式是 Python 非常重要的一个特性之一。列表推导式提供了一个更简单的创建列表的方法。

常见的用法是把某种操作应用于序列或可迭代对象的每个元素上,然后使用其结果来创建列表,或者通过满足某些特定条件元素来创建子序列。

比如:假设我们想创建一个平方列表。

通常我们是这么做:

>>> squares = []
>>> for x in range(10):
...     squares.append(x**2)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

这样做尽管可以达到目的,但是这里创建(或被重写)的名为 x 的变量在 for 循环后仍然存在,可能存在副作用。我们可以通过以下方法计算平方列表的值而不会产生任何副作用。

方法一:squares = list(map(lambda x: x**2, range(10)))

方法一等价于:

方法二:squares = [x**2 for x in range(10)]

这里的方法二就是列表推导式,我们可以看到列表推导式更加简洁易读。

列表推导式的结构是:

[ 表达式 for子句(必须有一个) 0 或多个 for 或者 if 子句]

说明:由一对方括号([])所包含以下内容:一个表达式,后面跟一个 for 子句,然后是零个或多个 for 或 if 子句。根据后面的 for 等子句计算表达式的值,然后把所有计算的值存为一个新列表。如:以下列表推导式会将两个列表中不相等的元素组合起来变为一个新的列表:

[(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]

它等价于

>>> combs = []
>>> for x in [1,2,3]:
...     for y in [3,1,4]:
...         if x != y:
...             combs.append((x, y))
...
>>> combs
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

可见,使用列表推导式非常简洁。注意在上面两个代码片段中, for 和 if 的顺序是相同的。如果表达式是一个元组(例如上面的 (x, y)),那么就必须加上括号。

列表推导式可以使用复杂的表达式和嵌套函数。如:

>>> from math import pi
>>> [str(round(pi, i)) for i in range(1, 6)]
['3.1', '3.14', '3.142', '3.1416', '3.14159']

嵌套的列表推导式:列表推导式中的初始表达式可以是任何表达式,包括另一个列表推导式。

考虑下面这个 3x4 的矩阵,它由 3 个长度为 4 的列表组成

>>> matrix = [
...     [1, 2, 3, 4],
...     [5, 6, 7, 8],
...     [9, 10, 11, 12],
... ]

下面的列表推导式将交换其行和列

>>> [[row[i] for row in matrix] for i in range(4)]
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

如上节所示,嵌套的列表推导式是基于跟随其后的 for 进行求值的,所以这个例子等价于:

>>> transposed = []
>>> for i in range(4):
...     transposed.append([row[i] for row in matrix])
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

反过来说,也等价于

>>> transposed = []
>>> for i in range(4):
...     # the following 3 lines implement the nested listcomp
...     transposed_row = []
...     for row in matrix:
...         transposed_row.append(row[i])
...     transposed.append(transposed_row)
...
>>> transposed
[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

实际应用中,使用内置函数去组成复杂的流程语句是更好的选择。 zip() 函数将会很好地处理这种情况

>>> list(zip(*matrix))  # *matrix 是列表解包,会去除最外层的[]
[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

列表推导式小结:

  1. 列表推导式格式: [表达式(可嵌套列表推导式) for子句 0或多个 for 或 if 子句]
  2. 列表推导式创建新列表更加简洁易读,没有副作用。

注意,虽然元组和列表类似,但是列表是可变的,元组是不可变的,所以元组没有推导式。
元组是不可变的,其序列通常包含不同种类的元素,并且通过解包或者索引来访问。一个元素的元组的创建必须在一个元素后面添加一个逗号,如 t = (1, )

列表是可变的,一般列表的元素都是同种类型的,并且通过迭代访问。

4.2 集合推导式

集合是无序、确定、互异的。它通常用于成员测试和去重操作。此外还可以进行并集、交集、差集、补集等操作。

集合也和列表一样支持推导式,集合推导式(set comprehensions)如下:

>>> {x for x in 'abracadabra' if x not in 'abc'}
{'r', 'd'}

即:把[] 变为 {} 即可,其他和列表规则一样。

4.3 字典推导式

字典也支持字典推导式(dict comprehensions),如:

>>> {x: x**2 for x in (2, 4, 6)}
{2: 4, 4: 16, 6: 36}
>>> {x: y for x, y in zip("abcd", [1, 2, 3, 4])}
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

即:把[] 变为 {},同时表达式符合字典的键值对形式 key: value。其他规则同列表推导式。

5. 生成器和生成器表达式

5.1 生成器

生成器(Generator):是一个用于创建迭代器的简单而强大的工具。 它们的写法类似标准的函数,但当它们要返回数据时会使用 yield 语句。 每次对生成器调用 next() 时,它会从上次离开位置恢复执行(它会记住上次执行语句时的所有数据值)。就是把函数中 return 关键字换成了 yield 关键字,这样定义出来的就不是函数了,而是一个生成器,通常我们用 for 循环去迭代生成器,而不是用 next()函数一个一个调用,示例如下:

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]

>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

可以用生成器来完成的操作同样可以用基于类的迭代器来完成。 但生成器的写法更为紧凑,因为它会自动创建 __iter__()__next__() 方法。

另一个关键特性在于局部变量和执行状态会在每次调用之间自动保存。

除了会自动创建方法和保存程序状态,当生成器终结时,它们还会自动引发 StopIteration。 这些特性结合在一起,使得创建迭代器能与编写常规函数一样容易。

生成器比较难理解的一点在于生成器的执行流程和函数流程不一样,函数是顺序执行,遇到 return 语句,或者最后一行函数语句就返回。而变成生成器的函数时,在每次调用 next()函数的时候执行,遇到 yield 语句返回,再次执行时从上次返回的 yield 语句处继续执行。

举个栗子:

>>> def language():
...     print("Step 1")
...     yield "Python"
...     print("Step 2")
...     yield "Java"
...     print("Step 3")
...     yield "C"
...
>>> lang = language()
>>> next(lang)
Step 1
'Python'
>>> next(lang)
Step 2
'Java'
>>> next(lang)
Step 3
'C'
>>> next(lang)
Traceback (most recent call last):
  File "", line 1, in <module>
StopIteration

调用 language 生成器时,首先要生成一个 generator 对象,然后用 next()函数不断获得下一个返回值。在执行的过程中,遇到 yield 就中断,下次又继续执行,执行 3 次 yield 后,没有 yield 可以执行了,所以第 4 次调用 next(lang)就报错了。

5.2 生成器表达式

生成器除了用类似于函数的定义方法外,还可以用类似于列表推导式的方式生成,所用语法类似列表推导式,就是把外层的方括号换成圆括号即可。这种表达式被设计用于生成器将立即被外层函数所使用的情况。
生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。因为生成器表达式是生成迭代器,迭代器是惰性计算,它存储的是算法,需要多少就计算多少,而列表推导式是直接全部计算出来,放到内存。

例如:

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> unique_words = set(word for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

6. 匿名函数

当我们传入函数时,有些时候不需要显示的定义函数,直接传入匿名函数( anonymous functions)更方便。

匿名函数是通过 lambda 关键字来创建的。基本语法是:lambda 参数: 表达式
其中参数可以是多个,用逗号分隔。如:lambda a, b: a+b,这个匿名函数返回两个参数的和。再举个栗子看看匿名函数的常用用法:

>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

Lambda 函数可以在需要函数对象的任何地方使用。它们在语法上限于单个表达式。从语义上来说,它们只是正常函数定义的语法糖,不用写 return,返回值就是表达式的结果。

匿名函数的优势在于没有名字,不用担心命名冲突。此外匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数。如:

>>> s = lambda x, y: x + y  # 计算两数之和
>>> s
<function <lambda> at 0x000002B457B3AF78>
>>> s(2, 3)
5
>>> s(3, 7)

此外,与嵌套函数定义一样,lambda 函数可以引用所包含域的变量,把匿名函数作为返回值返回,举个官网栗子

>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43

7. 装饰器

装饰器(decorator)是一个非常有用的设计。它可以在代码运行期间动态增加功能。本质上,装饰器是一个返回函数的高阶函数。
比如:我们现在要定义一个能打印日志的装饰器,可以定义如下:

def log(func):
    def wrapper(*args, **kwargs):
        print(f"call {func.__name__}()")
        return func(*args, **kwargs)
    return wrapper

这个 log 其实就是一个装饰器,它接收一个函数作为参数,并返回一个函数。我们借助 Python 的 @ 语法,把装饰器置于函数的定义处:

def log(func):
    def wrapper(*args, **kwargs):
        print(f"call {func.__name__}()")  # func.__name__获取该函数的名字
        return func(*args, **kwargs)
    return wrapper

@log
def now():
    print("2020-05-05")

if __name__ == '__main__':
    now()

结果输出:
call now()
2020-05-05

@log 放到 now() 函数的定义处,相当于执行了语句:`now = log(now)。
由于 log()是一个装饰器,返回一个函数,所以,原来的 now()函数仍然存在,只是现在同名的 now 变量指向了新的函数,于是调用 now()将执行新函数,即在 log()函数中返回的 wrapper()函数。

wrapper()函数的参数定义是(*args, **kwargs),因此 wrapper()可以接收任意参数的调用,在 wrapper()函数内,首先打印日志,再紧接着调用原始函数。

如果 decorator 本身需要传入参数,那就需要编写一个返回 decorator 的高阶函数,写出来会更复杂,比如,自定义 log 的文本:

def log(text):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"{text} call {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log("开始")
def now():
    print("2020-05-05")

if __name__ == '__main__':
    now()

结果输出:
开始 call now
2020-05-05

与 2 层嵌套相比,3 层嵌套效果如:now = log("开始")(now)
说明:首先执行 log(“我们”),然后返回 decorator 函数,在调用返回的函数,参数是 now 函数,返回值最终是 wrapper 函数。

这两种 decorator 定义方式都没有问题,不过还差最后一步,因为函数也是对象,它有__name__等属性,经过 decorator 装饰之后的函数,它们的__name__已经从原来的 now 变成了,wrapper:

def log(text):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"{text} call {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log("开始")
def now():
    print("2020-05-05")

if __name__ == '__main__':
    now()
    print(now.__name__)

结果输出:
开始 call now
2020-05-05
wrapper

这是因为返回的 wrapper()函数名字就是 wrapper,所以需要把原始函数的__name__等属性复制到 wrapper()函数中,否则,有些依赖函数签名的代码执行会出错。Python 内置的 functools.wraps 就是干这个事的,所以一个完整的 decorator 的写法如下:

import functools


def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"call {func.__name__}()")  # func.__name__获取该函数的名字
        return func(*args, **kwargs)
    return wrapper

@log
def now():
    print("2020-05-05")

if __name__ == '__main__':
    now()
    print(now.__name__)

结果输出:
call now()
2020-05-05
now

带参数的装饰器:

import functools


def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{text} call {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log("现在")
def now():
    print("2020-05-05")

if __name__ == '__main__':
    now()
    print(now.__name__)

结果输出:
现在 call now
2020-05-05
now

8. 巨人的肩膀

  1. The Python Tutorial
  2. 廖雪峰 Python3 教程

推荐阅读:

  1. 编程小白安装Python开发环境及PyCharm的基本用法
  2. 一文了解Python基础知识
  3. 一文了解Python数据结构
  4. 一文了解Python流程控制
  5. 一文了解Python函数
  6. 一文了解Python的模块和包

后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技,扫描下方二维码或者搜索每日学一技关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!
每日学一技

你可能感兴趣的:(python,装饰器,迭代器,生成器,切片)