当前位置: 首页 > 工具软件 > Python-Cactus > 使用案例 >

python学习指南—Python 进阶(Python Cookbook)

慕仲渊
2023-12-01

系列文章目录

Python数据科学家养成计划(Python学习指南)



前言

随着人工智能的不断发展,数据科学相关技术也越来越重要,很多人都开启了学习数据科学相关知识体系的学习之旅,本文就以 《Python CookBook》一书为基础,结合自己10多年来在工作中遇到的实际代码场景,介绍了数据科学中 Python 的进阶内容。

一、数据结构和算法

Python内置了许多非常有用的数据结构,比如列表(list)、集合(set)以及字典(dictionary)。就绝大部分情况而言,我们可以直接使用这些数据结构。但是,通常我们还需要考虑比如搜索、排序、排列以及筛选等这一类常见的问题。因此,本章的目的就是来讨论常见的数据结构和同数据有关的算法。此外,在 collections 模块中也包含了针对各种数据结构的解决方案。


1. 将序列分解为单独的变量

问题

现在有一个包含 N 个元素的元组或序列,怎样将它里面的值分解并赋值给 N 个单独的变量?

解决方案

任何序列(或可迭代的对象)都可以通过一个简单的赋值操作来分解为单独的变量。唯一的要求是变量的总数和结构要与序列相吻合。

示例代码(字符串 string):

>>> data = "Hello"
>>> a, b, c, d, e = data
>>> a
H
>>> b
e
>>> c
l
>>> d
l
>>> e
o

示例代码(元组 tuple):

>>> p = (4, 5)
>>> x, y = p
>>> x
4
>>> y
5

示例代码(集合 set):不建议使用此方法,因为集合(set)是一个无序的不重复元素序列,实际得到的结果和我们想要的结果会存在偏差。

>>> data = set(["ACME", 50, 91.1, (2022, 5, 3)])
>>> name, shares, price, date = data
>>> name
(2022, 5, 3)
>>> shares
50
>>> price
91.1
>>> date
(2022, 12, 21)

示例代码(列表 list):

>>> data = ["ACME", 50, 91.1, (2022, 5, 3)]
>>> name, shares, price, date = data
>>> name
ACME
>>> shares
50
>>> price
91.1
>>> date
(2022, 12, 21)

示例代码(字典 dictionary):

>>> data = {"Name": "mahua", "Age": 18, "Gender": "male"}
>>> name, age, gender = data.items()
>>> name
('Name', 'mahua')
>>> age
('Age', 18)
>>> gender
('Gender', 'male')

示例代码(迭代器 iterator):

>>> data = iter(["ACME", 50, 91.1, (2022, 5, 3)])
>>> name, shares, price, date = data
>>> name
ACME
>>> shares
50
>>> price
91.1
>>> date
(2022, 5, 3)

示例代码(生成器 generator):

def generator(): 
    for i in ["ACME", 50, 91.1, (2022, 5, 3)]:
        yield i
f = generator() # f 是一个迭代器,由生成器返回生成

>>> name, shares, price, date = f
>>> name
ACME
>>> shares
50
>>> price
91.1
>>> date
(2022, 5, 3)

如果元素的数量不匹配,将得到一个错误提示。

示例代码(元组 tuple):

>>> p = (4, 5)
>>> x, y, z = p
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [1], in <cell line: 2>()
      1 p = (4, 5)
----> 2 x, y, z = p

ValueError: not enough values to unpack (expected 3, got 2)

讨论

