1.13 通过某个关键字排序一个dict list

有一个dict list 根据某个或某几个dict 字段 来排序

通过使用 operator 模块的itemgetter函数 可非常容易的排序这样的data structure 假设你从DB 中检索出来网站会员信息列表 并且以下列data structure 返回


In [4]:
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}
]
# 根据任意dict field 来排序输入结果行
from operator import itemgetter
rows_by_fname = sorted(rows,key=itemgetter('fname'))
rows_by_uid = sorted(rows,key=itemgetter('uid'))
print(rows_by_fname,'\n')
print(rows_by_uid)


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

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

itemgetter function 也支持多个keys


In [5]:
rows_by_lfname = sorted(rows,key=itemgetter('lname','fname'))
print(rows_by_lfname)


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

rows 被传递给接受一个keys 参数的sorted 内置fun 此 参数is callable 类型 并从rows接受一个单一元素 然后返回被用来排序的值 itemgetter 负责创建callable 对象
operator.itemgetter func 有一个被rows张的记录用来查找值得index参数 可以是一个dict.key名称 一个 int 值 or 任何能够传入一个object 的getitem方法的值 if can 传入多个index 参数给itemgetter 其生成 callable object 来返回一个包含 所有element 值得tuple 并且搜热的函数会根据这个tuple 中element 去排序

itemgetter 亦可用lambda 来代替


In [7]:
rows_by_fname = sorted(rows,key=lambda r:r['fname'])
rows_by_lfname = sorted(rows,key=lambda r:(r['fname'],r['lname']))
# 以上方法 与 itemgetter 类似 主要是 sorted func 中 key argument

使用itemgetter 方式 运行速度快 性能 要求比较高的话使用itemgetter 同理在sorted 中使用 亦可推广到max min中


In [8]:
mi = min(rows,key=itemgetter('uid'))
ma = max(rows,key=itemgetter('uid'))
print(mi,'||\n',ma)


{'lname': 'Cleese', 'uid': 1001, 'fname': 'John'} ||
 {'lname': 'Jones', 'uid': 1004, 'fname': 'Big'}

1.14 排序不支持比较的对象

想要排序类型相同的对象,but 不支持 原生的比较操作

内置的sorted 函数有一个关键字参数key 可传入callable 对象给他 此callable 对象对每个传入的对象返回一个值,此值会被sorted 用来排序这些对象 类似,if 你在程序中有一个User instance Sequence and want to 通过他们的user_id 属性进行排序 可提供一个以User instance 作为输入并输出对应的user_id 值的callable 对象


In [13]:
class User:
    def __init__(self,user_id):
        self.user_id = user_id
    
    def __repr__(self):
        return  'User({})'.format(self.user_id)
# 这里__repr__很重要!!!!!

users = [User(23), User(3), User(88)]
print(users)
print(sorted(users, key=lambda u: u.user_id))


[User(23), User(3), User(88)]
[User(3), User(23), User(88)]

另一招是使用 operator.attrgetter() 来代替 lambda 函数


In [12]:
from operator import attrgetter
sorted(users, key=attrgetter('user_id'))


Out[12]:
[User(3), User(23), User(88)]

itemgetter(作用于dict type) & attrgetter 都是 from operator import 很类似
他们都可以 用lambda 函数去替代 不过前两者 运行速度快一点 and 可同时允许多个字段进行比较

if User instance 还有一个first name and last name 属性 则可向下面进行排序


In [ ]:
by_name = sorted(users,key=attrgettter('last_name','first_name'))

同样 attrgetter 适用于min max 之类函数


In [14]:
a = min(users,key=attrgetter('user_id'))
b = max(users,key=attrgetter('user_id'))
print(a,'\n',b)


User(3) 
 User(88)

1.15 通过某个字段将记录进行分组

有个dict or instance 的 sequence 想根据某个特定字段比如date 来分组迭代访问

itertools.groupby() 函数


