系列文章目录

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


文章目录

  • 系列文章目录
  • 前言
  • 一、数据结构和算法
    • 1. 将序列分解为单独的变量
    • 2. 从任意长度的可迭代对象中分解元素
    • 3. 保留最后 N 个元素
    • 4. 查找最大或最小的 N 个元素
    • 5. 实现优先级队列
    • 6. 在字典中将键映射到多个值上
    • 7. 让字典保持有序
    • 8. 与字典有关的计算问题
    • 9. 在两个字典中寻找相同点
    • 10. 从序列中移除重复项且保持元素间顺序不变
    • 11. 对切片命名
    • 12. 找出序列中出现次数最多的元素
    • 13. 通过公共键对字典列表排序
    • 14. 排序不支持原生比较操作的对象

前言

随着人工智能的不断发展,数据科学相关技术也越来越重要,很多人都开启了学习数据科学相关知识体系的学习之旅,本文就以 《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 = pValueError: 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 fmeandef drop_first_last(grades):first, *middle, last = gradesreturn 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 dequedef search(lines, pattern, history=5):previous_lines = deque(maxlen=history)for line in lines.split("\n"):if pattern in line:yield line, previous_linesprevious_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)中实现这一操作(append 、 del),但队列的解决方案要更加优雅、运行速度也快得多。

(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 heapqnums = [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 heapqportfolio = [{'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 heapqclass PriorityQueue():def __init__(self):self._queue = []self._index = 0def push(self, item, priority):# heapq.heappush(heap, item):将 item 的值加入 heap 中,保持堆的不变性heapq.heappush(self._queue, (-priority, self._index, item))    self._index += 1def pop(self):# heapq.heappop(heap):弹出并返回 heap 的最小的元素,保持堆的不变性。如果堆为空,抛出 IndexError 。# 使用 heap[0] ,可以只访问最小的元素而不弹出它。return heapq.heappop(self._queue)[-1]class Item():def __init__(self, name):self.name = namedef __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 = namedef __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 < bTypeError: '<' not supported between instances of 'Item' and 'Item'

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

class Item():def __init__(self, name):self.name = namedef __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 = namedef __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 < cTypeError: '<' not supported between instances of 'Item' and 'Item'

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

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

class Item():def __init__(self, name):self.name = namedef __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 defaultdictparams = [('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 OrderedDictd = 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 OrderedDictd = 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 itemseen.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 itemseen.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 itemseen.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 Counterword_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] += 1print(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 Countera = 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 itemgetterrows_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 itemgetterrows_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 itemgetterrows_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 attrgetterclass User(object):def __init__(self, user_id, last_name, first_name):self.user_id = user_idself.first_name = first_nameself.last_name = last_namedef __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_idself.first_name = first_nameself.last_name = last_namedef __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_idself.first_name = first_nameself.last_name = last_namedef __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, 李, 清照)

python学习指南—Python 进阶(Python Cookbook)相关推荐

  1. 简明GISer Python学习指南

    简明GISer Python学习指南 缘起 GISer学Python Python简介 学什么 学习资源 视频 图书 基础入门 进阶 高级(专业) 开发库 webGIS开发 遥感影像处理 爬虫 机器学 ...

  2. 2019年python课本_2019年Python学习指南

    Python是一种用LISP和Java编译的语言,JPthon提供了访问Java图像用户界面的途径,可以让他使用可移植的GUI演示和可移植的http/ftp/html库,因此非常适合作为人工智能语言. ...

  3. Python学习day13-函数进阶(1)

    Python学习day13-函数进阶(1) 闭包函数 闭包函数,从名字理解,闭即是关闭,也就是说把一个函数整个包起来.正规点说就是指函数内部的函数对外部作用域而非全局作用域的引用. 为函数传参的方式有 ...

  4. python 学习指南_Python类型检查终极指南

    python 学习指南 In this guide, you will get a look into Python type checking. Traditionally, types have ...

  5. 半个月爆肝,Python学习指南:学习方法、路径图、资料都备齐了!

    1.6米长,八年Python使用经验,耗时半月,制作了这张Python学习路径图! 视频大纲: 自己的课代表 1.为什么学习python 00:43 2.python学习路径图和python备忘手册( ...

  6. python 学习指南_Python学习指南

    原标题:Python学习指南 <Python数据分析 第2版(影印版)> (美)韦斯·麦金尼/ISBN:9787564175191/东南大学出版社/定价:¥99 本书由Pythonpand ...

  7. Python学习入门7:python学习从基础到高手,再到就业

    其实入门Python并不难,难的是选定自己的学习方向. 简单说下Python可以做什么: 后台开发(Django / Flask / Tornado) 科学计算(Numpy / Scipy / Mat ...

  8. python学习第八讲,python中的数据类型,列表,元祖,字典,之字典使用与介绍

    目录 python学习第八讲,python中的数据类型,列表,元祖,字典,之字典使用与介绍.md 一丶字典 1.字典的定义 2.字典的使用. 3.字典的常用方法. python学习第八讲,python ...

  9. Python学习笔记:使用Python操作数据库

    Python学习笔记:使用Python操作数据库 一.数据库编程接口 为了对数据库进行统一的操作,大多数语言都提供了简单的.标准化的数据库接口(API).在Python Database API 2. ...

最新文章

  1. UFS和eMMC闪存差异在哪?
  2. WIn32中CInternetSession运行异常(afxCurrentAppName 为空)
  3. Linux2.6 内核进程调度分析
  4. Android开发究竟该如何学习,重难点整理
  5. python初学者教程我要自学网-我要自学网--json 数据解析-python。
  6. c++ 暂停功能_10月10日丨注意:东部华侨城茶溪谷暂停开放!深圳9月街道环卫榜出炉!前五街道盐田占其三!…今日盐田大小事...
  7. php代码以什么开始以什么结束,【后端开发】php语句以什么符号结束
  8. 敏捷个人第六次练习讨论:个人价值观练习
  9. eclipse中为了format的代码更加好看,少换行,可以设置java、xml、jsp的代码line width。
  10. python 控件类多个实例_Python笔记_第四篇_高阶编程_GUI编程之Tkinter_2.控件类
  11. Spring-aop-TargetSource/ProxyFactory/DefaultAopProxyFactory
  12. Tableau 连接mysql详细教程
  13. 白嫖华为云后15分钟建站
  14. python微信转账记录_python 处理微信对账单数据的实例代码
  15. First Order Methods in Optimization Ch5. Smoothness and Strong Convexity
  16. iOS学习之系统历史版本概览
  17. explore exploit
  18. CAJ转pdf在线网址
  19. 我们为什么这么拼?(转载自微信)
  20. 如何打造陆金所营销活动配置发布平台

热门文章

  1. spring boot 通过浏览器下载文件
  2. xp系统怎样添加桌面计算机,xp系统怎么在电脑桌面上创建PPPoE连接
  3. 基于折线生成平行线的C#算法实现
  4. VNPY 软件架构分析
  5. (转载)模拟银行自助终端系统
  6. python 生孩子朋友圈_适合生孩子发朋友圈的经典句子说说
  7. 期待腾讯SOSO的崛起
  8. MySQL 查询性能优化
  9. layui弹出层之应用实例讲解
  10. SSO单点登录流程详解