如何写出Pythonic风格的代码
索引
>>> my_numbers = (4, 5, 3, 9)
>>> my_numbers[0]
4
>>> my_numbers[1]
5
>>> my_numbers[-1]
9
>>> my_numbers[-2]
3
切片
>>> my_numbers = (4, 5, 3, 9)
>>> my_numbers[0:2]
(4, 5)
>>> my_numbers[0:3:2] # 步长为可选参数
(4, 3)
>>> my_numbers[0:-2] # 只能从左往右
(4, 5)
>>> my_numbers[0:-1:2]
(4, 3)
>>> my_numbers[2:] # 可以缺省起始和终止索引
(3, 9)
>>> my_numbers[:2]
(4, 5)
>>> my_numbers[:] # 相当于浅拷贝
(4, 5, 3, 9)
>>> my_numbers[::]
(4, 5, 3, 9)
>>> interval = slice(0, 3, 2)
>>> my_numbers[interval] # => my_numbers[0:3:2]
(4, 3)
>>> interval = slice(None, 3)
>>> my_numbers[interval] == my_numbers[:3]
True
创建自定义序列类型
class Items:
def __init__(self, *values):
self._values = list(values)
def __len__(self):
return len(self._values)
def __getitem__(self, item):
return self._values.__getitem__(item)
上面的例子封装了内置的list结构。另外一种是使用继承,通过拓展基类collections.UserList。
当你需要完完全全实现自己的序列结构时,请牢记两点:
- 按照范围切片时,结果应该是相同类型的实例序列。
- 在切片提供的范围内,请尊重Python使用的语义,排除最后的元素。
另外range也可以进行切片
>>> range(1, 100)[25:50]
range(26, 51)
上下文管理
上下文管理器是Python提供的独特功能,它十分有用,非常适合这样的模式:运行一些代码,需要前置和后置条件。例如处理文件:open->处理->close。
fd = open(filename)
try:
process_file(fd)
finally:
fd.close()
使用with
with open(filename) as fd:
process_file(fd)
上下文管理器依赖两个魔法方法__enter__和__exit__,分别对应前置和后置处理逻辑。
通过类实现上下文管理器
def stop_database():
print("systemctl stop postgresql.service")
def start_database():
print("systemctl start postgresql.service")
def db_backup():
print("pg_dump database")
class DBHandler:
def __enter__(self):
stop_database()
return self # "with xxx as obj" obj is self here.
def __exit__(self, exc_type, ex_value, ex_traceback):
start_database()
def main():
with DBHandler():
db_backup()
通过方法实现上下文管理器,需要使用contextlib.contextmanager装饰器
import contextlib
@contextlib.contextmanager
def db_handler():
stop_database()
yield # "with xxx as obj" obj is "yield {obj}".
start_database()
with db_handler():
db_backup()
甚至连with也不用,需要使用contextlib.ContextDecorator基类
import contextlib
class dbhandler_decorator(contextlib.ContextDecorator):
def __enter__(self):
stop_database()
def __exit__(self, ext_type, ex_value, ex_traceback):
start_database()
@dbhandler_decorator()
def offline_backup():
print("pg_dump database")
但是这种用法你没法使用__enter__函数的返回值
使用上下文管理器处理异常
import contextlib
with contextlib.suppress(DataConversionException):
parse_data(input_json_or_dict)
如果引发DataConversionException,直接跳过。
属性
单下划线表示只会从内部访问(保护)的属性和方法。对象只应该公开那些外部调用者关心的属性和方法,也就是接口,一切不是严格属于接口的都应该使用单下划线。
双下划线并非定义私有变量的方式,访问双下划线会导致AttributeError,这是因为双下划线会导致重命名:
class Connector:
def __init__(self):
self.__timeout = 60 # ___
def __run(self):
print('Run')
conn = Connector()
print(conn.__timeout) # raise AttributeError
print(conn._Connector__timeout) # 60
print(conn.__run) # raise AttributeError
print(conn._Connector__run) # function
双下划线是非Pythonic方法。如果您需要将属性定义为私有的,使用单个下划线,并遵守Pythonic约定,即它是私有的属性。
使用@properties和@xxxx.setter装饰器
class User:
def __init__(self, username):
self.username = username
self._email = None
@property
def email(self):
return self._email
@email.setter
def email(self, new_email):
self._email = new_email
迭代对象
python确定对象是否可迭代的,主要依据两点:
- 是否实现了__next__或__iter__迭代方法
- 是否是一个序列,实现了__len__和__getitem__
python会依照顺序尝试调用这些方法来迭代对象
class A:
def __init__(self, count=0):
self._count = count
def __iter__(self):
return self
def __next__(self):
if self._count >= 5:
raise StopIteration
self._count += 1
return self._count
class B:
def __iter__(self):
return A(1)
a = list(A())
b = list(B())
print(a) # [1, 2, 3, 4, 5]
print(b) # [2, 3, 4, 5]
调用__iter__返回的值再调用__next__
或者__iter__直接返回生成器
from datetime import date, timedelta
class DateRangeContainerIterable:
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
def __iter__(self):
current_day = self.start_date
while current_day < self.end_date:
yield current_day
current_day += timedelta(days=1)
d = DateRangeContainerIterable(date(2018, 1, 1), date(2018, 1, 5))
print(", ".join(map(str, d))) # '2018-01-01, 2018-01-02, 2018-01-03, 2018-01-04'
包含对象
flag = element in container
# => flag = container.__contains__(element)
动态获取属性
python调用存在的属性会调用__getattribute__,当属性不存在时,会调用__getattr__。
可调用对象
当我们调用一个对象object(*args, *kwargs)时,实际上转换为了object.__call__(args, **kwargs)
from collections import defaultdict
class CallCount:
def __init__(self):
self._counts = defaultdict(int)
def __call__(self, argument):
self._counts[argument] += 1
return self._counts[argument]
cc = CallCount()
print(cc(1)) # 1
print(cc(2)) # 1
print(cc(1)) # 2
print(cc(1)) # 3
print(cc(3)) # 1
魔法方法总结
[图片上传失败...(image-bd2aa1-1571013774916)]
Python注意事项
不要将引用类型的对象作为函数的默认参数,否则结果不可预期
def wrong_user_display(user_metadata: dict = {"name": "John", "age": 30}):
name = user_metadata.pop("name")
age = user_metadata.pop("age")
return f"{name} ({age})"
print(wrong_user_display()) # 'John (30)
print(wrong_user_display({"name": "Jane", "age": 25})) # John (25)
print(wrong_user_display()) # raise KeyError: 'name'
拓展内置数据类型
拓展内置数据类型的正确方式是使用collections模块