Effective Python 笔记

《Effective Python》的读书笔记~

Pythonic Thinking

  • Python开发者用Pythonic这个形容词来形容那种符合特定风格的代码。
  • 用import this 可以以查看python的编程之禅

PEP8风格指南

针对Python代码格式编订的风格指南

空白Whitespace

  • 使用空格而不是制表符来缩进
  • 和语法相关的为4个空格
  • 每行字符不超过79
  • 对于多行的长表达式,除首行外其余各行都应该在通常缩进级别之上再加4个空格
  • 文件中的函数与类之间应该用两个空行隔开
  • 在同一个类中,各方法之间用一个空行隔开
  • 对于下表取值、函数调用、给关键字参数赋值不要在两旁写空格
  • 变量赋值时赋值符号各自写一个空格

命名

  • 函数、变量和属性用小写+下划线的形式
  • 受保护的实例属性用单下划线开头_leading_underscore
  • 私有的实例属性用两个下划线开头__double_leading_underscore
  • 类和异常每个单词首字母均大写 CapitalizedWord
  • 模块级别的常量全部大写+下划线:ALL_CAPS
  • 类中的实例方法首个参数命名为self,表示对象本身
  • 类方法class method首个参数应该用cls,表示该类自身

表达式和语句

  • 使用(if a is not b)而不是if not a is b
  • 不要通过长度检测 if len(somelist) == 0 来判断是否为空,直接用if not somelist这种方式来判断,空值默认判断为False
  • 检测somelist是否为[1]或’hi’等非空值,也应该直接用if somelist,会把非空为True
  • 避免单行的if 、for、while循环以及except复合语句,应该拆分为多行使得更加清晰
  • import 语句总是放在文件的开头
  • 引入模块应该才用绝对的路径而不要用相对的路径,如引入bar中的foo,import bar import foo,而不是import foo
  • 如果一定要相对名称,就用明确的写法:from . import foo
  • import语句应该按顺序分为三部分:标准库模块、第三方模块、自用模块。每一部分中,各import语句按模块的字母顺序排列

其它

  • 在同一个切片操作内,不要同时使用start、end、stride,如果确实需要执行这样的语句,那就考虑将其拆解为两条赋值语句,其中一条作范围切割,另一条做步进切割。如果对时间或者内存要求严格,可以考虑内置itertools中的islice
    • a = [‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’, ‘h’]
      • a[::2] # [‘a’, ‘c’, ‘e’, ‘g’]
      • a[::-2] # [‘h’, ‘f’, ‘d’, ‘b’]
    • 下面的会让人困惑
      • a[2::2] # [‘c’, ‘e’, ‘g’]
      • a[-2::-2] # [‘g’, ‘e’, ‘c’, ‘a’]
      • a[-2:2:-2] # [‘g’, ‘e’],从-2开始取到下标为2,且步长为2
      • a[2:2:-2] # []
    • 不要使用含有两个以上的列表推导,因为难以理解
    • 使用生成器表达式来改写数据量较大的列表推导
      • 把实现列表推导的[]改为()就变成了生成器表达式
      • 数据量大用列表推导占用太多内存(内存不一定够)
      • 串在一起的生成器表达式执行速度很快
        • it = (len(x) for x in open(‘/tmp/my_file.txt’))
        • roots = ((x, x**0.5) for x in it)
  • 尽量用enurmate取代range
  • 用zip来平行的遍历多个迭代器
    • python3中zip相当于生成器。
    • 如果迭代器长度不等,那么zip会提前自动终止。
    • 使用zip_longest可以平行遍历多个迭代器
  • 不要在for 和 while后写else块
    • 容易让人误解
    • 只有当整个循环都没有break的时候,循环后面的else才会执行
  • 合理利用try/except/else/finally中的每个代码块
    • 无论try块是否发生异常,都可以用finally来执行清理工作
    • else 可以用来缩减try块中的代码量,并把没有发生异常时所要执行的语句和try/except代码块隔开

函数

在闭包中使用外围作用域的变量

  • 若是当前作用域没有这个变量,python会把这次赋值视为对变量的定义
  • 使用nonlocal来修改外围作用域中同名变量,但不能延伸到模块级别,防止污染全局作用域
  • nonlocal与global互为补充,会直接修改模块作用域里的那个变量

生成器

  • 生成器是使用yield的函数,调用生成器函数时,并不会真的执行,而是会返回生成器。每个在这个迭代器上面调用next函数时,迭代器把生成器推进到下一个yield表达式那里

当函数参数若是迭代器…

  • 需要注意的时,迭代器只能返回一轮结果,在跑出过StopIteration异常的迭代器或生成器上继续迭代第二轮是不会有结果的。因此,如果将迭代器作为参数并在函数中想要遍历两次,那么代码不能按我们期望的方式进行。
  • 解决的办法是新编一种实现迭代器协议的容器类。
  • python在for循环及相关表达式中遍历某种容器的内容时,就要依靠这个迭代器协议。在执行类似for x in foo这样的语句时,python实际上会调用iter(foo),iter又会调用foo.__iter__这个方法。这个方法返回迭代器对象,而那个迭代器本身,实现了__next__特殊方法,此后for循环在迭代器上反复调用next函数,直到耗尽并产生stopIteration异常。
  • 在使用类时,只需要令自己的类把__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)
  • 想判断某个值是容器还是迭代器,可以拿该值为参数,两次调用iter函数。若结果,相同,则为迭代器 if iter(numbers) is iter(numbers)

