第二章:处理数据

来源:互联网 发布:萧瀚 李静睿 知乎 编辑:程序博客网 时间:2024/05/22 03:25

2.1 列表

2.1.1 使用列表推导式将一个已经存在的列表转化为一个新的列表

利用已存在的数据创建新的列表时,合理使用列表推导式将增加代码的清晰度,尤其需要对迭代器中元素进行判断和转换时,这一点体现的更为明显。

由于cPython解释器的优化,使用列表表达式(或者生成式表达式)将提高程序的性能。

2.1.1.1 糟糕的写法

some_other_list = range(10)some_list = list()for element in some_other_list:    if is_prime(element):        some_list.append(element + 5)

2.1.1.2 地道的表达

some_orther_list = range(10)some_list = [element + 5             for element in some_orther_list            if is_prime(element)]

2.1.2 使用*操作符代表列表中的“剩余元素”

通常情况下,尤其在处理函数参数时,提取列表的几个起始(或者结尾)元素,同时保留其它元素以后使用很实用。python2只能通过切片来实现这一功能。Python3允许你在代表剩余元素的变量左边使用*操作符来实现这一功能。

2.1.2.1 糟糕的写法

some_list = ['a', 'b', 'c', 'd', 'e'](first, second, rest) = some_list[0], some_list[1], some_list[2:] print(rest)(first, middle, last) = some_list[0], some_list[1:-1], some_list[-1] print(middle)(head, penultimate, last) = some_list[:2], some_list[-2], some_list[-1]print(head)

2.1.2.2 地道的表达

some_list = ['a', 'b', 'c', 'd', 'e'](first, second, *rest) = some_listprint(rest)(first, *middle, last) = some_listprint(middle)(*head, penultimate, last) = some_listprint(head)

2.2 字典

2.2.1 使用dict.get的默认参数提供默认值

在dict.get的定义中经常忽略默认参数。不使用默认参数你的代码里将充满令人困惑的if语句。时刻记住:尽力使代码更清晰。

2.2.1.1 糟糕的写法

log_severity = Noneif 'severity' in configuration:    log_severity = configuration['severity']else:    log_severity = 'Info'

2.2.1.2 地道的表达

log_severity = configuration.get('severity', 'Info')

2.2.2 使用dict推导式清晰高效的创建一个dict

列表推导式是Python中著名的概念。很少有人知道dict推导式。他们的用途是一样的:使用广为所知的推导式语法构造字典。

2.2.2.1 糟糕的写法

user_email = {}for user in users_list:    if user.email:        user_email[user.name] = user.email

2.2.2.2 地道的表达

user_email = {user.name : user.email             for user in users_list if user.email}

2.3 字符串

2.3.1 优先使用format函数格式化字符串

有三种方法格式化字符串。简单但是同时也是最糟糕的是使用+操作符连接静态的字符串和变量。使用“old-style” 格式化字符串稍微好点,它使用一个格式化字符串然后用%填充值,就像其他语言中printf函数中做的那样。

最清新最地道的格式化字符串的方法是使用format函数。像“old-style”它采用格式化字符串然后用值去替换占位符,访问它们的属性,控制冗余和字符串的宽度,在一个长字符串中。format函数使字符串格式化更简洁。

2.3.1.1 糟糕的写法

def get_formatted_user_info_worst(user):    # Tedious to type and prone to conversion errors    return 'Name: ' + user.name + ', Age: ' + \            str(user.age) + ', Sex: ' + user.sex def get_formatted_user_info_slightly_better(user):     # No visible connection between the format string placeholders     # Don't these types all have __str__ functions?     return 'Name: %s, Age: %i, Sex: %c' % (         user.name, user.age, user.sex)

2.3.1.2 地道的表达

def get_formatted_user_info(user):    # Clear and concise. At a glance I can tell exactly what     # the output should be. Note: this string could be returned     # page.    output = 'Name: {user.name}, Age: {user.age}'     ', Sex: {user.sex}'.format(user=user)     return output