当做分解操作时,有时候可能想丢弃某些特定的值。Python 并没有提供特殊的语法来实现这一点,但是通常可以选择一个用不到的变量名,以此来作为要丢弃的值的名称。(注意:必须确保选择的变量名没有在其他地方用到过。

示例代码(列表 list):

>>> data = ["ACME", 50, 91.1, (2022, 5, 3)]
>>> _, shares, price, _ = data
>>> shares
50
>>> price
91.1
>>> data = ["ACME", 50, 91.1, (2022, 5, 3)]
>>> name, shares, *_ = data    # 注意,使用 *(星号) 表达式时,需要确定 头部/尾部 变量名
>>> name
ACME
>>> shares
50
>>> data = ["ACME", 50, 91.1, (2022, 5, 3)]
>>> name, *_, date = data    # # 注意,使用 *(星号) 表达式时,需要确定 头部/尾部 变量名
>>> name
ACME
>>> date
(2022, 5, 3)

2. 从任意长度的可迭代对象中分解元素

问题

需要从某个可迭代对象中分解出 N 个元素,但是这个可迭代对象的长度可能超过 N,这会导致出现 “分解的值过多(too many values to unpack)” 的异常。

解决方案

Python 的 “*号表达式” 可以用来解决这个问题。由 * 修饰的变量可以位于列表的第一个位置。

例 1:

假设开设了一门课程,并决定在期末的作业成绩中去掉第一个和最后一个分数,只对中间剩下的成绩做平均分统计。如果只有 4 个成绩,也许可以简单地将 4 个都分解出来,但是如果有 24 个呢?*号表达式 使得这一切都变得简单:

from statistics import fmean

def drop_first_last(grades):
    first, *middle, last = grades
    return middle

>>> grade_list = [100, 98, 99, 87, 64, 82, 71]
>>> average_score = fmean(drop_first_last(grade_list))
>>> average_score
86.0

例 2:

假设有一些用户记录,记录由姓名和电子邮件地址组成,后面跟着任意数量的电话号码。则可以这样分解记录:

>>> record = ("Mahua", "zhinengmahua@163.com", "773-555-1212", "847-555-1212")
>>> name, email, *phone_numbers = record
>>>> name
Mahua
>>> email
zhinengmahua@163.com
>>> phone_numbers
['773-555-1212', '847-555-1212']

注意:不管需要分解出多少个电话号码(0 个或多个),变量 phone_numbers 永远都是列表类型。如此一来,对于任何用到了变量 phone_numbers 的代码都不需要对其进行类型检查。

例 3: * 修饰的变量可以位于列表的第一个位置

假设用一系列的值来代表公司过去 8 个季度的销售额,现在需要对近一个季度的销售额同前 7 个季度的销售额平均值作比较。则可以这样分解:

def avg_comparison(avg, current):
    return current - avg
    
>>> sales_record = [10, 8, 7, 1, 9, 5, 10, 3]
>>> *trailing, current = sales_record    # * 修饰的变量位于列表的第一个位置
>>> trailing_avg = sum(trailing) / len(trailing)
>>> result = avg_comparison(trailing_avg, current)
>>> trailing
[10, 8, 7, 1, 9, 5, 10]
>>> current
3
>>> trailing_avg
7.142857142857143
>>> result
-4.142857142857143

讨论

对于分解未知或任意长度的可迭代对象,这种扩展的分解操作可谓是量身定做的工具。

通常,这类可迭代对象中会有一些已知的组件或模式(例如,元素 1 之后的所有内容都是电话号码),利用 * 表达式分解可迭代对象使得开

发者能够轻松利用这些模式,而不必在可迭代对象中做复杂花哨的操作才能得到相关的元素。

(1) * 表达式的语法可用于迭代一个可变长度元组序列。

例如,假设有一个带标记的元组序列:

records = [("foo", 1, 2), ("bar", "hello"), ("foo", 3, 4)]

def do_foo(x, y):
    print("foo", x, y)
    
def do_bar(s):
    print("bar", s)
    
for tag, * args in records:
    if tag == "foo":
        do_foo(*args)
    elif tag == "bar":
        do_bar(*args)
        
# 运行结果:        
foo 1 2
bar hello
foo 3 4

(2) * 表达式的语法支持和某些特定的字符串处理操作相结合。

例如,做拆分(splitting)操作:

>>> line = "nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false"
>>> uname, *fields, homedir, sh = line.split(":")
>>> uname
nobody
>>> fields
['*', '-2', '-2', 'Unprivileged User']
>>> homedir
/var/empty
>>> sh
/usr/bin/false

(3) * 表达式的语法可用于分解出某些值然后丢弃它们。

注意:在分解的时候,不能只是指定一个单独的 *,但是可以使用几个常用来表示待丢弃值的变量名,比如 _ 或者 ign(ignored)

>>> record = ("ACME", 50, 123.45, (5, 4, 2022))
>>> name, *_, (*_, year) = record
>>> name
ACME
>>> year
2022

(4) * 表达式的语法和各种函数式语言中的列表处理功能有着一定的相似性。

例如,将一个列表分解为头部和尾部:

>>> items = [1, 10, 7, 4, 5, 9]
>>> head, *tail = items
>>>> head
1
>>> tail
[10, 7, 4, 5, 9]

3. 保留最后 N 个元素

问题

在迭代或是其他形式的处理过程中,如何对最后几项记录做一个有限的历史记录统计?

解决方案

使用 collections.deque 实现保留有限的历史记录。

例 1:

对一系列文本做简单的文本匹配操作,当发现有匹配时就输出当前的匹配行以及最后检查过的 N 行文本。

lines = """两只老虎,两只老虎;
跑得快,跑得快;
一只没有眼睛,一只没有尾巴;
真奇怪!真奇怪!
两只老虎,两只老虎;
跑得快,跑得快;
一只没有耳朵,一只没有尾巴;
真奇怪!真奇怪!"""

from collections import deque

def search(lines, pattern, history=5):
    previous_lines = deque(maxlen=history)
    for line in lines.split("\n"):
        if pattern in line:
            yield line, previous_lines
        previous_lines.append(line)

for line, previous_lines in search(lines, "尾巴", 5):
    for previous_line in previous_lines:
        print(previous_line, end=" ")
    print(line, end=" ")
    print("-" * 20)

# 运行结果:
两只老虎,两只老虎; 跑得快,跑得快; 一只没有眼睛,一只没有尾巴; --------------------
跑得快,跑得快; 一只没有眼睛,一只没有尾巴; 真奇怪!真奇怪! 两只老虎,两只老虎; 跑得快,跑得快; 一只没有耳朵,一只没有尾巴; --------------------

讨论

当编写搜索某项纪录的代码时,通常会用到含有 yield 关键字的生成器函数。这将处理搜索过程的代码和使用搜索结果的代码成功解耦开来。

(1) 有界限队列

deque(maxlen=N) 创建了一个固定长度的队列。当有新纪录加入而队列已满时会自动移除最老的那条记录。

示例代码:

>>> from collections import deque

>>> q = deque(maxlen=3)
>>> q.append(1)
>>> q.append(2)
>>> q.append(3)
>>> q
deque([1, 2, 3], maxlen=3)

>>> q.append(4)
deque([2, 3, 4], maxlen=3)

>>> q.append(5)
deque([3, 4, 5], maxlen=3)

注意:尽管可以手动在列表(list)中实现这一操作(appenddel),但队列的解决方案要更加优雅、运行速度也快得多。

(2) 无界限队列

deque(maxlen=None) 创建了一个可以增长到任意长度的队列。可以在两端执行添加和弹出操作。

示例代码:

>>> from collections import deque

>>> q = deque(maxlen=None)    # q = deque()
>>> q.append(1)
>>> q.append(2)
>>> q.append(3)
>>> q
deque([1, 2, 3])

>>> q.appendleft(4)
>>> q
deque([4, 1, 2, 3])

>>> q.pop()
3
>>> q
deque([4, 1, 2])

>>> q.popleft()
4
>>> q
deque([1, 2])

注意:从队列两端添加或弹出元素的复杂度都是 O(1)。区别于列表,当从列表头部插入或移除元素时,列表的复杂度为 O(N)


4. 查找最大或最小的 N 个元素

问题

如何从某个集合中找出最大或最小的 N 个元素?

解决方案

heapq 模块中有两个函数:nlargest()nsmallest() 可以完美解决这个问题。

函数 nlargest()nsmallest() 都可以接受一个参数 key,从而允许它们工作在更加复杂的数据结构之上。

例 1:

在一个列表中分别找出最大和最小的 3 个元素。

import heapq

nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]

print(f"最大的 3 个元素列表:{heapq.nlargest(3, nums)}")
print(f"最小的 3 个元素列表:{heapq.nsmallest(3, nums)}")

