Effective Python

Table of Contents


用 python 的方式来思考

第一条 确认 python 使用的版本

了解 CPython, JPython, IronPython, PyPy 等流行的 python 运行环境。

python –version

可以通过运行 python3 来运行 python 3

import sys
print sys.version

第二条 遵循 pep8 规范

可以利于多人协作,后续修改工作更加的容易

指南:www.python.org/dev/peps/pep-0008

  • 使用 space 来缩进
  • 每行字符数不超过 79
  • 文件中的函数和类之间 两个空行
  • 同一类中,各个方法之间用一个空行隔开
  • 赋值是左右都要有空格 a = b; 而不是 a=b;

函数命名

  • 函数,变量,属性 用小写
  • 受保护的用单个下划线开头 _leadingunderscore
  • 私有实例属性 用两个下划线 _doubleleadingunderscore
  • 类和异常 全部采用大写字母来拼写 ALLCAPS
  • 类中的实例方法应该把首个参数命名为 self
  • 类方法的首个参数命名为 cls

表达式和语句

  • 采用内联形式的否定词 例如 if a is not b 而不是 if not a is b
  • 不要通过检验长度的方法来判断 list 是否为空,直接使用 if not somelist;
  • import 总应该放到文件的开头
  • import 应该按顺序划分成三个部分,分别表示标准库模块,第三方模块,自用模块 ,每一部分按模块的字母顺序排列

pylint 是流行的 Python 源码静态分析工具,可以自动检测代码是否符合 PEP 8 风格,还可以找出常见的错误

了解 bytes, str, unicode 的区别

python3 有两种表示字符序列的类型:bytes 和 str,前者包含 8 位原始的二进制值;后者包含 Unicode 字符。

python2 也有两种,str 和 unicode。它们分别对应上面的两种情况。

在 python2 中:

type("你好") ⇒ <type 'str'>
type(u"你好") ⇒ <type 'unicode'>

在 python3 中:

type("你好") ⇒ <class 'str'>
type(u"你好") ⇒ <class 'str'>

我们在 Python 代码中经常出现两种使用场景:

  • 使用 UTF-8 编码的字符串
  • 操作没有特定编码的 Unicode 字符

    所以,写了两个辅助函数用于转换: 在 Python 3 中,接受 str 或 bytes,

    def to_str(bytes_or_str):
            if isinstance(bytes_or_str, bytes):
                    value = bytes_or_str.decode('utf-8')
    else:
                    value = bytes_or_str
    
            return value
    
    
    def to_bytes(bytes_or_str):
            if isinstance(bytes_or_str, str):
                    value = bytes_or_str.encode('utf-8')
            else:
                    value = bytes_or_str
    
            return value
    

    在 Python 2 中同理。

    在 Python 3 中,str 于 bytes 在任何情况下绝不等价,而 Python 2 却不是。

    另外一个问题是在 Python3 中,使用 open 函数获取文件句柄,这个句柄默认采用 UTF-8 编码,而在 Python 2 中默认是二进制形式。 所以我们在 Python 2 中可以这样,但 3 中不行:

    with open('file', 'w') as f:
            f.write()
    

    我们要这样:

    with open('file', 'wb') as f:
            f.write()
    

    读取也一样,要用 'rb' 模式。

用辅助函数取代复杂表达式

表达式如果变得复杂应该考虑将其拆成小块。

了解切割序列方法

a = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
fist four: a[:4]
last four: a[-4:]

切割列表时,start end 越界也不会出现问题。

在单次切片操作,不要同时指定 start, end 和 stride

Python 还支持 somelist[start:end:stride] stride 是步长。

a = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']
odds = a[::2]
evens = a[1::2]

有一种技巧,就是翻转字符串:

x = b'mongoose'
y = x[::-1]

不要同时指定 start,end,stride 否则程序会变得非常难理解,可以分两步切割。

用列表推到来取代 map 和 filter

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = [x**2 for x in a]
squares = map(lambda x: x ** 2, a)

even_squares = [x**2 for x in a if x % 2 == 0]
alt = map(lambda x: x**2, filter(lambda x: x % 2 == 0, a))