变长参数

  • 变长参数在传给函数时,总是要先转化为元祖,因此,若对生成器使用*操作符,就会将生成器完整迭代一轮,并把结果每个值都放入元祖中,这可能会消耗大量内存,导致程序崩溃。
  • 使用*arg参数的第二个问题是将来要给函数添加新的位置参数,就必须修改原来调用函数的代码。为了避免此情况,我们应该使用只以关键字形式指定的参数来扩展接受*args的函数

关键字参数

  • 位置参数必须出现在关键字参数之前
    • 如这样是错的:remainder(number=20,7)
  • 动态默认值的参数应该把形式上的默认值写成None,然后在函数中初始化。因为参数的默认值会在每个模块加载进来的时候求出,而很多模块都在程序启动时加载。模块一旦加载进来,参数默认固定值就不变。
  • 如下代码的when不会变
def log(message, when=datetime.now()):
	print(‘%s: %s’ % (when, message))
  • 参数列表里的*号,标志着位置参数就此终结,之后的参数只能以关键字形式来指定(only py3)
def safe_division_c(number, divisor, *,
		ignore_overflow=False,
		ignore_zero_division=False):

 

类和继承

namedtuple

  • 如果容器中包含简单又不可变的数据,那么先用namedtuple来表示,稍后有需要的时候再修改为完整的类。
  • 不过无法指定默认参数
  • 使用例子:
Point = collections.namedtuple('Point', ['x', 'y'])
p = Point(11, y=22)
print(p[0] + p[1]) # 33
print(p.x + p.y) # 33

简单的接口应该接受函数而非类的实例

  • 对于连接各种Python的简单接口,通常应该给其直接传入函数,而不是先定义某个类,然后再传入这个类的实例。
  • python中的函数和方法都可以像一级类那样引用。
  • python中的函数之所以能充当挂钩,原因在于函数是一级对象,可以像语言中其它值那样传递和引用。
    • nums.sort(key=lambda x:len(x))
  • 通过名为__call__的方法,可以使类的实例能像普通的python函数那样得到调用。
  • 如果要用函数来保存状态,那么就定义新的类,并实现__call__方法,而不是定义带状态的闭包。

@classmethod来多态构建对象

python只允许__init__的构造器方法,可以使用@classmethod的多态。

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

上面的例子中, LineCountWorker为Worker的子类。

假如此时需要处理别的work的话,那就得重写这个函数。

因此可以这样:

class GenericWorker(object):
# …
    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

然后让具体的子类继承GenericWorker即可。

用super初始化父类

钻石形继承体系:

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


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


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


class ThisWay(TimesFive, PlusTwo):
    def __init__(self, value):
        TimesFive.__init__(self, value)
        PlusTwo.__init__(self, value)


foo = ThisWay(5)
print(foo.value)  # 7  want to 5 * 5 + 2 = 27

用super的话,钻石顶部的MyBaseClass类中的__init__方法只会运行一次。而其它超类初始化顺序,则与这些超类在class语句中出现的顺序相同。

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


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


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


class GoodWay(TimesFive, PlusTwo):
    def __init__(self, value):
        super().__init__(value)
        super().__init__(value)


foo = GoodWay(5)
print(foo.value)  # 35
print(GoodWay.mro())  
# [<class '__main__.GoodWay'>, <class '__main__.TimesFive'>, <class '__main__.PlusTwo'>, <class '__main__.MyBaseClass'>, <class 'object'>]

可以用mro类方法查询程序的运行顺序,调用GoodWay(5),首先调用TimesFive.__init__,然后TimesFive.__init__调用PlusTwo.__init__,然后PlusTwo.__init__调用MyBaseClass.__init__,到达顶部后,先设为5,然后PlusTwo.__init__加2,然后TimesFive.__init__乘以5,得到了35。

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

Mix-in可以认为是工具类,继承的子类便具有了这些功能,子类可以复写方法来改进。能用Mix-in组件实现的效果,就不要用多重继承来做。

class ToDictMixin(object):
    def to_dict(self):
        return self._traverse_dict(self.__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


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())

多用public属性,少用private属性

子类无法访问private属性,原因在于变换后的属性名和待访问的属性名称不相符:

class MyParentObject(object):
    def __init__(self):
        self.__private_field = 71


class MyChildObject(MyParentObject):
    def get_private_field(self):
        return self.__private_field


baz = MyChildObject()
baz.get_private_field()
# AttributeError: 'MyChildObject' object has no attribute '_MyChildObject__private_field'

调用MyChildObject.get_private_field,它将translates __private_field变换为_MyChildObject__private_field,然后进行访问。而__private_field只在MyParentObject.__init__做了定义,因此这个私有属性的名称是_MyParentObject__private_field。