2.3.2 当为列表中的元素创建一个字符串使用’’.join

这种方式更快,占用更少的内存,你到处都能看到它的存在。两个单引号代表列表字符串中元素之间的分隔符。’’仅仅意味着我们希望连接这些元素,中间不使用任何字符。

2.3.2.1 糟糕的写法

return_list = ['True', 'False', 'File not found'] result_string = '' for result in result_list:     result_string += result

2.3.2.2 地道的表达

result_list = ['True', 'False', 'File not found'] result_string = ''.join(result_list)

2.3.3 链式字符串函数使一系列简单的转化更清晰

对一些数据应用一系列简单的转换,在一个表达式里应用链式调用比每一步转换创建一个临时变量更清晰。一个好的经验:“在一个表达式中不要超过三次链式调用。”

2.3.3.1 糟糕的写法

book_info = ' The Three Musketeers: Alexandre Dumas' formatted_book_info = book_info.strip() formatted_book_info = formatted_book_info.upper() formatted_book_info = formatted_book_info.replace(':', ' by')

2.3.3.2 地道的表达

book_info = ' The Three Musketeers: Alexandre Dumas' formatted_book_info = book_info.strip().upper().replace(':', ' by')

2.4 类

2.4.1 在函数和变量名中用下划线来标记其属于私有数据

类中的所有属性,无论是数据还是函数,在Python中先天为可的。一个类被定义后客户端可以不受限制的向类中添加新的属性。并且,如果这个类可以被继承,子类会无意间改变父类的属性。最后,给你类的使用者一些标志,部分是逻辑可见的,然而其它的是内部实现,不可以被使用该类的客户端代码直接使用。

一些被广泛遵循的约定随之兴起,可以使作者的意图更清晰,而且可以避免无意的命名冲突。以下作为’nothing more than conventions’经常被提前的两个习惯。事实上,这两点在使用时改变了编译器的行为。

首先,声明为protected的属性,不能被客户端直接使用,在名字前加上一个_作为前缀。其次,不能被子类访问的私有属性加两个_作为前缀。当然,这仅仅是约定。没有什么能够阻挡客户端访问你的私有属性,但是这个约定被广泛使用,你很难遇到一个故意不这样做的开发者。这是Python开发者社区选择唯一方式完成某事的又一个例子。

之前,我提到加一个或者两个_作为前缀仅仅是约定。很少有开发者意识到一个类中带有前缀的属性名实际做了什么。在属性名字加前缀在一个类中实际做了什么。在名字前面加单个_意味着all被使用时,不会被导入。在导入语句中使用all属性起作用时(即from module import *),这个属性不会被导入。单个_是这样一个标志,性名前加两个_引起python的名字改变。这带来一个影响,子类不会无意间替换父类的属性。如果Foo是一个类,def __bar() 将变为 _classname__attributename.

2.4.1.1 糟糕的写法

class Foo():    def __init__(self):        self.id = 8        self.value = self.gete_value()    def get_value(self):        pass    def should_destory_earth(self):        return self.id == 42class Baz(Foo):    def get_value(self, some_new_parameter):        """Since 'get_value' is called from the base class's __init__ method and the base class definition doesn't take a parameter, trying to create a Baz instance will fail.         """         passclass Qux(Foo):    """We aren't aware of Foo's internals, and we innocently create an instance attribute named 'id' and set it to 42. This overwrites Foo's id attribute and we inadvertently blow up the earth.     """     def __init__(self):        super(Qux, self).__init__()        self.id = 42        # No relation to Foo's id, purely coincidental q = Qux() b = Baz() # Raises 'TypeError' q.should_destroy_earth() # returns True q.id == 42 # returns True

2.4.1.2 地道的写法