# 运行结果:
最大的 3 个元素列表:[42, 37, 23]
最小的 3 个元素列表:[-4, 1, 2]

例 2:

在一个包含字典(dictionary)的列表(list)中分别找出价格(price)最高和最低的 3 个子字典(dictionary)。

import heapq

portfolio = [
    {'name': 'IBM', 'shares': 100, 'price': 91.1},
    {'name': 'AAPL', 'shares': 50, 'price': 543.22},
    {'name': 'FB', 'shares': 200, 'price': 21.09},
    {'name': 'HPQ', 'shares': 35, 'price': 31.75},
    {'name': 'YHOO', 'shares': 45, 'price': 16.35},
    {'name': 'ACME', 'shares': 75, 'price': 115.65}
]

cexpensive = heapq.nlargest(3, portfolio, key=lambda s: s["price"])
cheap = heapq.nsmallest(3, portfolio, key=lambda s: s["price"])

print(f"price 最高的 3 个子字典列表:{cexpensive}")
print(f"price 最低的 3 个子字典列表:{cheap}")

# 运行结果:
price 最高的 3 个子字典列表:[{'name': 'AAPL', 'shares': 50, 'price': 543.22}, {'name': 'ACME', 'shares': 75, 'price': 115.65}, {'name': 'IBM', 'shares': 100, 'price': 91.1}]
price 最低的 3 个子字典列表:[{'name': 'YHOO', 'shares': 45, 'price': 16.35}, {'name': 'FB', 'shares': 200, 'price': 21.09}, {'name': 'HPQ', 'shares': 35, 'price': 31.75}]

讨论

(1) 通过 heapq 模块的 heapify() 函数将列表在线性时间内原地转化成堆后,使用 heapq 模块的 heappop() 方法获取最小的元素。

堆最重要的特性就是 heap[0] 总是最小的那个元素。接下来的元素可依次通过 heapq.heappop() 方法获取。

heapq.heappop() 方法会将第一个元素(最小的)弹出,然后以第二小的元素取代被弹出元素(这个操作的复杂度是 O(logN),N 代表堆的大小)。

示例代码:找到第 3 小的元素。

>>> import heapq

>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2, 15, 14, 28, 70, 4, -61]
>>> heap = list(nums)
>>> heapq.heapify(heap)    # heapq.heapify(x):将 list x 转换成堆,原地,线性时间内
>>> heap    # 在底层将数据结构转化为列表,且元素以堆的顺序排序
[-61, 1, -4, 4, 2, 2, 18, 8, 42, 37, 7, 15, 14, 28, 70, 23, 23] 

>>> heapq.heappop(heap)
-61
>>> heapq.heappop(heap)
-4
>>> heapq.heappop(heap)
1

适用场景:寻找最大或最小的 N 个元素,且同集合中元素的总数相比,N 很小。

(2) 通过 max() 和 min() 函数寻找最大和最小的元素(N=1)。

>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2, 15, 14, 28, 70, 4, -61]
>>> max(nums)    # 获取最大值
70
>>> min(nums)    # 获取最小值
-61

适用场景:寻找最大或最小的 N 个元素,且 N =1。

(3) 通过先对集合进行排序,然后做切片操作获取最大和最小的 N 个元素。

>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2, 15, 14, 28, 70, 4, -61]
>>> sorted(nums)[:(len(nums) - 2)]    # 获取最小的(集合大小-2)个元素
[-61, -4, 1, 2, 2, 4, 7, 8, 14, 15, 18, 23, 23, 28, 37]
>>> sorted(nums)[-(len(nums) - 2):]    # # 获取最大的(集合大小-2)个元素
[1, 2, 2, 4, 7, 8, 14, 15, 18, 23, 23, 28, 37, 42, 70]

适用场景:寻找最大或最小的 N 个元素,且 N 和集合本身的大小差不多大。


5. 实现优先级队列

问题

如何实现一个按优先级排序的队列?并且在对该队列进行 pop 操作时都会返回优先级最高的那个元素。

解决方案

利用 heapq 模块实现一个简单的优先级队列。

例 1:

利用 heapq 模块实现一个简单的优先级队列:

import heapq

class PriorityQueue():
    def __init__(self):
        self._queue = []
        self._index = 0
        
    def push(self, item, priority):
        # heapq.heappush(heap, item):将 item 的值加入 heap 中,保持堆的不变性
        heapq.heappush(self._queue, (-priority, self._index, item))    
        self._index += 1

    def pop(self):
        # heapq.heappop(heap):弹出并返回 heap 的最小的元素,保持堆的不变性。如果堆为空,抛出 IndexError 。
        # 使用 heap[0] ,可以只访问最小的元素而不弹出它。
        return heapq.heappop(self._queue)[-1]
    
    
class Item():
    def __init__(self, name):
        self.name = name
        
    def __repr__(self):
        return "Item({!r})".format(self.name)
    
q = PriorityQueue()
print(q._queue)    # []
q.push(Item("foo"), 1)
print(q._queue)    # [(-1, 0, Item('foo'))]
q.push(Item("bar"), 5)
print(q._queue)    # [(-5, 1, Item('bar')), (-1, 0, Item('foo'))]
q.push(Item("spam"), 4)
print(q._queue)    # [(-5, 1, Item('bar')), (-1, 0, Item('foo')), (-4, 2, Item('spam'))]
q.push(Item("grok"), 1)
print(q._queue)    # [(-5, 1, Item('bar')), (-1, 0, Item('foo')), (-4, 2, Item('spam')), (-1, 3, Item('grok'))]

print(q.pop())     # Item('bar')
print(q._queue)    # [(-4, 2, Item('spam')), (-1, 0, Item('foo')), (-1, 3, Item('grok'))]
print(q.pop())     # Item('spam')
print(q._queue)    # [(-1, 0, Item('foo')), (-1, 3, Item('grok'))]
print(q.pop())     # Item('foo')
print(q._queue)    # [(-1, 3, Item('grok'))]
print(q.pop())     # Item('grok')
print(q._queue)    # []

讨论