因此,上面的代码可以直接用print(baz._MyParentObject__private_field)#71 访问私有属性。

Python编译器无法严格保证private字段的私密性。宁可叫子类更多地取访问超类的protected属性,也不要设置为private。应当在文档中说明每个protected字段的含义,解释哪些字段是可供子类使用的内部API,哪些字段是完全不应该触碰的数据。

只有当子类不受自己控制的时候,才可以考虑用private属性来避免名称冲突。

继承collecions.abc实现自定义容器类型

  • 如果要定制的子类比较简单,可以直接从Python的容器类型继承(如List或dict)
  • 编写自制的容器类型,可以从collections.abc模块的抽象基类中继承,那些基类能确保我们子类具备适当的接口及行为。如继承Sequence的话,要求实现__getitem__以及__len__方法

元类及属性

元类这个词只是模糊的描述了一种高于类又超乎类的概念。就是我们可以把python的class语句转译为元类,并令其在每次定义具体的类时,都提供独特的行为。

python还可以动态的定义对属性的访问操作。

用@property取代get和set方法

  • 如果访问对象的某个属性,需要表现出特殊的行为(如修改电压同时修改电流),可以用@property来修饰方法
  • setter和getter的名称必须要相关属性相符
  • 可以在setter的时候设置相关的属性,或者进行数值验证
  • @property方法需要执行得迅速一点,缓慢或复杂的工作应该放在普通方法里
class Resistor(object):
    def __init__(self, ohms):
        self.ohms = ohms
        self.voltage = 0
        self.current = 0


class BoundedResistance(Resistor):
    def __init__(self, ohms):
        super().__init__(ohms)

    @property
    def ohms(self):
        return self._ohms

    @ohms.setter
    def ohms(self, ohms):
        if ohms <= 0:
            raise ValueError('%f ohms must be > 0' % ohms)
        self._ohms = ohms


r3 = BoundedResistance(1e3)  # 1e3改成0也会ValueError
r3.ohms = 0  # raise ValueError('%f ohms must be > 0' % ohms)

用@property代替属性重构

@property可以把一个原有的属性变为新的。

如下面的例子中,原来的属性有quota,现在改成了max_quota和quota_consumed:

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

    @quota.setter
    def quota(self, amount):
        # ...

如果@property用得太过频繁,那么就应该考虑彻底重构该类并修改相关的调用代码。

用 __getattr__、 __getattribute__、和__setattr__ 实现按需生成的属性

如果某个类定义了__getattr__,系统在该类对象实例的字典中又找不到待查询的属性,那么系统就会调用这个方法。适合实现按需访问,初次执行__getattr__把相关属性加载,以后在访问该属性时,只需从现有的结果中获取即可。

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__)  # Before: {'exists': 5}
print('foo: ', data.foo)  # foo:  Value for foo
print('After: ', data.__dict__)  # After:  {'foo': 'Value for foo', 'exists': 5}
  •  __getattribute__方法:每次访问对象属性时,都会调用这个方法(即使属性字典有也会)
  • __setattr__:赋值操作时均会触发(无论是内置的setattr函数还是直接赋值)
  • 如果要在__getattribute__和__setattr__中访问实例属性,那么应该直接通过super()来避免无限递归:
class BrokenDictionaryDB(object):
    def __init__(self, data):
        self._data = data

    def __getattribute__(self, name):
        print('Called __getattribute__(%s)' % name)
        return self._data[name]


data = BrokenDictionaryDB({'foo': 3})
data.foo

这样才是对的

class BrokenDictionaryDB(object):
    def __init__(self, data):
        self._data = data

    def __getattribute__(self, name):
        _data = super().__getattribute__('_data')
        return _data[name]


data = BrokenDictionaryDB({'foo': 3})
print(data.foo)

用元类验证子类

  • 定义元类的时候,要从type中继承。
  • 对于使用该元类的其他类,python会把那些类的class语句体中所含的相关内容,发送给元类的__new__方法。于是我们可以在系统构建出那个类之前,先修改类的信息。
class Meta(type):
    def __new__(mcs, *args, **kwargs):
        print(args)  # name,base,class_dict
        return type.__new__(mcs, *args, **kwargs)


class MyClass(object, metaclass=Meta):
    stuff = 123

    def foo(self):
        pass


MyClass()
# ('MyClass', (<class 'object'>,), 
# {'stuff': 123, '__qualname__': 'MyClass', '__module__': '__main__', 'foo': <function MyClass.foo at 0x00000140C57518C8>})

通过元类,我们可以在生成子类对象之前,先验证子类的定义是否合乎规范。

python把子类的整个class语句处理完后,就调用其元类的__new__方法。

如下面的例子中,多边形至少三条边,而Line设置一条边就不行:

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


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

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


class Line(Polygon):
    sides = 1 # raise ValueError('Polygons need 3+ sides')

用元类注解类的属性

用下面的代码将数据库的行和列建立对应关系:

class Field(object):
    def __init__(self, name):
        self.name = name
        self.internal_name = '_' + self.name

    def __get__(self, instance, instance_type):
        if instance is None: return self
        return getattr(instance, self.internal_name, '')

    def __set__(self, instance, value):
        setattr(instance, self.internal_name, value)