class Foo():    def __init__(self):        """Since 'id' is of vital importance to us, we don't want a derived class accidentally overwriting it. We'll prepend with double underscores to introduce name mangling.         """         self.__id = 8         self.value = self.__get_value() # Our 'private copy'    def get_value(self):        pass    def should_destroy_earth(self):         return self.__id == 42         # Here, we're storing an 'private copy' of get_value,         # and assigning it to '__get_value'. Even if a derived         # class overrides get_value is a way incompatible with         __get_value = get_value class Baz(Foo):         def get_value(self, some_new_parameter):         passclass Qux(Foo):     def __init__(self):         """Now when we set 'id' to 42, it's not the same 'id' that 'should_destroy_earth' is concerned with. In fact, if you inspect a Qux object, you'll find it doesn't have an __id attribute. So we can't mistakenly change Foo's __id attribute even if we wanted to.         """         self.id = 42        # No relation to Foo's id, purely coincidental         super(Qux, self).__init__() q = Qux()b = Baz() # Works fine nowq.should_destory_earth() # returns False q.id == 42 # returns True with pytest.raises(AttributeError):    getattr(q, '__id')

2.4.2 在类中定义__str__显示可读懂的描述

当我们定义的类可能使用print(),Python默认的描述不会有太大帮助。通过定义一个__str__方法,当一个实例调用print方法时,你可以控制输出的具体内容。

2.4.2.1 糟糕的代码

class Point():    def __init__(self, x, y):        self.x = x        self.y = yp = Point(1, 2)print(p)# Prints '<__main__.Point object at 0x91ebd0>'

2.4.2.2 地道的表达

class Point():    def __init__(self, x, y):        self.x = x        self.y = y    def __str__(self):        return '{0}, {1}'.format(self.x, self.y)p = Point(1, 2)print(p)# Prints '1,  2'

2.5 集合

2.5.1 使用集合消除可迭代容器中的重复元素

列表和字典里可以有重复的元素。一个大公司所有员工名字构成的列表,一定会遇到不止一个常见的姓氏。如果我们想得到一个只包含唯一姓氏的清单。我们可以使用集合。集合主要有三个功能:
1. 集合中只能包含唯一的元素没有重复元素
2. 向集合里添加已存在元素的操作将被忽略
3. 集合可以由任何一个元素可以被散列化的可迭代容器来构造

继续上面的例子,我们有一个名为display的函数,该函数接收一个序列用。并用特定的格式显示其中的元素。从原始列表创建一个集合之后,我们需要改变display函数吗?

不。假设我们的display函数已经被合理的实现,我们的集合可以作为列表的随机取代来使用。这样可以工作是因为集合和列表一样都是可迭代的,可以用在for循环,列表推导式中。

2.5.1.1 糟糕的写法

unique_surnames = [] for surname in employee_surnames:     if surname not in unique_surnames:         unique_surnames.append(surname)def display(elements, output_format='html'):     if output_format == 'std_out':         for element in elements:             print(element)    elif output_format == 'html':         as_html = '<ul>'        for element in elements:                as_html += '<li>{}</li>'.format(element)         return as_html + '</ul>'     else:        raise RuntimeError('Unknown format {}'.format(output_format))

2.5.1.2 地道的表达

unique_surnames = set(employee_surnames) def display(elements, output_format='html'):     if output_format == 'std_out':         for element in elements:             print(element)     elif output_format == 'html':         as_html = '<ul>'        for element in elements:              as_html += '<li>{}</li>'.format(element)         return as_html + '</ul>'     else:        raise RuntimeError('Unknown format {}'.format(output_format))

2.5.2 使用集合推导式简洁的生成集合

集合推导式是一个新的Python语法,因此,很少有人知道。就像列表可以由列表推导式生成,集合也可以由集合推导式生成。事实上,语法是一样的。

2.5.2.1 糟糕的写法

users_first_names = set() for user in users:     users_first_names.add(user.first_name)

2.5.2.2 地道的表达

users_first_names = {user.first_name for user in users}

2.5.3 理解并使用数学上的集合操作

集合是一种很容易理解的数据结构。就像一个没有值的字典,集合类实现了Iterable和Container接口,因此,集合可以被用在for循环中,或者作为in语句的对象。