(1) heapq.heappush(heap, item):将 item 的值加入 heap 中,保持堆的不变性。

(2) heapq.heappop(heap):弹出并返回 heap 的最小的元素,保持堆的不变性。如果堆为空,抛出 IndexError 。使用 heap[0] ,可以只访问最小的元素而不弹出它。。

(3) (-priority, self._index, item):队列以元组 (-priority, self._index, item) 的形式组成。

问题:为什么要将 priority 取负值?


回答:为了让队列能够按元素的优先级从高到低的顺序排列。这和正常的堆排列顺序相反,一般情况下堆是按从小到大的顺序排序的。

(4) index:保证同等优先级元素的正确排序。

通过维护一个不断递增的 index 下标变量,可以确保元素按照它们插入的顺序排序。


index 变量也在相同优先级元素比较的时候起到重要作用。

Item 实例是没办法进行次序比较的:

class Item():
    def __init__(self, name):
        self.name = name
        
    def __repr__(self):
        return "Item({!r})".format(self.name)
    
>>> a, b = Item("foo"), Item("bar")
>>> a < b
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [1], in <cell line: 10>()
      6         return "Item({!r})".format(self.name)
      8 a, b = Item("foo"), Item("bar")
---> 10 a < b

TypeError: '<' not supported between instances of 'Item' and 'Item'

以元组 (priority, item) 的形式表示元素,只要优先级不同就可以进行比较:

class Item():
    def __init__(self, name):
        self.name = name
        
    def __repr__(self):
        return "Item({!r})".format(self.name)
    
>>> a, b = (1, Item("foo")), (5, Item("bar"))
>>> a < b
True

以元组 (priority, item) 的形式表示元素,如果两个元组的优先级相同,比较操作会失败:

class Item():
    def __init__(self, name):
        self.name = name
        
    def __repr__(self):
        return "Item({!r})".format(self.name)
    
>>> a = (1, Item("foo"))
>>> b = (5, Item("bar"))
>>> c = (1,  Item("grok"))
>>> a < b
True
>>> a < c
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [11], in <cell line: 11>()
     10 c = (1,  Item("grok"))
---> 11 a < c

TypeError: '<' not supported between instances of 'Item' and 'Item'

通过引入额外的索引值,以 (priority, index, item) 的方式建立元组,可以很好的避免因 priority 值相同而引发的比较失败的问题。

因为不可能存在两个元组具有相同 index 值的情况(一旦比较操作的结果可以确定,Python 就不会再去比较剩下的元组元素了)。

class Item():
    def __init__(self, name):
        self.name = name
        
    def __repr__(self):
        return "Item({!r})".format(self.name)
    
>>> a = (1, 0, Item("foo"))
>>> b = (5, 1, Item("bar"))
>>> c = (1, 2, Item("grok"))
>>> a < b
True
>>> a < c
True

6. 在字典中将键映射到多个值上

问题

如何实现一个能将键(key)映射到多个值的字典(即一键多值字典[multidict])?

解决方案

字典是一种关联容器,每个键都映射到一个单独的值上。

如果想让键映射到多个值,需要将这多个值保存到另一个容器中(如:列表、集合)。

注意:要使用列表还是集合完全取决于应用的意图。(希望保留元素插入的顺序—列表;希望消除重复元素(且不在意它们的顺序)—集合)。

例 1:

创建一个字典,让键映射到多个值,将这多个值保存到另一个容器中:

data = {
    "a": [1, 2, 3],
    "b": [4, 5],
    "c": {1, 2, 3},
    "d": {4, 5}
}

例 2:

利用 collections 模块中的 defaultdict 类,创建一个字典,让键映射到多个值:

>>> from collections import defaultdict

>>> d = defaultdict(list)
>>> d["a"].append(1)
>>> d["a"].append(2)
>>> d["b"].append(3)
>>> d
defaultdict(<class 'list'>, {'a': [1, 2], 'b': [3]})
>>> type(d)
collections.defaultdict
>>> from collections import defaultdict

>>> d = defaultdict(set)
>>> d["a"].add(1)
>>> d["a"].add(2)
>>> d["b"].add(3)
>>>> d
defaultdict(<class 'set'>, {'a': {1, 2}, 'b': {3}})
>>> type(d)
collections.defaultdict

例 3:

在普通的字典上调用 setdefault() 方法创建一个字典,让键映射到多个值,将这多个值保存到另一个容器中:

>>> d = dict()
>>> d.setdefault("a", [])
>>> d.setdefault("b", [])
>>> d["a"].append(1)
>>> d["a"].append(2)
>>> d["b"].append(3)
>>> d
{'a': [1, 2], 'b': [3]}

该方式的另一种写法如下:

>>> d = dict()
>>> d.setdefault("a", []).append(1)
>>> d.setdefault("a", []).append(2)
>>> d.setdefault("b", []).append(3)
>>> d
{'a': [1, 2], 'b': [3]}

讨论

原则上,构建一个一键多值字典是很容易的。但是,如果每次都需要对第一个值做初始化操作,就会变得很杂乱。

您可能会会这样写代码:

params = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]

d = dict()
for key, value in params:
    if key not in d:
        d[key] = []
    d[key].append(value)

>>> d
{'yellow': [1, 3], 'blue': [2, 4], 'red': [1]}

使用 collections 模块中的 defaultdict 类后代码清晰很多:

from collections import defaultdict

params = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]

d = defaultdict(list)
for key, value in params:
    d[key].append(value)

>>> d
defaultdict(list, {'yellow': [1, 3], 'blue': [2, 4], 'red': [1]})

7. 让字典保持有序

问题

如何创建一个有序字典(当对字典进行迭代或序列化操作时,字典内元素顺序保持不变)?

解决方案

可以使用 collections 模块中的 OrderedDict 类,控制字典中元素的顺序 。

例 1:

使用 collections 模块中的 OrderedDict 类创建一个字典,并迭代输出字典中的每一对键值,观察字典迭代时的输出顺序与字典中元素添加顺序之间的关系:

from collections import OrderedDict

