如何优雅高效地使用Python——这些Python技巧你必须学会!

文章目录

  • 前言
  • Python之禅
  • Python:优雅高效的写法
    • 多变量赋值
    • 变量交换
    • 格式化字符串
    • 序列并包(pack)
    • 序列解包(unpack)
    • 条件表达式
    • if结构简化
    • if链式条件表达式
    • any & all
    • eval
    • 遍历元素与下标
    • for/else
    • dict映射代替多条件查找
    • 访问字典元素
    • defaultdict
    • 列表/字典解析式
    • 字符串连接
    • "_"的妙用
    • map函数
    • reduce函数
    • filter函数
    • 生成器(generator)
    • yield
    • partial函数
    • lru_cache
    • 枚举
  • Reference

前言

目前Python已经更新到了3.7版本,不必多说,Python 3比Python 2更是多出了许多新的功能。Python是一门友好的语言,其区别于以往C++,Java的特点不仅是代码易于阅读,同时代码也更加优雅简洁,实现同样的功能相对于Python只需要短短几行代码,这给予了开发人员更大的便利,同时也易于初学者学习。本文将介绍Python中一些有趣实用的(代码)功能,希望这些代码能够帮助大家更加轻松优雅地解决一些问题。
注:本博客代码结果均为Python 3.6版本运行结果

Python之禅

The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea – let’s do more of those!

Python之禅 by Tim Peters

优美胜于丑陋(Python以编写优美的代码为目标)
明了胜于晦涩(优美的代码应当是明了的,命名规范,风格相似)
简洁胜于复杂(优美的代码应当是简洁的,不要有复杂的内部实现)
复杂胜于凌乱(如果复杂不可避免,那代码间也不能有难懂的关系,要保持接口简洁)
扁平胜于嵌套(优美的代码应当是扁平的,不能有太多的嵌套)
间隔胜于紧凑(优美的代码有适当的间隔,不要奢望一行代码解决问题)
可读性很重要(优美的代码是可读的)
即便假借特例的实用性之名,也不可违背这些规则(这些规则至高无上)
不要包容所有错误,除非您确定需要这样做(精准地捕获异常,不写 except:pass 风格的代码)
当存在多种可能,不要尝试去猜测
而是尽量找一种,最好是唯一一种明显的解决方案(如果不确定,就用穷举法)
虽然这并不容易,因为您不是 Python 之父(这里的 Dutch 是指 Guido )
做也许好过不做,但不假思索就动手还不如不做(动手之前要细思量)
如果您无法向人描述您的方案,那肯定不是一个好方案;反之亦然(方案测评标准)
命名空间是一种绝妙的理念,我们应当多加利用(倡导与号召)

Python:优雅高效的写法

多变量赋值

当你想要初始化多个变量的时候:

  1. Bad
x = []
y = []
z = []
  1. Better
x, y, z = [], [], []

这样做的话代码更加简洁,同时可读性更高

变量交换

  1. Bad
# edchange x and y
t = x
x = y
y = t
  1. Better
# edchange x and y
x, y = y, x

这样做的话不止代码简洁,让人一眼就能看出是要交换变量,同时也能免去考虑中间变量赋值的先后顺序,并且后者的效率更是高于前者。

格式化字符串

如果你想要格式化输出一串字符串,你会怎么做?

  1. Bad
name = "James"
country = "USA"
string = "My name is %s, from %s, I love %s" % (name, country, country)
>>> string

'My name is James, from USA, I love USA'
  1. Better
name = "James"
country = "USA"
string = "My name is {}, from {}, I love {}".format(name, country, country)
>>> string

'My name is James, from USA, I love USA'
  1. Best
name = "James"
country = "USA"
string = "My name is {name}, from {country}, I love {country}".format(name=name, country=country)

'My name is James, from USA, I love USA'

# or you can simplipy it by using f-strings
name = "James"
country = "USA"
string = f"My name is {name}, from {country}, I love {country}"
>>> string

'My name is James, from USA, I love USA'

使用format函数比使用%s可读性更高,同时也更易于控制输出格式。