In [3]:
rows = [
    {'address': '5412 N CLARK', 'date': '07/01/2012'},
    {'address': '5148 N CLARK', 'date': '07/04/2012'},
    {'address': '5800 E 58TH', 'date': '07/02/2012'},
    {'address': '2122 N CLARK', 'date': '07/03/2012'},
    {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
    {'address': '1060 W ADDISON', 'date': '07/02/2012'},
    {'address': '4801 N BROADWAY', 'date': '07/01/2012'},
    {'address': '1039 W GRANVILLE', 'date': '07/04/2012'},
]
# 假设想在date 分组后的data 块上进行迭代 首先需要按照指定字段date 进行排序 后调用itertools.groupby()进行分组
from operator import itemgetter
from itertools import groupby
# Sort by the desited field first
rows.sort(key=itemgetter('date'))
# Iterate in groups
for date, item in groupby(rows,key=itemgetter('date')):
    print(date)
    for i in item:
        print(' ',i)


07/01/2012
  {'date': '07/01/2012', 'address': '5412 N CLARK'}
  {'date': '07/01/2012', 'address': '4801 N BROADWAY'}
07/02/2012
  {'date': '07/02/2012', 'address': '5800 E 58TH'}
  {'date': '07/02/2012', 'address': '5645 N RAVENSWOOD'}
  {'date': '07/02/2012', 'address': '1060 W ADDISON'}
07/03/2012
  {'date': '07/03/2012', 'address': '2122 N CLARK'}
07/04/2012
  {'date': '07/04/2012', 'address': '5148 N CLARK'}
  {'date': '07/04/2012', 'address': '1039 W GRANVILLE'}

groupby 函数 扫描整个序列 并且查找连续相同值(或根据key 函数return值相同)的元素sequence 在每次迭代时 其return 一个值和一个迭代对象 这个迭代对象 可生成 元素值全不等于上面那个值的组中的所有对象

一个非常重要的准备步骤 即根据 指定字段进数据排序 因为groupby 函数仅仅检查连续的元素IF 实现并没有排序完成 的话 分组函数金得到意想不到的结果

IF 仅仅是根据date field 来将data 分组到一个大的数据结构 中 同时允许随机访问 则最好说那个defaultdict 函数来构建一个多值dict


In [6]:
from collections import defaultdict
rows_by_date = defaultdict(list)
for row in rows:
    rows_by_date[row['date']].append(row)
# 可轻松访问就能对每个指定date 进行对应记录访问
for r in rows_by_date['07/01/2012']:
    print(r)


{'date': '07/01/2012', 'address': '5412 N CLARK'}
{'date': '07/01/2012', 'address': '4801 N BROADWAY'}

上述例子并没有对记录进行先排序 这种方式 快鱼 先排后groupby 函数迭代方式运行得快

1.16 过滤序列元素

针对data sequence 利用其中规则从中提取需要的值 或者 是缩短序列

  1. 最简单的过滤序列元素的方法就是使用列表推导
  2. 将过滤代码放至一个函数 后使用内建filter 函数
  3. 利用过滤工具 itertools.compress() 以一个iterable 对象 and 相对应Boolean 选择器 sequence 作为输入 输出 iterable 对象中对应选择器为True 的element

In [7]:
# way 1
mlist = [1,4,-5,10,2,-7,10,3,2,-1]
[n for n in mlist if n > 0]


Out[7]:
[1, 4, 10, 2, 10, 3, 2]

In [8]:
[n for n in mlist if n < 0]


Out[8]:
[-5, -7, -1]

利用 list 推导的潜在 缺陷 if input 非常大 会产生一个较大结果set 占用内存 则使用生成器表达式了迭代来产生过滤element


In [10]:
pos = (n for n in mlist if n > 0)
pos
for x in pos:
    print(x)


1
4
10
2
10
3
2

过滤规则比较复杂 不能简单的在list 对到或者生成器 表达式中表达出来 假设过滤的时候 需要处理一些异常或其他负责情况
此时将过滤代码放到一个函数中 然后 使用内建filter 函数


In [11]:
values = ['1','2','-4','-',4,'N/A',5]
def is_int(val):
    try:
        x = int(val)
        return True
    except ValueError:
        return False
ivals = list(filter(is_int,values))

In [13]:
print(ivals)
# filter 函数创建一个迭代器 so if 以一个list 就得像instance 那样使用list 来转换


['1', '2', '-4', 4, 5]

列表推导 and 生成器表达式 通常情况下是最简单的方式
可在过滤时转换数据


In [14]:
mylist = [1,4,2,-5,4,7,9,2,3,-1]
import math
[math.sqrt(n) for n in mylist if n > 0]


Out[14]:
[1.0,
 2.0,
 1.4142135623730951,
 2.0,
 2.6457513110645907,
 3.0,
 1.4142135623730951,
 1.7320508075688772]

过滤操作 一个变种即 将不符合的值用新值进行替代 而不是丢弃他们
IF 在一列data 中 不仅要找出正数 同时也要将不是正数的数替换成指定的数 通过 过滤条件 放到条件表达式中去 可很容易的解决此问题


In [15]:
clip = [n if n > 0 else 0 for n in mylist]

In [16]:
clip


Out[16]:
[1, 4, 2, 0, 4, 7, 9, 2, 3, 0]

In [18]:
slip = [n if n < 0 else 0 for n in mylist]

In [19]:
slip


Out[19]:
[0, 0, 0, -5, 0, 0, 0, 0, 0, -1]

另外一个过滤工具 是itertools.compress


In [28]:
'''
    当你需要用另一个相关联的sequence来过滤某个序列时
'''
addresses = [
    '5412 N CLARK',
    '5148 N CLARK',
    '5800 E 58TH',
    '2122 N CLARK'
    '5645 N RAVENSWOOD',
    '1060 W ADDISON',
    '4801 N BROADWAY',
    '1039 W GRANVILLE',
]
counts = [ 0, 3, 10, 4, 1, 7, 6, 1]
'''
    将那些count 值对应大于 5 的地址全部输出
'''
if len(addresses) == len(counts):
    print('addresses == counts')
from itertools import  compress
from collections import defaultdict
more5 = [n > 5 for n in counts]
cm = dict()
for i in range(len(counts)):
    cm[counts[i]] = more5[i]

In [29]:
cm


Out[29]:
{0: False, 1: False, 3: False, 4: False, 6: True, 7: True, 10: True}

In [30]:
list(compress(addresses,more5))


Out[30]:
['5800 E 58TH', '4801 N BROADWAY', '1039 W GRANVILLE']

这里关键点 在于 需要先创建一个Boolean 序列,指示哪些元素复合条件。 然后 compress() 函数根据这个序列去选择输出对应位置为 True 的元素
和 filter() 函数类似, compress() 也是返回的一个迭代器。因此,如果你需要得到一个列表, 那么你需要使用 list() 来将结果转换为列表类型