d = OrderedDict()
d["foo"] = 1
d["bar"] = 2
d["spam"] = 3
d["grok"] = 4
print(d)    # OrderedDict([('foo', 1), ('bar', 2), ('spam', 3), ('grok', 4)])

# Outputs "foo 1" "bar 2" "spam 3" "grok 4" 
for key, value in d.items():
    print(key, value, end=" ")

在对使用 collections 模块中的 OrderedDict 类创建的字典进行迭代时,它会严格按照元素初始添加的顺序进行。

例 2:

使用 collections 模块中的 OrderedDict 类创建一个字典,并对其进行 JSON 编码操作,精确控制各字段的顺序:

import json
from collections import OrderedDict

d = OrderedDict()
d["foo"] = 1
d["bar"] = 2
d["spam"] = 3
d["grok"] = 4
print(d)    # OrderedDict([('foo', 1), ('bar', 2), ('spam', 3), ('grok', 4)])

result = json.dumps(d)
print(result)    # '{"foo": 1, "bar": 2, "spam": 3, "grok": 4}'

讨论

(1)OrderedDict 内部维护了一个双向链表,它会根据元素加入的顺序来排列键的位置。第一个新加入的元素被放置在链表的末尾,接下来对已存在的键做重新赋值时不会改变键的顺序。

(2)由于 OrderedDict 需要额外的创建链表,导致 OrderedDict 的大小是普通字典的 2 倍多。

如果打算构建一个涉及大量 OrderedDict 实例的数据结构(例如,从 CSV 文件中读取 100000 行内容到 OrderedDict 列表中),那么需要认真对应用做需求分析,从而判断使用 OrderedDict 所带来的好处是否能超越因额外的内存开销所带来的的缺点。


8. 与字典有关的计算问题

问题

如何在字典上对数据执行各式各样的计算(比如,求最小值、最大值、排序等)?

解决方案

可以使用 Python 内置函数 zip() 配合 Python 内置函数 min()、max()、sorted() 实现。

zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个元组,然后返回由这些元组组成的对象。大大节约了内存。

a, b = [1, 2, 3], [4, 5, 6]
zipped = zip(a, b)     # 返回一个对象    <zip object at 0x103abc288>

可以使用 Python 内置函数 list() 转换来输出列表。

a, b = [1, 2, 3], [4, 5, 6]
zipped = zip(a, b)     # 返回一个对象    <zip object at 0x103abc288>
zip_list = list(zipped)    # list() 转换为列表    [(1, 4), (2, 5), (3, 6)]

如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同。

a, b = [1, 2, 3], [4, 5, 6, 7, 8]
zipped = zip(a, b)     # 返回一个对象    <zip object at 0x0000017A16A824C0>
zip_list = list(zipped)    # list() 转换为列表    [(1, 4), (2, 5), (3, 6)]

利用 * 号操作符,可以将元组解压为列表。

a, b = [1, 2, 3], [4, 5, 6, 7, 8]
zipped = zip(a, b)     # 返回一个对象    <zip object at 0x0000017A16A824C0>
a1, a2 = zip(*zipped)          # 与 zip 相反,zip(*) 可理解为解压,返回二维矩阵式    (1, 2, 3) (4, 5, 6)

例 1:

找出价格最低的股票:

prices = {
    "ACME": 45.23,
    "AAPL": 612.78,
    "IBM": 205.55,
    "HPQ": 37.20,
    "FB": 10.75
}

>>> min_price = min(zip(prices.values(), prices.keys()))
>>> min_price
(10.75, 'FB')

慢动作回放:

prices = {
    "ACME": 45.23,
    "AAPL": 612.78,
    "IBM": 205.55,
    "HPQ": 37.20,
    "FB": 10.75
}

>>> values_list, keys_list = prices.values(), prices.keys()
>>> values_list
dict_values([45.23, 612.78, 205.55, 37.2, 10.75])
>>> keys_list
dict_keys(['ACME', 'AAPL', 'IBM', 'HPQ', 'FB'])

>>> zipped = zip(values_list, keys_list)
>>> zipped
<zip object at 0x0000017A16A58A40>

>>> min_price = min(zipped)
>>> min_price
(10.75, 'FB')

例 2:

找出价格最高的股票:

prices = {
    "ACME": 45.23,
    "AAPL": 612.78,
    "IBM": 205.55,
    "HPQ": 37.20,
    "FB": 10.75
}
>>> max_price = max(zip(prices.values(), prices.keys()))
>>> max_price
(612.78, 'AAPL')

例 3:

根据股票价格对股票信息进行排序(升序):

prices = {
    "ACME": 45.23,
    "AAPL": 612.78,
    "IBM": 205.55,
    "HPQ": 37.20,
    "FB": 10.75
}
>>> prices_sorted = sorted(zip(prices.values(), prices.keys()))
>>> prices_sorted
(612.78, 'AAPL')
[(10.75, 'FB'),
 (37.2, 'HPQ'),
 (45.23, 'ACME'),
 (205.55, 'IBM'),
 (612.78, 'AAPL')]

错误用法:

Python 内置函数 zip() 创建的是一个迭代器,它的内容只能被消费一次:

prices = {
    "ACME": 45.23,
    "AAPL": 612.78,
    "IBM": 205.55,
    "HPQ": 37.20,
    "FB": 10.75
}
>>> prices_iter = zip(prices.values(), prices.keys())
>>> prices_iter
<zip object at 0x0000017A14EA3000>
>>> type(prices_iter)
<class 'zip'>
>>> min(prices_iter)
(10.75, 'FB')
>>> max(prices_iter)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [1], in <cell line: 6>()
      2 print(prices_iter, type(prices_iter))
      4 print(min(prices_iter))
----> 6 print(max(prices_iter))

ValueError: max() arg is an empty sequence

讨论

(1) 不使用 Python 内置函数 zip(),如何获取价格最低的股票?

我们可以提供一个 key 参数传递给 min() 和 max() 函数,就能得到最大值和最小值所对应的键是什么:

prices = {
    "ACME": 45.23,
    "AAPL": 612.78,
    "IBM": 205.55,
    "HPQ": 37.20,
    "FB": 10.75
}
min_key = min(prices, key=lambda k: prices[k])
max_key  = max(prices, key=lambda k: prices[k])
print(min_key, max_key)

