如何编写优雅(地道)的Python代码 - 第二部分

2. 使用数据

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 python的风格

some_other_list = range(10)
some_list = [element + 5
                for element in some_other_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 python的风格

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

2.2 字典

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

在dict.get的定义中经常被忽略的是默认参数。没有使用默认(或collections.defaultdict类)值,代码将会被if语句搞晕。记住,要力求清晰。

+

2.2.1.1 不好的风格

log_severity = None
if 'severity' in configuration:
    log_severity = configuration['severity']
else:
    log_severity = 'Info'

2.2.1.2 python的风格

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

2.2.2 使用字典推导来更清晰和有效地构建字典

列表推导是python知名的构造方式,鲜为人知的是字典推导。不过,目的都是一样的:使用容易理解的推导语法构造字典。

2.2.2.1 不好的风格

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

2.2.2.2 python的风格

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

2.3 字符串

2.3.1 倾向使用format函数构造字符串

有三种方式格式化字符串(就是创建一个由硬编码字符串和字符串变量混合而成的字符串)。最容易不过不好的方式是通过+运算符连接静态字符串和变量。使用“老式”的字符串格式化方法相对好一点。它就其它语言的printf一样,使用格式字符串和%运算符来填充值。格式化字符串的最清晰和最习惯的方式是使用format函数。就像老式的格式化一样,它利用了格式字符串并用值来替换其中的占位符。两者相似之处仅此而言。通过format函数,代码可以使用具名占位符、访问其中的属性、控制填充空白和字符串宽度,以及其它内容等。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
    # and values to use. Also, why do I have to know the type?
    # 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 python的风格

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
    # directly, but the string itself is too long to fit on the
    # page.
    output = 'Name: {user.name}, Age: {user.age}'
        ', Sex: {user.sex}'.format(user=user)
    return output

2.3.2 使用.join函数将列表元素转换为单一字符串

这种方式更快,占用更少内存,很多地方都在使用。请注意,两个引号代表想要连接成字符串的列表元素之间的分隔符。''表示连接的列表元素之间没有任何字符内容。

2.3.2.1 不好的风格

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

2.3.2.2 python的风格

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 python的风格

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

2.4 类

2.4.1 在函数名和变量名中使用下划线来帮助标记为私有数据

Python类中的所有属性,不论是数据还是函数,本质上都是公有的。用户可以在类定义完成后自动地添加属性。此外,如果这个类是为了继承而来的,子类可能在不知不觉中更改基类的属性。最后,告知用户关于类的某些部分逻辑上公开的(不会变得向后不兼容),而其它属性是纯粹的内部实现,不应该被使用本类的客户端代码直接使用,关于这一点是非常有用的。

一些被广泛遵循的约定已经出现,使得作者的意图更加明确,有助于避免无意的命名冲突。于这两种用法,虽然普遍认为是公共约定,但是事实上在使用中会使解释器也产生不同的行为。

首先,用单下划线开始命名的表明是受保护的属性,用户不应该直接访问。其次,用两个连续地下划线开头的属性,表明是私有的,即使子类都不应该访问。当然了,这些更多的是约定,并不能真正阻止用户访问到这些私有的属性,但这些约定在整个Python社区中被广泛使用,不太可能遇到那些故意不遵循的开发者。从某种角度上来说这也是Python里用一种办法完成一件事情的哲学体现。

之前,本文提到过单下划线和双下划线不仅仅是使用习惯问题,一些开发者意识到这种写法是有实际作用的。以单下划线开头的变量在import *时不会被导入。以双下划线开头的变量则会触发Python中name mangling,如果Foo是一个类,那么Foo中定义的__bar()方法将会被展开成_classname__attributename.

2.4.1.1 不好的风格

class Foo():
    def __init__(self):
        self.id = 8
        self.value = self.get_value()
    def get_value(self):
        pass
    def should_destroy_earth(self):
        return self.id == 42

class 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.
        """
        pass
class 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 python的风格

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
    # ours, we're fine
    __get_value = get_value
class Baz(Foo):
    def get_value(self, some_new_parameter):
            pass
class 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 now
q.should_destroy_earth() # returns False
q.id == 42 # returns True
with pytest.raises(AttributeError):
    getattr(q, '__id')

2.4.2 在类中定义str方法用于可读性显示

当定义可能被print()方法使用的类时,默认的Python呈现方法帮助不大。通过定义str方法,可以控制类实例打印呈现出来的内容。

2.4.2.1 不好的风格

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

2.4.2.2 python的风格

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. 可迭代容器中可以hash的元素都可以构建到集合中

继续上面的例子,有一个接受序列参数的display函数,可以以某一格式显示参数中的元素。从原始列表创建集合后,是否需要改变display函数?

答案是:否。假定我们的display函数实现是合理的,那么集合可以直接替换列表。这得益于集合事实上像列表一样,是可迭代的,可以在循环、列表推导中等使用。

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 python的风格

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 python的风格

users_first_names = {user.first_name for user in users}

2.5.3 理解和使用数学集合操作

集合是非常容易理解的数据结构。集合类似有索引没有值的字典,集合类实现了Iterable和Container接口。因此,集合可以在for循环和in语句中使用。

之前不了解集合数据类型的程序员,可能不能灵活运用。了解它们的关键是了解他们的数学起源。集合论是学习集合set的数学基础,了解基本的数学集合操作是发挥集合set能力的关键。

不用担心,无需深入理解和使用数学上的集合。仅需要记住简单的几个操作:

并集 集合A和B中的元素,或者说A和B中所有的元素(Python中写法:A | B)

交集 A和B中同时包含的元素(Python中写法:A & B)

+

补集(差集) A中但是不再B中的元素(Python中写法:A - B)

注意:这里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()
    popular_and_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 python的风格

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 使用生成器惰性加载无穷序列

通常能够提供一种可以迭代无穷序列的方式是很有用的,否则,需要提供一个异常昂贵开销的接口来实现,并且用户还需花时间来等待列表的构建。

面临这些情况,使用生成器就很有帮助。生成器是协程的一种特殊类型,它返回iterable类型。生成器的状态会被保存,当再次调用生成器时,可以继续上次离开时的状态。下面的例子,介绍了如何生用生成器处理上面提到的情况。

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 python的风格

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 out
for 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 倾向使用生成器表达式到列表推导中用于简单的迭代

在处理序列时,通常需要在轻微修改的序列版本上迭代一次。譬如,你可以需要以首字母大写的方式打印所有用户的姓。

第一本能可能是就地构建并迭代序列。列表推导可能是理想的,不过Python有个内建的更好的处理方式:生成器表达式。

有什么不同?列表推导构建列表对象并立即填充所有的元素。对于大型的列表,可能非常耗资源。生成器返回生成器表达式,换句话说,按需产生每一个元素。你想要大写字母转换的列表?问题不大。但是,如果你想要写出国会图书馆中已知的每本书的标题时,可能在列表推导的过程中就会耗尽系统内存,而生成器表达式可以泰然处之。生成器表达式工作的逻辑扩展方式使得我们可以把它们应用到无穷序列中。

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 python的风格

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操作。

+

看一下下面不好的代码。如果发生了异常会,怎么样?因为代码中没有捕获异常,异常会向上传递。代码中的退出点可能被跳过,导致没法关闭已经打开的文件。

标准库中有很多支持和使用上下文管理器的类。此外,通过定义enterexit方法,用户自定义类也可以很容易地支持上下文管理器。函数可以通过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 python的风格

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 使用元组解包(unpack)数据

在Python中,可使用解包数据实现多重赋值。这类似于LISP中的desctructuring bind。

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 python的风格

list_from_comma_separated_value_file = ['dog', 'Fido', 10]
(animal, name, age) = list_from_comma_separated_value_file
output = ('{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 python的风格

(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 = foo
foo = bar
bar = temp

2.9.1.2 python的风格

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

猜你喜欢

转载自blog.csdn.net/zhujf21st/article/details/79125973