目录
1.1 字符串常用方法
01: find() 查找
例:my_str = "hello world hello python"
格式:print(my_str.find("python"))
02: index() 下标
格式:print(my_str.index("py"))
03: count() 次数
格式:print(my_str.count("l"))
04: replace() 替换
格式:print(my_str.replace("hello","HELLO"))
05: split() 切割
格式:print(my_str.split(" "))
06:startswith() 检查字符串是否是以 str 开头, 是则返回 True,否则返回
格式:print(my_str.startswith("hello"))
07: endswith() 检查字符串是否以obj结束,如果是返回True,否则返回
格式:print(my_str.endswith("hello"))
08: upper() 转换 mystr 中的小写字母为大写
格式:print(my_str.upper())
09: lower() 转换 mystr 中所有大写字符为小写
格式:print(my_str.lower())
10: title() 把字符串的每个单词首字母大写
格式:print(my_str.title())
11: capitalize() 把字符串的第一个字符大写
格式:print(my_str.capitalize())
12: partition() 把mystr以str分割成三部分,str前,str和str后
格式:print(my_str.partition("hello"))
13: rpartition() 类似于 partition()函数,不过是从右边开始.
格式:print(my_str.rpartition("hello"))
14:splitlines() 按照行分隔,返回一个包含各行作为元素的列表
格式:print(my_str.splitlines())
15:isalpha() 如果 mystr 所有字符都是字母 则返回 True,否则返回 False
格式:print(my_str.isalpha())
16:isdigit() 如果 mystr 只包含数字则返回 True 否则返回 False.
格式:print(my_str.isdigit())
17:isalnum() 如果 mystr 所有字符都是字母或数字则返回 True,否则返回
格式:print(my_str.isalnum())
18:isspace() 如果 mystr 中只包含空格,则返回 True,否则返回 False.
格式:print(my_str.isspace())
19:rjust() 返回一个原字符串右对齐,并使用空格填充至长度 width 的新字符串
格式:my_str.rjust(width)
20:ljust() 返回一个原字符串左对齐,并使用空格填充至长度 width 的新字符串
格式:my_str.ljust(width)
21:center() 返回一个原字符串居中对齐,并使用空格填充至长度 width 的新字符串
格式:my_str.center(width)
22:lstrip() 删除 my_str 左边的空白字符
格式:my_str.lstrip()
23:rstrip() 删除 mystr 字符串末尾的空白字符
格式:my_str.rstrip()
24:strip() 删除mystr字符串两端的空白字符
格式:my_str.strip()
25:rfind() 类似于 find()函数,不过是从右边开始查找.
格式:my_str.rfind(str, start=0,end=len(mystr) )
26:join() str 中每个字符后面插入my_str,构造出一个新的字符串
格式:my_str.join(str)
1.2 列表常用方法
<1>添加元素("增"append, extend, insert)
append
通过append可以向列表(尾部)添加元素
extend
通过extend可以将另一个集合中的元素逐一添加到列表中
insert
insert(index, object) 在指定位置index前插入元素object
<2>修改元素("改")
修改元素的时候,要通过下标来确定要修改的是哪个元素,然后才能进行修改
<3>查找元素("查"in, not in, index, count)
所谓的查找,就是看看指定的元素是否存在
in, not in
python中查找的常用方法为:
in(存在),如果存在那么结果为true,否则为false
not in(不存在),如果不存在那么结果为true,否则false
<4>删除元素("删"del, pop, remove)
列表元素的常用删除方法有:
del:根据下标进行删除
pop:删除最后一个元素
remove:根据元素的值进行删除
<5>排序(sort, reverse)
sort方法是将list按特定顺序重新排列,默认为由小到大,参数reverse=True可改为倒序,由大到小。
reverse方法是将list逆置。
1.3 字典常用方法
<1>修改元素
字典的每个元素中的数据是可以修改的,只要通过key找到,即可修改
<2>添加元素
如果在使用 变量名['键'] = 数据 时,这个“键”在字典中,不存在,那么就会新增这个元素
<3>删除元素
对字典进行删除操作,有以下几种:
(1)del 删除整个字典
(2)clear() ---- 清空整个字典
demo:del删除指定的元素(删除后不能访问,否则会报错)
-------------------------------------------------------------------------------------------------------------------------------------------
字典的常见操作(升级)
<1>len()
测量字典中,键值对的个数
<2>keys
返回一个包含字典所有KEY的列表
<3>values
返回一个包含字典所有value的列表
<4>items
返回一个包含所有(键,值)元祖的列表
遍历
通过for ... in ... 我们可以遍历字符串、列表、元组、字典等
注意python语法的缩进
字符串遍历
列表的遍历
元祖的遍历
字典的遍历(遍历字典的key(键))
字典的遍历(遍历字典的value(值))
字典的遍历(遍历字典的items(元素))
字典的遍历(遍历字典的items(键值对))
1.4 集合常用方法
集合是无序的,集合中的元素是唯一的,集合一般用于元组或者列表中的元素去重
添加元素(add,update)
add
set1 = {1, 2, 4, 5}
#添加元素
set1.add(8)
update
set1 = {1, 2, 4, 5}
#是把要传入的元素拆分,做为个体传入到集合中
set1.update("abcd")
-----------------------------------------------------------------------------------------------------------------------------------------------
删除元素(remove,pop,discard)
remove
set1 = {1, 2, 4, 5}
# 使用remove删除集合中的元素 如果有 直接删除 如果没有 程序报错
set1.remove(22)
pop
set1 = {1, 2, 4, 5}
# 使用pop删除是随机删除集合中的元素 如果set1没有元素讲程序报错
set1.pop()
discard
set1 = {1, 2, 4, 5}
# 使用discard删除 如果元素存在 直接删除 如果元素不存在 不做任何操作
set1.discard(2)
---------------------------------------------------------------------------------------------------------------------------------------------
集合的交集和并集
交集和并集( & 和 | )
交集
set1 = {1, 2, 3, 4}
set2 = {3, 4, 5, 6}
new_set = set1 & set2
print(new_set)
# {3, 4}
并集
set1 = {1, 2, 3, 4}
set2 = {3, 4, 5, 6}
new_set = set1 | set2
print(new_set)
# {1, 2, 3, 4, 5, 6}
python 的内置函数
序号 | 方法 | 描述 |
---|---|---|
1 | len(item) | 计算容器中元素个数 |
2 | max(item) | 返回容器中最大的值 |
3 | min(item) | 返回容器中最小的值 |
4 | del(item) | 删除变量 |
del 有两种方法,一种是del加空格,另一种是del()
1.5 进程
进程:一个程序运行起来后,代码+用到的资源称之为进程,它是操作系统分配资源的基本单元。
-----------------------------------------------------------------------------------------------------------------------------------------
创建进程-multiprocessing 模块?
由于Python是跨平台的,自然应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。
multiprocessing模块提供了一个Process类来代表一个进程对象,创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
进程之间不共享全局变量?
多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响
------------------------------------------------------------------------------------------------------------------------------------------
进程池?
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态生成多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。
初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会用之前的进程来执行新的任务,
close():关闭Pool,使其不再接受新的任务;
terminate():不管任务是否完成,立即终止;
join():主进程阻塞,等待子进程的退出, 必须在close或terminate之后使用;
进程池的作用:快速批量创建进程
----------------------------------------------------------------------------------------------------------------------------------------
进程的状态?
工作中,任务数往往大于cpu的核数,即一定有一些任务正在执行,而另外一些任务在等待cpu进行执行,因此导致了有了不同的状态
----------------------------------------------------------------------------------------------------------------------------------------
进程,线程对比?
一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
进程在执行过程中拥有独立的内存单元,而多个线程共享进(进程所分配的资源)内存,从而极大地提高了程序的运行效率
1.6 线程
python中如何创建线程?
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
python中,默认情况下主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束。如果需要主线程等待其他的子线程执行结束之后再终止,需要子线程调用join()函数。
多线程程序的执行顺序是不确定的
---------------------------------------------------------------------------------------------------------------------------------
同步的概念?
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。
------------------------------------------------------------------------------------------------------------------------------------
互斥锁?
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
---------------------------------------------------------------------------------------------------------------------------------
锁的优点:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的缺点:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
--------------------------------------------------------------------------------------------------------------------------------
死锁?
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
尽管死锁很少发生,但一旦发生就会造成应用的停止响应。
避免死锁
程序设计时要尽量避免(银行家算法)
添加超时时间等待
1.7 协程
协程:
协程,又称微线程,纤程。英文名Coroutine。
协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。
协程看上去是函数,但执行过程中,在函数内部可中断,然后转而执行别的函数,在适当的时候再返回来接着执行。
python可以通过 yield/send 的方式实现协程,也可以使用第三方库中的greenlet来实现协程。
-----------------------------------------------------------------------------------------------------------------------------
协程的优势?
协程的特点在于是一个线程执行。(本质)
所以协程最大的优势就是极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。(内部封装了cpu上下文)
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
-----------------------------------------------------------------------------------------------------------------------------
生产者消费者模型?
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。
-----------------------------------------------------------------------------------------------------------------------------
greenlet?
为了更好使用协程来完成多任务,python中的greenlet模块对其封装,从而使得切换任务变的更加简单
安装方式
使用如下命令安装greenlet模块:
pip3 install greenlet
---------------------------------------------------------------------------------------------------------------------------
协程的缺陷?
1.无法利用多核资源:协程的本质是个单线程,它不能同时将 CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上。当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。(gevent)
2.进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序。
-------------------------------------------------------------------------------------------------------------------------
同步I/O和异步I/O?
同步IO操作:导致请求进程阻塞,直到IO操作完成。
异步IO操作:不导致进程阻塞。
--------------------------------------------------------------------------------------------------------------------------
gevent
greenlet已经实现了协程,但是这个还的人工切换,是不是觉得太麻烦了,不要捉急,python还有一个比greenlet更强大的并且能够自动切换任务的模块gevent
其原理是当一个greenlet遇到IO(指的是input output 输入输出,比如网络、文件操作等)操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
安装方式
使用如下命令安装gevent模块:
pip3 install gevent
gevent 的基本使用方法?
gevent.spawn(func, *args, ...)方法用来生成协程,他接受一个函数作为参数
gevent.joinall([t1, t2, ...])方法用来启动协程轮询并等待运行结果
gevent.sleep()用来模拟一个IO操作,阻塞后自动切换到另一个协程执行
1.8 sellect,poll,epoll
select、poll、epoll 都是 IO 多路复用的机制,但 select,poll,epoll 本质上都是同步
I/O,
因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的.
【https://baijiahao.baidu.com/s?id=1611547498841608701&wfr=spider&for=pc】
select模型:
说的通俗一点就是各个客户端连接的文件描述符也就是套接字,都被放到了一个集合中,调用select函数之后会一直监视这些文件描述符中有哪些可读,如果有可读的描述符那么我们的工作进程就去读取资源。
1.9 装饰器
装饰器:在函数运行时增加功能且不影响这个函数原有内容
1.10 生成器
生成器会生成一系列的值用于迭代,这样看他又是一个可迭代对象。它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。
1.11 迭代器
迭代器是访问集合元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不能后退。我们使用inter()函数创建迭代器。
1.12 面向对象
【【1.9.1 面向对象三大特性:封装,继承,多态】】
封装的意义:
将属性和方法放到一起做为一个整体,然后通过实例化对象来处理;
隐藏内部实现细节,只需要和对象及其属性和方法交互就可以了;
对类的属性和方法增加 访问权限控制。
------------------------------------------------------------------------------------------------------------------------
继承的意义:
继承:定义一个新类的时候,新的类称为子类(Subclass),而被继承的类称为基类、父类或超类(Base class、Super class)。继承最大的好处是子类获得了父类的全部变量和方法的同时,又可以根据需要进行修改、扩展。
-----------------------------------------------------------------------------------------------------------------------
多态的意义:
所谓多态:定义时的类型和运行时的类型不一样,此时就成为多态
python的多态,就是弱化类型,重点在于对象参数是否有指定的属性和方法,如果有就认定合适,而不关心对象的类型是否正确。
【【1.9.2 静态方法,类方法,属性方法】】
普通实例方法,第一个参数需要是self,它表示一个具体的实例本身。
如果用了staticmethod,那么就可以无视这个self,而将这个方法当成一个普通的函数使用。
而对于classmethod,它的第一个参数不是self,是cls,它表示这个类本身。
@classmethod修饰符对应的函数不需要实例化,不需要self参数,第一个参数需要是表示自身类的cls参数,cls参数可以用来调用类的属性,类的方法,实例化对象等。
@staticmethod返回函数的静态方法,该方法不强制要求传递参数
【【1.9.3 魔法方法】】
Python 的类里提供的,两个下划线开始,两个下划线结束的方法,就是魔法方法,
__init__()就是一个魔法方法,通常用来做属性初始化 或 赋值 操作(作用)。
如果类面没有写__init__方法,Python会自动创建,但是不执行任何操作,
__str__方法通常返回一个字符串,作为这个对象的描述信息
当删除对象时,python解释器也会默认调用一个方法,这个方法为__del__()方法
1). 当有变量保存了一个对象的引用时,此对象的引用计数就会加1;
2). 当使用del() 删除变量指向的对象时,则会减少对象的引用计数。如果对象的引用计数不为1,那么会让这个对象的引用计数减1,当对象的引用计数为0的时候,则对象才会被真正删除(内存被回收)。
魔法方法 | 含义 |
---|---|
new(cls[, ...]) | 1. new 是在一个对象实例化的时候所调用的第一个方法 |
2. 它的第一个参数是这个类,其他的参数是用来直接传递给 init 方法 | |
3. new 决定是否要使用该 init 方法,因为 new 可以调用其他类的构造方法或者直接返回别的实例对象来作为本类的实例,如果 new 没有返回实例对象,则 init 不会被调用 | |
4. new 主要是用于继承一个不可变的类型比如一个 tuple 或者 string | |
init(self[, ...]) | 构造器,当一个实例被创建的时候调用的初始化方法 |
del(self) | 析构器,当一个实例被销毁的时候调用的方法 |
call(self[, args...]) | 允许一个类的实例像函数一样被调用:x(a, b) 调用 x.call(a, b) |
len(self) | 定义当被 len() 调用时的行为 |
repr(self) | 定义当被 repr() 调用时的行为 |
str(self) | 定义当被 str() 调用时的行为 |
bytes(self) | 定义当被 bytes() 调用时的行为 |
hash(self) | 定义当被 hash() 调用时的行为 |
bool(self) | 定义当被 bool() 调用时的行为,应该返回 True 或 False |
format(self, format_spec) | 定义当被 format() 调用时的行为 |
【【1.9.4 反射:hasattr, getattr, setattr 和 delattr】】
hasattr()函数用于判断是否包含对应的属性
语法:hasattr(object,name)
参数:object--对象
name--字符串,属性名
返回值:如果对象有该属性返回True,否则返回False
------------------------------------------------------------------------------------------------------------------------
getattr()函数
描述:getattr()函数用于返回一个对象属性值
语法:getattr(object,name,default)
参数:object--对象
name--字符串,对象属性
default--默认返回值,如果不提供该参数,在没有对于属性时,将触发AttributeError。
返回值:返回对象属性值
--------------------------------------------------------------------------------------------------------------------------
setattr()函数
描述:setattr函数,用于设置属性值,该属性必须存在
语法:setattr(object,name,value)
参数:object--对象
name--字符串,对象属性
value--属性值
返回值:无
-----------------------------------------------------------------------------------------------------------------------
delattr()函数
描述:delattr函数用于删除属性
delattr(x,'foobar)相当于del x.foobar
语法:setattr(object,name)
参数:object--对象
name--必须是对象的属性
返回值:无
1.13 深浅拷贝
copy模块用于对象的拷贝操作。
该模块只提供了两个主要的方法:copy.copy与copy.deepcopy,分别表示浅拷贝与深拷贝。
浅拷贝
浅拷贝: 不管是多么复杂的数据结构,浅拷贝只会拷贝第一层.
浅拷贝是对于一个对象的顶层拷贝
通俗的理解是:拷贝了引用,并没有拷贝内容
------------------------------------------------------------------------------------------------------------------------
深拷贝
深拷贝会完全复制原变量的所有数据(递归),在内存中生成一套完全一样的内容,我们对这两个变量中的一个进行任意修改都不会影响另一个变量。
------------------------------------------------------------------------------------------------------------------------
注意点:
浅拷贝对不可变类型和可变类型的copy不同
copy.copy对于可变类型,会进行浅拷贝
copy.copy对于不可变类型,不会拷贝,仅仅是指向
1.14 python垃圾回收机制
我们从三个方面来了解一下Python的垃圾回收机制。
一、引用计数
Python垃圾回收主要以引用计数为主,分代回收为辅。引用计数法的原理是每个对象维护一个ob_ref,用来记录当前对象被引用的次数,也就是来追踪到底有多少引用指向了这个对象,当该对象的引用计数器+1,引用失效减1,当引用计数为0时,则进行回收。
二,标记,清除
『标记清除(Mark—Sweep)』算法是一种基于追踪回收(tracing GC)技术实现的垃圾回收算法。它分为两个阶段:第一阶段是标记阶段,GC会把所有的『活动对象』打上标记,第二阶段是把那些没有标记的对象『非活动对象』进行回收。那么GC又是如何判断哪些是活动对象哪些是非活动对象的呢?
对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。
三,分代回收
分代回收是一种以空间换时间的操作方式,Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象.
1.15 上下文管理
with是一种上下文管理协议,目的在于从流程图中把 try,except 和finally 关键字和资源分配释放相关代码统统去掉,简化try….except….finlally的处理流程。with通过enter方法初始化,然后在exit中做善后以及处理异常。所以使用with处理的对象必须有enter()和exit()这两个方法。其中enter()方法在语句体(with语句包裹起来的代码块)执行之前进入运行,exit()方法在语句体执行完毕退出后运行。 with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
with的使用场景 如果某项工作完成后需要有释放资源或者其他清理工作,比如说文件操作时,就可以使用with优雅的处理,不用自己手动关闭文件句柄,而且with还能很好的管理上下文异常。
with工作原理 with后面的语句被求值后,该语句返回的对象的enter()方法被调用,这个方法将返回的值赋给as后面的变量,当with包围的语句块全部执行完毕后,自动调用对象的exit()方法。 with处理异常会更方便,省去try…else…finally复杂的流程,这个用到的就是对象的exit()方法: exit( exc_type, exc_value, exc_trackback) 后面三个参数是固定的,用来接收with执行过程中异常类型,异常名称和异常的详细信息
1.16 高阶函数
map
一般情况map()函数接收两个参数,一个函数(该函数接收一个参数),一个序列,将传入的函数依次作用到序列的每个元素,并返回一个新的Iterator(迭代器)。
-------------------------------------------------------------------------------------------------------------------
filter
filter()同样接收一个函数和一个序列,然后把传入的函数依次作用于序列的每个元素,如果传入的函数返回true则保留元素,否则丢弃,最终返回一个Iterator。
-------------------------------------------------------------------------------------------------------------------
reduce
和map()用法类似,reduce把传入的函数作用在一个序列上,但传入的函数需要接收两个参数,传入函数的计算结果继续和序列的下一个元素做累积计算。
-----------------------------------------------------------------------------------------------------------------
sorted
sorted()函数就是用来排序的,同时可以自己定义排序的规则。
>>> sorted([6, -2, 4, -1])
[-2, -1, 4, 6]
>>> sorted([6, -2, 4, -1], key=abs)
[-1, -2, 4, 6]
>>> sorted([6, -2, 4, -1], key=abs, reverse=True)
[6, 4, -2, -1]
>>> sorted(['Windows', 'iOS', 'Android'])
['Android', 'Windows', 'iOS']
>>> d = [('Tom', 170), ('Jim', 175), ('Andy', 168), ('Bob', 185)]
>>> def by_height(t):
... return t[1]
...
>>> sorted(d, key=by_height)
[('Andy', 168), ('Tom', 170), ('Jim', 175), ('Bob', 185)]