min_value = prices[min_key]
max_value  = prices[max_key]
print(min_value, max_value)

# 运行结果:
FB AAPL
10.75 612.78

另一种实现方式:

prices = {
    "ACME": 45.23,
    "AAPL": 612.78,
    "IBM": 205.55,
    "HPQ": 37.20,
    "FB": 10.75
}
new_prices = {value: key for key, value in prices.items()}  
min_value = min(new_prices)
max_value  = max(new_prices)
print(min_value, max_value)

min_key = new_prices[min_value]
max_key = new_prices[max_value]
print(min_key, max_key)

# 运行结果:
10.75 612.78
FB AAPL

(2) 当涉及 (value, key) 对的比较时,如果碰巧有多个条目拥有相同的 value 值,将如何进行比较?

当涉及 (value, key) 对的比较时,如果碰巧有多个条目拥有相同的 value 值,那么此时 key 将用来作为判定结果的依据。

例如,在计算 min() 和 max() 时,如果碰巧 value 的值相同,则将返回拥有最小或最大 key 值的那个条目:

prices = {"AAA": 45.23, "ZZZ": 45.23}

min_price = min(zip(prices.values(), prices.keys()))
max_price = max(zip(prices.values(), prices.keys()))
print(min_price, max_price)

# 运行结果:
(45.23, 'AAA') (45.23, 'ZZZ')

9. 在两个字典中寻找相同点

问题

有两个字典,找出它们中间可能相同的地方(相同的键,相同的值等)。

解决方案

通过 keys() 或者 items() 方法执行常见的集合操作,找出两个字典中的相同之处。

例 1:

找出 a、b 两个字典中共同存在的 key 值:

>>> a = {"x": 1, "y": 2, "z": 3}
>>> b = {"w": 10, "x": 11, "y": 2}

>>> a.keys() & b.keys()
{'x', 'y'}

例 2:

找出存在于字典 a 中,但不存在于字典 b 中的 key 值 :

>>> a = {"x": 1, "y": 2, "z": 3}
>>> b = {"w": 10, "x": 11, "y": 2}

>>> a.keys() - b.keys()
{'x', 'y'}

例 3:

找出 a、b 两个字典中共同存在的 (key, value) 对:

>>> a = {"x": 1, "y": 2, "z": 3}
>>> b = {"w": 10, "x": 11, "y": 2}

>>> a.items() & b.items()
{('y', 2)}

例 4:

存在一个字典 a = {“w”: 1, “x”: 2, “y”: 3, “z”: 4},现需要在字典 a 的基础上创建一个新的字典 c,并去掉 “w” 键 和 “z” 键:

>>> a = {"w": 1, "x": 2, "y": 3, "z": 4}

>>> c = {key: a[key] for key in a.keys() - {"w", "z"}}
>>> c
{'x': 2, 'y': 3}

讨论

(1) 字典的 keys() 方法返回 keys-view 对象,其中暴露了所有的键。

关于字典的键有一个鲜为人知的特性:字典的键支持常见的集合操作,比如求并集、交集和差集。
因此,如果需要对字典的键做常见的集合操作,那么就能直接使用 keys-view 对象而不必先将它们转化为集合。

(2) 字典的 items() 方法返回由 (key, value) 对组成的 items-view 对象。

items-view 对象支持常见的集合操作,比如:可用来完成找出两个字典间有哪些键值对有相同之处的操作。

(3) 字典的 values() 方法返 values-view 对象。

注意:values-view 对象不支持集合操作。因为在字典中键和值是不同的,从值的角度来看并不能保证所有的值都是唯一的。


10. 从序列中移除重复项且保持元素间顺序不变

问题

去除序列中出现的重复元素,但仍然保持剩下的元素顺序不变。

解决方案

针对序列中的值是否是可哈希(hashable)的,可以通过使用集合(或列表)和生成器解决这个问题。

例 1:

在序列中的值是可哈希(hashable)的序列中去除重复项:

def dedupe(items):
    """ 集合 + 生成器 """
    seen = set()
    for item in items:
        if item not in seen:    # 检测去除重复项
            yield item
            seen.add(item)
            
a = [1, 5, 2, 1, 9, 1, 5, 10]
result = list(dedupe(a))
print(result)   

# 运行结果:
[1, 5, 2, 9, 10]
def dedupe(items):
    """ 列表 + 生成器 """
    seen = list()
    for item in items:
        if item not in seen:    # 检测去除重复项
            yield item
            seen.append(item)
            
a = [1, 5, 2, 1, 9, 1, 5, 10]
result = list(dedupe(a))
print(result)

# 运行结果:
[1, 5, 2, 9, 10]

例 2:

在序列中的值是不可哈希(hashable)的序列中去除重复项:

def dedupe(items, key=None):
    """ 集合 + 生成器 """
    seen = set()
    for item in items:
        val = item if key is None else key(item)    # 指定一个函数用来将序列中的元素转换为可哈希(hashable)的类型
        if val not in seen:    # 检测去除重复项
            yield item
            seen.add(val)
            
a = [{"x": 1, "y": 2}, {"x": 1, "y": 3}, {"x": 1, "y": 2}, {"x": 2, "y": 4}]

result1 = list(dedupe(a, key=lambda d: (d["x"], d["y"])))    # 去除 key "x" 和 "y" 对应的 value 值同时重复的项
print(result1)

result2 = list(dedupe(a, key=lambda d: d["x"]))    # 去除 key "x" 对应的 value 值重复的项
print(result2)

