优雅的写Python代码?小技巧

Geek代码风,如何优雅的书写Python代码?优雅的命名?

变量命名技巧

用有意义易读的命名

  • 错误:毫无意义的命名
1
ymdstr = datetime.date.today().strftime("%y-%m-%d")
  • 正确:有意义的命名
1
current_date: str = datetime.date.today().strftime("%y-%m-%d")

同类型使用相同词汇

  • 错误:这三个函数都是和用户相关的信息,却使用了三个名字
1
2
3
get_user_info()
get_client_data()
get_customer_record()
  • 正确:如果实体相同,你应该统一名字
1
2
3
get_user_info()
get_user_data()
get_user_record()
  • 最佳:Python 是一门面向对象的语言,用一个类来实现更加合理,分别用实例属性、property 方法和实例方法来表示。
1
2
3
4
5
6
7
8
9
class User:
info : str

@property
def data(self) -> dict:
# ...

def get_record(self) -> Union[Record, None]:
# ...

可搜索的名字

大部分时间你都是在读代码而不是写代码,所以我们写的代码可读且可被搜索尤为重要,一个没有名字的变量无法帮助我们理解程序,也伤害了读者,记住:确保可搜索。

  • 错误:随意量,看不懂什么意思
1
time.sleep(86400);
  • 正确:命名常量,写清思路,清晰多了。
1
2
3
4
# 在全局命名空间声明变量,一天有多少秒
SECONDS_IN_A_DAY = 60 * 60 * 24

time.sleep(SECONDS_IN_A_DAY)

自我描述的变量

  • 错误:看不懂什么意思
1
2
3
matches = re.match(r'正则匹配', address)

zip_code(matches[1], matches[2]) # 看不懂什么意思
  • 正确:matches.groups() 自动解包成两个变量,分别是 city,zip_code
1
2
3
4
matches = re.match(r'正则匹配', address)

city, zip_code = matches.groups() # 清晰明了
zip_code(city, zip_code)

不要取隐晦的名字

  • 错误:seq 是什么?序列?什么序列呢?没人知道
1
2
3
4
5
6
7
8
seq = ('Austin', 'New York', 'San Francisco')

for item in seq:
do_stuff()
do_some_other_stuff()
# ...
# Wait, what's `item` for again?
dispatch(item)
  • 正确:用 locations 表示,一看就知道这是几个地区组成的元组
1
2
3
4
5
6
7
locations = ('Austin', 'New York', 'San Francisco')

for location in locations:
do_stuff()
do_some_other_stuff()
# ...
dispatch(location)

精简不重复

  • 错误:感觉画蛇添足,如无必要,勿增实体。
1
2
3
4
class Car:
car_make: str
car_model: str
car_color: str
  • 正确:简洁明了
1
2
3
4
class Car:
make: str
model: str
color: str

默认参数代替运算和条件

  • 错误:多余累赘。
1
2
3
4
def create_micro_brewery(name):
name = "Hipster Brew Co." if name is None else name
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.
  • 正确:既然函数里面需要对没有参数的变量做处理,为啥不直接设置默认值呢?
1
2
3
def create_micro_brewery(name = "Hipster Brew Co."):  # 直接设置默认值
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.

实用小窍门

变量值交换

  • 错误:普通思维。
1
2
3
tmp = a
a = b
b = tmp
  • 正确:Python中可以直接交换两个变量
1
a, b = b, a

列表推导式

  • 错误:列表推导式是Java及C++等语言没有的特性,能够很简洁的实现for循环,可以应用于列表,集合或者字典。。
1
2
3
4
numbers = []
for x in xrange(20):
if x % 3 == 0:
numbers.append(x*x)
  • 正确:一行代码即可实现
1
2
3
4
5
6
7
numbers = [x*x for x in range(20) if x % 3 == 0]

# 集合
numbers = {x * x for x in range(0, 20) if x % 3 == 0}

# 字典
numbers = {x: x * x for x in range(0, 20) if x % 3 == 0}

字符串拼接 join

  • 错误:由于像字符串这种不可变对象在内存中生成后无法修改,合并后的字符串会重新开辟出一块内存空间来存储。因此每合并一次就会单独开辟一块内存空间,这样会占用大量的内存空间,严重影响代码的效率。
1
2
3
4
5
words = ['I', ' ', 'love', ' ', 'Python', '.']

sentence = ''
for word in words:
sentence += '' + word
  • 正确:解决这个问题的办法是使用字符串连接的join,Python写法如下
1
2
3
words = ['I', ' ', 'love', ' ', 'Python', '.']

sentence = ''.join(words)

快速翻转字符串

  • Java或者C++等语言的写法是新建一个字符串,从最后开始访问原字符串。
1
2
3
4
5
a = 'I love Python.'

reverse_a = ''
for i in range(0, len(a)):
reverse_a += a[len(a) - i - 1]
  • Python则将字符串看作list,而列表可以通过切片操作来实现反转
1
2
a = 'I love Python.'
reverse_a = a[::-1]

