翻译《Writing Idiomatic Python》(四):字典、集合、元组
原书参考:http://www.jeffknupp.com/blog/2012/10/04/writing-idiomatic-python/
上一篇:翻译《Writing Idiomatic Python》(三):变量、字符串、列表
下一篇:翻译《Writing Idiomatic Python》(五):类、上下文管理器、生成器
2.4 字典
2.4.1 用字典实现switch...case
和其他许多语言不一样,Python不支持switch...case。switch通常用来判断一个表达式的值,并跳转到响应的case。这是一种实时从各种可能性中调用其中一种对应代码的便捷方式。例如,假设我们正在编写一个基于命令行的计算器程序,我们可能就需要switch语句来接收用户输入的运算符。“+”会对应调用addtion()函数,“*”则对应调用multiplication()函数,等等。
在Python中一个比较朴素的替代方案是用一连串的if...else语句来实现switch...case。这是一种比较古老的方案。所幸的是在Python中函数和对象一样也是一等公民,所以我们可以把函数和其他对象一样看成一个变量。这是一个非常有用的概念,许多其他很有用的概念都是基于这种第一级函数的概念建立起来的。
那么这种特性是如何帮助我们实现switch...case的呢?利用函数是第一级对象的特性,我们可以把函数作为值存储在一个字典里。具体回到计算器的例子中,把要进行的操作符作为字典的键,而对应的函数作为值,如此我们就实现了一个可读性良好的和switch...case同样的功能。
字典这种惯用法远不止是利用字符串键调用函数这么简单,而是可以被推广到所有能够作为字典键的情境中去。利用这种方法我们可以很轻易地创建工厂类,根据参数来初始化不同的类型。或是利用字典来存储状态和构建转化规则,进而构建一个状态机。在Python中,一旦你真正领会了“万物皆对象”的真谛,你会发现曾经在其他一些语言中你觉得很困难的事情,在Python中都能找到非常优雅的解决办法。
不良风格:
1 def apply_operation(left_operand, right_operand, operator): 2 if operator == '+': 3 return left_operand + right_operand 4 elif operator == '-': 5 return left_operand - right_operand 6 elif operator == '*': 7 return left_operand * right_operand 8 elif operator == '/': 9 return left_operand / right_operand
地道Python:
1 def apply_operation(left_operand, right_operand, operator): 2 import operator as op 3 operator_mapper = {'+': op.add, '-': op.sub, '*': op.mul, '/': op.truediv} 4 return operator_mapper[operator](left_operand, right_operand)
2.4.2 使用dict.get的默认参数提供默认值
在字典的get函数中有一个default参数常常在使用中被忽略。不使用这个方法的情况下,代码看上去可能会显得比较乱。
不良风格:
1 log_severity = None 2 if 'severity' in configuration: 3 log_severity = configuration['severity'] 4 else: 5 log_severity = 'Info'
地道Python:
1 log_severity = configuration.get('severity', 'Info')
2.4.3 使用字典解析高效而明确地创建字典
用Python的人可能大都知道列表解析,然而直到字典解析的人可能就没那么多。和列表解析的目的一样,字典解析利用很好理解的解析语法高效地构建字典。
不良风格:
1 user_email = {} 2 for user in users_list: 3 if user.email: 4 user_email[user.name] = user.email
地道Python:
1 user_email = {user.name: user.email for user in users_list if user.email}
2.5 集合
2.5.1 理解并应用数学意义上的集合操作
集合是一种很容易理解的数据结构。和字典很类似有键,但是没有值。集合类算是对可遍历和容器的一种实现,所以可以在被用于for和in语句中。
对于之前没有见过集合数据类型的程序员来说,可能会觉得没多大用处。而要真正理解集合的用处还是需要从先理解集合的数学概念。集合论是数学中一个专门研究集合的分支,理解集合最基本的数学/逻辑操作是用好集合类型的关键。
不用担心,使用好集合类型,你并不需要一个数学学位。只需要记住如下的简单操作:
并集 A | B
交集 A & B
差集 属于A但不属于B的元素集合,A - B (注意:根据定义和B-A不一定是一回事)
对称差集 属于A或B,但不同时属于A和B的元素集合, A ^ B
// 随便提一句,A^B==(A-B)|(B-A)
当处理列表数据时,一个常见的任务是找到在所有列表中都出现过的元素。任何时候你需要从两个或更多个序列中根据某种属性选择元素的时候,先想想用集合是不是能帮你办到这件事。
接下来是一些例子。
不良风格:
1 def get_both_popular_and_active_users(): 2 # Assume the following two functions each return a 3 # list of user names 4 most_popular_users = get_list_of_most_popular_users() 5 most_active_users = get_list_of_most_active_users() 6 popular_and_active_users = [] 7 for user in most_active_users: 8 if user in most_popular_users: 9 popular_and_active_users.append(user) 10 return popular_and_active_users
地道Python:
1 def get_both_popular_and_active_users(): 2 # Assume the following two functions each return a 3 # list of user names 4 return(set(get_list_of_most_active_users()) & set(get_list_of_most_popular_users()))
2.5.2 使用集合解析来产生集合
集合解析语法在Python中相对(列表解析和字典解析)较新。和列表解析以及字典解析目的一样,语法也几乎一样。
不良风格:
1 users_first_names = set() 2 for user in users: 3 users_first_names.add(user.first_name)
地道Python:
1 users_first_names = {user.first_name for user in users}
2.5.3 使用集合从一个可遍历容器中剔除重复的元素
在一个列表或字典中有重复的值是很常见的事情。比方说,在一个公司所有员工的姓氏列表中,我们几乎一定会发现出现超过一次以上的姓氏。如果我们需要一个列表只列出不重复的姓氏,那么就可以用集合来办到这件事,集合以下的特性使得它在一些问题中成为完美的解决方案:
1. 集合只包含不重复的元素
2. 向一个集合中加入一个已存在的元素的操作会被忽略
3. 一个集合可以从任何一个元素能够被散列化的可遍历结构来构建
回到上面提到的例子,我们也许需要一个显示函数,接收一个序列并且以某种格式显示其中的元素。那么在原来的列表基础上产生集合之后,我们是否需要相应地改变显示函数呢?
答案是不用。假设显示函数是以一种合理的方式实现的,那么集合应该可以直接直接替换列表。而这样的基础则是集合和列表以及字典douban相似的地方,比如可以用for遍历,可以用解析生成等。
不良风格:
1 unique_surnames = [] 2 for surname in employee_surnames: 3 if surname not in unique_surnames: 4 unique_surnames.append(surname) 5 6 def display(elements, output_format='html'): 7 if output_format == 'std_out': 8 for element in elements: 9 print(element) 10 elif output_format == 'html': 11 as_html = '<ul>' 12 for element in elements: 13 as_html += '<li>{}</li>'.format(element) 14 return as_html + '</ul>' 15 else: 16 raise RuntimeError('Unknown format {}'.format(output_format))
地道Python:
1 unique_surnames = set(employee_surnames) 2 3 def display(elements, output_format='html'): 4 if output_format == 'std_out': 5 for element in elements: 6 print(element) 7 elif output_format == 'html': 8 as_html = '<ul>' 9 for element in elements: 10 as_html += '<li>{}</li>'.format(element) 11 return as_html + '</ul>' 12 else: 13 raise RuntimeError('Unknown format {}'.format(output_format))
2.2.4 用format函数来进行字符串格式化
一般来说又三种方式来进行字符串格式化:最简单但是最不推荐的就是用+来连接字符串。另一种方式是老式的利用%来格式化字符串的办法,在其他许多语言中也能看到这种方式,比如一些语言中的printf。这种方法比用+的方法好一些。
在Python中,最地道和清晰的用法当属用format函数来进行字符串格式化。和老式的格式化方法类似,这种办法用带有格式的字符串作为模板并用一些值替换占位符生成最终字符串。比老式的格式化方法更好的地方是,在format函数中,我们可以命名占位符,获取占位符的对应变量的属性,控制字符宽度和填充等。format函数让字符串格式化显得简明。
不良风格:
1 def get_formatted_user_info_worst(user):
2 # Tedious to type and prone to conversion errors
3 return 'Name: ' + user.name + ', Age: ' + str(user.age) + ', Sex: ' + user.sex
4
5 def get_formatted_user_info_slightly_better(user):
6 # No visible connection between the format string placeholders
7 # and values to use. Also, why do I have to know the type?
8 # Don't these types all have __str__ functions?
9 return 'Name: %s, Age: %i, Sex: %c' % (user.name, user.age, user.sex)
地道Python:
1 def get_formatted_user_info(user):
2 # Clear and concise. At a glance I can tell exactly what
3 # the output should be. Note: this string could be returned
4 # directly, but the string itself is too long to fit on the
5 # page.
6 output = 'Name: {user.name}, Age: {user.age}, Sex: {user.sex}'.format(user=user)
7 return output
2.6 元组
2.6.1 使用collections.namedtuple让重度使用元组的代码更简明
元组在Python中是一种极为有用的结构。许多库都用元组来表示逻辑上类似电子表格的结构。比如说大部分数据库都使用元组来表示数据库表格中的一行。返回多行数据的查询通常用由元组组成的列表表示。
当这样使用元组时,每一列的索引事实上都对应一个具体的含义。在我们的数据库列表例子中,每个索引对应一个具体的列字段。在写代码的时候往往就需要你记住每一个索引对应的是哪列(比如,result[3]是表示工资的列),而这样做通常是非常容易出错的。
所幸的是,collection模块提供了一个优雅的解决方案:collection.namedtuple。namedtuple就是普通的元组多了些额外的功能,其中最重要的,namedtuple给了你通过名字而不是索引来访问字段的能力。
collections.namedtuple是用来增加代码可读性和可维护性的一个非常有力的工具。上面提到的例子只不过是collections.namedtuple众多优点的一个。
不良风格:
1 # Assume the 'employees' table has the following columns: 2 # first_name, last_name, department, manager, salary, hire_date 3 def print_employee_information(db_connection): 4 db_cursor = db_connection.cursor() 5 results = db_cursor.execute('select * from employees').fetchall() 6 for row in results: 7 # It's basically impossible to follow what's getting printed 8 print(row[1] + ', ' + row[0] + ' was hired ' 9 'on ' + row[5] + ' (for $' + row[4] + ' per annum)' 10 ' into the ' + row[2] + ' department and reports to ' + row[3])
地道Python:
1 # Assume the 'employees' table has the following columns: 2 # first_name, last_name, department, manager, salary, hire_date 3 from collections import namedtuple 4 5 EmployeeRow = namedtuple('EmployeeRow', ['first_name', 6 'last_name', 'department', 'manager', 'salary', 'hire_date']) 7 8 EMPLOYEE_INFO_STRING = '{last}, {first} was hired on {date} \ 9 ${salary} per annum) into the {department} department and reports to \ 10 ager}' 11 12 def print_employee_information(db_connection): 13 db_cursor = db_connection.cursor() 14 results = db_cursor.execute('select * from employees').fetchall() 15 for row in results: 16 employee = EmployeeRow._make(row) 17 18 # It's now almost impossible to print a field in the wrong place 19 print(EMPLOYEE_INFO_STRING.format( 20 last=employee.last_name, 21 first=employee.first_name, 22 date=employee.hire_date, 23 salary=employee.salary, 24 department=employee.department, 25 manager=employee.manager))
// collections里虽然很多使用的功能,但是似乎都有些鸡肋的感觉,比如这个功能其实我见到更多的人都是用pandas的dataframe。
2.6.2 使用_作为元组中需要忽略的值的占位符
当元组作为某些排列好的数据使用时,常常并不是每一个数据都真的需要使用。比起创建一些临时变量来取这些值的办法,用_能更明确地告诉读代码的人“这是不需要的数据”。
不良风格:
1 (name, age, temp, temp2) = get_user_info(user) 2 if age > 21: 3 output = '{name} can drink!'.format(name=name) 4 # "Wait, where are temp and temp2 being used?"
地道Python:
1 (name, age, _, _) = get_user_info(user) 2 if age > 21: 3 output = '{name} can drink!'.format(name=name) 4 # "Clearly, only name and age are interesting"
// 顺带一提,_在命令行模式下存储最近求的一次值
2.6.3 使用元组解包数据
和LISP中的destructuring-bind类似,在Python中可以通过赋值来解包数据。
不良风格:
1 list_from_comma_separated_value_file = ['dog', 'Fido', 10] 2 animal = list_from_comma_separated_value_file[0] 3 name = list_from_comma_separated_value_file[1] 4 age = list_from_comma_separated_value_file[2] 5 output = ('{name} the {animal} is {age} years old'.format(animal=animal, name=name, age=age))
地道Python:
1 list_from_comma_separated_value_file = ['dog', 'Fido', 10] 2 (animal, name, age) = list_from_comma_separated_value_file 3 output = ('{name} the {animal} is {age} years old'.format(animal=animal, name=name, age=age))
2.6.4 使用元组从函数中返回多个值
许多时候,我们会需要或是很希望能从一个函数中返回多个值。我看到过很多新手写的Python代码里因为误解Python只能返回一个值所以导致代码乱七八糟。尽管一个函数一次只能返回一个对象确实是事实,但是这个对象本身也能包含多个返回值。而元组就是最适合于从函数中返回多个值的方式。这在Python中是一个约定俗成的惯用法,在许多标准库或是第三方库中都能见到这样的用法。
不良风格:
1 from collections import Counter 2 3 STATS_FORMAT = """Statistics: 4 Mean: {mean} 5 Median: {median} 6 Mode: {mode}""" 7 8 def calculate_mean(value_list): 9 return float(sum(value_list) / len(value_list)) 10 11 def calculate_median(value_list): 12 return value_list[int(len(value_list) / 2)] 13 14 def calculate_mode(value_list): 15 return Counter(value_list).most_common(1)[0][0] 16 17 values = [10, 20, 20, 30] 18 mean = calculate_mean(values) 19 median = calculate_median(values) 20 mode = calculate_mode(values) 21 22 print(STATS_FORMAT.format(mean=mean, median=median, mode=mode))
地道Python:
1 from collections import Counter 2 3 STATS_FORMAT = """Statistics: 4 Mean: {mean} 5 Median: {median} 6 Mode: {mode}""" 7 8 def calculate_staistics(value_list): 9 mean = float(sum(value_list) / len(value_list)) 10 median = value_list[int(len(value_list) / 2)] 11 mode = Counter(value_list).most_common(1)[0][0] 12 return (mean, median, mode) 13 14 (mean, median, mode) = calculate_staistics([10, 20, 20, 30]) 15 print(STATS_FORMAT.format(mean=mean, median=median, mode=mode))
转载请注明出处:達聞西@博客园