# 运行结果:
[{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]
[{'x': 1, 'y': 2}, {'x': 2, 'y': 4}]

讨论

如果只是单纯的去除重复项,而不需要保持原序列中元素间的顺序,我们可以直接通过构建一个集合实现。例如:

a = [1, 5, 2, 1, 9, 1, 5, 10]

result= list(set(a))
print(result)

# 运行结果:
[1, 2, 5, 9, 10]

关于上面的 dedupe() 函数,我们可能会希望尽可能的通用—不必绑定在只能对列表进行处理。例如,读取一个文件,去除其中重复的文本行:

def dedupe(items):
    """ 集合 + 生成器 """
    seen = set()
    for item in items:
        if item.strip() not in seen:    # 检测去除重复项
            yield item.strip()
            seen.add(item.strip())

# 文本内容:
"""两只老虎,两只老虎;
跑得快,跑得快;
一只没有眼睛,一只没有尾巴;
真奇怪!真奇怪!
两只老虎,两只老虎;
跑得快,跑得快;
一只没有耳朵,一只没有尾巴;
真奇怪!真奇怪!"""

with open("../../dataset/dedupe.txt", "r", encoding="utf-8") as f:
    for line in dedupe(f.readlines()):
        print(line)
       
# 运行结果:
两只老虎,两只老虎;
跑得快,跑得快;
一只没有眼睛,一只没有尾巴;
真奇怪!真奇怪!
一只没有耳朵,一只没有尾巴;

11. 对切片命名

问题

面对到处都是硬编码的切片索引,导致完全无法阅读的代码,我们如何将其清理干净?

解决方案

可以通过 Python 的内置函数 slice() 函数实现切片对象,从而避免使用许多神秘难懂的硬编码索引,使代码变得清晰。

描述:

  • slice() 函数实现切片对象,主要用在切片操作函数里的参数传递。

语法:

  • class slice(stop)
  • class slice(start, stop[, step])

参数说明:

  • start – 起始位置
  • stop – 结束位置
  • step – 间距

例 1:

从字符串的固定位置中取出具体的数据(硬编码切片索引):

record = "....................100 .......513.25 .........."

cost = int(record[20:23]) * float(record[31:37])

例 2:

从字符串的固定位置中取出具体的数据(切片命名):

record = "....................100 .......513.25 .........."

SHARES = slice(20, 23)
PRICE = slice(31, 37)

cost = int(record[SHARES]) * float(record[PRICE])

讨论

(1) Python 内置的 slice() 函数会创建一个切片对象,可以用在任何允许切片操作的地方。

>>> items = [1, 2, 3, 4, 5]
>>> items[2:4]
[3, 4]

>>> a = slice(2, 4)
>>> items[a]
[3, 4]

>>> items[a] = [10, 11]
>>> items
[1, 2, 10, 11, 5]

>>> del items[a]
>>> items
[1, 2, 5]

(2) 一个 slice 对象的实例 s,可以分别通过 s.start、s.stop 以及 s.step 属性来得到关于该对象的信息 。

>>> a = slice(5, 50, 2)
>>> a.start
5
>>> a.stop
50
>>> a.step
2

(3) 通过调用切片的 indices(size) 方法将切片映射到一个确定大小的序列上。

该方法返回一个三元元组 (start, stop, step),所有值都会被合适的缩小以满足边界限制,从而使用的时候避免出现 IndexError 异常。

s = "HelloWorld"
a = slice(5, 50, 2)

print(a.indices(len(s)))    # (5, 10, 2)

for i in range(*a.indices(len(s))):
    print(s[i])

# 运行结果:
(5, 10, 2)
W
r
d

12. 找出序列中出现次数最多的元素

问题

在一个元素序列中,找出序列中出现次数最多的元素是什么?

解决方案

collections 模块中的 Counter 类正是为此类问题设计的。它甚至有一个非常方便的 most_common() 方法可以直接告诉我们答案。

例 1:

假设有一个列表,列表中是一系列的单词,找出出现最为频繁的 3 个单词:

words = [
    'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
    'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the',
    'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into',
    'my', 'eyes', "you're", 'under'
]

from collections import Counter

word_counts = Counter(words)
top_three = word_counts.most_common(3)
print(top_three)

# 运行结果:
[('eyes', 8), ('the', 5), ('look', 4)]

讨论

Counter 对象可以接受任意可 hashable 的对象序列作为输入。

(1) 在底层实现中,一个 Counter 对象就是一个字典,将元素出现的次数映射到了元素上。

>>> type(word_counts)
<class 'collections.Counter'>

>>> word_counts["eyes"]
8

>>> word_counts["the"]
5

>>> word_counts["look"]
4

(2) 可以通过自增的方式手动增加计数。

morewords = ['why','are','you','not','looking','in','my','eyes']

print(f"原始计数:{word_counts.most_common()}")
    
for word in morewords:
    word_counts[word] += 1

print(f"计数自增:{word_counts.most_common()}")

# 运行结果:
原始计数:[('eyes', 10), ('my', 5), ('the', 5), ('look', 4), ('into', 3), ('not', 3), ('around', 2), ('why', 2), ('are', 2), ('you', 2), ('looking', 2), ('in', 2), ("don't", 1), ("you're", 1), ('under', 1)]

计数自增:[('eyes', 11), ('my', 6), ('the', 5), ('look', 4), ('not', 4), ('into', 3), ('why', 3), ('are', 3), ('you', 3), ('looking', 3), ('in', 3), ('around', 2), ("don't", 1), ("you're", 1), ('under', 1)]

(3) 可以使用 update() 方法手动增加计数。

>>> morewords = ['why','are','you','not','looking','in','my','eyes']
>>> word_counts.most_common()
[('eyes', 10), ('my', 5), ('the', 5), ('look', 4), ('into', 3), ('not', 3), ('around', 2), ('why', 2), ('are', 2), ('you', 2), ('looking', 2), ('in', 2), ("don't", 1), ("you're", 1), ('under', 1)]

>>> word_counts.update(morewords)
[('eyes', 11), ('my', 6), ('the', 5), ('look', 4), ('not', 4), ('into', 3), ('why', 3), ('are', 3), ('you', 3), ('looking', 3), ('in', 3), ('around', 2), ("don't", 1), ("you're", 1), ('under', 1)]

(4) Counter 对象可以轻松的同各种数学运算操作相结合。

words = [
    'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
    'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the',
    'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into',
    'my', 'eyes', "you're", 'under'
]

morewords = ['why','are','you','not','looking','in','my','eyes']

from collections import Counter

a = Counter(words)
b = Counter(morewords)

print(f"a: {a}")
print(f"b: {b}")

c = a + b
print(f"c: {c}")

d = a - b
print(f"d: {d}")

# 运行结果:
a: Counter({'eyes': 8, 'the': 5, 'look': 4, 'into': 3, 'my': 3, 'around': 2, 'not': 1, "don't": 1, "you're": 1, 'under': 1})
b: Counter({'why': 1, 'are': 1, 'you': 1, 'not': 1, 'looking': 1, 'in': 1, 'my': 1, 'eyes': 1})
c: Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2, 'around': 2, "don't": 1, "you're": 1, 'under': 1, 'why': 1, 'are': 1, 'you': 1, 'looking': 1, 'in': 1})
d: Counter({'eyes': 7, 'the': 5, 'look': 4, 'into': 3, 'my': 2, 'around': 2, "don't": 1, "you're": 1, 'under': 1})