方便的语句

for/else语句

  • 在C语言或Java语言中,我们寻找一个字符是否在一个list中,通常会设置一个布尔型变量表示是否找到
1
2
3
4
5
6
7
8
9
10
11
cities = ['BeiJing', 'TianJin', 'JiNan', 'ShenZhen', 'WuHan']
tofind = 'Shanghai'

found = False
for city in cities:
if tofind == city:
print 'Found!'
found = True
break
if not found:
print 'Not found!'
  • 而Python中的通过for…else…会使得代码很简洁,注意else中的代码块仅仅是在for循环中没有执行break语句的时候执行:
1
2
3
4
5
6
7
8
9
10
cities = ['BeiJing', 'TianJin', 'JiNan', 'ShenZhen', 'WuHan']
tofind = 'Shanghai'

for city in cities:
if tofind == city:
print 'Found!'
break
else:
# 执行else中的语句意味着没有执行break
print 'Not found!'

善用enumerate

  • enumerate类接收两个参数,其中一个是可以迭代的对象,另外一个是开始的索引。比如,我们想要打印一个列表的索引及其内容,可以用如下代码实现:
1
2
3
4
5
6
cities = ['BeiJing', 'TianJin', 'JiNan', 'ShenZhen', 'WuHan']

index = 0
for city in cities:
index = index + 1
print index, ':', city
  • 而通过使用enumerate则极大简化了代码,这里索引设置为从1开始(默认是从0开始)
1
2
3
cities = ['BeiJing', 'TianJin', 'JiNan', 'ShenZhen', 'WuHan']
for index, city in enumerate(cities, 1):
print index, ":", city

lambda来定义函数

  • lambda可以返回一个可以调用的函数对象,会使得代码更为简洁。若不使用lambda则需要单独定义一个函数:
1
2
3
4
def f(x):
return x * x

map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
  • 使用lambda后仅仅需要一行代码:
1
2
3
4
5
6
7
8
map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9])

# 普通情况
def f(x):
return x * x

# 等价于
lambda x: x * x

善用装饰器

装饰器在Python中应用特别广泛,其特点是可以在具体函数执行之前或者之后做相关的操作,比如:执行前打印执行函数的相关信息,对函数的参数进行校验;执行后记录函数调用的相关流水日志等。使用装饰器最大的好处是使得函数功能单一化,仅仅处理业务逻辑,而不附带其它功能。

  • 在函数调用前打印时间函数名相关的信息,不使用装饰器可以用如下代码实现:
1
2
3
4
5
from time import ctime

def foo():
print('[%s] %s() is called' % (ctime(), foo.__name__))
print('Hello, Python')
  • 这样写的问题是业务逻辑中会夹杂参数检查,日志记录等信息,使得代码逻辑不够清晰。所以,这种场景需要使用装饰器:
1
2
3
4
5
6
7
8
9
10
11
from time import ctime

def deco(func):
def decorator(*args, **kwargs):
print('[%s] %s() is called' % (ctime(), func.__name__))
return func(*args, **kwargs)
return decorator

@deco
def foo():
print('Hello, Python')

解决方案

生成器

生成器与列表最大的区别就是,列表是一次性生成的,需要较大的内存空间;而生成器是需要的时候生成的,基本不占用内存空间。生成器分为生成器表达式生成器函数

先看一下列表:

1
l = [x for x in range(10)]

改为生成器只需要将[…]变为(…),即生成器表达式

1
g = (x for x in range(10))

至于生成器函数,是通过yield关键字来实现的,我们以计算斐波那契数列为例,使用列表可以用如下代码来实现:

1
2
3
4
5
6
7
8
def fib(max):
n, a, b = 0, 0, 1
fibonacci = []
while n < max:
fibonacci.append(b)
a, b = b, a + b
n = n + 1
return fibonacci

生成器

1
2
3
4
5
6
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1

词频统计Counter

通常的词频统计中,我们的思路是:
需要一个字典,key值存储单词,value存储对应的词频。当遇到一个单词,判断是否在这个字典中,如果是,则词频加1;如果否,则字典中新增这个单词,同时对应的词频设置为1。

wordList 是一个列表 [‘足球’,’篮球’,’乒乓球’,’足球’….]

  • 普通情况下对应的Python代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
#统计单词出现的频次
def computeFrequencies(wordList):
#词频字典
wordfrequencies = {}

for word in wordList:
if word not in wordfrequencies:
# 单词不在单词词频字典中, 词频设置为1
wordfrequencies[word] = 1
else:
# 单词在单词词频字典中, 词频加1
wordfrequencies[word] = wordfrequencies[word] + 1
return wordfrequencies
  • 有没有更简单的方式呢?答案是肯定的,就是使用Counter。collection 中的 Counter 类就完成了这样的功能,它是字典类的一个子类。Python代码变得无比简洁:
1
2
3
4
5
# 统计单词出现的频次
def computeFrequencies(wordList):
#词频字典
wordfrequencies = Counter(wordList)
return wordfrequencies