class Customer(object):
    first_name = Field('first_name')
    last_name = Field('last_name')
    prefix = Field('prefix')


suffix = Field('suffix')

但是Field还需要指定字段名称如first_name = Field(‘first_name’),比较繁琐,可以用元类来做

class Field(object):
    def __init__(self):
        self.name = None
        self.internal_name = None  # '_' + self.name

    def __get__(self, instance, instance_type):
        if instance is None: return self
        return getattr(instance, self.internal_name, '')

    def __set__(self, instance, value):
        setattr(instance, self.internal_name, value)


class Meta(type):
    def __new__(mcs, *args, **kwargs):
        for key, value in args[2].items():
            if isinstance(value, Field):
                value.name = key
                value.internal_name = '_' + key
        return type.__new__(mcs, *args, **kwargs)


class DatabaseRow(object, metaclass=Meta):
    pass


class BetterCustomer(DatabaseRow):
    first_name = Field()
    last_name = Field()
    prefix = Field()
    suffix = Field()


foo = BetterCustomer()
print('Before:', repr(foo.first_name), foo.__dict__)
# Before: '' {}
foo.first_name = 'Euler'
print('After: ', repr(foo.first_name), foo.__dict__)
# After:  'Euler' {'_first_name': 'Euler'}

 

并发及并行

用subprocess来管理子进程

roc = subprocess.Popen(['ping', 'baidu.com'], stdout=subprocess.PIPE)
out, err = proc.communicate()

out = subprocess.check_output(['ping', 'baidu.com'], stderr=subprocess.STDOUT)

可以一边定期查询子进程状态,一遍处理其它事务

roc = subprocess.Popen(['ping', 'baidu.com'], stdout=subprocess.PIPE)
out, err = proc.communicate()

out = subprocess.check_output(['ping', 'baidu.com'], stderr=subprocess.STDOUT)

proc = subprocess.Popen(['sleep', '0.3'], stdout=subprocess.PIPE)
while proc.poll() is None:
    print("working")
    # some time-consuming work
print('Exit status', proc.poll())

还可以给 communicate传入timeout参数,避免子进程死锁或挂起

python线程并非并行

  • 标准的python实现叫做CPython,CPython分两步来运行Python程序。首先把文本形式的源代码解析并编译成字节码。然后,用一种基于栈的解释器来运行这份字节码。执行python程序时,字节码解释器必须保持协调一致的状态。Python才用GIL(global interpreter lock,全局解释器锁)来确保这种协调性。
  •  GIL为一把互斥锁,用以防止Cpython收到抢占式多线程切换的干扰。由于收到GIL保护,同一时刻只有一条线程向前执行。(虽然同一时刻只有一条线程,但仍需锁等机制来防止数据竞争)
  • 可以用concurrent.futures中的ProcessPoolExecutor来执行真正的并行。
      • 子进程的GIL是独立的
      • 原理:将数据通过pickle来执行序列化,变为二进制形式,然后用local socket发给子解释器,子进程用pickle反序列化操作,然后执行。将结果进行序列化操作,然后用socket发送回主进程。主进程反序列化得到结果。主进程和子进程之间,必须进行序列化和反序列化操作,开销较大。
      • 官方注释如下:
    |======================= In-process =====================|== Out-of-process ==|
    
    +----------+     +----------+       +--------+     +-----------+    +---------+
    |          |  => | Work Ids |    => |        |  => | Call Q    | => |         |
    |          |     +----------+       |        |     +-----------+    |         |
    |          |     | ...      |       |        |     | ...       |    |         |
    |          |     | 6        |       |        |     | 5, call() |    |         |
    |          |     | 7        |       |        |     | ...       |    |         |
    | Process  |     | ...      |       | Local  |     +-----------+    | Process |
    |  Pool    |     +----------+       | Worker |                      |  #1..n  |
    | Executor |                        | Thread |                      |         |
    |          |     +----------- +     |        |     +-----------+    |         |
    |          | <=> | Work Items | <=> |        | <=  | Result Q  | <= |         |
    |          |     +------------+     |        |     +-----------+    |         |
    |          |     | 6: call()  |     |        |     | ...       |    |         |
    |          |     |    future  |     |        |     | 4, result |    |         |
    |          |     | ...        |     |        |     | 3, except |    |         |
    +----------+     +------------+     +--------+     +-----------+    +---------+
    • Queue(queue中) 为线程安全
      • 具备阻塞式队列操作
      • 指定缓冲区尺寸
      • join等