对于一些不知道数学上集合概念的程序员来说可能会影响对Python集合的使用。关键是理解集合在数学中的用途。理解数学中基本的集合操作是掌握集合类的关键。

不用担心,你不需要很深入的掌握数学中的集合。只需要知道一些简单的操作就可以了:
并:元素在集合A,集合B,或者既在A中又在B中(在Python中记作A|B)
交:元素既在A中又在B中(在Python中记作A & B)
不同:在集合A中但是不在集合B中元素(在Python中记作A - B)
注:A – B 和 B – A 不同

对称不同:在集合A或者在集合B中的元素,但是不同时在A和B中的元素(在Python中记作A ^ B)

当处理列表数据时,一个常用的工作查找所有列表中出现的元素。任何时候想要基于序列之间的关系从两个或者更多的序列中选择一些元素,一定些典型的例子。下面是一些典型的例子。

2.5.3.1 糟糕的代码

def get_both_popular_and_active_users():     # Assume the following two functions each return a     # list of user names     most_popular_users = get_list_of_most_popular_users()     most_active_users = get_list_of_most_active_users()     for user in most_active_users:         if user in most_popular_users:             popular_and_active_users.append(user)     return popular_and_active_users

2.5.3.2 地道的表达

def get_both_popular_and_active_users():     # Assume the following two functions each return a     # list of user names     return(set(         get_list_of_most_active_users()) & set(             get_list_of_most_popular_users()))

2.6 生成器

2.6.1 使用生成器懒加载无穷序列

生成器提供了一种很有用的迭代无穷序列的方式。当然,你也可以为序列提供一个接口,但是这样计算起来非常慢,你肯定不想你的用户坐在那里等你完成一个列表的构建。

有两种情况,生成器是你的好帮手。生成器是一种特殊类型的协程,它返回一个可迭代的对象。生成器的状态被保存,所以下次调用生成器将从上次停止的地方继续。在下面的例子中我们将看到,针对我们上述提到的每一种情况,迭代器怎么给我们提供帮助。

2.6.1.1 糟糕的代码

def get_twitter_stream_for_keyword(keyword):     """Get's the 'live stream', but only at the moment     the function is initially called. To get more entries,     the client code needs to keep calling     'get_twitter_livestream_for_user'. Not ideal.     """      imaginary_twitter_api = ImaginaryTwitterAPI()      if imaginary_twitter_api.can_get_stream_data(keyword):          return imaginary_twitter_api.get_stream(keyword) current_stream = get_twitter_stream_for_keyword('#jeffknupp') for tweet in current_stream:     process_tweet(tweet) # Uh, I want to keep showing tweets until the program is quit. # What do I do now? Just keep calling # get_twitter_stream_for_keyword? That seems stupid. def get_list_of_incredibly_complex_calculation_results(data):     return [first_incredibly_long_calculation(data),         second_incredibly_long_calculation(data),         third_incredibly_long_calculation(data),         ]

2.6.1.2 地道的表达

def get_twitter_stream_for_keyword(keyword):     """Now, 'get_twitter_stream_for_keyword' is a generator    and will continue to generate Iterable pieces of data    one at a time until 'can_get_stream_data(user)' is     False (which may be never).     """     imaginary_twitter_api = ImaginaryTwitterAPI()    while imaginary_twitter_api.can_get_stream_data(keyword):          yield imaginary_twitter_api.get_stream(keyword) # Because it's a generator, I can sit in this loop until # the client wants to break outfor tweet in get_twitter_stream_for_keyword('#jeffknupp'):    if got_stop_signal:        break     process_tweet(tweet)def get_list_of_incredibly_complex_calculation_results(data):     """A simple example to be sure, but now when the client code    iterates over the call to     'get_list_of_incredibly_complex_calculation_results',      we only do as much work as necessary to generate    the current item.    """    yield first_incredibly_long_calculation(data)    yield second_incredibly_long_calculation(data)    yield third_incredibly_long_calculation(data)