序列并包(pack)

当你想同时访问两个列表的时候,你的做法是?

  1. Bad
names = ['James', 'Tim', 'Katty']
ages = [18, 19, 20]
for i in range(len(names)):
    print("name:", names[i],"age:", ages[i])

name: James age: 18
name: Tim age: 19
name: Katty age: 20

  1. Better
names = ['James', 'Tim', 'Katty']
ages = [18, 19, 20]
for name, age in zip(names,ages):
    print("name:", name,"age:", age)

name: James age: 18
name: Tim age: 19
name: Katty age: 20

后者的方法不仅一目了然,同时遍历元素的行为比遍历下标的行为更加高效!

序列解包(unpack)

当你需要将一个二元组序列拆成两列,你会怎么做?

  1. Bad
Data = [('James', 18), ('Tim', 19), ('Katty', 20)]
names, ages = [], []
for name, age in Data:
    names.append(name)
    ages.append(age)
  1. Better
Data = [('James', 18), ('Tim', 19), ('Katty', 20)]
names = [data[0] for data in Data]
ages = [data[1] for data in Data]
  1. Best
Data = [('James', 18), ('Tim', 19), ('Katty', 20)]
names, ages = zip(*Data)

zip()zip(*)是一对互逆操作,不过需要注意,zip()zip(*)在Python 3返回的都是迭代器,然后zip(*)通过解包返回多个元组。

条件表达式

  1. Bad
if x<y:
    small=x
else:
    small=y
  1. Better
small = x if x<y else y

后者不仅表达意思更加明了,同时代码量也少了好几行。

if结构简化

如果你需要检查几个数值时:

  1. Bad
if x==1 or x==2 or x==3 or x==4:
    print("x =1 or 2 or 3 or 4")
  1. Better
if x in (1,2,3,4):
    print("x =1 or 2 or 3 or 4")

if链式条件表达式

  1. Bad