协程

  • 线程有三个显著的缺点:
    • 需要特殊的工具来保证数据的安全。于是多线程的代码更加难懂,不便于扩展维护。
    • 线程需占用大量内存,每个线程大约需要8MB。如果程序中运行成千上万个函数并且想要用线程来模拟出同时运行的效果,那就会出现问题。
    • 线程启动开销比较大。
  • Python中的协程可以解决上述的问题。协程,又称微线程,纤程。英文名Coroutine。协程可以理解为用户级线程,协程和线程的区别是:线程是抢占式的调度,而协程是协同式的调度,协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
  • Python协程的工作原理是:通过send给生成器传值,生成器将yield作为表达式的执行结果。执行完当前的yield,生成器推进到下一个yield表达式那里,并将那个yield关键字右侧的内容,当成send方法的返回值,返回给外界。
  • 生成器通过这个输出值,来推进其他的生成器函数,使得那些生成器函数也执行到它们各自的下一条yield表达式处。接连推进多个独立的生成器,可以模拟出python线程的并发行为,令程序真正看上去好像是在同时执行多个函数。(用yield from 推进其他的生成器)
  • 下面的代码中统计当前的最小值统计当前的最小值:
def minimize():
    current = yield
    while True:
        value = yield current
        current = min(value, current)


it = minimize()
next(it)  # Prime the generator
print(it.send(10))  # 10
print(it.send(4))  # 4
print(it.send(22))  # 4
print(it.send(-1))  # -1

 

内置模块

装饰器

contextlib和with语句改写try/finally语句

  • with open(‘./data’) as f: 比使用try 打开文件然后finally中关闭好得多
  • 一个简单的函数,只需要经过contextlib中的contextmanager修饰,就可以用在with语句中。
  • 由于py的默认信息级别是WARNING,因此不会打印debug的(只会打印不小于当前级别的)。下面用一个经过contextmanager修饰的函数来临时提升信息的级别,执行完毕后,再恢复原有级别。yield表达式所在的地方,就是with语句中要执行的地方
def my_function():
    logging.debug('Some debug data')
    logging.error('Error log here')
    logging.debug('More debug data')


@contextmanager
def debug_logging(level):
    logger = logging.getLogger()
    old_level = logger.getEffectiveLevel()
    logger.setLevel(level)
    try:
        yield
    finally:
        logger.setLevel(old_level)


with debug_logging(logging.DEBUG):
    logging.warning('Inside: ')
    my_function()

logging.warning('After: ')
my_function()

如果yield返回一个值,那么此值会赋值给由as关键字所指定的变量。

 

用copyreg实现可靠的pickle操作

pickle处理之后的数据,不一种不安全的格式。如果混入了恶意信息,那么python程序对其进行反序列化操作的时候,这些恶意信息可能对程序照成伤害。

json模块产生的则是一种安全的格式。

可以把内置的copyreg和pickle结合起来使用,以便为旧数据添加缺失的属性值、进行类的版本管理,并给序列化后的数据提供固定的引入路径。

用datetime模块来处理本地时间,而非time模块

time模块需要依赖操作系统而运作。不要用time模块在不同的时区之间进行转换。

如果要在不同时区之间,进行可靠的转换,应该把内置的datetime模块与开发者社区提供的pytz模块搭配起来使用。

先把时间转换成UTC格式,然后执行各种转换操作,最后再转换回本地时间。

使用内置的算法和数据结构

  • deque 双向队列
    • append
    • popleft
    • pop
  • OrderedDict 有序字典
  • heapq 堆
  • bisect_left 二分查找
  • 和迭代器有关的:
    • 把迭代器连接起来:
      • chain: 将多个迭代器按顺序连成一个迭代器
      • cycle无限重复某个迭代器中各个元素
      • tee 把一个迭代器拆分成多个平行的迭代器
      • zip_longest:和zip类似,但可以应对长度不同的迭代器
    • 能够从迭代器中过滤元素的函数
      • islice:不进行赋值的前提下,根据索引值来切割迭代器
      • takewhile:再判定函数为True的时候,从迭代器中逐个返回元素
      • dropwhile:从判定函数为False的地方开始,逐个返回元素
      • filterfalse:和filter相反,从迭代器中返回令判定函数为False的所有元素
    • 把迭代器元素组合起来的函数
      • product:根据迭代器中的元素计算笛卡儿积,并将其返回。可以用product来改写深度嵌套的列表推导操作
      • permutation:排列
      • combination:组合

在重视精度的场合用decimal

如下面的代码,按照每分钟rate收费,但是第一个输出我们期望的是0.01,第二个是5.37:

def do_cost(rate, second):
    cost = rate * second / 60
    return cost, round(cost, 2)


print(do_cost(0.05, 5))  # (0.004166666666666667, 0.0)
print(do_cost(1.45, 222))  # (5.364999999999999, 5.36)

Decimal类中非常适合用在那种对精度要求很高,且对舍入行为要求很严的场合,如涉及货币计算:

def do_cost(rate, second):
    rate = Decimal(str(rate))
    second = Decimal(str(second))
    cost = rate * second / Decimal('60')
    return cost, cost.quantize(Decimal('0.01'), rounding=ROUND_UP)


print(do_cost(0.05, 5))  # (Decimal('0.004166666666666666666666666667'), Decimal('0.01'))
print(do_cost(1.45, 222))  # (Decimal('5.365'), Decimal('5.37'))

协作开发

编写文档

  • Python将文档视为第一等级(first-class)对象
  • 通过__doc__来访问文档
  • 文档应该用三重双引号”””

