编写高效且优雅的 Python 代码
Python 作为一门入门极易并容易上瘾的语音,相信已经成为了很多人 “写着玩” 的标配脚本语言。但很多教材并没有教授 Python 的进阶和优化。本文作为进阶系列的文章,从基础的语法到函数、迭代器、类,还有之后系列的线程 / 进程、第三方库、网络编程等内容,共同学习如何写出更加 Pythonic 的代码部分提炼自书籍:《Effective Python》&《Python3 Cookbook》,但也做出了修改,并加上了我自己的理解和运用中的最佳实践
Pythonic
列表切割
list[start:end:step]
如果从列表开头开始切割,那么忽略 start 位的 0,例如list[:4]
如果一直切到列表尾部,则忽略 end 位的 0,例如list[3:]
切割列表时,即便 start 或者 end 索引跨界也不会有问题
列表切片不会改变原列表。索引都留空时,会生成一份原列表的拷贝
b = a[:]
assert b == a and b is not a # true
列表推导式
使用列表推导式来取代map和filter
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# use map
squares = map(lambda x: x ** 2, a)
# use list comprehension
squares = [x ** 2 for x in a]
# 一个很大的好处是,列表推导式可以对值进行判断,比如
squares = [x ** 2 for x in a if x % 2 == 0]
# 而如果这种情况要用 map 或者 filter 方法实现的话,则要多写一些函数
不要使用含有两个以上表达式的列表推导式
# 有一个嵌套的列表,现在要把它里面的所有元素扁平化输出
list = [[
[1, 2, 3],
[4, 5, 6]
]]
# 使用列表推导式
flat_list = [x for list0 in list for list1 in list0 for x in list1]
# [1, 2, 3, 4, 5, 6]
# 可读性太差,易出错。这种时候更建议使用普通的循环
flat_list = []
for list0 in list:
for list1 in list0:
flat_list.extend(list1)
数据多时,列表推导式可能会消耗大量内存,此时建议使用生成器表达式
# 在列表推导式的推导过程中,对于输入序列的每个值来说,都可能要创建仅含一项元素的全新列表。因此数据量大时很耗性能。
# 使用生成器表达式
list = (x ** 2 for x in range(0, 1000000000))
# 生成器表达式返回的迭代器,只有在每次调用时才生成值,从而避免了内存占用
迭代
需要获取 index 时使用enumerate
enumerate可以接受第二个参数,作为迭代时加在index上的数值
list = ['a', 'b', 'c', 'd']
for index, value in enumerate(list):
print(index)
# 0
# 1
# 2
# 3
for index, value in enumerate(list, 2):
print(index)
# 2
# 3
# 4
# 5
用zip同时遍历两个迭代器
list_a = ['a', 'b', 'c', 'd']
list_b = [1, 2, 3]
# 虽然列表长度不一样,但只要有一个列表耗尽,则迭代就会停止
for letter, number in zip(list_a, list_b):
print(letter, number)
# a 1
# b 2
# c 3
zip遍历时返回一个元组
a = [1, 2, 3]
b = ['w', 'x', 'y', 'z']
for i in zip(a,b):
print(i)
# (1, 'w')
# (2, 'x')
# (3, 'y')
关于
for
和while
循环后的else
块
循环正常结束之后会调用
else
内的代码循环里通过
break
跳出循环,则不会执行else
要遍历的序列为空时,立即执行
else
for i in range(2):
print(i)
else:
print('loop finish')
# 0
# 1
# loop finish
for i in range(2):
print(i)
if i % 2 == 0:
break
else:
print('loop finish')
# 0
反向迭代
对于普通的序列(列表),我们可以通过内置的reversed()函数进行反向迭代:
list_example = [i for i in range(5)]
iter_example = (i for i in range(5)) # 迭代器
set_example = {i for i in range(5)} # 集合
# 普通的正向迭代
# for i in list_example
# 通过 reversed 进行反向迭代
for i in reversed(list_example):
print(i)
# 4
# 3
# 2
# 1
# 0
# 但无法作用于 集合 和 迭代器
reversed(iter_example) # TypeError: argument to reversed() must be a sequence
除此以外,还可以通过实现类里的__reversed__方法,将类进行反向迭代:
class Countdown:
def __init__(self, start):
self.start = start
# 正向迭代
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
# 反向迭代
def __reversed__(self):
n = 1
while n <= self.start:
yield n
n += 1
for i in reversed(Countdown(4)):
print(i)
# 1
# 2
# 3
# 4
for i in Countdown(4):
print(i)
# 4
# 3
# 2
# 1
try/except/else/finally
如果try内没有发生异常,则调用else内的代码
else会在finally之前运行
最终一定会执行finally,可以在其中进行清理工作
函数
使用装饰器
装饰器用于在不改变原函数代码的情况下修改已存在的函数。常见场景是增加一句调试,或者为已有的函数增加log监控
举个栗子:
def decorator_fun(fun):
def new_fun(*args, **kwargs):
print('current fun:', fun.__name__)
print('position arguments:', args)
print('key arguments:', **kwargs)
result = fun(*args, **kwargs)
print(result)
return result
return new_fun
@decorator_fun
def add(a, b):
return a + b
add(3, 2)
# current fun: add
# position arguments: (3, 2)
# key arguments: {}
# 5
除此以外,还可以编写接收参数的装饰器,其实就是在原本的装饰器上的外层又嵌套了一个函数:
def read_file(filename='results.txt'):
def decorator_fun(fun):
def new_fun(*args, **kwargs):
result = fun(*args, **kwargs)
with open(filename, 'a') as f:
f.write(result + '\n')
return result
return new_fun
return decorator_fun
# 使用装饰器时代入参数
@read_file(filename='log.txt')
def add(a, b):
return a + b
但是像上面那样使用装饰器的话有一个问题:
@decorator_fun
def add(a, b):
return a + b
print(add.__name__)
# new_fun
也就是说原函数已经被装饰器里的new_fun函数替代掉了。调用经过装饰的函数,相当于调用一个新函数。查看原函数的参数、注释、甚至函数名的时候,只能看到装饰器的相关信息。为了解决这个问题,我们可以使用 Python 自带的functools.wraps方法。
stackoverflow: What does functools.wraps do?
functools.wraps是个很 hack 的方法,它本事作为一个装饰器,做用在装饰器内部将要返回的函数上。也就是说,它是装饰器的装饰器,并且以原函数为参数,作用是保留原函数的各种信息,使得我们之后查看被装饰了的原函数的信息时,可以保持跟原函数一模一样。
from functools import wraps
def decorator_fun(fun):
@wraps(fun)
def new_fun(*args, **kwargs):
result = fun(*args, **kwargs)
print(result)
return result
return new_fun
@decorator_fun
def add(a, b):
return a + b
print(add.__name__)
# add
此外,有时候我们的装饰器里可能会干不止一个事情,此时应该把事件作为额外的函数分离出去。但是又因为它可能仅仅和该装饰器有关,所以此时可以构造一个装饰器类。原理很简单,主要就是编写类里的__call__方法,使类能够像函数一样的调用。
from functools import wraps
class logResult(object):
def __init__(self, filename='results.txt'):
self.filename = filename
def __call__(self, fun):
@wraps(fun)
def new_fun(*args, **kwargs):
result = fun(*args, **kwargs)
with open(filename, 'a') as f:
f.write(result + '\n')
return result
self.send_notification()
return new_fun
def send_notification(self):
pass
@logResult('log.txt')
def add(a, b):
return a + b
使用生成器
考虑使用生成器来改写直接返回列表的函数
# 定义一个函数,其作用是检测字符串里所有 a 的索引位置,最终返回所有 index 组成的数组
def get_a_indexs(string):
result = []
for index, letter in enumerate(string):
if letter == 'a':
result.append(index)
return result
用这种方法有几个小问题:
每次获取到符合条件的结果,都要调用append方法。但实际上我们的关注点根本不在这个方法,它只是我们达成目的的手段,实际上只需要index就好了
返回的result可以继续优化
数据都存在result里面,如果数据量很大的话,会比较占用内存
因此,使用生成器generator会更好。生成器是使用yield表达式的函数,调用生成器时,它不会真的执行,而是返回一个迭代器,每次在迭代器上调用内置的next函数时,迭代器会把生成器推进到下一个yield表达式:
def get_a_indexs(string):
for index, letter in enumerate(string):
if letter == 'a':
yield index
获取到一个生成器以后,可以正常的遍历它:
string = 'this is a test to find a\' index'
indexs = get_a_indexs(string)
# 可以这样遍历
for i in indexs:
print(i)
# 或者这样
try:
while True:
print(next(indexs))
except StopIteration:
print('finish!')
# 生成器在获取完之后如果继续通过 next() 取值,则会触发 StopIteration 错误
# 但通过 for 循环遍历时会自动捕获到这个错误
如果你还是需要一个列表,那么可以将函数的调用结果作为参数,再调用list方法
results = get_a_indexs('this is a test to check a')
results_list = list(results)
可迭代对象
需要注意的是,普通的迭代器只能迭代一轮,一轮之后重复调用是无效的。解决这种问题的方法是,你可以定义一个可迭代的容器类:
class LoopIter(object):
def __init__(self, data):
self.data = data
# 必须在 __iter__ 中 yield 结果
def __iter__(self):
for index, letter in enumerate(self.data):
if letter == 'a':
yield index
这样的话,将类的实例迭代重复多少次都没问题:
string = 'this is a test to find a\' index'
indexs = LoopIter(string)
print('loop 1')
for _ in indexs:
print(_)
# loop 1
# 8
# 23
print('loop 2')
for _ in indexs:
print(_)
# loop 2
# 8
# 23
但要注意的是,仅仅是实现__iter__方法的迭代器,只能通过for循环来迭代;想要通过next方法迭代的话则需要使用iter方法:
string = 'this is a test to find a\' index'
indexs = LoopIter(string)
next(indexs) # TypeError: 'LoopIter' object is not an iterator
iter_indexs = iter(indexs)
next(iter_indexs) # 8
使用位置参数
有时候,方法接收的参数数目可能不一定,比如定义一个求和的方法,至少要接收两个参数:
def sum(a, b):
return a + b
# 正常使用
sum(1, 2) # 3
# 但如果我想求很多数的总和,而将参数全部代入是会报错的,而一次一次代入又太麻烦
sum(1, 2, 3, 4, 5) # sum() takes 2 positional arguments but 5 were given
对于这种接收参数数目不一定,而且不在乎参数传入顺序的函数,则应该利用位置参数*args:
def sum(*args):
result = 0
for num in args:
result += num
return result
sum(1, 2) # 3
sum(1, 2, 3, 4, 5) # 15
# 同时,也可以直接把一个数组带入,在带入时使用 * 进行解构
sum(*[1, 2, 3, 4, 5]) # 15
但要注意的是,不定长度的参数args在传递给函数时,需要先转换成元组tuple。这意味着,如果你将一个生成器作为参数带入到函数中,生成器将会先遍历一遍,转换为元组。这可能会消耗大量内存:
def get_nums():
for num in range(10):
yield num
nums = get_nums()
sum(*nums) # 45
# 但在需要遍历的数目较多时,会占用大量内存
使用关键字参数
关键字参数可提高代码可读性
可以通过关键字参数给函数提供默认值
便于扩充函数参数
定义只能使用关键字参数的函数
普通的方式,在调用时不会强制要求使用关键字参数
# 定义一个方法,它的作用是遍历一个数组,找出等于(或不等于)目标元素的 index
def get_indexs(array, target='', judge=True):
for index, item in enumerate(array):
if judge and item == target:
yield index
elif not judge and item != target:
yield index
array = [1, 2, 3, 4, 1]
# 下面这些都是可行的
result = get_indexs(array, target=1, judge=True)
print(list(result)) # [0, 4]
result = get_indexs(array, 1, True)
print(list(result)) # [0, 4]
result = get_indexs(array, 1)
print(list(result)) # [0, 4]
使用 Python3 中强制关键字参数的方式
# 定义一个方法,它的作用是遍历一个数组,找出等于(或不等于)目标元素的 index
def get_indexs(array, *, target='', judge=True):
for index, item in enumerate(array):
if judge and item == target:
yield index
elif not judge and item != target:
yield index
array = [1, 2, 3, 4, 1]
# 这样可行
result = get_indexs(array, target=1, judge=True)
print(list(result)) # [0, 4]
# 也可以忽略有默认值的参数
result = get_indexs(array, target=1)
print(list(result)) # [0, 4]
# 但不指定关键字参数则报错
get_indexs(array, 1, True)
# TypeError: get_indexs() takes 1 positional argument but 3 were given
使用 Python2 中强制关键字参数的方式
# 定义一个方法,它的作用是遍历一个数组,找出等于(或不等于)目标元素的 index
# 使用 **kwargs,代表接收关键字参数,函数内的 kwargs 则是一个字典,传入的关键字参数作为键值对的形式存在
def get_indexs(array, **kwargs):
target = kwargs.pop('target', '')
judge = kwargs.pop('judge', True)
for index, item in enumerate(array):
if judge and item == target:
yield index
elif not judge and item != target:
yield index
array = [1, 2, 3, 4, 1]
# 这样可行
result = get_indexs(array, target=1, judge=True)
print(list(result)) # [0, 4]
# 也可以忽略有默认值的参数
result = get_indexs(array, target=1)
print(list(result)) # [0, 4]
# 但不指定关键字参数则报错
get_indexs(array, 1, True)
# TypeError: get_indexs() takes 1 positional argument but 3 were given
关于参数的默认值
算是老生常谈了:函数的默认值只会在程序加载模块并读取到该函数的定义时设置一次
也就是说,如果给某参数赋予动态的值( 比如[]或者{}),则如果之后在调用函数的时候给参数赋予了其他参数,则以后再调用这个函数的时候,之前定义的默认值将会改变,成为上一次调用时赋予的值:
def get_default(value=[]):
return value
result = get_default()
result.append(1)
result2 = get_default()
result2.append(2)
print(result) # [1, 2]
print(result2) # [1, 2]
因此,更推荐使用None作为默认参数,在函数内进行判断之后赋值:
def get_default(value=None):
if value is None:
return []
return value
result = get_default()
result.append(1)
result2 = get_default()
result2.append(2)
print(result) # [1]
print(result2) # [2]
类
__slots__
默认情况下,Python 用一个字典来保存一个对象的实例属性。这使得我们可以在运行的时候动态的给类的实例添加新的属性:
test = Test()
test.new_key = 'new_value'
然而这个字典浪费了多余的空间 — 很多时候我们不会创建那么多的属性。因此通过__slots__可以告诉 Python 不要使用字典而是固定集合来分配空间。
class Test(object):
# 用列表罗列所有的属性
__slots__ = ['name', 'value']
def __init__(self, name='test', value='0'):
self.name = name
self.value = value
test = Test()
# 此时再增加新的属性则会报错
test.new_key = 'new_value'
# AttributeError: 'Test' object has no attribute 'new_key'
__call__
通过定义类中的__call__方法,可以使该类的实例能够像普通函数一样调用。
class AddNumber(object):
def __init__(self):
self.num = 0
def __call__(self, num=1):
self.num += num
add_number = AddNumber()
print(add_number.num) # 0
add_number() # 像方法一样的调用
print(add_number.num) # 1
add_number(3)
print(add_number.num) # 4
通过这种方式实现的好处是,可以通过类的属性来保存状态,而不必创建一个闭包或者全局变量。
@classmethod & @staticmethod
资料:
Python @classmethod and @staticmethod for beginner
Difference between staticmethod and classmethod in python
@classmethod和@staticmethod很像,但他们的使用场景并不一样。
类内部普通的方法,都是以self作为第一个参数,代表着通过实例调用时,将实例的作用域传入方法内;
@classmethod以cls作为第一个参数,代表将类本身的作用域传入。无论通过类来调用,还是通过类的实例调用,默认传入的第一个参数都将是类本身
@staticmethod不需要传入默认参数,类似于一个普通的函数
来通过实例了解它们的使用场景:
假设我们需要创建一个名为Date的类,用于储存 年/月/日 三个数据
class Date(object):
def __init__(self, year=0, month=0, day=0):
self.year = year
self.month = month
self.day = day
@property
def time(self):
return "{year}-{month}-{day}".format(
year=self.year,
month=self.month,
day=self.day
)
上述代码创建了Date类,该类会在初始化时设置day/month/year属性,并且通过property设置了一个getter,可以在实例化之后,通过time获取存储的时间:
date = Date('2016', '11', '09')
date.time # 2016-11-09
但如果我们想改变属性传入的方式呢?毕竟,在初始化时就要传入年/月/日三个属性还是很烦人的。能否找到一个方法,在不改变现有接口和方法的情况下,可以通过传入2016-11-09这样的字符串来创建一个Date实例?
你可能会想到这样的方法:
date_string = '2016-11-09'
year, month, day = map(str, date_string.split('-'))
date = Date(year, month, day)
但不够好:
在类外额外多写了一个方法,每次还得格式化以后获取参数
这个方法也只跟Date类有关
没有解决传入参数过多的问题
此时就可以利用@classmethod,在类的内部新建一个格式化字符串,并返回类的实例的方法:
# 在 Date 内新增一个 classmethod
@classmethod
def from_string(cls, string):
year, month, day = map(str, string.split('-'))
# 在 classmethod 内可以通过 cls 来调用到类的方法,甚至创建实例
date = cls(year, month, day)
return date
这样,我们就可以通过Date类来调用from_string方法创建实例,并且不侵略、修改旧的实例化方式:
date = Date.from_string('2016-11-09')
# 旧的实例化方式仍可以使用
date_old = Date('2016', '11', '09')
好处:
在@classmethod内,可以通过cls参数,获取到跟外部调用类时一样的便利
可以在其中进一步封装该方法,提高复用性
更加符合面向对象的编程方式
而@staticmethod,因为其本身类似于普通的函数,所以可以把和这个类相关的 helper 方法作为@staticmethod,放在类里,然后直接通过类来调用这个方法。
# 在 Date 内新增一个 staticmethod
@staticmethod
def is_month_validate(month):
return int(month) <= 12 and int(month) >= 1
将与日期相关的辅助类函数作为@staticmethod方法放在Date类内后,可以通过类来调用这些方法:
month = '08'
if not Date.is_month_validate(month):
print('{} is a validate month number'.format(month))
创建上下文管理器
上下文管理器,通俗的介绍就是:在代码块执行前,先进行准备工作;在代码块执行完成后,做收尾的处理工作。with语句常伴随上下文管理器一起出现,经典场景有:
with open('test.txt', 'r') as file:
for line in file.readlines():
print(line)
通过with语句,代码完成了文件打开操作,并在调用结束,或者读取发生异常时自动关闭文件,即完成了文件读写之后的处理工作。如果不通过上下文管理器的话,则会是这样的代码:
file = open('test.txt', 'r')
try:
for line in file.readlines():
print(line)
finally:
file.close()
比较繁琐吧?所以说使用上下文管理器的好处就是,通过调用我们预先设置好的回调,自动帮我们处理代码块开始执行和执行完毕时的工作。而通过自定义类的__enter__和__exit__方法,我们可以自定义一个上下文管理器。
class ReadFile(object):
def __init__(self, filename):
self.file = open(filename, 'r')
def __enter__(self):
return self.file
def __exit__(self, type, value, traceback):
# type, value, traceback 分别代表错误的类型、值、追踪栈
self.file.close()
# 返回 True 代表不抛出错误
# 否则错误会被 with 语句抛出
return True
然后可以以这样的方式进行调用:
with ReadFile('test.txt') as file_read:
for line in file_read.readlines():
print(line)
在调用的时候:
with语句先暂存了ReadFile类的__exit__方法
然后调用ReadFile类的__enter__方法
__enter__方法打开文件,并将结果返回给with语句
上一步的结果被传递给file_read参数
在with语句内对file_read参数进行操作,读取每一行
读取完成之后,with语句调用之前暂存的__exit__方法
__exit__方法关闭了文件
要注意的是,在__exit__方法内,我们关闭了文件,但最后返回True,所以错误不会被with语句抛出。否则with语句会抛出一个对应的错误。
编写高效且优雅的 Python 代码相关推荐
- python写出的程序如何给别人使用-涨姿势!这些小技巧让小白也可以写出更优雅的Python代码!...
原标题:涨姿势!这些小技巧让小白也可以写出更优雅的Python代码! 一.前言 我前两天回答了两个Python相关的问题,收到了很多赞,从答案被收藏的情况来看,确实对不少人都很有帮助,所以我也很开心. ...
- 如何写出清晰又优雅的Python代码?我们给你这26条建议
来源:大数据DT本文约1900字,建议阅读5分钟PEP 8非常详细地描述了如何编写清晰的Python代码. [ 导读 ] Python Enhancement Proposal #8叫作PEP 8,它 ...
- 11个技巧让你编写出更好的Python代码,值得收藏!!
在本教程中,我们将展示11个技巧来编写更好的Python代码!我们展示了许多最佳实践,它们通过使代码更加简洁和更具python风格来改进代码.以下是所有技巧的概述: 1)使用enumerate()而不 ...
- python如何写代码_如何写出优雅的Python代码?
有时候你会看到很Cool的Python代码,你惊讶于它的简洁,它的优雅,你不由自主地赞叹:竟然还能这样写.其实,这些优雅的代码都要归功于Python的特性,只要你能掌握这些Pythonic的技巧,你一 ...
- 6种更优雅书写Python代码!
1 简介 一些比较熟悉pandas的读者朋友应该经常会使用query().eval().pipe().assign()等pandas的常用方法,书写可读性很高的「链式」数据分析处理代码,从而更加丝滑流 ...
- 12个优雅的 python 代码使用案例
文章目录 1. 切片 2. not运算符 3. F-strings 4. print函数 4.1 end参数 4.2. sep参数 5. 合并字典 6. 条件语句 7. 下划线 7.1 调用结果 7. ...
- vim编写python_用vim写python代码
适用环境:linux系统,windows的没有研究过,可能路径不同 如何使用: git clone https://github.com/itnihao/vimrc-python.git cd vim ...
- python代码写名字_必知必会系列_python代码优雅之道之代码命名约定
代码的命名约定对代码的提高可读性影响巨大.本文中,我们将总结并向您提供一些关于命名的最佳实践的示例,以帮助您编写更优雅的Python代码,使将来可能阅读和使用您的代码的人(包括您自己)受益. Pyth ...
- 【Python基础】如何编写简洁美观的Python代码
作者 | ANIRUDDHA BHANDARI 编译 | VK 来源 | Analytics Vidhya 概述 Python风格教程将使你能够编写整洁漂亮的Python代码 在这个风格教程中学习不同 ...
最新文章
- 【Kubernetes】两篇文章 搞懂 K8s 的 fannel 网络原理
- Netty 4.1 Getting Start (翻译) + Demo
- ArrayList之坑点
- 关于WM_NCHITTEST消息(移动无标题对话框多个)
- Luogu P2580 于是他错误的点名开始了 Trie树 字典树
- php编写开机启动脚本,设置 msyql php-fpm 开机自动启动脚本
- OpenCV Mat遍历的方法
- 维特比算法(Viterbi Algorithm)
- CREO图文教程:三维设计案例之水龙头的螺旋弹簧设计图文教程之详细攻略
- 鸡兔同笼编程语言c,鸡兔同笼用c语言编程
- 核磁计算机系统包括,核磁共振和CAT扫描系统
- WARN: Establishing SSL connection without server‘s identity verification is not recommended. Acco...
- 骁龙778gplus什么水平 骁龙778gplus什么级别 骁龙778gplus相当于什么档次
- m4a转mp3,flac转mp3,wav转mp3
- 电影之《变形金刚4》
- 【CVPR2022教程】普渡大学《通过大气湍流成像:理论、模拟和恢复》教程
- B/S程序设计 经典好书(备忘)
- python 写入excel数字格式_从Pandas写入Excel时设置默认数字格式
- js转换时间戳一直转换成1970的解决方法
- 打印机控制 佳博 Gprinter GP-9134T
热门文章
- 哪有什么岁月静好,不过是有人替你负重前行
- 3分钟快速理解什么是Docker容器
- 【吊炸天】TensorFlow什么的都弱爆了,强者只用Numpy搭建神经网络
- AttributeError: 'dict' object has no attribute 'iteritems' .iteritems(): 修改为items()
- 图解数字签名Digital Signature 和数字证书Public-key certificate
- mac maven安装位置_Maven的安装以及仓库的作用
- 苹果ios15.4RC版发布:新增口罩面容解锁功能
- 苹果因芯片短缺优先生产iPhone 13 常规iPad平均交付时间超7周
- 成立仅一年的天猫好房,凭什么让55万人排队领钱?
- 十三不香了?不止去掉刘海,iPhone14或改用QLC闪存:最高2TB容量