在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是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]
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
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)
##不推荐
import threading
lock = threading.Lock()
lock.acquire()
try:
# 互斥操作...
finally:
lock.release()
##推荐
import threading
lock = threading.Lock()
with lock:
# 互斥操作...
从高层来看,什么定义了容器?
答案是:各个容器类型实现的接口协议定义了容器。不同的容器类型在我们的眼里,应该是 是否可以迭代、是否可以修改、有没有长度 等各种特性的组合。我们需要在编写相关代码时,更多的关注容器的抽象属性,而非容器类型本身,这样可以帮助我们写出更优雅、扩展性更好的代码。
让函数依赖“可迭代对象”这个抽象概念,而非实体列表类型。
(i for in range(100))
[i for in range(100)]
re.finditer
替代 re.findall
for line in fp
,而不是for line in fp.readlines()
列表是基于数组结构(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
一些小提示:
partial(func, *args, **kwargs)
当我们使用返回结果或者异常处理错误,上层函数调用的时候会频繁的使用if/else或者try/except,为了减少这种情况的出现,可以使用合理的“空对象”来代替判断。
python对递归的支持非常有限,而且debug的时候非常痛苦,我之前就遇到过一个递归的问题,折磨了我很久,后来发现是因为——递归函数的每个逻辑分支都要有返回值,不能返回None。
还有几点:
sys.getrecursionlimit()
functools.lru_cache
等缓存工具函数来降低递归层数