对于字典结构,我们也可以用:

    chile_ranks = {'ghost': 1, 'habanero': 2, 'cayenne': 3}
rank_dict = {rank: name for name, rank in chile_ranks.items()}
chile_len_set = {len(name) for name in rank_dict.values()}

不要使用含有两个以上表达式的列表推导

flat = [x for row in matrix for x in row]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

squared = [[x**2 for x in row] for row in matrix]
[[1, 4, 9], [16, 25, 36], [49, 64, 81]]

像下面这种就不是很好了:

b = [x for x in a if x > 4 if x % 2 == 0]

我们可以搭配 if for 或辅助函数去处理。

用生成器表达式来改写数据量较大的列表推导

当输入的数据量较大时,列表推导可能会因为占用太多的内存而出问题, 所以我们这时要使用生成器表达式。

# 这时迭代器
value = [len(x) for x in open('my_file.txt')]
# 这个就是生成器
it = (len(x) for x in open('my_file.txt'))
print(next(it))

生成器表达还有个好处,就是可以相互组合。

c = [1 , 2 , 3]
d = (x for x in c)
e = (y**2 for y in d)
next(e)
next(e)

串在一起的生成器表达式执行速度很快。

尽量用 enumerate 取代 range

flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for flavor in flavor_list:
    print('%s is delicious' % flavor)

for i, flavor in enumerate(flavor_list):
    print('%d: %s' % (i + 1, flavor))

# 还可以指定开始计数使用的值
for i, flavor in enumerate(flavor_list, 1):
    print('%d: %s' % (i, flavor))

用 zip 函数同时遍历两个迭代器

在 Python3 中,zip 可以把两个或以上的迭代器封装成生成器,以便稍后求值。

    names = ['Cecilia', 'Lise', 'Marie']
    letters = [len(n) for n in names]

    longest_name = None
    max_letters = 0
for name, count in zip(names, letters):
    if count > max_letters:
        longest_name = name
        max_letters = count
print(longest_name)

如果在 Python 2 中用 zip 迭代数据量比较大的迭代器时,会占用大量内存并导致程序崩溃。 这时应该用 itertools 中的 izip 函数。

如果提供的迭代器长度不等,zip 会自动终止。 例如

names.append('Rosalind')
for name, count in zip(names, letters):
print(name)

不要在 for 和 while 后面写 else

for/else 结构中,会使得整个循环执行完立即运行 else。

try/except/else,中 else 是如果 try 没有失败,就执行 else。

for/else 中奇怪的是,如果 for 结构中意外的结束,会导致不执行 else。

合理利用 try/except/else/finally 结构中的每个代码块

  1. finally

    try/finally 可以将异常向上传播,而且在异常发生时可以执行清理工作。 这种有一个常见的用途,就是确保程序能可靠的关闭文件句柄。

        handle = open('random_data.txt')  # May raise IOError
    try:
        data = handle.read()  # May raise UnicodeDecodeError
    finally:
        handle.close()        # Always runs after try:
    
  2. else

    try/except/else 结构可以清晰的描述哪些异常会有自己代码处理,哪些异常传播到上一级。 如果 try 块没有发生异常,就执行 else 块。

    def load_json_key(data, key):
    try:
        result_dict = json.loads(data)  # May raise ValueError
    except ValueError as e:
        raise KeyError from e
    else:
        return result_dict[key]         # May raise KeyError
    
  3. 混合使用

        import json
    UNDEFINED = object()
    
    def divide_json(path):
        handle = open(path, 'r+')   # May raise IOError
        try:
            data = handle.read()    # May raise UnicodeDecodeError
            op = json.loads(data)   # May raise ValueError
            value = (
                op['numerator'] /
                op['denominator'])  # May raise ZeroDivisionError
        except ZeroDivisionError as e:
            return UNDEFINED
        else:
            op['result'] = value
            result = json.dumps(op)
            handle.seek(0)
            handle.write(result)    # May raise IOError
            return value
        finally:
            handle.close()          # Always runs
    

    这几块相互配合非常的有用。

函数

尽量用异常来表示特殊情况,而不要返回 None

def divide(a, b):
try:
    return a / b
except ZeroDivisionError:
    return None