为模块编写文档

  • 每个模块都应有顶级的doctring,用来介绍当前这个模块以及模块中的内容
  • 文档第一行为一句话,介绍本模块的用途
  • 它下面的那段话,应该包含一些细节信息,把与本模块的操作有关内容,告诉模块使用者。可以强调本模块中比较重要的类和函数,使得开发者能据此了解该模块的用法。
#!/usr/bin/env python3
"""Library for testing words for various linguistic patterns.
Testing how words relate to each other can be tricky sometimes!
This module provides easy ways to determine when words you’ve
found have special properties.
Available functions:
- palindrome: Determine if a word is a palindrome.
- check_anagram: Determine if two words are anagrams.
… """
# …

为类编写文档

  • 每个类都应该有类级别的doctring。
  • 头一行也是一句话介绍该类用途
  • 类中比较重要的public属性及方法,也应该再这个docstring里面加以强调
class Player(object):
    """Represents a player of the game.
    Subclasses may override the ‘tick’ method to provide
    custom animations for the player’s movement depending
    on their power level, etc.
    Public attributes:
    - power: Unused power-ups (float between 0 and 1).
    - coins: Coins found during the level (integer).
    """
    # …

为函数编写文档

  • 每个函数和方法也应该有docstring
  • 第一行为一句话描述本函数的功能
  • 接下来为一段话用来描述具体的行为和参数。(如果函数没有参数,且有且仅有一个简单的返回绘制,那么只需要一句话来描述该函数就够了)
  • 若有返回值,则应该再docstring中写明。如果没有返回值就不要写。
  • 如果可能抛出某些调用者必须处理的异常,而这些异常又是函数接口的一部分,那么docstring应该对其做出解释。同样的,没有异常就不要写
  • 如果函数接受数量可变的位置参数或数量可变的关键字参数,那么就应该再文档的参数列表中,使用*args和**kwargs来描述它们的用途
  • 如果函数的参数有默认值,那么应该指出这些默认值
  • 如果函数是个生成器,那么应该描述该生成器在迭代时产生的内容
  • 如果函数时个协程,那么应该描述协程所产生的返回值,以及这个协程希望通过yield表达式来接纳的值,同时还要说明该协程何时会停止迭代
def find_anagrams(word, dictionary):
    """Find all anagrams for a word.
    This function only runs as fast as the test for
    membership in the ‘dictionary’ container. It will
    be slow if the dictionary is a list and fast if
    it’s a set.
    Args:
        word: String of the target word.
        dictionary: Container with all strings that
            are known to be actual words.
    Returns:
        List of anagrams that were found. Empty if
        none were found.
    """

用包来安排模块

可以编写__all__的特殊属性,减少其暴露给外围API使用者的信息。
__all__时一个列表,其中每个名称都将作为本模块的一条公共API,导出给外部代码。
如果外部用户以from foo import *形式使用foo模块,那么只有再__all__里的那些属性才会从foo引入。如果foo没有提供__all__,那么只会引入public属性

# __init__.py
__all__ = []
from . models import *
__all__ += models.__all__
from . utils import *
__all__ += utils.__all__

自定义异常

好处:

  • 调用者在使用API的时候,通过捕获根异常,可以知道他们使用的调用代码是否正确
  • 调用者可以捕获python的Exception基类,帮助模块的研发者寻找API实现中的bug

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

下面的代码会出异常(AttributeError: ‘module’ object has no attribute ‘prefs’)

# dialog.py
import app

class Dialog(object):
    def __init__(self, save_dir):
        self.save_dir = save_dir
        # …

save_dialog = Dialog(app.prefs.get(‘save_dir’))

def show():
    # …


# app.py
import dialog

class Prefs(object):
    # …
def get(self, name):
    # …
prefs = Prefs()
dialog.show()

引入模块的时候,python按照深度优先的顺序执行下列操作:

  1. 在sys.path所制定的路径中,搜寻待引入的模块
  2. 从模块中加载代码,并保证这段代码能够正确编译
  3. 创建与该模块相对应的空对象
  4. 把这个空的模块对象添加到sys.modules里
  5. 运行模块对象中的代码,定义其内容

因为某些属性必须等系统把对用的代码执行完毕之后(第5步),才具备完整的定义。因为app模块在未定义任何内容的时候就引入了dialog模块,然后dialog又引入了app模块。而app模块尚未定义完整个引入的过程,还处在引入dialog的状态之中。按照上面第4步的规则,此时的app模块只是个空壳而已。而dialog模块却需要这个prefs,就抛出了AtrributeError异常。

方法一调整引入顺序

在app模块中移动到底部。当dialog模块反向引用app时,第五步几乎执行完 了,于是dialog能找到app.prefs的定义。
但是该方法和PEP 8 风格不符(import 应该在顶部)

# app.py
class Prefs(object):
    # …
prefs = Prefs()
import dialog # Moved
dialog.show()

方法二 先引入、在配置、最后运行

只在模块中给出函数、类和常量的定义,而不要在引入的时候真正去运行那些函数。每个模块都将提供configure函数,等其他模块都引入完毕之后,我们在该模块上面调用一次configure,而这个函数访问其他模块的属性,以便将本模块的状态准备好。

