提高篇5深入语法细节 | 33随机抽样只需一个函数,列表排序可以自定规则
本节要点与难点
- for x in list 方式的循环只适合读取列表,因为在循环体中修改 x 并不能直接修改 list 中相应元素。
1. 如果需要修改列表,必须通过下标形式即 list[x] 引用列表元素并修改。具体可以使用 while x1. 可以设置 sort 方法的 key 参数为某个函数,从而自定义排序规则。实际排序时,sort 将把列表中每个元素代入该函数、并根据返回值排序。
1. 标准置函数 sorted 与 列表的sort方法的功能和用法相似,但不会修改原列表内容,而是创建一个新列表。
1. 可以使用字典等容器,替代某些多分支判断结构。
1. 可以使用列表的 copy 方法对列表进行拷贝。
1. 可以使用 random 模块的 shuffle 方法,将列表顺序随机打乱
1. 可以使用 random 模块的 sample 方法,从列表中随机抽取若干个元素。这些元素(即使只有一个)会被放到一个列表中返回,该列表中的顺序也是随机。
作业1:列表读写基本练习请先在程序中定义一个列表 a=[ 3, 5, 7, 9 ] ,然后分别使用下面三种方式输出列表中的全部数据(每种方式均可单独写一个程序): - for x in a:
- while x<len(a):
- for x in range(len(a)):
完成之后,请再编写程序,分别使用以下两种方式修改列表 a ,使其中的数据全部变成 2 倍:
- while x<len(a):
- for x in range(len(a)): 接下来请思考:能否使用 for x in a: 的方式实现对 a 列表的修改、使之每个元素都乘以2?思考之后可以参见本题的“思路提示”,并继续思考:“思路提示”中给出的方案与前面使用 while 的方案相比,本质上有什么区别?
思路提示本练习的目的是让初学者亲自体验列表的不同循环形式,并加深对相关知识的理解。其中代码和原理已经在本节课程视频中详细说明,其中最后一个思考题 “怎样使用 for in 循环修改列表” 思路解析如下:
- 首先我们已经知道,在 for x in a: 这种形式中,直接修改 x 并不会修改原列表 a 中的任何元素。
1. 但是,a变量本身只是一个指向某个内存地址的“指针”,所以我们完全可以新建一个符合条件的列表,然后让变量 a 指向该列表,从效果上看,就是实现了“修改a”的功能。
1. 因此可以先在程序中创建一个空列表比如 b ,然后用 for x in a: 循环遍历 a 中的每个元素;每找到一个元素的数值 x ,就将其乘以2并添加的 b 中。这样循环结束时,列表 b 就是我们想要的结果。最后通过赋值语句 a=b ,使 a 也指向这个新列表即可。
1. 上面思路的代码形式大体如下:
b = [] for x in a: b.append( x*2 ) a = b print(a)
使用上面这种思路,虽然最后打印输出的a列表符合预期效果(每个元素变成2倍),但与while方式相比仍然存在本质区别。因为这种情况下,其实内存中原来 a 所指向的那个列表并没有发生任何变化,我们只是新建了一个列表并让 a 指向它、同时“抛弃”了原来的列表。而在 while 等方式中,我们并没有创建过任何新列表,而是直接在原来列表的内存空间中进行修改。
作业2:列表读写扩展练习请编写一个函数,能够将指定的列表中所有元素都乘以指定倍数。该函数接收两个参数:需要修改的列表、需要乘以的倍数。具体调用形式如下(假设该函数名字是 listtimes ):
if name == ‘_main‘: lst = [3,5,7,9] list_times( lst, 5 ) print(lst)
思路提示本练习的代码比较简单,只要定义一个函数,比如 list_times( a, k ) ,然后在函数体内用 while 等循环遍历 a 中每个元素,分别将其乘以 k 即可。但是在代码背后,体现了前几节课讲述的原理,值得同学思考:
- 以题干中的主程序为例:在调用 list_times 函数时主程序将列表型全局变量 lst 传递给函数参数 a 。这时 a 虽然是函数内部的局部变量,但是其记录的地址(“门牌号码”)与 lst 是一样的,因此也指向了同一个列表。
1. 所以修改a中每个元素,就相当于修改lst中的每个元素。即使在函数 list_times 执行结束、局部变量 a 被销毁后,lst 中的元素也都已经被修改完毕、永远变成了原来的两倍。
1. 同理,本函数无需返回任何数值,因为函数执行时就已经完成了“修改列表”的使命,不需要返回值,直接写 return 即可。
作业3:自己编写shuffle函数本节视频中我们看到 random 模块的 shuffle 函数可以实现列表随机打乱。现在请自己编写一个同样功能的函数(可以使用 random.randint 函数,但不能调用 random.shuffle 函数)。
提示:随机打乱有很多种方式,比如可以做一个循环,每次循环时随机生成两个下标数值,然后将列表中这两个下标处的元素数值互换。这样多次循环后(循环次数也可以随机),就相当于洗牌结束。
思路提示假设自定义函数的名字和参数为: my_shuffle( a ) ,那么按照题干中给出的提示思路,该函数的实现要点如下:
- 该思路中每次循环可以交换列表中的两个元素,所以首先先要指定一个“循环总次数”。这个数字越高,被交换的元素就越多,随机效果也就越好。这个数字可以使用随机数,也可以固定,比如指定为列表长度 len(a) —— 如果列表有50个元素就交换50次 。在本题的示例代码中,为降低理解难度,我们使用了固定数字 100 。
1. 每次循环时需要生成两个随机数(比如 x 和 y),代表本次用于交换数值的两个元素下标。注意这两个随机数的范围应该在 0 和 len(a)-1 之间,也就是列表中的最小下标和最大下标。
1. 生成随机下标后,就可以将 a 列表中下标为 x 和 y 的两个元素交换数值。我们在《基础篇》中介绍过一个专门用于交换数值的语法糖,即 m,n = n,m ,这里可以使用。注意:被交换的是 a[x]与a[y],而不是x与y。
1. 循环结束后,原列表已经被乱序修改,所以直接 return 即可。
作业4:自定义百家姓排序《百家姓》的前八位顺序是“赵钱孙李周吴郑王”。假设现在有一个列表 ,其中是公司里多位员工的姓氏(该公司每位员工都是这八姓之一):
names=[‘钱’,’李’,’吴’,’王’,’李’,’吴’,’赵’,’王’,’王’,’郑’,’周’,’孙’,’王’,’李’,’孙’,’赵’,’周’ ]
请编写一个排序函数,从而能够按照《百家姓》顺序对该列表排序。
完成上述功能后,请对该函数进行修改,使之能够对下面的完整姓名列表,按照《百家姓》顺序做逆向排序:
names=[‘钱图’,’李义’,’吴天’,’王力’,’李新’,’吴永’,’赵清’,’王刚’,’王伍’,’郑朵’,’周青’,’孙莲’,’王义’,’李宏’,’孙月’,’赵婷’,’周显’ ]
完成上述任务后,请将代码中的 sort 方法替换为 sorted 函数(注意 sorted 函数不会修改 names 列表,而是返回一个新列表)。
思路提示本作业第一题与课程视频中的案例非常相似,最基本的办法就是编写自定义函数,然后使用多分支判断语句对参数(传来的姓氏字符串)进行判断,并返回一个数值(赵-0、钱-1、孙-2、李-3 …… )。
不过正如视频中演示的,我们完全可以使用容器来替代列表,比如像视频课程中那样定义字典 {‘赵’:0,’钱’:1,’孙’:2,’李’:3, ……} ,然后在函数中直接将传来的姓氏字符串作为键,到字典中取得对应数值。
而对于本例,其实还有更简单的方式:利用字符串对象的find方法,直接在字符串 “赵钱孙李周吴郑王” 中查找某个姓氏字符串的出现位置,而该位置(即其在字符串中的下标)就可以作为该姓氏对应的数字。比如 s.find(‘孙’),得到的返回值就是字符串 s 中 “孙” 的下标 2 。本题参考答案中就使用了这个方法。
对于本作业第二题,由于传来的参数是整个姓名,所以只要用 s[0] 的切片操作取出该字符串第一个字符(姓氏),再按照前面一样的方法操作即可。另外需要注意:题干要求倒序排列,所以要指定 reverse 参数为 True。
作业5:Excel百家姓排序请先下载附件:工资表格,然后修改上个作业中开发的百家姓排序函数,对表格中 B3:D657 范围内的全部员工记录,按照员工姓氏排序。
思路提示排序结果可以直接用 print 打印在屏幕上,也可以写回到Excel文件中(比如同一工作表的 F3:H657 范围中)。
本作业与本节课程视频中的示例很相似,要点包括以下几点:
- 数据表的 B3:D657 读入后形成一个二维列表,其中每个子列表包含三个元素,第一个元素就是姓名。因此需要修改上一作业中的排序函数,使之接收一个列表作为参数,然后将列表中第一个元素的第一个字符作为键、到字符串“赵钱孙李周吴郑王”中取得顺序数字。
1. 列表排序后,可以直接用 range.value=list 的形式,将二维列表数据写回工作表指定区域中。
作业6:随机抽样练习请继续使用上节课的Excel文件,编写程序在所有员工中随机抽取 6 人发放彩金。要求一等奖1人、二等奖2人、三等奖3人。将中奖员工的姓名显示 print 到屏幕上即可。
思路提示本作业就是使用 random.sample 函数抽取一个长度为 6 的姓名列表,然后将列表中第一个元素定义为一等奖、第二和第三个元素定义为二等奖,其余定义为三等奖即可。
不过需要注意的是:本作业只需读取姓名列,所以代码中建议直接写 range(‘B3:B657’) 。这样读出的是一个一维列表(因为只有一列数据),大大简化了后面的操作。
此外为简化输出代码,可以将二三等奖的名单列表直接转换为字符串(使用《基础篇》中讲解的字符串对象 join 方法,从而免去书写循环的麻烦。