这种写法会给人一种误导,我们可以这样

    def divide(a, b):
    try:
        return True, a / b
    except ZeroDivisionError:
        return False, None

    success, result = divide(x, y)
if not success:
    print('Invalid inputs')

另一种方法,要抛出异常

    def divide(a, b):
try:
    return a / b
except ZeroDivisionError as e:
    raise ValueError('Invalid inputs') from e

这样调用者就不得不处理这个异常了

    try:
    result = divide(x, y)
except ValueError:
    print('Invalid inputs')
else:
    print('Result is %.1f' % result)

了解如何在闭包里使用外围作用域中的变量

闭包是一种定义在某个作用域中的函数。

    class Sorter(object):
    def __init__(self, group):
        self.group = group
        self.found = False

    def __call__(self, x):
        if x in self.group:
            self.found = True
            return (0, x)
        return (1, x)

    sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter.found is True
print('Found:', found)
print(numbers)

考虑用生成器改写直接返回列表的函数 ✓

下面的代码,用 append 方法将这些词的首字母索引加入到列表中,

def index_words(text):
result = []
if text:
    result.append(0)
for index, letter in enumerate(text):
    if letter == ' ':
        result.append(index + 1)
return result

这段代码有些问题。其一,写的太过于拥挤,有很多无用的信息。其二,如果数据量很大的话,在返回之前,将所有的信息放在列表里有可能耗尽内存资源。

其实,这里使用生成器来写更好。生成器是使用 yield 表达式的函数。调用生成器时,它并不真正运行,而是返回迭代器。生成器传给 yield 的每个值都会有迭代器返回给调用者。

def index_words_iter(text):
if text:
    yield 0
for index, letter in enumerate(text):
    if letter == ' ':
        yield index + 1

result = list(index_words_iter(address))

在参数上面迭代要小心

def normalize(numbers):
total = sum(numbers)
result = []
for value in numbers:
    percent = 100 * value / total
    result.append(percent)
return result

如果传递一个列表的参数是可以的。现在我们要扩大应用范围,从文件中读取。

def read_visits(data_path):
with open(data_path) as f:
    for line in f:
        yield int(line)


it = read_visits('my_numbers.txt')
percentages = normalize(it)
print(percentages)

但是现在却没有结果,原因就是迭代器只能产生一轮结果。

解决的办法是我们可以复制一份数据,但这样不好,如果数据大了就会有问题。

下面我们实现一个迭代器协议的容器类。 我们只需要让自己的类实现 iter 方法即可。

class ReadVisits(object):
def __init__(self, data_path):
    self.data_path = data_path

def __iter__(self):
    with open(self.data_path) as f:
        for line in f:
            yield int(line)

visits = ReadVisits(path)
percentages = normalize(visits)
print(percentages)

现在我们就可以这样了

visits = [15, 35, 80]
normalize_defensive(visits)  # No error
visits = ReadVisits(path)
normalize_defensive(visits)  # No error

用数量可变的位置参数

    def log(message, *values):  # The only difference
    if not values:
        print(message)
    else:
        values_str = ', '.join(str(x) for x in values)
        print('%s: %s' % (message, values_str))

log('My numbers are', 1, 2)
log('Hi there')  # Much better

对生成器使用 *

    def my_generator():
    for i in range(10):
        yield i

def my_func(*args):
    print(args)

it = my_generator()
my_func(*it)

在 def 中使用 *args, 可令函数接受数量可变的位置参数。 对迭代器使用 * 可能导致程序耗尽内存。

用关键字参数表达可选的行为

    def remainder(number, divisor):
        return number % divisor

    remainder(20, divisor=7)
    remainder(divisor=7, number=20)

def flow_rate(weight_diff, time_diff, period=1):
    return (weight_diff / time_diff) * period

用 None 和文档字符串来描述具有动态默认值的参数

    def log(message, when=datetime.now()):
    print('%s: %s' % (when, message))

    log('Hi there!')
sleep(0.1)
log('Hi again!')

上面显示的结果一样,因为 datatime.now()只是执行了一次。 参数的默认值,会在每个模块加载的时候求出。

def log(message, when=None):
"""Log a message with a timestamp.
Args:
    message: Message to print.
    when: datetime of when the message occurred.
        Defaults to the present time.
"""
when = datetime.now() if when is None else when
print('%s: %s' % (when, message))

改成这样就可以了

用只能以关键字形式指定的参数来确保代码明晰

# 这种是使用位置参数来指定
result = safe_division(1.0, 10**500, True, False)

# 这种使用关键字形式来指定参数
safe_division_b(1.0, 10**500, ignore_overflow=True)

在 python 3 里,我们使用 * 来结束位置参数

def safe_division_c(number, divisor, *,
                ignore_overflow=False,
                ignore_zero_division=False):
try:
    return number / divisor
except OverflowError:
    if ignore_overflow:
        return 0
    else:
        raise
except ZeroDivisionError:
    if ignore_zero_division:
        return float('inf')
    else:
        raise

在 Python 2 中,我们可以这样

def safe_division_d(number, divisor, **kwargs):
ignore_overflow = kwargs.pop('ignore_overflow', False)
ignore_zero_div = kwargs.pop('ignore_zero_division', False)
if kwargs:
    raise TypeError('Unexpected **kwargs: %r' % kwargs)
try:
    return number / divisor
except OverflowError:
    if ignore_overflow:
        return 0
    else:
        raise
except ZeroDivisionError:
    if ignore_zero_div:
        return float('inf')
    else:
        raise

类与继承

尽量用辅助类来维护程序的状态,而不要用字典和元组

不要使用包含字典的字典,这种结构维护会很麻烦。这时我们应该将其拆成类。

class Subject(object):
def __init__(self):
    self._grades = []

def report_grade(self, score, weight):
    self._grades.append(Grade(score, weight))

def average_grade(self):
    total, total_weight = 0, 0
    for grade in self._grades:
        total += grade.score * grade.weight
        total_weight += grade.weight
    return total / total_weight

class Student(object):
def __init__(self):
    self._subjects = {}

def subject(self, name):
    if name not in self._subjects:
        self._subjects[name] = Subject()
    return self._subjects[name]

def average_grade(self):
    total, count = 0, 0
    for subject in self._subjects.values():
        total += subject.average_grade()
        count += 1
    return total / count

简单的接口应该接受函数,而不是类实例

如下,用 λ 表达式充当 key 挂钩,根据名字的长度来排序

    names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
names.sort(key=lambda x: len(x))
print(names)

这里用到了 defaultdict 函数。如果使用 defaultdict,只要你传入一个默认的工厂方法,那么请求一个不存在的 key 时, 便会调用这个工厂方法使用其结果来作为这个 key 的默认值。

    def increment_with_report(current, increments):
    added_count = 0

    def missing():
        nonlocal added_count  # Stateful closure
        added_count += 1
        return 0

    result = defaultdict(missing, current)
    for key, amount in increments:
        result[key] += amount

    return result, added_count

    current = {'green': 12, 'blue': 3}
increments = [
    ('red', 5),
    ('blue', 17),
    ('orange', 9),
]

    result, count = increment_with_report(current, increments)

这里我们还可以通过定义新的类解决:

class CountMissing(object):
def __init__(self):
    self.added = 0

def missing(self):
    self.added += 1
    return 0

counter = CountMissing()
result = defaultdict(counter.missing, current)  # Method reference

上面是用过辅助类来改写带状态的闭包。但是还是不够清晰。 这里有一个 call 的特殊方法,它能使相关对象能够像函数那样得到调用。

    class BetterCountMissing(object):
    def __init__(self):
        self.added = 0

    def __call__(self):
        self.added += 1
        return 0

counter = BetterCountMissing()
    result = defaultdict(counter, current)

以 @classmethod 形式的多态去通用地构建对象 ✓

多态使得类能满足相同的接口或继承自相同的类,但却有着各自不同的功能。 为了实现 MapReduce 流程,我们需要定义公共基类来表示输入数据。

    class InputData(object):
def read(self):
    raise NotImplementedError

InputData 具体子类

class PathInputData(InputData):
def __init__(self, path):
    super().__init__()
    self.path = path

def read(self):
    return open(self.path).read()

另外,我们还需要定义 Worker

    class Worker(object):
    def __init__(self, input_data):
        self.input_data = input_data
        self.result = None

    def map(self):
        raise NotImplementedError

    def reduce(self, other):
        raise NotImplementedError

    class LineCountWorker(Worker):
    def map(self):
        data = self.input_data.read()
        self.result = data.count('\n')

    def reduce(self, other):
        self.result += other.result

    import os

def generate_inputs(data_dir):
    for name in os.listdir(data_dir):
        yield PathInputData(os.path.join(data_dir, name))


def create_workers(input_list):
    workers = []
    for input_data in input_list:
        workers.append(LineCountWorker(input_data))
    return workers

    from threading import Thread

def execute(workers):
    threads = [Thread(target=w.map) for w in workers]
    for thread in threads: thread.start()
    for thread in threads: thread.join()

    first, rest = workers[0], workers[1:]
    for worker in rest:
        first.reduce(worker)
    return first.result

    def mapreduce(data_dir):
    inputs = generate_inputs(data_dir)
    workers = create_workers(inputs)
    return execute(workers)

下面是测试函数

    from tempfile import TemporaryDirectory
import random

def write_test_files(tmpdir):
    for i in range(100):
        with open(os.path.join(tmpdir, str(i)), 'w') as f:
            f.write('\n' * random.randint(0, 100))

with TemporaryDirectory() as tmpdir:
    write_test_files(tmpdir)
    result = mapreduce(tmpdir)

print('There are', result, 'lines')

上面的代码能够完美的运行,但是不够通用。

    class GenericInputData(object):
    def read(self):
        raise NotImplementedError

    @classmethod
    def generate_inputs(cls, config):
        raise NotImplementedError


class PathInputData(GenericInputData):
    def __init__(self, path):
        super().__init__()
        self.path = path

    def read(self):
        return open(self.path).read()

    @classmethod
    def generate_inputs(cls, config):
        data_dir = config['data_dir']
        for name in os.listdir(data_dir):
            yield cls(os.path.join(data_dir, name))


class GenericWorker(object):
    def __init__(self, input_data):
        self.input_data = input_data
        self.result = None

    def map(self):
        raise NotImplementedError

    def reduce(self, other):
        raise NotImplementedError

    @classmethod
    def create_workers(cls, input_class, config):
        workers = []
        for input_data in input_class.generate_inputs(config):
            workers.append(cls(input_data))
        return workers


class LineCountWorker(GenericWorker):
    def map(self):
        data = self.input_data.read()
        self.result = data.count('\n')

    def reduce(self, other):
        self.result += other.result


def mapreduce(worker_class, input_class, config):
    workers = worker_class.create_workers(input_class, config)
    return execute(workers)


with TemporaryDirectory() as tmpdir:
    write_test_files(tmpdir)
    config = {'data_dir': tmpdir}
    result = mapreduce(LineCountWorker, PathInputData, config)
print('There are', result, 'lines')

用 super 初始化父类

    class MyBaseClass(object):
    def __init__(self, value):
        self.value = value

class MyChildClass(MyBaseClass):
    def __init__(self):
        MyBaseClass.__init__(self, 5)

这种方法对于简单的继承是可行的,但是在许多的情况下会出问题。

如果子类有多重继承以及出现砖石型继承就有很大的问题。

    class TimesFiveCorrect(MyBaseClass):
    def __init__(self, value):
        super(TimesFiveCorrect, self).__init__(value)
        self.value *= 5

class PlusTwoCorrect(MyBaseClass):
    def __init__(self, value):
        super(PlusTwoCorrect, self).__init__(value)
        self.value += 2

    class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
    def __init__(self, value):
        super(GoodWay, self).__init__(value)

只在使用 Mix-in 组件制作工具类时进行多重继承

在开发时我们要尽量避免使用多重继承,若一定要利用多重继承带来的便利性,我们就考虑编写 mix-in 类。

mix-in 是一种小型的类,它只定义了其他类可能需要提供的一套附加方法。 例如,我们要把内存中的 python 对象转换为字典形式,以便将其序列化。

    class ToDictMixin(object):
    def to_dict(self):
        return self._traverse_dict(self.__dict__)

    # 具体实现,我们只需要用 hasattr 函数动态的访问属性、用 isinstance 动态的检查对象类型,并用 __dict__ 来访问实例内部的字典
    def _traverse_dict(self, instance_dict):
        output = {}
        for key, value in instance_dict.items():
            output[key] = self._traverse(key, value)
        return output

def _traverse(self, key, value):
    if isinstance(value, ToDictMixin):
        return value.to_dict()
    elif isinstance(value, dict):
        return self._traverse_dict(value)
    elif isinstance(value, list):
        return [self._traverse(key, i) for i in value]
    elif hasattr(value, '__dict__'):
        return self._traverse_dict(value.__dict__)
    else:
        return value

下面演示了如何将 max-in 把二叉树表示为字典

class BinaryTree(ToDictMixin):
def __init__(self, value, left=None, right=None):
    self.value = value
    self.left = left
    self.right = right

tree = BinaryTree(10,
left=BinaryTree(7, right=BinaryTree(9)),
right=BinaryTree(13, left=BinaryTree(11)))

print(tree.to_dict)

mix-in 的功能是可以随时安插通用的功能,并且能在必要的时候覆盖它们。

class BinaryTreeWithParent(BinaryTree):
def __init__(self, value, left=None,
             right=None, parent=None):
    super().__init__(value, left=left, right=right)
    self.parent = parent


        # 覆盖原来的方法
def _traverse(self, key, value):
    if (isinstance(value, BinaryTreeWithParent) and
            key == 'parent'):
        return value.value  # Prevent cycles
    else:
        return super()._traverse(key, value)

多个 mix-in 可以相互组合,例如一个通用的 JSON 序列化方法。

    class JsonMixin(object):
    @classmethod
    def from_json(cls, data):
        kwargs = json.loads(data)
        return cls(**kwargs)

    def to_json(self):
        return json.dumps(self.to_dict())

    class DatacenterRack(ToDictMixin, JsonMixin):
    def __init__(self, switch=None, machines=None):
        self.switch = Switch(**switch)
        self.machines = [
            Machine(**kwargs) for kwargs in machines]

    class Switch(ToDictMixin, JsonMixin):
    def __init__(self, ports=None, speed=None):
        self.ports = ports
        self.speed = speed

class Machine(ToDictMixin, JsonMixin):
    def __init__(self, cores=None, ram=None, disk=None):
        self.cores = cores
        self.ram = ram
        self.disk = disk

serialized = """{
    "switch": {"ports": 5, "speed": 1e9},
    "machines": [
        {"cores": 8, "ram": 32e9, "disk": 5e12},
        {"cores": 4, "ram": 16e9, "disk": 1e12},
        {"cores": 2, "ram": 4e9, "disk": 500e9}
    ]
}"""

deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()

多用 public 属性,少用 privte 属性

编译器无法严格保证 private 的私密性

继承 collections.abc 以实现自定义的容器类型

如果要定制的子类比较简单,那就可以直接从 python 的容器类型,如 list 或 dict 入手

    class FrequencyList(list):
    def __init__(self, members):
        super().__init__(members)

    def frequency(self):
        counts = {}
        for item in self:
            counts.setdefault(item, 0)
            counts[item] += 1
        return counts

    foo = FrequencyList(['a', 'b', 'a', 'c', 'b', 'a', 'd'])
print('Length is', len(foo))
foo.pop()
print('After pop:', repr(foo))
print('Frequency:', foo.frequency())

如果想要编写自制的容器类型,可以从 collections.abc 模块入手。

元类及属性

谈到 python 特性时,元类经常被提到,但很少有人能理解它的实际用途。

metaclass 只是模糊的描述了一种高于类,而又超乎类的概念。

用纯属性取代 get 和 set 方法

不要编写 get set 方法。

class Resistor(object):
def __init__(self, ohms):
    self.ohms = ohms
    self.voltage = 0
    self.current = 0

r1 = Resistor(50e3)
r1.ohms = 10e3

如果想在设置属性的时候实现特殊的行为,可以改用@property 和 setter 方法。

    class VoltageResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)
        self._voltage = 0

    @property
    def voltage(self):
        return self._voltage

    @voltage.setter
    def voltage(self, voltage):
        self._voltage = voltage
        self.current = self._voltage / self.ohms

    r2 = VoltageResistance(1e3)
print('Before: %5r amps' % r2.current)
    # 会执行 setter 方法
r2.voltage = 10
print('After:  %5r amps' % r2.current)

@property 要遵循最小惊讶原则,不要产生奇怪的副作用。

考虑用 @property 来代替属性重构

@property 还有一种高级的用法,就是可以把简单的数值属性迁移为实时计算(on-the-fly calculation).

class Bucket(object):
def __init__(self, period):
    self.period_delta = timedelta(seconds=period)
    self.reset_time = datetime.now()
    self.max_quota = 0
    self.quota_consumed = 0

def __repr__(self):
    return ('Bucket(max_quota=%d, quota_consumed=%d)' %
            (self.max_quota, self.quota_consumed))


@property
def quota(self):
    return self.max_quota - self.quota_consumed

@property 可以为现有的实例属性添加功能

用描述符来改写需要复用的 @property 方法 ✓

@property 有个明显的缺点就是不便于复用

描述符可以提供 get set 方法。

getattr, getattributesetattr 实现按需生成的属性 ✓

    class LazyDB(object):
    def __init__(self):
        self.exists = 5

    def __getattr__(self, name):
        value = 'Value for %s' % name
        setattr(self, name, value)
        return value

    data = LazyDB()
print('Before:', data.__dict__)
print('foo:   ', data.foo)
print('After: ', data.__dict__)

当系统查询找不到查询的属性,而且定义了 getattr, 就会调用这个方法。 而 getattribute 方法就是查到了,也会调用

用元类来验证子类

首先,定义元类,我们要继承 type, python 默认会把那些类的 class 语句体中所含的相关内容,发送给元类的 new 方法。

    class Meta(type):
    def __new__(meta, name, bases, class_dict):
        print(meta, name, bases, class_dict)
        return type.__new__(meta, name, bases, class_dict)

    # 这是 python2 写法
class MyClassInPython2(object):
    __metaclass__ = Meta
    stuff = 123

    def foo(self):
        pass

    # python 3
    class MyClassInPython2(object, metaclass=Meta):
    stuff = 123

    def foo(self):
        pass
    class ValidatePolygon(type):
    def __new__(meta, name, bases, class_dict):
        # Don't validate the abstract Polygon class
        if bases != (object,):
            if class_dict['sides'] < 3:
                raise ValueError('Polygons need 3+ sides')
        return type.__new__(meta, name, bases, class_dict)

class Polygon(object, metaclass=ValidatePolygon):
    sides = None  # Specified by subclasses

    @classmethod
    def interior_angles(cls):
        return (cls.sides - 2) * 180

class Triangle(Polygon):
    sides = 3

print(Triangle.interior_angles())

用元类来注册子类

用元类来注解类的属性

我们可以在类完全定义好前,率先修改类的属性

并行及并发 ✓

用 subprocess 来管理子进程

可以用线程来执行阻塞式 IO,但不要它做平行计算

在线程中使用 LOCK 来防止数据竞争

用 Queue 来协调各线程之间的工作

考虑用协程来并发地运行多个函数

使用 concurrent.futures 来处理并行计算

内置模块

使用 functool.wraps 来定义函数装饰器

考虑以 contextlib 和 with 语句来改写可复用的 try/finally 代码

用 copyreg 实现可靠的 pickle 操作

应该用 datetime 模块来处理本地时间,而不是用 time 模块

使用内置算法与数据结构

在重视精确度的场合,应该使用 decimal

学会安装由 Python 开发者社区所构建的模块

协作开发

为每个函数、类和模块编写文档字符串

用包来安排模块,并提供稳固的 API

为自编的模块定义根异常,以便将调用者与 API 相隔离

用适当的方式打破循环依赖关系

用虚拟环境隔离项目,并重建其依赖关系

部署

考虑用模块级别的代码来配置不同的部署环境

通过 repr 字符串来输出调试信息

用 unittest 来测试全部代码

考虑用 pdb 实现交互调试

先分析性能,然后再优化

用 tracemalloc 来掌握内存的使用及泄漏情况