# dialog.py
import app

class Dialog(object):
    # …

save_dialog = Dialog()
def show():
    # …

def configure():
    save_dialog.save_dir = app.prefs.get(‘save_dir’)


# app.py
import dialog
class Prefs(object):
    # …
prefs = Prefs()
def configure():
    # …

调用时:

# main.py
import app
import dialog
app.configure()
dialog.configure()
dialog.show()

这个方案在很多情况下都很适合,而且方便开发者实现依赖注入等模式。但是有时候很难从代码中提取configure步骤。

另外模块内部划分不同阶段,会令代码不易理解(因为把对象的定义和配置分开了)

方法三 动态引入

# dialog.py
class Dialog(object):
    # …

save_dialog = Dialog()

def show():
    import app  # Dynamic import
    save_dialog.save_dir = app.prefs.get(‘save_dir’)
    # …

而app模块和最开始的一样。

一般来说,尽量不要使用这种动态引入的方案,因为import语句的执行开销,还是不小的。折中动态引入方案,还可能会在程序运行时导致非常奇怪的错误,如程序在运行很久后突然抛出SyntaxError异常。

不过这是最简单的方案,因为即可以缩减重构所花的精力,又可以尽量降低代码的复杂度。

配置虚拟环境

  • 用pip show xxx可以看依赖那些包
  • 比如Sphinx和flask都依赖jinja2的包,但是这个包如果发生重大变化,一个需要新版一个需要旧版那系统就没法运行了。
  • 虚拟环境工具pyvenv工具(python3.4自带)早期python需要 pip install virtualenv,并在命令行通过virtualenv来使用
  • 用pyvenv命令来新建名为myproject的虚拟环境,每一套虚拟环境都必须位于各自独立的目录之中(使用虚拟化环境的时候也不要去移动环境的目录)。该目录下面会产生响应的目录树与文件:
$ pyvenv /tmp/myproject
$ cd /tmp/myproject
$ ls
bin include lib pyvenv.cfg
  • 用source来运行bin/actiave脚本,该脚本修改所有环境变量,使之与虚拟环境相匹配。它还会更新命令提示符,把虚拟环境的名称包含进来,使得开发者可以明确知道自己所处的环境:
$ source bin/activate
(myproject)$
(myproject)$ which python3
/tmp/myproject/bin/python3
(myproject)$ ls -l /tmp/myproject/bin/python3
… -> /tmp/myproject/bin/python3.4
(myproject)$ ls -l /tmp/myproject/bin/python3.4
… -> /usr/local/bin/python3.4
  • 在这个环境中,除了pip和steuptools是没有安装任何软件包的。外围系统的包这里不可用。可以用pip把包安装在当前虚拟环境
  • 使用完虚拟环境后,通过deactivate命令回到默认的系统
(myproject)$ deactivate
$ which python3
/usr/local/bin/python3
  • 用pip freeze可以把开发环境对软件包的依赖关系,保存到文件之中。按照管理,文件名为requirements.txt.
(myproject)$ pip3 freeze > requirements.txt
(myproject)$ cat requirements.txt
numpy==1.8.2
pytz==2014.4
requests==2.3.0
  • 新的环境要安装只需要
    • (otherproject)$ pip3 install -r /tmp/myproject/requirements.txt

 

部署

通过repr来输出调试信息

  • 对内置的Python类型调用pring函数,会根据该值打印出一条易于阅读的字符串,这个字符串隐藏了类型信息。而repr函数,会根据该值返回一条可供打印的字符串。把这个repr传给内置的eval函数,就可以将其还原为初始的那个值
  • 在格式字符串使用%s就类似str函数返回的,使用%r就和repr相符
  • 类中可以定义__repr__方法
  • 在任意对象上查询__dict__属性,观察其内部信息

用unitest来测试全部代码

  • 要想确信Python程序能正常运行,唯一的办法就是编写测试。
  • 内置的unittest是编写测试最简单的方法
  • 如以下的代码:
# utils.py
def to_str(data):
    if isinstance(data, str):
        return data
    elif isinstance(data, bytes):
        return data.decode('utf-8')
    else:
        raise TypeError('Must supply str or bytes, ''found: %r' % data)


# utils_test.py
from unittest import TestCase, main
from utils import to_str

class UtilsTestCase(TestCase):
    def test_to_str_bytes(self):
        self.assertEqual('hello', to_str(b'hello'))

    def test_to_str_str(self):
        self.assertEqual('hello', to_str('hello'))

    def test_to_str_bad(self):
        self.assertRaises(TypeError, to_str, object())
  • 测试是以TestCase的形式来组织的。每个以test开头的方法,都是一项测试。如果测试方法在运行过程中没有抛出任何Exception,也没有因assert语句而导致AssertionError,那么测试就算顺利通过。
  • TestCase类提供了一些辅助方法,以供开发者在编写测试的时候做出各种断言。如assertEqual判断两值是否相等,assertTrue判断表达式是否为真,assertRaises验证程序是否能在适当的时候抛出相关的异常。
  • 在TestCase子类中,可以定义一些辅助方法来令测试代码更加便于阅读,只是要注意,这些辅助方法不能以test开头。
  • 有时候运行测试方法需要在TestCase类中把测试环境配置好。于是我们就覆写setUp和tearDown方法。系统在执行每个测试之前,都会调用一次setUp方法,在执行每个测试之后,执行一次tearDown方法,这样保证各项测试独立运行。
  • 通常把一组相关的测试放在一个TestCase中(一个模块内的所有函数),如果某函数有很多边界状况,那就针对这个函数专门编写一个TestCase子类,次外也会针对每个类来编写TestCase,来测试该类及类中的所有方法。