13. 通过公共键对字典列表排序

问题

有一个字典列表,如何根据某个或某几个字典字段(键)来排序这个列表?

解决方案

方案一:利用 operator 模块中的 itemgetter() 函数进行排序。

rows = [
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]

from operator import itemgetter

rows_by_fname = sorted(rows, key=itemgetter("fname"))
rows_by_uid = sorted(rows, key=itemgetter("uid"))
rows_by_lname_fname = sorted(rows, key=itemgetter("lname", "fname"))

>>> rows_by_fname
[
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}, 
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}, 
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, 
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
]
>>> rows_by_uid
[
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}, 
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, 
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}, 
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
>>> rows_by_lname_fname
[
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, 
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}, 
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}, 
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}
]

方案二:利用 lambda 表达式进行排序。

rows = [
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]

rows_by_fname = sorted(rows, key=lambda r: r["fname"])
rows_by_uid = sorted(rows, key=lambda r: r["uid"])
rows_by_lname_fname = sorted(rows, key=lambda r: (r["lname"], r["fname"]))

>>> rows_by_fname
[
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}, 
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}, 
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, 
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
]
>>> rows_by_uid
[
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}, 
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, 
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}, 
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
>>> rows_by_lname_fname
[
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, 
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}, 
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}, 
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}
]

讨论

使用 operator 模块中的 itemgetter() 函数代码运行效率比使用 lambda 表达式的高,因此如果需要考虑性能问题的话应该使用 itemgetter()。

(1) 可以利用 operator 模块中的 itemgetter() 函数根据一个或多个字典字段(键)求取字典列表的最大值和最小值行。

rows = [
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]

from operator import itemgetter

rows_min_by_uid = min(rows, key=itemgetter("uid"))
rows_max_by_uid = max(rows, key=itemgetter("uid"))

>>> rows_min_by_uid
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
>>> rows_max_by_uid
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}

(2) 可以利用 lambda 表达式根据一个或多个字典字段(键)求取字典列表的最大值和最小值行。

rows = [
    {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
    {'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
    {'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
    {'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]

from operator import itemgetter

rows_min_by_uid = min(rows, key=lambda r: r["uid"])
rows_max_by_uid = max(rows, key=itemgetter("uid"))

>>> rows_min_by_uid
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001}
>>> rows_max_by_uid
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}

14. 排序不支持原生比较操作的对象

问题

对于一个不支持原生排序的类,如何对这个类的多个实例进行排序操作?

解决方案

方案一:利用 operator 模块中的 attrgetter() 函数进行排序。

from operator import attrgetter

class User(object):
    def __init__(self, user_id, last_name, first_name):
        self.user_id = user_id
        self.first_name = first_name
        self.last_name = last_name
        
    def __repr__(self):
        return f"User({self.user_id}, {self.last_name}, {self.first_name})"
    
users = [User(1, "李", "白"), User(2, "杜", "甫"), User(3, "李", "清照")]	# [User(1, 李, 白), User(2, 杜, 甫), User(3, 李, 清照)]

# 利用 operator.attrgetter() 根据 user_id 排序
result = sorted(users, key=attrgetter("user_id"), reverse=True)    # [User(3, 李, 清照), User(2, 杜, 甫), User(1, 李, 白)]

# 利用 operator.attrgetter() 根据名字排序
result = sorted(users, key=attrgetter("last_name", "first_name"), reverse=True)    # [User(2, 杜, 甫), User(1, 李, 白), User(3, 李, 清照)]

方案二:利用 lambda 表达式进行排序。

class User(object):
    def __init__(self, user_id, last_name, first_name):
        self.user_id = user_id
        self.first_name = first_name
        self.last_name = last_name
        
    def __repr__(self):
        return f"User({self.user_id}, {self.last_name}, {self.first_name})"
    
users = [User(1, "李", "白"), User(2, "杜", "甫"), User(3, "李", "清照")]    # [User(1, 李, 白), User(2, 杜, 甫), User(3, 李, 清照)]

# 利用 lambda 表达式根据 user_id 排序
result = sorted(users, key=lambda u: u.user_id, reverse=True)    # # [User(3, 李, 清照), User(2, 杜, 甫), User(1, 李, 白)]

讨论

使用 operator 模块中的 attrgetter() 函数代码运行效率比使用 lambda 表达式的高,因此如果需要考虑性能问题的话应该使用 attrgetter()。

同时,operator 模块中的 attrgetter() 函数允许同时通过多个字段值进行比较。

(1) 可以利用 operator 模块中的 attrgetter() 函数对一个不支持原生排序的类的多个实例进行最大值和最小值求取。

class User(object):
    def __init__(self, user_id, last_name, first_name):
        self.user_id = user_id
        self.first_name = first_name
        self.last_name = last_name
        
    def __repr__(self):
        return f"User({self.user_id}, {self.last_name}, {self.first_name})"
    
users = [User(1, "李", "白"), User(2, "杜", "甫"), User(3, "李", "清照")]    # [User(1, 李, 白), User(2, 杜, 甫), User(3, 李, 清照)]

# 取出 user_id 最小的用户
min_user = min(users, key=attrgetter("user_id"))    # User(1, 李, 白)

# 取出 user_id 最大的用户
max_user = max(users, key=attrgetter("user_id"))    # User(3, 李, 清照)
 类似资料: