Python 新特性
使用f-Strings来格式化字符串
在Python3.6版本之前,主要有两种方式来格式化字符串:%和str.format()方法。
百分号类型的格式化字符串方法是从c语言继承过来的。由百分号和一个代表类型的字符组成占位符嵌入字符串,之后执行格式化。这里有几个问题:首先就是可读性,当字符串里百分号占位符多的时候,我们很可能会搞不清这个位置的占位符代表什么含义。其次是类型的问题,众所周知,Python的变量在使用的时候不用声明类型,而且在对一个变量操作的时候,更应该关注其抽象属性,而不是其类型本身。而且官方文档也不推荐这种做法。
format()方法是比较好的,定制化比较强,也更容易写出Pythonic的代码。但是仍然有可读性的问题。
f-Strings的想法很棒,就是直接用变量名来作为占位符,在运行时格式化,这样就可以做很多东西,包括直接调用函数。而且f-Strings默认调用对象的__str__()方法。并且可以使用在文档字符串。
>>> name = "john"
>>> gender = True
>>> age = 23
>>> person = f"{name}'s gender is {gender}, and his age is {age}"
>>> person
"john's gender is True, and his age is 23"
>>> person = f"{name.upper()}'s gender is {gender}, and his age is {age}"
>>> person
"JOHN's gender is True, and his age is 23"
>>>
用Type Hinting来代替Doc String
Type Hinting是Python3.5版本及之后的,为了兼容性还是老老实实使用Doc String吧。
>>>def foo(x: int, y: str) -> bool:
... return x < y
>>> foo.__annotations__()
{'y': <class 'str'>, 'return': <class 'bool'>, 'x': <class 'int'>}
这个特性有一个专门的库:typing 。
不使用可变的数据类型作为函数参数
关于可变数据类型
def add_to(num, target=[]):
target.append(num)
return target
add_to(1)
# Output: [1]
add_to(2)
# Output: [1, 2]
add_to(3)
# Output: [1, 2, 3]
使用property来处理getter和setter函数
- 使用property装饰器
class Clock(object):
def __init__(self):
self.__hour = 1
@property
def hour(self):
print("getting value ")
return self.__hour
@hour.setter
def hour(self, value):
print("setting value ")
self.__hour = value
@hour.deleter
def hour(self):
del self.__hour
c = Clock()
print(c.hour)
c.hour = 12
- 使用property函数
class Clock(object):
def __init__(self):
self.__hour = 10
print(self.__hour is self.hour)
def __setHour(self, hour):
print("setting value")
if 25 > hour > 0:
self.__hour = hour
else:
raise BadHourException
def __getHour(self):
print("Getting value")
return self.__hour
hour = property(__getHour, __setHour)
c = Clock()
print(c.hour)
c.hour = 12
需要注意的是,上面一段代码中print(self.__hour is self.hour)的输出是True。self.hour是property的一个实例,它操作的还是self.__hour
上面一行hour = property(__getHour, __setHour)也可以分开写:
# make empty property
hour = property()
# assign fget
hour = hour.getter(__getHour)
# assign fset
hour = hour.setter(__setHour)
使用with处理加锁
##不推荐
import threading
lock = threading.Lock()
lock.acquire()
try:
# 互斥操作...
finally:
lock.release()
##推荐
import threading
lock = threading.Lock()
with lock:
# 互斥操作...
容器类型使用技巧
从高层来看,什么定义了容器?
答案是:各个容器类型实现的接口协议定义了容器。不同的容器类型在我们的眼里,应该是 是否可以迭代、是否可以修改、有没有长度 等各种特性的组合。我们需要在编写相关代码时,更多的关注容器的抽象属性,而非容器类型本身,这样可以帮助我们写出更优雅、扩展性更好的代码。
写扩展性更好的代码
让函数依赖“可迭代对象”这个抽象概念,而非实体列表类型。
避免频繁扩充列表/创建新列表
- 更多的使用 yield 关键字,返回生成器对象
- 尽量使用生成器表达式替代列表推导表达式
- 生成器:
(i for in range(100)) - 列表推导式:
[i for in range(100)]
- 生成器:
- 尽量使用模块提供的懒惰对象
- 使用
re.finditer替代re.findall - 直接使用可迭代的文件对象:
for line in fp,而不是for line in fp.readlines()
- 使用
在列表头部操作多的场景用 deque 模块
列表是基于数组结构(Array)实现的,当你在列表的头部插入新成员(list.insert(0, item))时,它后面的所有其他成员都需要被移动,操作的时间复杂度是 O(n)。这导致在列表的头部插入成员远比在尾部追加(list.append(item) 时间复杂度为 O(1))要慢。
如果你的代码需要执行很多次这类操作,请考虑使用 collections.deque 类型来替代列表。因为 deque 是基于双端队列实现的,无论是在头部还是尾部追加元素,时间复杂度都是 O(1)。
使用集合/字典来判断成员是否存在
当你需要判断成员是否存在于某个容器时,用集合比列表更合适。因为 item in […] 操作的时间复杂度是 O(n),而 item in {…} 的时间复杂度是 O(1)。这是因为字典与集合都是基于哈希表(Hash Table)数据结构实现的。
最好不用“获取许可”,也无需“要求原谅”
如果用一个经典的需求:“计算列表内各个元素出现的次数” 来作为例子,两种不同风格的代码会是这样:
# AF: Ask for Forgiveness
# 要做就做,如果抛出异常了,再处理异常
def counter_af(l):
result = {}
for key in l:
try:
result[key] += 1
except KeyError:
result[key] = 1
return result
# AP: Ask for Permission
# 做之前,先问问能不能做,可以做再做
def counter_ap(l):
result = {}
for key in l:
if key in result:
result[key] += 1
else:
result[key] = 1
return result
整个 Python 社区对第一种 Ask for Forgiveness 的异常捕获式编程风格有着明显的偏爱。这其中有很多原因,首先,在 Python 中抛出异常是一个很轻量的操作。其次,第一种做法在性能上也要优于第二种,因为它不用在每次循环的时候都做一次额外的成员检查。
不过,示例里的两段代码在现实世界中都非常少见。为什么?因为如果你想统计次数的话,直接用collections.defaultdict就可以:
from collections import defaultdict
def counter_by_collections(l):
result = defaultdict(int)
for key in l:
result[key] += 1
return result
一些小提示:
- 操作字典成员时:使用collections.defaultdict类型,或者使用dict[key] = dict.setdefault(key, 0) + 1 内建函数
- 如果移除字典成员,不关心是否存在:调用 pop 函数时设置默认值,比如 dict.pop(key, None)
- 在字典获取成员时指定默认值:dict.get(key, default_value)
- 对列表进行不存在的切片访问不会抛出 IndexError 异常:[“foo”][100:200]
对于函数return的技巧
单个函数不要返回多种数据类型
使用partial构造新函数
partial(func, *args, **kwargs)
尽量使用异常而不是返回结果处理错误
合理使用空对象模式
当我们使用返回结果或者异常处理错误,上层函数调用的时候会频繁的使用if/else或者try/except,为了减少这种情况的出现,可以使用合理的“空对象”来代替判断。
尽量不使用递归
python对递归的支持非常有限,而且debug的时候非常痛苦,我之前就遇到过一个递归的问题,折磨了我很久,后来发现是因为——递归函数的每个逻辑分支都要有返回值,不能返回None。
还有几点:
- python不支持“尾递归优化”
- 递归最大层数有限制
sys.getrecursionlimit() - 尝试使用
functools.lru_cache等缓存工具函数来降低递归层数
参考
- https://github.com/piglei/one-python-craftsman
- https://realpython.com/python-f-strings/#old-school-string-formatting-in-python
- https://medium.com/@meghamohan/mutable-and-immutable-side-of-python-c2145cf72747