2.6.2 在简单的迭代中优先使用生成器表达式而不是列表表达式

处理序列的时候,通常在每一次迭代中需要改变序列中的元素。例如,你想用大写字母打印用户列表中的first name。

你最初想到的是迭代序列替换掉每一个元素。列表表达式看起来是理想的,但是python内建了一个更好的实现:生成器表达式。

两者之间的主要区别是什么?列表推导式生成一个list对象,立即填充所有元素。对于规模很大的列表这样做代价搞得离谱。通过生成器表达式生成的生成器,相反的,按需要生成每一个元素。在大写用户民的例子中,你想输出什么?这不是一个问题。但是如果你想要输出国会图书馆里每本书的书名呢?生成列表表达式会耗尽你的内存,而生成器表达式将不动声色的完成。生成器工作方式的逻辑上的扩展,你可以在无穷序列中用它们。

2.6.2.1 糟糕的写法

for uppercase_name in [name.upper() for name in get_all_usernames()]:   process_normalized_username(uppercase_name)

2.6.2.2 地道的表达

for uppercase_name in (name.upper() for name in get_all_usernames()):   process_normalized_username(uppercase_name)

2.7 上下文管理

2.7.1 使用上下文管理确保资源被恰当管理

就像在C++和D中使用的RAII原则,上下文管理(对象意味着使用with语句)可以使资源管理安全、清晰。典型的案是文件IO。

看一下下面糟糕的代码。如果raise_exception存在将会发生什么,引起一个异常吗?由于我们在下面的代码中没有捕获它,异常将被传递到栈。如果我们在代码中设置一个出口点,可能被忽略,我们没有办法关闭已经打开的文件。

标准库中有大量的类支持上下文管理。用户自定义的类可以很容易的通过定义__enter__和__exit__方法。通过Contextlib模块,函数可以被包括上下文管理。

2.7.1. 1糟糕的写法

file_handle = open(path_to_file, 'r')for line in file_handle.readlines():    if raise_exception(line):        print('No! An Exception!')

2.7.1.2 地道的表达

with open(path_to_file, 'r') as file_handle:    for line in file_handle:        if raise_exception(line):            print('No! An Exception!')

2.8 元组

2.8.1 利用元组取数据

在python中可以为多元数据取值。像lisp中的解构绑定。

2.8.1.1 糟糕的写法

list_from_comma_separated_value_file = ['dog', 'Fido', 10]animal = list_from_comma_separated_value_file[0]name = list_from_comma_separated_value_file[1]age = list_from_comma_separated_value_file[2]output = ('{name} the {animal} is {age} years old'.format(    animal=animal, name=name, age=age))

2.8.1.2 地道的表

list_from_comma_separated_value_file = ['dog', 'Fido', 10](animal, name, age) = list_from_comma_separated_value_fileoutput = ('{name} the {animal} is {age} years old'.format(    animal=animal, name=name, age=age))

2.8.2 使用_作为元组中数据占位符将会被忽略

设置元组等价于将数据排序,并不是所有的数据都需要这样做。取代创建带有令人困惑的名字的一次丢掉的变量,使用_告诉读者,“这些数据将被丢弃”

2.8.2.1 糟糕的写法

(name, age, temp, temp2) = get_user_info(user)if age > 21:    output = '{name} can drink!'.format(name=name)# "Wait, where are temp and temp2 being used?"

2.8.2.2 地道的表达

(name, age, _, _) = get_user_info(user)if age > 21:    output = '{name} can drink!'.format(name=name)# "Clearly, only name and age are interesting"

2.9 变量

2.9.1 交换两个值时避免使用临时变量

在Python中交换两个值没有理由使用临时变量。我们可以使用元组更清晰的实现我们的意图。

2.9.1.1 糟糕的写法

foo = 'Foo'bar = 'Bar'temp = foofoo = barbar = temp

2.9.1.2 地道的表达

foo = 'Foo'bar = 'Bar(foo, bar) = (bar, foo)