Python 整洁编程(一)
原文:Clean Python
一、Pythonic 式思维
Python 与其他语言的不同之处在于,它是一种简单而有深度的语言。因为简单,所以谨慎编写代码要重要得多,尤其是在大项目中,因为代码很容易变得复杂臃肿。Python 有一种哲学,叫做 Python 的禅,强调简单胜于复杂。 1
在这一章中,你将学习一些常见的实践,这些实践可以帮助你使你的 Python 代码更易读和更简单。我将介绍一些众所周知的实践,以及一些不太为人所知的实践。在编写下一个项目或处理当前项目时,请确保您非常了解这些 Python 实践,以便您可以改进代码。
注意
在 Python 世界中,遵循 Python 哲学的禅使您的代码“Python 化”Python 官方文档中推荐了许多好的实践,以使您的代码更整洁、可读性更好。阅读 PEP8 指南肯定会帮助你理解为什么推荐一些做法。
编写 Pythonic 代码
Python 有一些名为 PEP8 的官方文档,定义了编写 python 代码的最佳实践。这种风格指南随着时间的推移不断发展。您可以在 https://www.python.org/dev/peps/pep-0008/ 查看。
在这一章中,你将关注 PEP8 中定义的一些常见实践,并了解作为一名开发人员,遵循这些规则将如何使你受益。
命名
作为一名开发人员,我使用过不同的语言,比如 Java、NodeJS、Perl 和 Golang。所有这些语言对于变量、函数、类等等都有命名约定。Python 也推荐使用命名约定。在这一节中,我将讨论一些在编写 Python 代码时应该遵循的命名约定。
变量和函数
您应该用小写字母命名函数和变量,用下划线分隔单词,这样可以提高可读性。参见清单 1-1 。
names = "Python" # variable name
job_title = "Software Engineer" # variable name with underscore
populated_countries_list = [] # variable name with underscore
Listing 1-1Variable Names
您还应该考虑在代码中使用非欺诈方法名称,并使用一个下划线(_)或两个下划线(__)。参见清单 1-2 。
_books = {} # variable name to define private
__dict = [] # prevent name mangling with python in-build lib
Listing 1-2Nonmangling Names
您应该使用一个下划线(_)作为类的内部变量的前缀,而不希望外部类访问该变量。这只是一个惯例;Python 不会将只有一个下划线前缀的变量设为私有变量。
Python 对函数也有约定,如清单 1-3 所示。
# function name with single underscore
def get_data():
---
---
def calculate_tax_data():
----
Listing 1-3Normal Function Names
同样的规则也适用于私有方法和希望防止名称与内置 Python 函数混淆的方法。参见清单 1-4 。
# Private method with single underscore
def _get_data():
---
---
# double underscore to prevent name mangling with other in-build functions
def __path():
----
----
Listing 1-4Function Names to Represent Private Methods and Nonmangling
除了遵循这些命名规则之外,为函数或变量使用特定的名称而不是晦涩的名称也很重要。
让我们考虑一个函数,当提供一个用户 ID 时,它返回一个用户对象。参见清单 1-5 。
# Wrong Way
def get_user_info(id):
db = get_db_connection()
user = execute_query_for_user(id)
return user
# Right way
def get_user_by(user_id):
db = get_db_connection()
user = execute_user_query(user_id)
return user
Listing 1-5Function Names
在这里,第二个函数get_user_by确保使用相同的词汇传递变量,这为函数提供了正确的上下文。第一个函数get_user_info是不明确的,因为参数id可能意味着任何东西。是用户表索引 ID 还是用户付费 ID 还是其他什么 ID?这种代码可能会给使用您的 API 的其他开发人员造成混乱。为了解决这个问题,我在第二个函数中改变了两件事;我更改了函数名并传递了一个参数名,这使得代码可读性更好。当你读第二个函数时,你马上就知道这个函数的目的和期望值。
作为开发人员,您有责任在命名变量和函数时仔细考虑,以使代码对其他开发人员可读。
班级
像在大多数其他语言中一样,类的名字应该是驼色的。清单 1-6 显示了一个简单的例子。
class UserInformation:
def get_user(id):
db = get_db_connection()
user = execute_query_for_user(id)
return user
Listing 1-6Class Names
常数
你应该用大写字母定义常量名。清单 1-7 给出了一个例子。
TOTAL = 56
TIMOUT = 6
MAX_OVERFLOW = 7
Listing 1-7Constant Names
函数和方法参数
函数和方法参数应该遵循与变量和方法名称相同的规则。与不将self作为关键字参数传递的函数相比,类方法将self作为第一个关键字参数。参见清单 1-8 。
def calculate_tax(amount, yearly_tax):
----
class Player:
def get_total_score(self, player_name):
----
Listing 1-8Function and Method Arguments
代码中的表达式和语句
在某些时候,你可能试图用一种聪明的方式来编写代码,以节省一些代码行或者给你的同事留下深刻印象。然而,编写聪明的代码是有代价的:可读性和简单性。让我们来看看清单 1-9 中的这段代码,它对嵌套字典进行排序。
users = [{"first_name":"Helen", "age":39},
{"first_name":"Buck", "age":10},
{"first_name":"anni", "age":9}
]
users = sorted(users, key=lambda user: user["first_name"].lower())
Listing 1-9Sort a Nested Dictionary
这个代码有什么问题?
嗯,你在一行中使用 lambda 通过first_name对这个嵌套的字典进行排序,这看起来像是一个对字典进行排序的聪明方法,而不是使用循环。
然而,乍一看理解这段代码并不容易,尤其是对于新开发人员来说,因为 lambdas 由于其古怪的语法而不是一个容易掌握的概念。当然,这里使用 lambda 可以节省行数,因为它允许你以巧妙的方式对字典进行排序;然而,这并不能使代码正确或可读。这段代码无法解决诸如缺少键或者字典是否正确之类的问题。
让我们用一个函数重写这段代码,尽量让代码更易读、更正确;该函数将检查所有意外值,并且编写起来简单得多。参见清单 1-10 。
users = [{"first_name":"Helen", "age":39},
{"first_name":"Buck", "age":10},
{"name":"anni", "age":9}
]
def get_user_name(users):
"""Get name of the user in lower case"""
return users["first_name"].lower()
def get_sorted_dictionary(users):
"""Sort the nested dictionary"""
if not isinstance(users, dict):
raise ValueError("Not a correct dictionary")
if not len(users):
raise ValueError("Empty dictionary")
users_by_name = sorted(users, key=get_user_name)
return users_by_name
Listing 1-10Sorted Dictionary by Function
如您所见,这段代码检查所有可能的意外值,比前面的一行代码可读性更好。一行代码节省了代码行,但是增加了代码的复杂性。那不一定意味着单行代码不好;我在这里想说的是,如果你的一行代码使代码更难阅读,请避免使用它。
在编写代码时,你必须有意识地做出这些决定。有时候写一行代码可以让你的代码可读,有时候不行。
让我们再考虑一个例子,您试图读取一个 CSV 文件,并计算 CSV 文件处理的行数。清单 1-11 中的代码向您展示了为什么让您的代码可读是重要的,以及命名在让您的代码可读中是如何发挥重要作用的。
当您在生产代码中遇到特定的错误时,将代码分解成 helper 函数有助于使复杂的代码可读并易于调试。
import csv
with open("employee.csv", mode="r") as csv_file:
csv_reader = csv.DictReader(csv_file)
line_count = 0
for row in csv_reader:
if line_count == 0:
print(f'Column names are {", ".join(row)}')
line_count += 1
print(f'\t{row["name"]} salary: {row["salary"]}'
f'and was born in {row["birthday month"]}.')
line_count += 1
print(f'Processed {line_count} lines.')
Listing 1-11Reading a CSV File
这里,代码在with语句中做了多件事。为了提高可读性,您可以将带有 process salary的代码从 CSV 文件中提取出来,放到不同的函数中,以减少出错的可能性。当许多事情在几行代码中进行时,很难调试这种代码,所以在定义函数时,您需要确保有明确的目标和界限。因此,让我们在清单 1-12 中进一步分解它。
import csv
with open('employee.txt', mode="r") as csv_file:
csv_reader = csv.DictReader(csv_file)
line_count = 0
process_salary(csv_reader)
def process_salary(csv_reader):
"""Process salary of user from csv file."""
for row in csv_reader:
if line_count == 0:
print(f'Column names are {", ".join(row)}')
line_count += 1
print(f'\t{row["name"]} salary: {row["salary"]}')
line_count += 1
print(f'Completed {line_count} lines.')
Listing 1-12Reading a CSV File, with More Readable Code
这里,您创建了一个助手函数,而不是在with语句中编写所有内容。这让读者清楚了process_salary函数实际上是做什么的。如果您想要处理一个特定的异常或者想要从一个 CSV 文件中读取更多的数据,您可以进一步分解这个函数以遵循单一责任原则。
拥抱 Pythonic 式的代码编写方式
当你写代码时,PEP8 有一些建议可以遵循,这将使你的 Python 代码更整洁,可读性更好。让我们来看看其中的一些做法。
首选联接,而不是就地字符串连接
只要你关心代码的性能,就使用"".join()方法,而不是像在a += b或a = a + b中那样就地串联字符串。"".join()方法保证了各种 Python 实现之间更精简的时间连接。
原因是当你使用join时,Python 只为连接的字符串分配一次内存,但是当你连接字符串时,Python 必须为每次连接分配新的内存,因为 Python 字符串是不可变的。参见清单 1-13 。
first_name = "Json"
last_name = "smart"
# Not a recommended way to concatenate string
full_name = first_name + " " + last_name
# More performant and improve readability
" ".join([first_name, last_name])
Listing 1-13Using the join Method
每当需要与 None 进行比较时,可以考虑使用 is 和 is not
始终使用is或is not与None进行比较。在编写如下代码时,请记住这一点:
if val: # Will work when val is not None
请务必记住,您考虑的是val是None,而不是其他容器类型,如dict或set。让我们进一步了解这种代码能让您感到惊讶的地方。
在前一行代码中,val是一个空字典;然而,val被认为是 false,这可能不希望出现在您的代码中,所以在编写这类代码时要小心。
不要这样:
val = {}
if val: # This will be false in python context
相反,尽可能明确地编写代码,以减少代码出错的可能性。
这样做:
if val is not None: # Make sure only None value will be false
更喜欢用 is not 而不是 not … is
用is not和用not ... is没有区别。然而,is not语法比not ... is更具可读性。
不要这样:
if not val is None:
这样做:
if val is not None:
考虑在绑定到标识符时使用函数而不是 Lambda
当您将 lambda 表达式赋给特定标识符时,请考虑使用函数。lambda是 Python 中执行单行操作的关键字;然而,使用lambda编写函数可能不如使用def编写函数好。
不要这样:
square = lambda x: x * x
这样做:
def square(val):
return val * val
对于字符串表示和回溯来说,def square(val)函数对象比一般的<lambda>更有用。这种使用消除了 lambdas 的有用性。考虑在更大的表达式中使用 lambdas,这样就不会影响代码的可读性。
与退货声明一致
如果期望函数返回值,请确保该函数的所有执行路径都返回值。确保在函数存在的所有地方都有一个返回表达式是一个好习惯。但是如果期望一个函数只是执行一个动作而不返回值,Python 隐式地返回None作为函数的默认值。
不要这样:
def calculate_interest(principle, time rate):
if principle > 0:
return (principle * time * rate) / 100
def calculate_interest(principle, time rate):
if principle < 0:
return
return (principle * time * rate) / 100
这样做:
def calculate_interest(principle, time rate):
if principle > 0:
return (principle * time * rate) / 100
else:
return None
def calculate_interest(principle, time rate):
if principle < 0:
return None
return (principle * time * rate) / 100
首选使用“”。以()和""开始。端点()
当需要检查前缀或后缀时,可以考虑用"".startswith()和"".endswith()代替切片。slice是一个非常有用的分割字符串的方法,但是当你分割一个大的字符串或者执行字符串操作的时候可能会得到更好的性能。然而,如果你正在做一些简单的事情,比如检查前缀或后缀,使用startswith或endswith,因为这对读者来说是显而易见的,你正在检查一个字符串中的前缀或后缀。换句话说,它让你的代码更易读、更干净。
不要这样:
Data = "Hello, how are you doing?"
if data.startswith("Hello")
这样做:
data = "Hello, how are you doing?"
if data[:5] == "Hello":
使用 isinstance()方法代替 type()进行比较
当你比较两个对象的类型时,考虑使用isinstance()而不是type,因为isinstance()对于子类来说是正确的。考虑这样一个场景,您正在传递一个数据结构,它是像orderdict这样的dict的子类。type()对于特定类型的数据结构将失败;然而,isinstance()会识别出它是dict的子类。
不要这样:
user_ages = {"Larry": 35, "Jon": 89, "Imli": 12}
type(user_ages) == dict:
这样做:
user_ages = {"Larry": 35, "Jon": 89, "Imli": 12}
if isinstance(user_ages, dict):
比较布尔值的 Pythonic 方式
Python 中有多种比较布尔值的方法。
不要这样:
if is_empty = False
if is_empty == False:
if is_empty is False:
这样做:
is_empty = False
if is_empty:
为上下文管理器编写显式代码
当你在with语句中写代码时,考虑使用一个函数来做任何不同于获取和释放的操作。
不要这样:
class NewProtocol:
def __init__(self, host, port, data):
self.host = host
self.port = port
self.data = data
def __enter__(self):
self._client = Socket()
self._client.connect((self.host,
self.port))
self._transfer_data(data)
def __exit__(self, exception, value, traceback):
self._receive_data()
self._client.close()
def _transfer_data(self):
---
def _receive_data(self):
---
con = NewProtocol(host, port, data)
with con:
transfer_data()
这样做:
#connection
class NewProtocol:
def __init__(self, host, port):
self.host = host
self.port = port
def __enter__(self):
self._client = socket()
self._client.connect((self.host,
self.port))
def __exit__(self, exception, value, traceback):
self._client.close()
def transfer_data(self, payload):
...
def receive_data(self):
...
with connection.NewProtocol(host, port):
transfer_data()
在第二个语句中,Python 的__enter__和__exit__方法除了打开和关闭连接之外还做了一些事情。最好是显式的,编写不同的函数来完成不获取和关闭连接的其他操作。
使用林挺工具改进 Python 代码
代码链接器是使代码格式一致的重要工具。在整个项目中保持一致的代码格式是很有价值的。
林挺工具基本上为您解决了这些问题:
-
句法误差
-
结构,如未使用的变量或将正确的参数传递给函数
-
指出违反 PEP8 准则的行为
林挺工具使你作为一个开发人员更有效率,因为它们通过在运行时找出问题来节省你的大量时间。有多种适用于 Python 的林挺工具。一些工具处理林挺的特定部分,如代码质量的 docstring 风格,流行的 python liniting 工具如 flak8/pylint 检查所有 PEP8 规则,mypy 检查工具专门检查 python 类型。
您可以将它们全部集成到您的代码中,也可以使用一个包含标准检查的代码来确保您遵循 PEP8 风格指南。其中最著名的是 Flake8 和 Pylint。无论你使用什么工具,都要确保它遵守 PEP8 的规则。
林挺工具中有几个功能可供选择:
-
PEP8 规则遵守情况
-
进口订单
-
命名(变量、函数、类、模块、文件等的 Python 命名约定。)
-
循环进口
-
代码复杂性(通过查看行数、循环数和其他参数来检查函数的复杂性)
-
拼写检查器
-
文档字符串样式的检查
有不同的方式可以运行棉绒。
-
在使用 IDE 编程时
-
在提交时使用预提交工具
-
在 CI 时间,通过与 Jenkins、CircleCI 等集成
注意
这些是一定会改进您的代码的一些常见实践。如果您想最大限度地利用 Python 的良好实践,请查看 PEP8 官方文档。此外,阅读 GitHub 中的优秀代码将有助于您理解如何编写更好的 Python 代码。
使用文档字符串
文档字符串是用 Python 记录代码的一种强大方式。文档字符串通常写在方法、类和模块的开始。docstring 成为该对象的__doc__特殊属性。
Python 官方语言推荐使用"""Triple double quotes"""编写 docstrings。你可以在 PEP8 官方文档中找到这些做法。让我们简单谈谈用 Python 代码编写文档字符串的一些最佳实践。参见清单 1-14 。
def get_prime_number():
"""Get list of prime numbers between 1 to 100.""""
Listing 1-14Function with a Docstring
Python 推荐了一种编写文档字符串的特定方法。编写 docstrings 有不同的方法,我们将在本章后面讨论;然而,所有这些不同的类型都遵循一些共同的规则。Python 将规则定义如下:
-
即使字符串在一行中,也要使用三重引号。当您想要扩展时,这种做法很有用。
-
三重引号中的字符串前后不应有任何空行。
-
使用句点(。)来结束 docstring 中的语句。
类似地,Python 多行 docstring 规则可以应用于编写多行 docstring。在多行中编写 docstrings 是以更具描述性的方式记录代码的一种方式。通过利用 Python 多行文档字符串,您可以在 Python 代码中编写描述性的文档字符串,而不是在每一行都编写注释。这也有助于其他开发人员在代码本身中找到文档,而不是查阅冗长且令人生厌的文档。参见清单 1-15 。
def call_weather_api(url, location):
"""Get the weather of specific location.
Calling weather api to check for weather by using weather api and location. Make sure you provide city name only, country and county names won't be accepted and will throw exception if not found the city name.
:param url: URL of the api to get weather.
:type url: str
:param location: Location of the city to get the weather.
:type location: str
:return: Give the weather information of given location.
:rtype: str
"""
Listing 1-15Multiline Docstring
这里有几点需要注意。
-
第一行是函数或类的简短描述。
-
这一行的结尾有一个句号。
-
文档字符串中的简要描述和摘要之间有一行的间隔。
如果您使用 Python 3 和typing模块,您可以编写相同的函数,如清单 1-16 所示。
def call_weather_api(url: str, location: str) -> str:
"""Get the weather of specific location.
Calling weather api to check for weather by using weather api and location. Make sure you provide city name only, country and county names won't be accepted and will throw exception if not found the city name.
"""
Listing 1-16Multiline Docstring with typing
如果在 Python 代码中使用类型,则不需要编写参数信息。
正如我提到的不同的 docstring 类型,这些年来不同的来源已经引入了新样式的 docstring。没有更好的或推荐的方法来编写 docstring。但是,请确保在整个项目中对文档字符串使用相同的样式,以便它们具有一致的格式。
编写 docstring 有四种不同的方法。
-
这里有一个 Google docstrings 的例子:
"""Calling given url. Parameters: url (str): url address to call. Returns: dict: Response of the url api. """ -
下面是一个重构的文本示例(官方 Python 文档推荐这个):
""" Calling given url. :param url: Url to call. :type url: str :returns: Response of the url api. :rtype: dict """ -
下面是一个 NumPy/SciPy 文档字符串的例子:
""" Calling given url. Parameters ---------- url : str URL to call. Returns ------- dict Response of url """ -
这里有一个 Epytext 的例子:
"""Call specific api. @type url: str @param file_loc: Call given url. @rtype: dict @returns: Response of the called api. """
模块级文档字符串
模块级的 docstring 应该放在文件的顶部,以简要描述模块的使用。这些评论也应该放在import之前。一个模块 docstring 应该专注于模块的目标,包括模块中所有的方法/类,而不是谈论一个特定的方法或类。如果您认为某个方法或类在使用模块之前需要让客户端在较高的层次上知道一些东西,那么您仍然可以简单地指定一个特定的方法或类。参见清单 1-17 。
"""
This module contains all of the network related requests. This module will check for all the exceptions while making the network calls and raise exceptions for any unknown exception.
Make sure that when you use this module, you handle these exceptions in client code as:
NetworkError exception for network calls.
NetworkNotFound exception if network not found.
"""
import urllib3
import json
....
Listing 1-17Module Docstring
为模块编写 docstring 时,您应该考虑执行以下操作:
-
简要描述模块的用途。
-
如果您想指定任何对读者了解模块有用内容,比如清单 1-15 ,您可以添加异常信息,但是注意不要太详细。
-
将模块 docstring 视为提供关于该模块的描述性信息的一种方式,而无需深入每个函数或类操作的细节。
使类 Docstring 具有描述性
docstring 类主要用于简要描述该类的用途及其总体目标。让我们看一些例子,看看如何编写类 docstrings。参见清单 1-18 。
class Student:
"""This class handle actions performed by a student."""
def __init__(self):
pass
Listing 1-18Single-Line Docstring
这个类有一行 docstring,简单说一下Student类。如前所述,确保您遵循一行的所有规则。
让我们考虑清单 1-19 中所示的类的多行 docstring。
class Student:
"""Student class information.
This class handle actions performed by a student.
This class provides information about student full name, age, roll-number and other information.
Usage:
import student
student = student.Student()
student.get_name()
>>> 678998
"""
def __init__(self):
pass
Listing 1-19Multiline Class Docstring
此类 docstring 是多行的;关于Student类的用法以及如何使用,我们写得不多。
函数文档字符串
函数文档字符串可以写在函数之后,也可以写在函数顶部。函数文档字符串主要关注于描述函数的操作,如果您没有使用 Python 类型,也可以考虑包含参数,例如参见清单 1-20 。
def is_prime_number(number):
"""Check for prime number.
Check the given number is prime number or not by checking against all the numbers less the square root of given number.
:param number: Given number to check for prime.
:type number: int
:return: True if number is prime otherwise False.
:rtype: boolean
"""
...
Listing 1-20Function Docstring
一些有用的 Docstring 工具
Python 有大量的 docstrings 工具。Docstring 工具通过将文档字符串转换为 HTML 格式的文档文件来帮助记录 Python 代码。这些工具还通过运行简单的命令来帮助您更新文档,而不是手动维护文档。让它们成为你开发流程的一部分,从长远来看,会让它们更有用。
有一些有用的文档工具。每个文档工具都有不同的目标,所以选择哪一个取决于您的具体用例。
-
斯芬克斯 :
http://www.sphinx-doc.org/en/stable/这是 Python 最流行的文档工具。该工具将自动生成 Python 文档。它可以生成多种格式的文档文件。
-
Pycco:
https://pycco-docs.github.io/pycco/这是为 Python 代码生成文档的快捷方式。这个工具的主要特点是并排显示代码和文档。
-
阅读文档 :
https://readthedocs.org/这是开源社区中的一个流行工具。它的主要功能是为您构建、版本化和托管您的文档。
-
Epydocs:
http://epydoc.sourceforge.net/该工具基于 Python 模块的文档字符串为其生成 API 文档。
从长远来看,使用这些工具将使维护代码变得更加容易,并帮助您保持代码文档的一致格式。
注意
文档字符串是 Python 的一个很好的特性,它可以让编写代码变得非常容易。尽早开始在代码中使用 docstrings 将确保当您的项目变得更加成熟并拥有数千行代码时,您不需要投入太多时间。
编写 Pythonic 控制结构
控制结构是任何编程语言的基础部分,对于 Python 也是如此。Python 有多种方法来编写控制结构,但是有一些最佳实践可以让 Python 代码更加整洁。在这一节中,我们将研究这些 Python 控制结构的最佳实践。
使用列表理解
列表理解是一种编写代码来解决现有问题的方式,与 python for loop 的方式类似,但是它允许在列表中使用或不使用 if 条件。Python 中有多种方法可以从另一个列表中导出一个列表。Python 中完成这项工作的主要工具是filter和map方法。但是,推荐使用列表理解方式,因为与其他选项(如映射和过滤)相比,它使您的代码更具可读性。
在本例中,您尝试使用地图版本查找数字的平方:
numbers = [10, 45, 34, 89, 34, 23, 6]
square_numbers = map(lambda num: num**2, num)
以下是列表理解版本:
square_numbers = [num**2 for num in numbers]
让我们看另一个例子,在这个例子中,您对所有真值使用了一个过滤器。下面是filter版本:
data = [1, "A", 0, False, True]
filtered_data = filter(None, data)
以下是列表理解版本:
filtered_data = [item for item in filter if item]
您可能已经注意到,与过滤器和映射版本相比,列表理解版本可读性更好。Python 官方文档也建议使用 list comprehension,而不是filter和map。
如果你在for循环中没有复杂的条件或者复杂的计算,你应该考虑使用列表理解。但是如果你在一个循环中做很多事情,为了可读性,最好还是坚持使用循环。
为了进一步说明在for循环中使用列表理解的要点,让我们看一个例子,在这个例子中,您需要从字符列表中识别一个元音。
list_char = ["a", "p", "t", "i", "y", "l"]
vowel = ["a", "e", "i", "o", "u"]
only_vowel = []
for item in list_char:
if item in vowel:
only_vowel.append(item)
这里有一个使用列表理解的例子:
[item for item in list_char if item in vowel]
正如您所看到的,与使用循环相比,使用列表理解的例子可读性更好,但是代码行更少。此外,循环有额外的性能成本,因为每次都需要将项目追加到列表中,而在列表理解中不需要这样做。
类似地,与列表理解相比,filter和map函数调用这些函数有额外的成本。
不要做复杂的列表理解
您还需要确保对列表的理解不会太复杂,否则会影响代码的可读性,并容易出错。
让我们考虑另一个使用列表理解的例子。列表理解对于至多两个具有一个条件的循环是好的。除此之外,它可能会妨碍代码的可读性。
这里有一个例子,你想转置这个矩阵:
matrix = [[1,2,3],
[4,5,6],
[7,8,9]]
然后转换成这个:
matrix = [[1,4,7],
[2,5,8],
[3,6,9]]
使用列表理解,你可以这样写:
return [[ matrix[row][col] for row in range(0, height) ] for col in range(0,width) ]
这里的代码是可读的,使用列表理解是有意义的。您甚至可能希望以更好的格式编写代码,如下所示:
return [[ matrix[row][col]
for row in range(0, height) ]
for col in range(0,width) ]
当您有多个if条件时,您可以考虑使用循环来代替列表理解,如下所示:
ages = [1, 34, 5, 7, 3, 57, 356]
old = [age for age in ages if age > 10 and age < 100 and age is not None]
在这里,很多事情都发生在一行上,这很难阅读并且容易出错。在这里使用一个for循环而不是使用列表理解可能是一个好主意。
您可以考虑编写如下代码:
ages = [1, 34, 5, 7, 3, 57, 356]
old = []
for age in ages:
if age > 10 and age < 100:
old.append(age)
print(old)
正如你所看到的,这有更多的代码行,但它是可读和干净的。
因此,一个好的经验法则是从理解列表开始,当表达式开始变得复杂或者可读性开始受到阻碍时,就转换成使用循环。
注意
明智地使用列表理解可以改进你的代码;然而,过度使用列表理解会妨碍代码的可读性。所以,当你要处理复杂的语句时,不要使用列表理解,这些语句可能不止一个条件或循环。
应该使用 Lambda 吗?
你可以考虑在表达式中使用 lambda,而不是用它来代替函数。让我们考虑清单 1-21 中的例子。
data = [[7], [3], [0], [8], [1], [4]]
def min_val(data):
"""Find minimum value from the data list."""
return min(data, key=lambda x:len(x))
Listing 1-21Using a Lambda Without Assigning
这里的代码使用 lambda 作为一次性函数来寻找最小值。然而,我建议你不要像这样使用 lambda 作为匿名函数:
min_val = min(data, key=lambda x: len(x))
这里,min_val是使用λ表达式计算的。将 lambda 表达式编写为函数复制了def的功能,这违反了 Python 以一种且只有一种方式做事的哲学。
PEP8 文档是这样描述 lambdas 的:
始终使用 def 语句,而不是将 lambda 表达式直接绑定到名称的赋值语句。
是:
def f(x): return 2x*
否:
f =λx:2 * x
第一种形式意味着产生的函数对象的名字是明确的‘f’,而不是一般的‘??’λ>。一般来说,这对于回溯和字符串表示更有用。赋值语句的使用消除了 lambda 表达式相对于显式 def 语句所能提供的唯一好处(即它可以嵌入到更大的表达式中)
何时使用生成器与列表理解
生成器和列表理解之间的主要区别在于,列表理解将数据保存在内存中,而生成器不这样做。
在下列情况下使用列表理解:
-
当你需要多次遍历列表时。
-
当您需要列出方法来处理生成器中不可用的数据时
-
当您没有大量数据需要迭代,并且认为将数据保存在内存中不成问题时
假设您想从一个文本文件中获取一个文件的行,如清单 1-22 所示。
def read_file(file_name):
"""Read the file line by line."""
fread = open(file_name, "r")
data = [line for line in fread if line.startswith(">>")]
return data
Listing 1-22Read File from a Document
在这里,文件可能非常大,以至于在一个列表中有这么多行可能会影响内存并使您的代码变慢。所以,你可能想考虑在列表上使用迭代器。参见清单 1-23 中的示例。
def read_file(file_name):
"""Read the file line by line."""
with open(file_name) as fread:
for line in fread:
yield line
for line in read_file("logfile.txt"):
print(line.startswith(">>")
Listing 1-23Read a File from a Document Using Iterators
在清单 1-23 中,您不是使用列表理解将数据推入内存,而是一次读取每一行并采取行动。但是,list comprehension 可以传递给下一步操作,看看它是否找到了所有以> > >开头的行,而生成器每次都需要运行来找到以> > >开头的行。
这两者都是 Python 的优秀特性,按照描述使用它们将使您的代码具有高性能。
为什么不在循环中使用 else
Python 循环有一个else子句。基本上,你可以在代码中的 Python for或while循环之后有一个else子句。代码中的else子句仅在控制从循环中正常退出时运行。如果控制存在于带有break关键字的循环中,那么控制不会进入代码的else部分。
有一个带有循环的else子句有点令人困惑,这使得许多开发人员避免使用这个特性。考虑到正常流量中if/else条件的性质,这是可以理解的。
让我们看看清单 1-24 中的简单例子;该代码试图在一个列表上循环,并且在循环的外面和后面有一个else子句。
for item in [1, 2, 3]:
print("Then")
else:
print("Else")
Result:
>>> Then
>>> Then
>>> Then
>>> Else
Listing 1-24else Clause with for Loop
乍一看,您可能认为它应该只打印三个Then子句,而不是Else,因为在一个if/else块的正常场景中,这将被跳过。这是查看代码逻辑的自然方式。然而,这个假设在这里是不正确的。如果您使用while循环,这将变得更加混乱,如清单 1-25 所示。
x = [1, 2, 3]
while x:
print("Then")
x.pop()
else:
print("Else")
Listing 1-25else Clause with the for Loop
结果如下:
>>> Then
>>> Then
>>> Then
>>> Else
这里,while循环运行,直到列表不为空,然后运行else子句。
Python 中有这个功能是有原因的。一个主要的用例是在for和while循环之后有一个else子句,以便在循环结束后执行一个额外的动作。
让我们考虑清单 1-26 中的例子。
for item in [1, 2, 3]:
if item % 2 = 0:
break
print("Then")
else:
print("Else")
Listing 1-26else Clause with break
结果如下:
>>> Then
然而,有更好的方法来编写这段代码,而不是在循环之外使用else子句。您可以在循环中使用带有break的else子句,也可以不使用break条件。然而,有多种方法可以在不使用else条款的情况下达到相同的结果。你应该在循环中使用 condition 而不是else,因为有被其他开发者误解代码的风险,而且看一眼就理解代码也有点困难。见清单 1-27 。
flag = True
for item in [1, 2, 3]:
if item % 2 = 0:
flag = False
break
print("Then")
if flag:
print("Else")
Listing 1-27else Clause with break
结果如下:
>>> Then
这种代码更容易阅读和理解,并且在阅读代码时不存在混淆的可能性。else子句是一种有趣的编写代码的方法;但是,它可能会影响代码的可读性,因此避免它可能是解决问题的更好方法。
为什么 Python 3 中范围更好
如果您使用过 Python 2,您可能会使用xrange。在 Python 3 中,xrange被重命名为range,并增加了一些额外的特性。range类似于xrange,生成一个 iterable。
>>> range(4)
range(0, 5) # Iterable
>>> list(range(4))
[0, 1, 2, 3, 4] # List
Python 3 的range函数中有一些新特性。与列表相比,使用范围的主要优点是它不会将数据保存在内存中。与列表、元组和其他 Python 数据结构相比,range表示不可变的 iterable 对象,无论范围大小如何,它总是占用少量相同的内存,因为它只存储开始、停止和步长值,并根据需要计算值。
你可以用range做一些在xrange中不可能做的事情。
-
您可以比较范围数据。
>>> range(4) == range(4) True >>> range(4) == range(5) False -
你可以切片。
>>> range(10)[2:] range(2, 10) >>> range(10)[2:7, -1) range(2, 7, -1)
range有很多新特性,你可以在这里查看更多细节: https://docs.python.org/3.0/library/functions.html#range 。
此外,当你需要在代码中处理数字而不是数字列表时,你可以考虑使用range,因为它比列表要快得多。
当你处理数字的时候,也建议你尽可能的在你的循环中使用 iterables,因为 Python 给了你一个像range这样的方法来轻松的处理数字。
不要这样:
for item in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:
print(item)
这样做:
for item in range(10):
print(item)
就性能而言,第一个循环的代价要高得多,如果这个列表恰好足够大,那么由于内存情况和从列表中弹出数字,它会使您的代码慢得多。
引发异常
异常有助于报告代码中的错误。在 Python 中,异常由内置模块处理。很好地理解异常是很重要的。了解何时何地使用它们将使您的代码不容易出错。
异常可以毫不费力地暴露代码中的错误,所以永远不要忘记在代码中添加异常。异常有助于 API 或库的消费者理解代码的局限性,以便他们在使用代码时可以使用良好的错误机制。在代码中适当的位置引发一个异常可以极大地帮助其他开发人员理解您的代码,并让第三方客户在使用您的 API 时感到满意。
经常提出的例外
您可能想知道何时何地在 Python 代码中引发异常。
每当发现当前代码块的一个基本假设为假时,我通常会抛出一个异常。Python 更喜欢在代码失败时出现异常。即使您有一个连续的失败,您也想为它引发一个异常。
让我们考虑你需要将清单 1-28 中的两个数相除。
def division(dividend, divisor):
"""Perform arithmetic division."""
try:
return dividend/divisor
except ZeroDivisionError as zero:
raise ZeroDivisionError("Please provide greater than 0 value")
Listing 1-28Division of Numbers with Exceptions
正如您在这段代码中看到的,每当您认为代码中可能有错误时,您就引发了一个异常。这有助于调用代码确保每当代码中有ZeroDivisionError时代码都会出错,并以不同的方式处理它。参见清单 1-29 。
result = division(10, 2)
What happens if we return None here as:
def division(dividend, divisor):
"""Perform arithmetic division."""
try:
return dividend/divisor
except ZeroDivisionError as zero:
return None
Listing 1-29Division Without Exceptions
如果调用者不处理调用division(dividend, divisor)方法永远不会失败的情况,即使你的代码中有ZeroDivisionError,如果你在任何异常情况下都没有从division(dividend, divisor)方法返回,这可能会在将来代码规模增长或需求变化时使诊断变得困难。最好避免division(divident, divisor)函数在出现任何失败或异常时返回None,以便调用者更容易理解函数执行过程中失败的原因。当我们提出异常时,我们让调用者预先知道输入值不正确,需要提供正确的值,我们避免任何隐藏的错误。
从调用者的角度来看,获取异常比获取返回值更方便,这是 Python 指示失败的风格。
Python 的信条是“请求原谅比请求许可更容易。”这意味着你没有事先检查以确保你不会得到一个异常;相反,如果你得到异常,你处理它。
基本上,只要您认为代码中有失败的可能性,您就要确保引发异常,这样调用类就可以优雅地处理它们,而不会感到意外。
换句话说,如果你觉得你的代码无法合理运行,还没有想出答案,那就考虑抛出一个异常。
最终利用来处理异常
finally中的代码总是在 Python 中运行。finally关键字在处理异常时很有用,尤其是在处理资源时。您可以使用finally来确保文件或资源被关闭或释放,不管是否出现异常。即使您没有捕捉到异常或者没有特定的异常要捕捉,也是如此。参见清单 1-30 。
def send_email(host, port, user, password, email, message):
"""send email to specific email address."""
try:
server = smtlib.SMTP(host=host, port=port)
server.ehlo()
server.login(user, password)
server.send_email(message)
finally:
server.quite()
Listing 1-30finally Keyword Use
在这里,您使用finally处理异常,这有助于清理服务器连接中的资源,以防您在登录期间或在send_email中出现某种异常。
您可以使用finally关键字来编写关闭文件的代码块,如清单 1-31 所示。
def write_file(file_name):
"""Read given file line by line""""
myfile = open(file_name, "w")
try:
myfile.write("Python is awesome") # Raise TypeError
finally:
myfile.close() # Executed before TypeError propagated
Listing 1-31finally Keyword Use to close the file
这里您正在处理关闭finally块中的文件。无论是否有异常,在finally中的代码将总是运行并关闭文件。
因此,当您想执行某个代码块而不管是否存在异常时,您应该更喜欢使用finally来完成。使用finally将确保你明智地处理你的资源,此外将使你的代码更干净。
创建自己的异常类
当您正在创建 API 或库,或者正在处理一个项目,并且希望定义自己的异常以与项目或 API 保持一致时,建议您创建自己的异常类。这将极大地帮助您诊断或调试代码。这也有助于使您的代码更整洁,因为调用者会知道为什么会抛出错误。
让我们假设当您在数据库中找不到用户时,您必须引发异常。您希望确保异常类名反映了错误的意图。有了名字UserNotFoundError本身就说明了为什么你有一个例外和意图。
您可以在 Python 3+中定义自己的异常类,如清单 1-32 所示。
class UserNotFoundError(Exception):
"""Raise the exception when user not found."""
def __init__(self, message=None, errors=None):
# Calling the base class constructor with the parameter it needs
super().__init__(message)
# New for your custom code
self.errors = errors
def get_user_info(user_obj):
"""Get user information from DB."""
user = get_user_from_db(user_obj)
if not user:
raise UserNotFoundException(f"No user found of this id: {user_obj.id}")
get_user_info(user_obj)
>>> UserNotFoundException: No user found of this id: 897867
Listing 1-32Creating a Specific Exception Class
您还需要确保当您创建自己异常类时,这些异常是描述性的,且具有明确定义的边界。您将希望仅在代码找不到用户的地方使用UserNotFoundException,并且您将希望通知调用代码在数据库中没有找到用户信息。为自定义的异常设置一组特定的界限可以更容易地诊断代码。当您查看您的代码时,您确切地知道为什么代码抛出了那个特定的异常。
你也可以通过命名为一个异常类定义一个更大的范围,但是名字应该表明它处理特定种类的情况,如清单 1-33 所示。清单显示了ValidationError,您可以将它用于多个验证案例,但是它的范围是由所有与验证相关的异常定义的。
class ValidationError(Exception):
"""Raise the exception whenever validation failed.."""
def __init__(self, message=None, errors=None):
# Calling the base class constructor with the parameter it needs
super().__init__(message)
# New for your custom code
self.errors = errors
Listing 1-33Creating a Custom Exception Class with a Broader Scope
与UserNotFoundException相比,该例外的范围更广。ValidationError可以在您认为验证已经失败或特定输入没有有效输入的任何时候提出;但是,边界仍然由验证上下文定义。因此,请确保您知道异常的范围,并在该异常类的范围内发现错误时引发异常。
仅处理特定的异常
在捕获异常时,建议您只捕获特定的异常,而不是使用except:子句。
except: or except Exception will handle each and every exception, which can cause your code to hide critical bugs or exceptions which you don't intend to.
我们来看看下面的代码片段,它使用了try / catch块中的except子句来调用函数get_even_list。
不要这样:
def get_even_list(num_list):
"""Get list of odd numbers from given list."""
# This can raise NoneType or TypeError exceptions
return [item for item in num_list if item%2==0]
numbers = None
try:
get_even_list(numbers)
except:
print("Something is wrong")
>>> Something is wrong
这种代码隐藏了像NoneType或TypeError这样的异常,这显然是您代码中的一个错误,客户端应用或服务将很难弄清楚为什么它们会收到像“有问题”这样的消息相反,如果您用适当的消息提出特定类型的异常,API 客户端会感谢您的努力。
当您在代码中使用except时,Python 内部认为它是except BaseException。拥有一个特定的异常非常有帮助,尤其是在一个较大的代码库中。
这样做:
def get_even_list(num_list):
"""Get list of odd numbers from given list."""
# This can raise NoneType or TypeError exceptions
return [item for item in num_list if item%2==0]
numbers = None
try:
get_even_list(numbers)
except NoneType:
print("None Value has been provided.")
except TypeError:
print("Type error has been raised due to non sequential data type.")
处理特定的异常有助于调试或诊断您的问题。调用者将立即知道代码失败的原因,并强迫您添加代码来处理特定的异常。这也使您的代码不容易因调用和调用者代码而出错。
根据 PEP8 文档,在处理异常时,您应该在以下情况下使用except关键字:
-
异常处理程序何时打印或记录回溯。至少用户会意识到在这种情况下发生了错误。
-
当代码需要做一些清理工作,但随后用
raise让异常向上传播时。try...finally可以更好的处理这种情况。
注意
处理特定的异常是编写代码时的最佳实践之一,尤其是在 Python 中,因为它将帮助您在调试代码时节省大量时间。此外,它将确保您的代码快速失败,而不是隐藏代码中的错误。
注意第三方例外
在调用第三方 API 时,了解第三方库抛出的所有类型的异常非常重要。了解所有类型的异常有助于您以后调试问题。
如果您认为异常不太适合您的用例,可以考虑创建您自己的异常类。使用第三方库时,如果希望根据应用错误重命名异常名称,或者希望在第三方异常中添加新消息,可以创建自己的异常类。
让我们看看清单 1-34 中的botocore客户端库。
from botocore.exceptions import ClientError
ec2 = session.get_client('ec2', 'us-east-2')
try:
parsed = ec2.describe_instances(InstanceIds=['i-badid'])
except ClientError as e:
logger.error("Received error: %s", e, exc_info=True)
# Only worry about a specific service error code
if e.response['Error']['Code'] == 'InvalidInstanceID.NotFound':
raise WrongInstanceIDError(message=exc_info, errors=e)
class WrongInstanceIDError(Exception):
"""Raise the exception whenever Invalid instance found."""
def __init__(self, message=None, errors=None):
# Calling the base class constructor with the parameter it needs
super().__init__(message)
# New for your custom code
self.errors = errors
Listing 1-34Creating a Custom Exception Class with a Broader Scope
这里考虑两件事。
-
每当您在第三方库中发现特定错误时,添加日志将使调试第三方库中的问题变得更加容易。
-
这里您定义了一个新的错误类来定义您自己的异常。您可能不想对每个异常都这样做;但是,如果您认为创建一个新的异常类会使您的代码更整洁、可读性更好,那么可以考虑创建一个新的类。
有时很难找到正确的方法来处理第三方库/API 抛出的异常。至少了解一些由第三方库抛出的常见异常,会让你在处理产品 bug 时更容易。
更喜欢尝试最少的代码
每当在代码中处理异常时,尽量将代码保持在最少的try块中。这让其他开发人员清楚地知道代码的哪一部分有抛出错误的风险。拥有最少的代码或者有可能在try块中抛出异常的代码也有助于更容易地调试问题。没有用于异常处理的try / catch块可能会稍微快一些;但是,如果不处理该异常,可能会导致应用失败。因此,良好的异常处理使您的代码没有错误,并可以在生产中为您节省数百万美元。
让我们看一个例子。
不要这样:
def write_to_file(file_name, message):
"""Write to file this specific message."""
try:
write_file = open(file_name, "w")
write_file.write(message)
write.close()
except FileNotFoundError as exc:
FileNotFoundException("Please provide correct file")
如果仔细查看前面的代码,您会发现有机会出现不同种类的异常。一个是FileNotFound或者IOError。
您可以在一行中使用不同的异常,或者在不同的try块中编写不同的异常。
这样做:
def write_to_file(file_name, message):
"""Write to given file this specific message."""
try:
write_file = open(file_name, "w")
write_file.write(message)
write.close()
except (FileNotFoundError, IOError) as exc:
FileNotFoundException(f"Having issue while writing into file {exc}")
即使在其他行上没有出现异常的风险,最好还是在一个try块中编写最少的代码,如下所示。
不要这样:
try:
data = get_data_from_db(obj)
return data
except DBConnectionError:
raise
这样做:
try:
data = get_data_from_db(obj)
except DBConnectionError:
raise
return data
这使得代码更加简洁,并且清楚地表明只有在访问get_data_from_db方法时才会出现异常。
摘要
在这一章中,你学习了一些常见的做法,可以帮助你使你的 Python 代码更易读和更简单。
此外,异常处理是用 Python 编写代码的最重要的部分之一。很好地理解异常有助于维护您的应用。在大型项目中尤其如此,因为不同的开发人员开发的应用有不同的活动部分,所以您更有可能遇到各种各样的生产问题。在代码中适当的位置使用异常可以节省大量的时间和金钱,尤其是在生产中调试问题的时候。日志记录和异常是任何成熟软件应用的两个最重要的部分,因此提前为它们做好计划,并将它们视为软件应用开发的核心部分,将有助于您编写更易于维护和阅读的代码。
Footnotes 1https://www.python.org/dev/peps/pep-0020/
二、数据结构
数据结构是任何编程语言的基本构件。很好地掌握数据结构可以为您节省大量时间,并且使用它们可以使您的代码具有可维护性。Python 有许多使用数据结构存储数据的方法,并且很好地理解何时使用哪种数据结构在内存、易用性和代码性能方面有很大的不同。
在这一章中,我将首先介绍一些常见的数据结构,并解释何时在你的代码中使用它们。我还将介绍在特定情况下使用这些数据结构的优势。然后,您将详细考虑字典作为 Python 中的数据结构的重要性。
公共数据结构
Python 有许多主要的数据结构。在这一节中,您将看到最常见的数据结构。对数据结构概念有一个好的理解对你写有效的代码是重要的。明智地使用它们会使您的代码性能更高,错误更少。
使用速度设置
集合是 Python 中的基本数据结构。他们也是最被忽视的人之一。使用集合的主要好处是速度。那么,让我们来看看集合的其他一些特征:
-
他们不允许复制。
-
不能使用索引来访问集合元素。
-
集合可以在 O(1)时间内访问元素,因为它们使用哈希表。
-
集合不允许列表所做的一些常见操作,如切片和查找。
-
集合可以在插入时对元素进行排序。
考虑到这些约束,当你的数据结构中不需要这些通用功能时,最好使用 set,这将使你的代码在访问数据时速度更快。清单 2-1 展示了一个使用集合的例子。
data = {"first", "second", "third", "fourth", "fifth"}
if "fourth" in data:
print("Found the Data")
Listing 2-1Set Usage for Accessing Data
集合是使用哈希表实现的,所以每当一个新的项被添加到集合中时,该项在内存中的位置就由对象的哈希值决定。这就是哈希在访问数据时性能良好的原因。如果您有数千个项目,并且需要频繁地从这些元素中访问项目,那么使用集合来访问项目比使用列表要快得多。
让我们看看另一个例子(清单 2-2 ),集合是有用的,可以帮助确保您的数据没有被重复。
data = ["first", "second", "third", "fourth", "fourth", "fifth"]
no_duplicate_data = set(data)
>>> {"first", "second", "third", "fourth", "fifth"}
Listing 2-2Set Usage for Removing Duplicates
集合也用作字典的键,您可以将集合用作列表等其他数据结构的键。
让我们考虑清单 2-3 中的例子,其中有一个来自数据库的字典,ID 值作为键,值中有用户的名和姓。
users = {'1267':{'first': 'Larry', 'last':'Page'},
'2343': {'first': 'John', 'last': 'Freedom'}}
ids = set(users.keys())
full_names = []
for user in users.values():
full_names.append(user["first"] + "" + user["last"])
Listing 2-3Sets as First and Last Names
这将给出您的 id 集和全名列表。如您所见,集合可以从列表中派生出来。
注意
集合是有用的数据结构。当您需要频繁地访问数字列表中的项目,并在 O(1)时间内设置对项目的访问时,可以考虑使用它们。我建议下次需要数据结构时,在考虑使用列表或元组之前先考虑集合。
使用 namedtuple 返回和访问数据
namedtuple基本上是一个带有数据名称的元组。可以做元组可以做的事情,但也有一些元组没有的额外功能。使用namedtuple,很容易创建一个轻量级的对象类型。
使你的代码更加 Pythonic 化。
访问数据
使用namedtuple访问数据使其更具可读性。假设您想创建一个类,它的值在初始化后不会改变。你可以创建一个如清单 2-4 所示的类。
class Point:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
point = Point(3, 4, 5)
point.x
point.y
point.z
Listing 2-4Immutable Class
如果你不打算改变class Point的值,而更喜欢使用namedtuple来编写它们,这将使你的代码更具可读性,如清单 2-5 所示。
Point = namedtuple("Point", ["x", "y", "z"])
point = Point(x=3, y=4, z=5)
point.x
point.y
point.z
Listing 2-5namedtuple Implementation
正如您在这里看到的,这段代码比使用普通的类可读性强得多,而且行数也少得多。因为一个namedtuple和一个元组使用相同的内存,所以它们和元组一样高效。
你可能想知道为什么不用dict代替namedtuple,因为它们很容易编写。
无论是否命名,元组都是不可变的。通过使用名称而不是索引,使访问更加方便。namedtuple有严格的限制,字段名必须是字符串。另外,namedtuple不执行任何散列,因为它生成一个类型。
返回数据
通常你会在一个元组中返回数据。然而,您应该考虑使用namedtuple来返回数据,因为它使代码在没有太多上下文的情况下更具可读性。我甚至建议,每当你从一个函数向另一个函数传递数据时,你应该看看你是否能使用namedtuple,因为它使你的代码更加 Pythonic 化和可读。让我们考虑清单 2-6 中的例子。
def get_user_info(user_obj):
user = get_data_from_db(user_obj)
first_name = user["first_name"]
last_name = user["last_name"]
age = user["age"]
return (first_name, last_name, age)
def get_full_name(first_name, last_name):
return first_name + last_name
first_name, last_name, age = get_user_info(user_obj)
full_name = get_full_name(first_name, last_name)
Listing 2-6Return a Value from a Function as a Tuple
那么,这个功能有什么问题呢?问题在于返回值。正如您所注意到的,从数据库中获取数据后,您将返回用户的值first_name、last_name和age。现在,假设您需要将这些值传递给其他函数,如get_full_name。你在传递这些值,这给读者阅读你的代码制造了视觉噪音。如果像这样传递更多的值,想象一下用户理解你的代码有多困难。如果您能够将这些值绑定到一个数据结构,以便它提供上下文而无需编写额外的代码,那可能会更好。
让我们用namedtuple重写这段代码,这会更有意义,如清单 2-7 所示。
def get_user_info(user_obj):
user = get_data_from_db(user_obj)
UserInfo = namedtuple("UserInfo", ["first_name", "last_name", "age"])
user_info = UserInfo(first_name=user["first_name"],
last_name=user["last_name"],
age=user["age"])
return user_info
def get_full_name(user_info):
return user_info.first_name + user_info.last_name
user_info = get_user_info(user_obj)
full_name = get_full_name(user_info)
Listing 2-7Return a Value from a Function as a Tuple
使用namedtuple编写代码给了它上下文,而不需要你为代码提供额外的信息。在这里,user_info作为namedtuple给你额外的上下文,当从一个叫做get_user_info的函数返回时,不需要显式设置。因此,从长远来看,使用namedtuple会使您的代码更具可读性和可维护性。
如果有十个值要返回,在移动数据时,通常可以考虑使用tuple或dict。当数据被移动时,这两种数据结构都不太可读。tuple 不为tuple中的数据提供任何上下文或名称,并且dict不具有不可变性,当您不希望数据在第一次赋值后改变时,这会对您产生约束。namedtuple填补了这两个空白。
最后,如果你想把namedtuple转换成dict或者把一个列表转换成namedtuple,namedtuple给你提供了简单的方法。所以,它们也很灵活。下一次你用不可变数据创建一个类或者返回多个值时,为了可读性和可维护性,考虑使用namedtuple。
注意
只要您认为对象符号会使您的代码更具 Pythonic 化和可读性,您就应该使用namedtuple而不是 tuple。当我需要在某种上下文中传递多个值时,我通常会考虑它们;在这些情况下,namedtuple可以满足要求,因为它使代码更具可读性。
理解字符串、Unicode 和字节
理解 Python 语言中的一些基本概念将有助于开发人员在处理数据时成为更好的程序员。具体来说,在 Python 中,当您处理数据时,对str、Unicode 和byte有一个基本的了解会对您有所帮助。Python 对于数据处理或任何与数据相关的事情来说都很容易编码,因为它有内置的库和简单性。
您可能已经知道,str是 Python 中字符串的表示类型。参见清单 2-8 。
p = "Hello"
type(p)
>>> str
t = "6"
type(t)
>>> str
Listing 2-8Type str for Different Values
Unicode 为几乎所有语言中的每个字符提供了唯一的标识,例如:
0x59 : Y
0xE1 : á
0x7E : ~
Unicode 分配给字符的数字称为码点。那么,拥有 Unicode 的目的是什么呢?
Unicode 的目的是为几乎所有语言的每个字符提供一个唯一的 ID。无论使用何种语言,任何字符都可以使用 Unicode 码位。Unicode 通常以一个前导U+和一个至少填充到四位数的十六进制数值格式化。
所以,你需要记住的是,Unicode 所做的只是给每个字符分配一个称为码位的数字 ID,这样你就有了一个明确的引用。
当你把任何一个字符映射成一个位模式,就叫做编码。这些位模式由计算机内存或磁盘使用。有多种方法可以对字符进行编码;最常见的是 ASCII、ISO-8859-1 和 UTF-8。
Python 解释器使用 UTF-8 进行编码。
那么,让我们简单地谈谈 UTF-8。UTF-8 将所有 Unicode 字符映射到长度为 8、16、24 或 32 的位模式,对应的长度为 1、2、3 或 4。
例如,a将被 Python 解释器转换为 01100001,而将被转换为 11000011 01011111 (0xC3 0xA1)。所以,很容易理解 Unicode 为什么有用。
注意
在 Python 3 中,所有字符串都是 Unicode 字符序列。所以,你不应该考虑把字符串编码成 UTF 8 或者把 UTF 8 解码成字符串。您仍然可以使用字符串编码方法将字符串转换为字节,并将字节转换回字符串。
小心使用列表,优先选择生成器
迭代器非常有用,尤其是在处理大量数据的时候。我看到过这样的代码,人们使用一个列表来存储序列数据,但是这样会有内存泄漏的风险,从而影响系统的性能。
让我们考虑清单 2-9 中的例子。
def get_prime_numbers(lower, higher):
primes = []
for num in range(lower, higher + 1):
for prime in range(2, num + 1):
is_prime = True
for item in range(2, int(num ** 0.5) + 1):
if num % item == 0:
is_prime = False
break
if is_prime:
primes.append(num)
print(get_prime_numbers(30, 30000))
Listing 2-9Using a List of Return Prime Numbers
这样的代码有什么问题?首先,它很难阅读,其次,它在内存泄漏方面可能是危险的,因为你在内存中存储了大量的数据。如何使这段代码在可读性和性能方面更好?
这就是你可以考虑使用生成器的地方,生成器使用 yield 键生成数字,你可以把它们作为迭代器弹出值。让我们使用迭代器重写这个例子,如清单 2-10 所示。
def is_prime(num):
for item in range(2, int(math.sqrt(num)) + 1):
if num % item == 0:
prime = False
return prime
def get_prime_numbers(lower, higher):
for possible_prime in range(lower, higher):
if is_prime(possible_prime):
yield possible_prime
yield False
for prime in get_prime_numbers(lower, higher):
if prime:
print(prime)
Listing 2-10Using Generators for Prime Numbers
这段代码可读性更强,性能更好。此外,生成器会无意中迫使您考虑重构代码。这里,在列表中返回值会使代码更加臃肿,生成器很容易解决这个问题。
我观察到的一个常见情况是,当您从数据库中获取数据并且不知道将获取多少行时,迭代器非常有用。这可能是内存密集型工作,因为您可能会尝试将这些值保存在内存中。相反,尝试使用迭代器,它会立即返回一个值,并转到下一行给出下一个值。
假设您必须访问数据库,通过 ID 获取用户的年龄和姓名。您知道数据库中作为索引的 id,并且您知道数据库中的用户总数,即 1,000,000,000。我见过的大多数代码中,开发人员试图使用列表获取数据,这是解决内存问题的好方法。清单 2-11 显示了一个这样的例子。
def get_all_users_age(total_users=1000):
age = []
for id in total_users:
user_obj = access_db_to_get_users_by_id(id)
age.append([user.name, user.age])
return age
total_users = 1000000000
for user_info in range(total_users):
info = get_all_users_age()
for user in info:
print(user)
Listing 2-11Access a Database and Store the Result in a List as a Chunk
在这里,您试图通过访问数据库来获取用户的年龄和姓名。然而,当系统中没有太多内存时,这种方法可能并不好,因为您会随机选择一个您认为内存安全的数字来存储用户信息,但您不能保证这一点。Python 提供了一个生成器作为解决方案来避免这些问题,并在您的代码中处理这些情况。你可以考虑重写它,如清单 2-12 所示。
def get_all_users_age():
all_users = 1000000000
for id in all_users:
user_obj = access_db_to_get_users_by_id(id)
yield user.name, user.age
for user_name, user_age in get_all_users_age():
print(user_name, user_age)
Listing 2-12Using an Iterator Approach
注意
生成器是 Python 的一个有用特性,因为它们使您的代码对于数据密集型工作具有高性能。生成器也迫使你考虑让代码可读。
使用 zip 处理列表
当您有两个列表并且想要并行处理它们时,可以考虑使用zip。这是 Python 的内置函数,非常高效。
假设在数据库的用户表中有一个用户的姓名和薪水,您希望将它们合并到另一个列表中,并作为所有用户的列表返回。你有函数get_users_name_from_db和get_users_salary_from_db,给你一个用户列表和用户对应的工资。你如何将它们结合起来?清单 2-13 中显示了其中一种方法。
def get_user_salary_info():
users = get_users_name_from_db()
# ["Abe", "Larry", "Adams", "John", "Sumit", "Adward"]
users_salary = get_users_salary_from_db()
# ["2M", "1M", "60K", "30K", "80K", "100K"]
users_salary = []
for index in len(users):
users_salary.append([users[index], users_salary[index]])
return users_salary
Listing 2-13Combine a List
有没有更好的办法解决这个问题?当然了。Python 有一个名为zip的内置函数,可以为您轻松处理这部分内容,如清单 2-14 所示。
def get_user_salary_info():
users = get_users_name_from_db()
# ["Abe", "Larry", "Adams", "John", "Sumit", "Adward"]
users_salary = get_users_salary_from_db()
# ["2M", "1M", "60K", "30K", "80K", "100K"]
users_salary = []
for usr, slr in zip(users, users_salary):
users_salary.append(usr, slr)
return users_salary
Listing 2-14Using zip
如果你有很多数据,考虑在这里使用迭代器,而不是存储到一个列表中。zip使得合并两个列表并并行处理它们变得更加容易,因此使用zip将允许您高效地完成这些工作。
利用 Python 的内置函数
Python 有很多非常棒的内置库。在这一章中,我不能一一介绍,因为有很多这样的库。我将介绍一些基本的数据结构库,它们可以对您的代码产生重大影响,并提高您的代码质量。
收集
这是使用最广泛的库之一,具有有用的数据结构,特别是namedtuple、defaultdict和orderddict。
战斗支援车
使用csv读写 CSV 文件。这会节省你很多时间,而不是在读文件的时候写你自己的方法。
日期和时间
毫无疑问,这是最常用的两个库。其实你很可能已经遇到过他们了。如果没有,熟悉这些库中可用的不同方法在不同的场景中是有益的,尤其是当您处理计时问题时。
数学
lib 有很多有用的方法来执行基础到高级的数学计算。在找第三方库解数学题之前,先试试看这个库是否已经有了。
是
使用正则表达式解决问题的本库无可替代。事实上,re是 Python 语言中最好的库之一。如果你很了解正则表达式,你可以使用re库创造奇迹。它使您能够使用正则表达式轻松地执行一些更困难的操作。
临时文件
请将此视为创建临时文件的一次性库。是一个很好的内置库。
迭代工具
这个库中一些最有用的工具是排列和组合。但是,如果你进一步探索,你会发现你可以用itertools解决很多计算问题。它有一些有用的功能,如dropwhile、product、chain和islice。
函数工具
如果你是热爱函数式编程的开发人员,这个库就是为你准备的。它有很多功能,可以帮助你以一种更实用的方式思考你的代码。最常用的部分之一是在这个图书馆。
系统和操作系统
当您想要执行任何特定的系统级或操作系统级操作时,请使用这些库。sys和os让你能够用你的系统做很多令人惊奇的事情。
子过程
这个库可以帮助您轻松地在系统上创建多个进程。该库易于使用,它创建多个进程并使用多种方法处理它们。
记录
没有好的日志记录特性,任何大项目都不可能成功。Python 的logging库可以帮助您轻松地在系统中添加日志。它有不同的方式来输出日志,如控制台、文件和网络。
数据
JSON 是通过网络传递信息和 API 的事实上的标准。Python 的json库在处理不同场景方面做得很好。json库接口易于使用,文档也相当不错。
泡菜
您可能不会在日常编码中使用它,但是每当您需要序列化和反序列化 Python 对象时,没有比pickle更好的库了。
__ 未来 _ _
这是一个伪模块,支持与当前解释器不兼容的新语言功能。因此,如果您想在将来的代码中使用它们,您可能会考虑使用它们。参见清单 2-15 。
import __future__ import division
Listing 2-15Using __future__
注意
Python 有丰富的库,可以为你解决很多问题。了解他们是了解他们能为你做什么的第一步。从长远来看,熟悉内置的 Python 库会对你有所帮助。
既然您已经研究了 Python 中一些最常见的数据结构,那么让我们更深入地研究 Python 中最常用的数据结构之一:字典。如果你写的是专业的 Python 代码,肯定会用到字典,那就让我们多了解一下吧!
利用字典
字典是 Python 中最常用的数据结构之一。字典是访问数据的一种更快的方式。Python 有优雅的内置字典库,这也使它们易于使用。在这一部分,你将仔细观察字典的一些最有用的特性。
什么时候使用字典,什么时候使用其他数据结构
当您考虑可以映射数据的东西时,可能是时候考虑将字典作为代码中的数据结构了。
如果您正在存储需要某种映射的数据,并且您需要快速访问它,那么使用字典将是明智的;但是,您不希望考虑为每个数据存储使用一个字典。
因此,作为一个例子,考虑当您需要一个类的额外机制或需要一个对象时的情况,或者当您需要数据结构中的不变性时考虑使用 tuple 或namedtuple。在构建代码时,考虑一下需要哪种特定的数据结构。
收集
collections是 Python 中有用的模块之一。这是一种高性能的数据类型。collections有许多接口,对于用dictionary执行不同的任务非常有用。所以,我们来看看collections中的一些主要工具。
计数器
给你一个方便的方法来汇总相似的数据。例如,参见清单 2-16 。
from collections import Counter
contries = ["Belarus", "Albania", "Malta", "Ukrain", "Belarus", "Malta", "Kosove", "Belarus"]
Counter(contries)
>>> Counter({'Belarus': 2, 'Albania': 1, 'Malta': 2, 'Ukrain': 1, 'Kosove': 1})
Listing 2-16Counter
Counter是dict的子类。这是一个顺序集合,其中元素存储为字典键,它们的计数存储为值。这是计算数值的最有效的方法之一。Counter有多种有用的方法。most_common()顾名思义,返回最常见的元素及其计数。参见清单 2-17 中的示例。
from collections import Counter
contries = ["Belarus", "Albania", "Malta", "Ukrain", "Belarus", "Malta", "Kosove", "Belarus"]
contries_count = Counter(contries)
>>> Counter({'Belarus': 2, 'Albania': 1, 'Malta': 2, 'Ukrain': 1, 'Kosove': 1})
contries_count.most_common(1)
>>> [('Belarus', 3)]
Listing 2-17most_count() Method in Counter
其他方法如elements()返回一个迭代器,其中元素重复的次数与计数一样多。
双端队列
如果你想创建一个队列和堆栈,那么考虑使用deque。它允许您从左到右追加值。deque还支持线程安全、内存高效的追加和从任意一端弹出,具有相同的 O(1)性能。
deque有append(x)追加到右侧、appendleft(x)追加到左侧、clear()移除所有元素、pop()移除右侧、popleft()移除左侧、reverse()反转元素等方法。让我们来看一些案例。参见清单 2-18 。
from collections import deque
# Make a deque
deq = deque("abcdefg")
# Iterate over the deque's element
[item.upper() for item in deq]
>>> deque(["A", "B", "C", "D", "E", "F", "G"])
# Add a new entry to right side
deq.append("h")
>>> deque(["A", "B", "C", "D", "E", "F", "G", "h"])
# Add an new entry to the left side
deq.appendleft("I")
>>> deque(["I", "A", "B", "C", "D", "E", "F", "G", "h"])
# Remove right most element
deq.pop()
>>> "h"
# Remove leftmost element
deq.popleft()
>>> "I"
# empty deque
deq.clear()
Listing 2-18deque
defaultdict(预设字典)
一个defaultdict像dict一样工作,因为它是 dict 的子类。一个defaultdict用function("default factory")初始化,它不带参数,为一个不存在的键提供默认值。defaultdict不像dict那样养出一个KeyError。任何不存在的键都将获得默认工厂返回的值。
让我们看看清单 2-19 中的简单例子。
from collections import defaultdict
# Make a defaultdict
colors = defaultdict(int)
# Try printing value of non-existing key would give us default values
colors["orange"]
>>> 0
print(colors)
>>> defaultdict(int, {"orange": 0})
Listing 2-19defaultdict
命名元组
最流行的工具之一是collection模块中的namedtuple。它是tuple的子类,有一个命名字段和固定长度。namedtuple可以在代码中使用元组的任何地方使用。namedtuple是一个不可变的列表,使得阅读代码和访问数据变得更加容易。
我已经讨论过namedtuple,所以参考那个讨论来了解更多。
有序直接
ordereddict当你想以特定的顺序得到按键时可以使用。dict不给你的顺序是插入顺序,这是ordereddict的主要特点。在 Python 3.6+中,dict也有这个特性,默认情况下 dict 是按照插入顺序排序的。
因此,作为一个例子,请参见清单 2-20 。
from collections import ordereddict
# Make a OrderedDict
colors = OrderedDict()
# Assing values
colors["orange"] = "ORANGE"
colors["blue"] = "BLUE"
colors["green"] = "GREEN"
# Get values
[k for k, v in colors.items()]
>>> ["orange", "blue", "green"]
Listing 2-20OrderedDict
有序字典与默认字典和普通字典
我在前面的章节中提到了其中的一些主题。现在让我们仔细看看一些不同类型的字典。
OrderedDict和DefaultDict字典类型是dict类(一个普通字典)的子类,增加了一些特性使它们与dict有所区别。然而,它们拥有与普通词典相同的所有功能。Python 中的这些字典类型是有原因的,我将讨论在哪里可以使用这些不同的字典来最好地利用这些库。
从 Python 3.6 开始,dict现在按插入顺序排序,这实际上降低了ordereddict的有用性。
现在我们来谈谈 Python 之前版本的OrderedDict。OrderedDict在您将值插入字典时为您提供有序的值。有时在代码中,您可能希望以有序的方式访问数据;这就是你可以使用OrderedDict的地方。OrderedDict与字典相比,没有任何额外的成本,所以在性能方面两者是一样的。
假设您想存储一种编程语言首次引入的时间。您可以使用OrderedDict来获取该语言的信息,就像您插入该语言的创建年份一样,如清单 2-21 所示。
from collections import OrderedDict
# Make a OrderedDict
language_found = OrderedDict()
# Insert values
language_found ["Python"] = 1990
language_found ["Java"] = 1995
language_found ["Ruby"] = 1995
# Get values
[k for k, v in langauge_found.items()]
>>> ["Python", "Java", "Ruby"]
Listing 2-21
OrderedDict
当您在字典中访问或插入键时,有时您希望将默认值分配给键。在普通字典中,如果键不存在,你会得到KeyError。但是,defaultdict会为您创建密钥。见清单 2-22 。
from collections import defaultdict
# Make a defaultdict
language_found = defaultdict(int)
# Try printing value of non-existing key
language_found["golang"]
>>> 0
Listing 2-22defaultdict
在这里,当你调用DefaultDict并试图访问不存在的golang键时,defaultdict会在内部调用函数对象(在language_found的例子中是int),这个函数对象是你在构造函数中传递的。它是一个可调用的对象,包括函数和类型对象。所以,你传递的int和list是进入defaultdict的函数。当您试图访问不存在的键时,它会调用已传递的函数,并将其返回值指定为新键的值。
正如您已经知道的,字典是 Python 中的键值集合。许多像defaultdict和OrderedDict这样的高级库正在字典的基础上构建,以添加一些在性能方面没有额外成本的新特性。dict肯定会稍微快一点;但是,大多数情况下会有一个疏忽的区别。因此,在为这些问题编写自己的解决方案时,请考虑使用它们。
使用字典的 switch 语句
Python 没有switch关键字。然而,Python 有许多特性可以以更简洁的方式实现这一功能。您可以利用dictionary来创建一个switch语句,并且当您基于特定的标准有多个选项可供选择时,您也应该考虑以这种方式编写代码。
考虑一个根据特定国家的税收规则计算每个县的税收的系统。有多种方法可以做到这一点;然而,拥有多个选项最困难的部分是不要在代码中添加多个if else条件。让我们看看如何以更优雅的方式使用dictionary来解决这个问题。见清单 2-23 。
def tanzania(amount):
calculate_tax = <Tax Code>
return calculate_tax
def zambia(amount):
calculate_tax = <Tax Code>
return calculate_tax
def eritrea(amount):
calculate_tax = <Tax Code>
return calculate_tax
contry_tax_calculate = {
"tanzania": tanzania,
"zambia": zambia,
"eritrea": eritrea,
}
def calculate_tax(country_name, amount):
country_tax_calculate"contry_name"
calculate_tax("zambia", 8000000)
Listing 2-23switch Statement Using a Dictionary
这里,您只需使用字典来计算税款,与使用典型的switch语句相比,这使得您的代码更加优雅,可读性更好。
合并两本词典的方法
假设您有两本想要合并的词典。与以前的版本相比,在 Python 3.5+中这样做要简单得多。合并任何两个数据结构都很棘手,因为在合并数据结构时,您需要小心内存使用和数据丢失。如果您使用额外的内存来保存合并的数据结构,那么考虑到字典中的数据大小,您应该知道系统的内存限制。
丢失数据也是一个问题。您可能会发现,由于特定数据结构的限制,一些数据已经丢失;例如,在字典中,不能有重复的键。因此,无论何时在字典之间执行合并操作,都要记住这些事情。
在 Python 3.5+中,可以这样做,如清单 2-24 所示。
salary_first = {"Lisa": 238900, "Ganesh": 8765000, "John": 3450000}
salary_second = {"Albert": 3456000, "Arya": 987600}
{**salary_first, **salary_second}
>>> {"Lisa": 238900, "Ganesh": 8765000, "John": 345000, "Albert": 3456000, "Ary": 987600}
Listing 2-24Merge Dictionaries in Python 3.5+
然而,在 Python 3.5 之前的版本中,只需做一点额外的工作就可以做到这一点。参见清单 2-25 。
salary_first = {"Lisa": 238900, "Ganesh": 8765000, "John": 3450000}
salary_second = {"Albert": 3456000, "Arya": 987600}
salary = salary_first.copy()
salary.update(salary_second)
>>> {"Lisa": 238900, "Ganesh": 8765000, "John": 345000, "Albert": 3456000, "Ary": 987600}
Listing 2-25Merge Dictionaries in Pre-3.5 Python
Python 3.5+有 PEP 448,它提出了对* iterable 解包操作符和**字典解包操作符的扩展使用。
这无疑使代码更具可读性。这不仅适用于字典,也适用于 Python 3.5 以后的列表。
漂亮地印刷一本字典
Python 有一个名为pprint的模块,所以你可以很好地打印。您需要导入pprint来执行操作。
在打印任何数据结构时,给你提供缩进的选项。缩进将应用于您的数据结构。参见清单 2-26 。
import pprint
pp = pprint.PrettyPrinter(indent=4)
pp.pprint(colors)
Listing 2-26pprint for a Dictionary
对于嵌套更深、数据量更大的复杂词典来说,这可能无法达到预期效果。您可以考虑使用 JSON 来实现这一点,如清单 2-27 所示。
import json
data = {'a':12, 'b':{'x':87, 'y':{'t1': 21, 't2':34}}
json.dumps(data, sort_keys=True, indent=4)
Listing 2-27Using json to Print Dictionaries
摘要
数据结构是每一种编程语言的核心。正如您在阅读本章时了解到的,Python 提供了许多数据结构来存储和操作数据。Python 以数据结构的形式为您提供了各种工具,用于对不同类型的对象或数据集执行各种操作。作为一名 Python 开发人员,了解不同种类的数据结构非常重要,这样您就可以在编写应用时做出正确的决定,尤其是在资源密集型应用中。
我希望这一章能帮助你了解 Python 中一些最有用的数据结构。熟悉不同类型的数据结构及其不同的行为会使你成为更好的开发人员,因为你的工具箱中可以有不同类型的工具。