if x>0 and x<10:
    print("0)
  1. Better
if 0<x<10:
    print("0)

前者是其他语言的判断方法,而后者显然更加简洁明了。

any & all

当存在多个条件判断语句时:

  1. Bad
if a>0 or b>0 or c>0:
    print("one of them greater than 0")

if a>0 and b>0 and c>0:
    print("all of them greater than 0")
  1. Better
if any([a,b,c]):
    print("one of them greater than 0")

if all([a,b,c]):
    print("all of them greater than 0")

eval

eval函数可以轻易的将字符串转化成元素,甚至可以转化表达式:

>>> eval('[1,2,3,4]')
[1,2,3,4]
>>> eval('(1,2,3,4)')
(1,2,3,4)
>>> eval('1+1')
2

eval功能可谓非常强大,即可以做stringlisttupledict之间的类型转换,还可以做计算器使用。它可以对能解析的字符串都做处理,而不顾忌可能带来的后果!所以说eval强大的背后,是巨大的安全隐患。
例如用户恶意输入下面的字符串:

open(r'D://filename.txt', 'r').read()

__import__('os').system('dir')

__import__('os').system('rm -rf /etc/*')

那么eval就会不管三七二十一,显示你电脑目录结构,读取文件,删除文件……如果是格盘等更严重的操作,它也会照做不误。因此,更加安全的做法是使用ast.literal_eval:

>>> ast.literal_eval("__import__('os').system('dir')")

ValueError                                Traceback (most recent call last)
<ipython-input-95-788ef7e6407f> in <module>()
----> 1 ast.literal_eval("__import__('os').system('dir')")

~\Anaconda3\lib\ast.py in literal_eval(node_or_string)
     83                     return left - right
     84         raise ValueError('malformed node or string: ' + repr(node))
---> 85     return _convert(node_or_string)
     86 
     87 

~\Anaconda3\lib\ast.py in _convert(node)
     82                 else:
     83                     return left - right
---> 84         raise ValueError('malformed node or string: ' + repr(node))
     85     return _convert(node_or_string)
     86 

ValueError: malformed node or string: <_ast.Call object at 0x000001C9DBA145F8>

当你试图转化一些"危险"的表达式时,它会阻止你执行并报错。出于安全考虑,对字符串进行类型转换的时候最好使用ast.literal_eval

遍历元素与下标

当你需要遍历元素得同时,获取元素的位置下标:

  1. Bad
names = ['James', 'Tim', 'Katty']
for i in range(len(names)):
    print("Id:{},name:{}".format(i,names[i]))

Id:0,name:James
Id:1,name:Tim
Id:2,name:Katty
  1. Better
names = ['James', 'Tim', 'Katty']
for i,name in enumerate(names):
    print("Id:{},name:{}".format(i,names[i]))

Id:0,name:James
Id:1,name:Tim
Id:2,name:Katty

前者的代码不仅难看,同时通过下标访问元素比遍历元素效率更低,而后者使用enumerate则优雅高效得多。

for/else

如果让你判断某个列表是否存在偶数,存在则输出该偶数,若不存在任何偶数,则输出"There are no even Numbers"

  1. Bad
# The variable exit_even_number is redundant
exit_even_number = False
for i in [1,3,5,7,9]:
    if i%2 == 0:
        print("{} is even number".format(i))
        exit_even_number = True

if not exit_even_number:
    print("There are no even Numbers")
  1. Better
for i in [1,3,5,7,9]:
    if i%2 == 0:
        print("{} is even number".format(i))
else:
    print("There are no even Numbers")

前者多了一个exit_even_number变量,使得代码显得有点臃肿,而使用后者for/else则优雅得多。

dict映射代替多条件查找

  1. Bad
if x == 1:
    y = 100
elif x == 2:
    y = 200
elif x == 
    y = 300
  1. Better
condition = {1:100, 2:200, 3:300}
y = condition[x]

访问字典元素

访问字典元素的方法想必大家都清楚,但是如果字典中不存在该键值对呢?

  1. Bad
>>> phone_number = {'James':123456,'Tim':678910,'Katty':111213 }
>>> phone_number['James']
123456
>>> phone_number['james']

KeyError                                  Traceback (most recent call last)
<ipython-input-64-6f91c5f93ae0> in <module>()
      1 phone_number = {'James':123456,'Tim':678910,'Katty':111213 }
----> 2 phone_number['james']

KeyError: 'james'

  1. Better
>>> phone_number = {'James':123456,'Tim':678910,'Katty':111213 }
>>> phone_number['James'] if 'james' in phone_number else "Not Found"
123456
>>> phone_number['james'] if 'james' in phone_number else "Not Found"
'Not Found'
  1. Best
>>> phone_number = {'James':123456,'Tim':678910,'Katty':111213 }
>>> phone_number.get('James', "Not Found")
123456
>>> phone_number.get('james', "Not Found")
'Not Found'

defaultdict

当你的字典中,每一个键值对应的是一个列表时,如何使用append操作?

  1. Bad
my_dict = {}
names = ['James', 'Tim', 'Katty', 'James']
numbers = [123456, 678910, 111213, 456789]
for name, number in zip(names, numbers):
    if name in my_dict:
        my_dict[name].append(number)
    else:
        my_dict[name] = []
        my_dict[name].append(number)

>>> my_dict
{'James': [123456, 456789], 'Tim': [678910], 'Katty': [111213]}
from collections import defaultdict

my_dict = defaultdict(list)
names = ['James', 'Tim', 'Katty', 'James']
numbers = [123456, 678910, 111213, 456789]
for name, number in zip(names, numbers):
    my_dict[name].append(number)

>>> my_dict
defaultdict(list, {'James': [123456, 456789], 'Tim': [678910], 'Katty': [111213]})

后者使用defaultdict,省去了判断字典中否存在某个键值对应的列表,使得代码更加简洁易懂。default还有inttuple等类型。
2. Better

my_dict = {}
names = ['James', 'Tim', 'Katty', 'James']
numbers = [123456, 678910, 111213, 456789]
for name, number in zip(names, numbers):
    if name in my_dict:
        my_dict[name].append(number)
    else:
        my_dict[name] = []

列表/字典解析式

当你想要生成一个列表或者字典的时候:

  1. Bad

生成列表

my_list = []
for i in range(10):
    my_list.append(i*i)
>>> my_list
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

生成字典

my_dict = {}
for i in range(10):
    my_dict[i]=i*i
>>> my_dict
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
  1. Better

生成列表

>>> [i*i for i in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

生成字典

>>> {i:i*i for i in range(10)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

列表/字典推导式是Python独具特色的功能之一,使用可以使得你的代码更加简洁高效。类似地,你也可以使用元组推导式。

字符串连接

当你需要创建一串字符串类似0123456789,你会如何做?

  1. Bad
string = ''
for i in range(10):
    string += str(i)

>>> string
'0123456789'
  1. Better
string = []
for i in range(10):
    string .append(str(i))

>>> ''.join(string)
'0123456789'

# or like this
string = [str(i) for i in range(10)]

>>> ''.join(string)
'0123456789'
  1. Best
string = map(str, range(10))

>>> ''.join(string)
'0123456789'

join 是一种更加高效的字符串连接方式,使用 + 操作时,每执行一次 + 操作就会导致在内存中生成一个新的字符串对象,遍历10次有10个字符串生成,造成无谓的内存浪费。而用 join 方法整个过程只会产生一个字符串对象。最后一个方法使用了map函数,在某些情况下,map函数更易于理解,效率更高。

"_"的妙用

在Python中,_的作用主要用来充当一个临时变量,例如:

for _ in range(5)print("Hello")

当你只需要一个循环多次重复做某件事,但是并不需要循环体的变量,就可以使用_当作一个占位符代替,同时也省去了命名的麻烦。
同时,在Python解释器中,_还可以充当一个保存临时结果的容器:

>>> 1 + 2
3
>>> _
3

在这里_保存了上一次解释器运行的结果。
同时,也可以作为多变量赋值的一个承载容器:

L = [1,2,3,4,5]
first, *_, last = L

>>> fitst
1
>>> last
5

map函数

如果我们有一个函数,希望将其作用在一个list[0,1,2,3,4]上,如何实现?

  1. Bad
L = []
for i in [0,1, 2, 3, 4]:
    L.append(f(i))
>>> L
[0, 1, 4, 9, 16]
  1. Better
>>> list(map(lambda i:i*i, range(5)))
[0, 1, 4, 9, 16]

后者的代码显然一目了然。map接收两个参数,一个是函数,一个是序列(迭代器),map将传入的函数依次作用到序列(迭代器)的每个元素,并把结果作为新的迭代器返回。同时,作为Python内建的高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的 f ( x ) = x 2 f(x)=x^2 f(x)=x2,还可以计算任意复杂的函数,例如作类型转换:

>>> list(map(str, range(5)))
['0', '1', '2', '3', '4']

最后很重要的一点是,map可以接受多个迭代器序列,并且并行地对每个序列对应元素执行该函数,会比普通的函数更加高效。

reduce函数

如果我们需要将[1,2,3,4]转化成1234,如何做?

  1. Bad
sum = 0
for i in  [1,2,3,4]:
    sum= sum*10 + i

>>> sum
1234
  1. Better
# reduce is not built-in function from python 3
from functools import reduce

>>> reduce(lambda x,y: x*10+y,[1,2,3,4])
1234

在Python 3中,reduce函数已经不再是内置函数,而是放入了functools模块。reduce把一个函数作用在一个序列[x1, x2, x3…]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算

filter函数

当你需要过滤一个列表的元素,例如,将列表中的奇数删除,只留下偶数:

L = list(range(10))
for i in L:
    if i%2==0:
        L.remove(i)

>>> L
[0, 2, 4, 6, 8]
  1. Better
L = list(range(10))

>>> list(filter(lambda x: x%2==0, L))
[0, 2, 4, 6, 8]

这里filter返回的是一个迭代器,显然后者的方法比前者更加优雅简洁。

生成器(generator)

前面介绍过,我们可以直接使用列表推导式创建一个完整的列表,当列表元素剧增的时候,如果只需要访问某几个元素,那将是十分浪费存储空间的。而生成器正是为了解决这一问题,一边循环一边计算,列表元素按照某种算法推算出来,从而节省大量空间。

  1. Bad
>>> [x * x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
  1. Better
>>> (x * x for x in range(10))
<generator object <genexpr> at 0x000001EB028DBA98>

二者的区别只是一个用了[],另一个用了(),前者生成一个完整的列表,后者生成一个生成器。生成器的作用无疑是强大的,这里不作过多介绍,更多内容请参加Python文档。

yield

写一个简单的斐波那契数列吧

  1. Bad
def fib(n):
    x, y = 0, 1
    L = []
    for i in range(n):
        L.append(y)
        x, y = y, x+y
    return L

>>> fib(5)
[1, 1, 2, 3, 5]
  1. Better
def fib(n):
    x, y = 0, 1
    L = []
    for i in range(n):
        yield y
        x, y = y, x+y

>>> list(fib(5))
[1, 1, 2, 3, 5]

后者使用了yield生成器,该生成器的特点是在哪里使用yield在哪里中断,下次返回时候从中断处开始执行,并且返回的是一个生成器,从而节省空间,提高代码效率。关于yield的强大之处我这里不便详细介绍,如果你需要详细了解,请参见The Python yield keyword explained。

partial函数

函数在执行时,如果不是默认参数,就必须在调用前传入。但是,有些参数是可以在函数被调用之前提前获知的,这种情况下,一个函数有一个或多个参数预先就能知道,以便函数能用更少的参数进行调用。
我们先看一个乘法的例子,让每个传入的参数都乘以一个固定的常数:

# partial is not built-in function from python 3
from functools import partial
def mul(a, b):
    return a*b

mul_partial = partial(mul,b=10)
for i in range(10):
    print(mul_partial(i),end=' ')
#  0 10 20 30 40 50 60 70 80 90 

也许你会说,这不就和默认参数一样吗,那我只要在定义函数mul()里将b固定为10,效果不是一样的?

# using default parameters
def mul(a, b=10):
    return a*b

但是,如果你要传入的b,你事先并不知道,而是需要在程序运行的时候才能获取到,那么你如何提前使用固定参数确定好呢?这时候partial就有着极大的用处!

lru_cache

仍然是一个计算斐波那契数列的例子,这次我们使用递归实现(虽然递归效率远低于循环,但这里只是作为一个例子演示,实际中最好少用递归)

  1. Bad
import time

def fib(n):
    if n == 0: return 0
    if n == 1: return 1

    return fib(n-1) + fib(n-2)

start = time.time()

>>> fib(40)
102334155
>>> f'Duration: {time.time() - start}s'
Duration: 40.126065492630005s
  1. Better
import time
from functools import lru_cache

@lru_cache(maxsize=512)
def fib(n):
    if n == 0: return 0
    if n == 1: return 1
    
    return fib(n-1) + fib(n-2)
    
start = time.time()

>>> fib(40)
102334155
>>> f'Duration: {time.time() - start}s'
Duration: 0.0009968280792236328s

可以看到,使用了LRU缓存后,二者的运行时间简直天差地别。

枚举

from enum import Enum, auto
class Animal(Enum):
    bird = auto()
    dog = auto()
    cat = auto()

>>> print(Animal.cat)
Animal.cat

Python 3 中的 Enum 类支持枚举功能,可以使我们的程序变得更加简洁。 Enum 是一种便捷的变量列表的打包方式,使用该方法能够避免多个变量在代码各处分布而显得杂乱无章。枚举是一个符号集合,每个符号都和唯一的变量对应。通过使用枚举,我们可以通过符号标识来比较各个成员,我们还可以对枚举本身进行迭代。

Reference

[1] Data, what now?
[2] The Hitchhiker’s Guide to Python
[3] The Python yield keyword explained
[4] Generators

你可能感兴趣的:(Python,教程,Python,优雅,高效,技巧)