用pdb实现交互测试

通常写在一行,使得不用能够方便的注释
import pdb; pdb.set_trace()

先分析性能在优化

Python提供了内置的性能分析工具,可以计算出程序某个部分的执行时间在总体的执行时间中所占的比率。

采用内置的cProfile模块比profile模块好,因为对受测代码的效率只会产生很小的影响。

下面的代码测试插入排序的:

def insert_value(array, value):
    for i, existing in enumerate(array):
        if existing > value:
            array.insert(i, value)
            return
    array.append(value)


def insertion_sort(data):
    result = []
    for value in data:
        insert_value(result, value)
    return result


from random import randint

max_size = 10 ** 4
data = [randint(0, max_size) for _ in range(max_size)]
test = lambda: insertion_sort(data)
profiler = Profile()
profiler.runcall(test)
stats = Stats(profiler)
stats.strip_dirs()
stats.sort_stats('cumulative')
stats.print_stats()

结果如下:

20003 function calls in 2.288 seconds
	
Ordered by: cumulative time

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
	1    0.000    0.000    2.288    2.288 test.py:36(<lambda>)
	1    0.006    0.006    2.288    2.288 test.py:27(insertion_sort)
10000    2.259    0.000    2.282    0.000 test.py:20(insert_value)
 9992    0.023    0.000    0.023    0.000 {method 'insert' of 'list' objects}
	8    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
	1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

说明如下:

  1. ncalls: The number of calls to the function during the profiling period.
  2. tottime: The number of seconds spent executing the function, excluding time spent executing other functions it calls.
  3. tottime percall: The average number of seconds spent in the function each time it was called, excluding time spent executing other functions it calls. This is tottime divided by ncalls.
  4. cumtime: The cumulative number of seconds spent executing the function, including time spent in all other functions it calls.
  5. cumtime percall: The average number of seconds spent in the function each time it was called, including time spent in all other functions it calls. This is cumtime divided by ncalls.

可以用stats.print_callers()查看该函数所消耗的执行时间究竟是哪些调用者分别引发的。

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

调用内存的使用情况的第一种是内置的gc模块,让它列出垃圾收集器当前所知的每个对象。

# using_gc.py
import gc
found_objects = gc.get_objects()
print('%d objects before' % len(found_objects))
import waste_memory
x = waste_memory.run()
found_objects = gc.get_objects()
print('%d objects after' % len(found_objects))
for obj in found_objects[:3]:
	print(repr(obj)[:100])
>>>
4756 objects before
14873 objects after
<waste_memory.MyObject object at 0x1063f6940>
<waste_memory.MyObject object at 0x1063f6978>
<waste_memory.MyObject object at 0x1063f69b0>

但是gc模块不能告诉我们这些对象是如何分配出来的。可以用tracemalloc解决(Python3.4及之后的才有)

下面的打印导致内存增大的前三个对象,可以立即看出导致内存变大的主要因素以及分配那些对象的语句在源代码中的位置。

import tracemalloc
tracemalloc.start(10) # Save up to 10 stack frames
time1 = tracemalloc.take_snapshot()
import waste_memory
x = waste_memory.run()
time2 = tracemalloc.take_snapshot()
stats = time2.compare_to(time1, 'lineno')
for stat in stats[:3]:
    print(stat)
>>>
waste_memory.py:6: size=2235 KiB (+2235 KiB), count=29981 (+29981),
average=76 B
waste_memory.py:7: size=869 KiB (+869 KiB), count=10000 (+10000), average=89B 
waste_memory.py:12: size=547 KiB (+547 KiB), count=10000 (+10000), average=56B

tracemalloc模块也可以打印出py在执行每一个分配内存操作时,具备的完整的堆栈信息.下面找到程中最消耗内存的那个内存分配操作,并将该操作的堆栈信息打印出来.

§ stats = time2.compare_to(time1, 'traceback')
top = stats[0]
print('\n'.join(top.traceback.format()))
>>>
File “waste_memory.py”, line 6
self.x = os.urandom(100)
File “waste_memory.py”, line 12
obj = MyObject()
File “waste_memory.py”, line 19
deep_values.append(get_data())
File “with_trace.py”, line 10
x = waste_memory.run()

 

 

 

本博客若无特殊说明则由 hrwhisper 原创发布
转载请点名出处:细语呢喃 > Effective Python 笔记
本文地址:https://www.hrwhisper.me/note-for-effective-python/

打赏一杯咖啡钱呗

python learning, 读书笔记 . permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *