Fork me on GitHub

Python进阶笔记

学习python进阶的Jupyter笔记。

In [ ]:
print('python3')

*args 和**kwargs用法

主要用于函数定义。 你可以将不定数量的参数传递给一个函数。
*args 是用来发送一个非键值对的可变数量的参数列表给一个函数.

**kwargs允许你将不定长度的键值对, 作为参数传递给一个函数。 如果你想要在一个函数里处理带名字的参数, 你应该使用**kwargs

例子

In [1]:
def test_var_args(f_arg, *argv):
    print("first normal arg:", f_arg)
    for arg in argv:
        print("another arg through *argv:", arg)

test_var_args('yasoob', 'python', 'eggs', 'test')
first normal arg: yasoob
another arg through *argv: python
another arg through *argv: eggs
another arg through *argv: test
In [7]:
def greet_me(**kwargs):
    for key, value in kwargs.items():
        print("{0} == {1}".format(key, value))
        
greet_me(name='niuhe',gender='male')
gender == male
name == niuhe

生成器Generator

首先我们要理解迭代器(iterators)。根据维基百科,迭代器是一个让程序员可以遍历一个容器(特别是列表)的对象。
其中有三个概念:

  • 可迭代对象iterable
  • 迭代器itertool
  • 迭代iteration

具体解释如下:

  1. Python中任意的对象,只要它定义了可以返回一个迭代器的iter方法,或者定义了可以支持下标索引的getitem方法(这些双下划线方法会在其他章节中全面解释),那么它就是一个可迭代对象。简单说,可迭代对象就是能提供迭代器的任意对象。
  2. 任意对象,只要定义了next(Python2) 或者next方法,它就是一个迭代器。就这么简单。现在我们来理解迭代(iteration)
  3. 用简单的话讲,它就是从某个地方(比如一个列表)取出一个元素的过程。当我们使用一个循环来遍历某个东西时,这个过程本身就叫迭代。

生成器解释:

  1. 概念:生成器也是一种迭代器,但是你只能对其迭代一次。这是因为它们并没有把所有的值存在内存中,而是在运行时生成值。你通过遍历来使用它们,要么用一个“for”循环,要么将它们传递给任意可以进行迭代的函数和结构。大多数时候生成器是以函数来实现的。然而,它们并不返回一个值,而是yield一个值。
  2. 应用场景:你不想同一时间将所有计算出来的大量结果集分配到内存当中
In [8]:
def generator_function():
    for i in range(3):
        yield i

gen = generator_function()
print(next(gen))
print(next(gen))
print(next(gen))
print(next(gen))
# 迭代完会抛出StopIteration异常,使用for进行迭代时候,会自动捕获并处理这个异常
0
1
2
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
 in ()
     10 print(next(gen))
     11 # Output: 2
---> 12 print(next(gen))

StopIteration: 

str是一个可迭代对象,而不是一个迭代器。这意味着它支持迭代,但我们不能直接对其进行迭代操作。
可以使用内置函数iter。它将根据一个可迭代对象返回一个迭代器对象。这里是我们如何使用它:

In [9]:
# Python中所有对象都是可迭代对象,并不一定是迭代器
my_string = "Yasoob"
next(my_string)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
      1 my_string = "Yasoob"
----> 2 next(my_string)

TypeError: 'str' object is not an iterator
In [13]:
my_string = "Yasoob"
my_iter = iter(my_string)
print(next(my_iter))
print(next(my_iter))
Y
a

map,filter,reduce (略)

set集合

set(集合)是一个非常有用的数据结构。它与列表(list)的行为类似,区别在于set不能包含重复的值。

下面例子显示出列表中的重复元素的集合set

In [43]:
some_list = ['a', 'b', 'c', 'b', 'd', 'm', 'n', 'n']
duplicates = set([x for x in some_list if some_list.count(x) > 1])
print(duplicates)
{'b', 'n'}

三元运算符

三元运算符通常在Python里被称为条件表达式,这些表达式基于真(true)/假(false)的条件判断

例子

In [15]:
is_fat = True
state = "fat" if is_fat else "not fat"
print(state)
fat

装饰器Decorators

装饰器(Decorators)是Python的一个重要部分。简单地说:他们是修改其他函数的功能的函数。他们有助于让我们的代码更简短,也更Pythonic。
几个基础概念:

  • Python中一切皆为对象
  • Python可以在函数中定义函数
  • Python可以从函数中返回函数
  • Python可以将参数传递给另一个函数

现在你已经具备所有必需知识,来进一步学习装饰器真正是什么了。装饰器让你在一个函数的前后去执行代码。(略)

例子

In [17]:
from functools import wraps

def logit(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logit
def addition_func(x):
   """Do some math."""
   return x + x


result = addition_func(4)
print(result)
addition_func was called
8

Global和Return

尽量避免使用global定义全局变量,这会让程序变得复杂。

In [21]:
# 这种写法很糟糕
def profile():
    global name
    global age
    name = "Danny"
    age = 30

profile()
print(name)
# Output: Danny

print(age)
Danny
30
In [20]:
def profile():
    name = "Danny"
    age = 30
    return name, age

print(profile())
('Danny', 30)

对象变动Mutation

  • mutable意味着可以被改动
  • immutable意味着不可被改动为常量constant

例子

In [23]:
def add_to(num, target=[]):
    target.append(num)
    return target

print(add_to(1))
# Output: [1]
print(add_to(2))
# Output: [1, 2]
print(add_to(3))
# Output: [1, 2, 3]
[1]
[1, 2]
[1, 2, 3]

你可能预期它表现的不是这样子。你可能希望,当你调用add_to时,有一个新的列表被创建。

没有达到预期是列表的可变性在作怪。在Python中当函数被定义时,默认参数只会运算一次,
(target=[]只在定义时执行一次)
而不是每次被调用时都会重新运算。你应该永远不要定义可变类型的默认参数,除非你知道你正在做什么。
你应该像这样做:

In [24]:
# 现在每当你在调用这个函数不传入target参数的时候,一个新的列表会被创建
def add_to(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target

print(add_to(42))
# Output: [42]

print(add_to(42))
# Output: [42]

print(add_to(42))
[42]
[42]
[42]

slots魔法

在Python中,每个类都有实例属性。默认情况下Python用一个字典来保存一个对象的实例属性。这非常有用,因为它允许我们在运行时去设置任意的新属性。
然而,对于有着已知属性的小类来说,它可能是个瓶颈。这个字典浪费了很多内存。Python不能在对象创建时直接分配一个固定量的内存来保存所有的属性。因此如果你创建许多对象(我指的是成千上万个),它会消耗掉很多内存。
不过还是有一个方法来规避这个问题。这个方法需要使用slots来告诉Python不要使用字典,而且只给一个固定集合的属性分配空间。

In [1]:
# 此模块展示ipython模式下每行代码内存占用
import ipython_memory_usage.ipython_memory_usage as imu

num = 204800

imu.start_watching_memory()

class Slot_Class(object):
  __slots__ = ['name', 'identifier']
  def __init__(self, name, identifier):
      self.name = name
      self.identifier = identifier

print('Slot_Class Memery Cost:')
x = [Slot_Class(i,i) for i in range(num)]
Slot_Class Memery Cost:
In [1] used 19.5625 MiB RAM in 0.58s, peaked 0.00 MiB above current, total RAM usage 69.97 MiB
In [1]:
import ipython_memory_usage.ipython_memory_usage as imu

num = 204800

imu.start_watching_memory()

class NoSlot_Class(object):
  def __init__(self, name, identifier):
      self.name = name
      self.identifier = identifier
    
print('NoSlot_Class Memery Cost:')
y = [NoSlot_Class(i,i) for i in range(num)]
NoSlot_Class Memery Cost:
In [1] used 40.8945 MiB RAM in 0.86s, peaked 0.00 MiB above current, total RAM usage 91.25 MiB

可以观察到,使用slots的实例对象占用19.56M内存,而不使用slot而用字典存储实例属性的时候,占用内存40.89M。
可见,slots可以节省一半的内存,同时可以缩减运行时间。

虚拟环境

现在已经开始流行pipenv,来取代virtualenv

$ pip install virtualenv

容器Collections

Python附带一个模块,它包含许多容器数据类型,名字叫作collections。我们将讨论它的作用和用法。 我们将讨论的是:

  • defaultdict
  • counter
  • deque
  • namedtuple
  • enum.Enum (包含在Python 3.4以上)

defaultdict

defaultdict与dict类型不同,你不需要检查key是否存在。difaultdict还有其他很多用处。

In [5]:
from collections import defaultdict


colours = (
    ('Yasoob', 'Yellow'),
    ('Ali', 'Blue'),
    ('Arham', 'Green'),
    ('Ali', 'Black'),
    ('Yasoob', 'Red'),
    ('Ahmed', 'Silver'),
)

favourite_colours = defaultdict(list)

for name, colour in colours:
    favourite_colours[name].append(colour)

print(favourite_colours)
defaultdict(, {'Ali': ['Blue', 'Black'], 'Yasoob': ['Yellow', 'Red'], 'Ahmed': ['Silver'], 'Arham': ['Green']})

另一种重要的是例子就是:当你在一个字典中对一个键进行嵌套赋值时,如果这个键不存在,会触发keyError异常。 defaultdict允许我们用一个聪明的方式绕过这个问题。 首先我分享一个使用dict触发KeyError的例子,然后提供一个使用defaultdict的解决方案。

In [6]:
some_dict = {}
some_dict['colours']['favourite'] = "yellow"
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
 in ()
      1 some_dict = {}
----> 2 some_dict['colours']['favourite'] = "yellow"

KeyError: 'colours'

解决方案:

In [7]:
import collections
import json

tree = lambda: collections.defaultdict(tree)
some_dict = tree()
some_dict['colours']['favourite'] = "yellow"

print(json.dumps(some_dict))
{"colours": {"favourite": "yellow"}}

counter计数器

counter计数器可以帮助我们针对某项数据进行计数

In [8]:
from collections import Counter

colours = (
    ('Yasoob', 'Yellow'),
    ('Ali', 'Blue'),
    ('Arham', 'Green'),
    ('Ali', 'Black'),
    ('Yasoob', 'Red'),
    ('Ahmed', 'Silver'),
)

favs = Counter(name for name, colour in colours)
print(favs)
Counter({'Ali': 2, 'Yasoob': 2, 'Ahmed': 1, 'Arham': 1})

deque双端队列

deque提供了一个双端队列,你可以从头/尾两端添加或删除元素。
它的用法就像python的list,并且提供了类似的方法。

In [9]:
from collections import deque

d = deque()
d.append('1')
d.append('2')
d.append('3')
# 从左侧添加数据
d.appendleft('4')

print(d)
deque(['4', '1', '2', '3'])
In [13]:
# 创建双端队列
e = deque(range(5))
print(e)
# 从右侧取出数据
print(e.pop())
# 从左侧取出数据
print(e.popleft())

print(e)
deque([0, 1, 2, 3, 4])
4
0
deque([1, 2, 3])

我们也可以限制这个列表的大小,当超出你设定的限制时,数据会从对队列另一端被挤出去(pop)。

例子

In [21]:
f = deque(range(12),maxlen=10)

print(f)

f.extend([14,15,16])
print(f)
f.extendleft('ab')
print(f)
f.append(15)
print(f)
deque([2, 3, 4, 5, 6, 7, 8, 9, 10, 11], maxlen=10)
deque([5, 6, 7, 8, 9, 10, 11, 14, 15, 16], maxlen=10)
deque(['b', 'a', 5, 6, 7, 8, 9, 10, 11, 14], maxlen=10)
deque(['a', 5, 6, 7, 8, 9, 10, 11, 14, 15], maxlen=10)

namedtuple

一个元组是一个不可变immutable的列表 你可以存储一个数据的序列,它和命名元组(namedtuples)非常像,但有几个关键的不同。 主要相似点是都不像列表,你不能修改元组中的数据。为了获取元组中的数据,你需要使用整数作为索引:

man = ('Ali', 30)
print(man[0])
# output:'Ali'

嗯,那namedtuples是什么呢?它把元组变成一个针对简单任务的容器。你不必使用整数索引来访问一个namedtuples的数据。你可以像字典(dict)一样访问namedtuples,但namedtuples是不可变的。

In [23]:
from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="perry", age=31, type="cat")

print(perry)

print(perry.name)
Animal(name='perry', age=31, type='cat')
perry

分析:

一个命名元组(namedtuple)有两个必需的参数。它们是元组名称和字段名称。
在上面的例子中,我们的元组名称是Animal,字段名称是'name','age'和'type'。

namedtuple让你的元组变得自文档了。你只要看一眼就很容易理解代码是做什么的。 你也不必使用整数索引来访问一个命名元组,这让你的代码更易于维护。 而且,namedtuple的每个实例没有对象字典,所以它们很轻量,与普通的元组比,并不需要更多的内存。这使得它们比字典更快。

但是,要记住它是一个元组,属性值在namedtuple中是不可变的,

此外,你可以将namedtuple转换为字典:

In [24]:
from collections import namedtuple

Animal = namedtuple('Animal', 'name age type')
perry = Animal(name="Perry", age=31, type="cat")
print(perry._asdict())
OrderedDict([('name', 'Perry'), ('age', 31), ('type', 'cat')])

enum.Enum枚举对象

另一个有用的容器是枚举对象,它属于enum模块

enumerate枚举

它允许我们遍历数据并自动计数

In [25]:
my_list = ['apple', 'banana', 'grapes', 'pear']
# 1定义了从1开始计数,默认是0
for c, value in enumerate(my_list, 1):
    print(c, value)
1 apple
2 banana
3 grapes
4 pear
In [26]:
# enumerate()创建包含索引的元组列表
my_list = ['apple', 'banana', 'grapes', 'pear']
counter_list = list(enumerate(my_list, 1))
print(counter_list)
[(1, 'apple'), (2, 'banana'), (3, 'grapes'), (4, 'pear')]

introspection自省

对象自省是指在运行时来判断一个对象的类型的能力。它是Python的强项之一。Python中所有一切都是一个对象,而且我们可以仔细勘察那些对象。

dir

它是用于自省的最重要的函数之一。它返回一个列表,列出了一个对象所拥有的属性和方法。这里是一个例子:

如果我们运行dir()而不传入参数,那么它会返回当前作用域的所有名字。

In [28]:
my_list = [1, 2, 3]
print(dir(my_list))
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

type和id

  • type函数返回一个对象的类型
  • id()函数返回任意不同种类对象的唯一ID
In [29]:
print(type(''))

print(type([]))

print(type({}))

print(type(dict))

print(type(3))

name = "Yasoob"
print(id(name))





139828854281808

ispect模块

inspect模块也提供了许多有用的函数,来获取活跃对象的信息。比方说,你可以查看一个对象的成员,只需运行:

In [37]:
import inspect
print('%.100s...' % inspect.getmembers(str))
[('__add__', ), ('__class__', ), ('__contains...

comprehensions推导式

推导式是可以从一个数据序列构建另一个新的数据序列的结构体。 共有三种推导,在Python2和3中都有支持:

  • 列表(list)推导式
  • 字典(dict)推导式
  • 集合(set)推导式
In [38]:
multiples = [i for i in range(30) if i % 3 is 0]
print(multiples)
[0, 3, 6, 9, 12, 15, 18, 21, 24, 27]
In [41]:
mcase = {'a': 10, 'b': 34, 'A': 7, 'Z': 3}
# 将key和value位置互换
print({v:k for k,v in mcase.items()})
{34: 'b', 3: 'Z', 10: 'a', 7: 'A'}

集合推导式(set comprehensions)
跟列表推导式也是类似的。 唯一的区别在于它们使用大括号{}

In [44]:
squared = {x**2 for x in [1, 1, 2]}
print(squared)
{1, 4}

异常

  • 处理多个异常
  • finally语句
  • try/else语句
# 处理多个异常
try:
    file = open('test.txt', 'rb')
except (IOError, EOFError) as e:
    print("An error occurred. {}".format(e.args[-1]))
In [46]:
try:
    print('I am sure no exception is going to occur!')
except Exception:
    print('exception')
else:
    # 这里的代码只会在try语句里没有触发异常时运行,
    # 但是这里的异常将 *不会* 被捕获
    print('This would only run if no exception occurs. And an error here '
          'would NOT be caught.')
finally:
    print('This would be printed in every case.')
I am sure no exception is going to occur!
This would only run if no exception occurs. And an error here would NOT be caught.
This would be printed in every case.

else从句只会在没有异常的情况下执行,而且它会在finally语句之前执行。

lambda表达式

lambda表达式是一行函数。 它们在其他语言中也被称为匿名函数。如果你不想在程序中对一个函数使用两次,你也许会想用lambda表达式,它们和普通的函数完全一样。

In [47]:
# 例子1
add = lambda x, y: x + y
print(add(3, 5))
8
In [49]:
# 例子2
a = [(1, 2), (4, 1), (9, 10), (13, -3)]
a.sort(key=lambda x: x[1])
print(a)
[(13, -3), (4, 1), (1, 2), (9, 10)]

一行式

将给出一些非常有用的一些一行式的Python命令

简易web server

可以通过网络快速共享文件,或者快速预览静态web项目

In [ ]:
# Python 2
python -m SimpleHTTPServer

# Python 3
python -m http.server

漂亮的打印

你可以在Python REPL漂亮的打印出列表和字典

In [50]:
from pprint import pprint

my_dict = {'name': 'Yasoob', 'age': 'undefined', 'personality': 'awesome'}
pprint(my_dict)
{'age': 'undefined', 'name': 'Yasoob', 'personality': 'awesome'}

这种方法在字典上更为有效。此外,如果你想快速漂亮的从文件打印出json数据,那么你可以这么做:

cat file.json | python -m json.tool

脚本性能分析

这可能在定位你的脚本中的性能瓶颈时,会非常奏效:

python -m cProfile my_script.py

备注:cProfile是一个比profile更快的实现,因为它是用c写的

CSV转换为json

python -c "import csv,json;print json.dumps(list(csv.reader(open('csv_file.csv'))))"

列表展开为一维

您可以通过使用itertools包中的itertools.chain.from_iterable轻松快速的辗平一个列表。

In [53]:
import itertools

a_list = [[1, 2], [3, 4], [5, 6]]
print(list(itertools.chain.from_iterable(a_list)))
# another way
print(list(itertools.chain(*a_list)))
[1, 2, 3, 4, 5, 6]
[1, 2, 3, 4, 5, 6]

一行式的构造器

避免类初始化时大量重复的赋值语句

In [63]:
class A(object):
    def __init__(self, a, b, c, d, e, f):
        self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})

a = A(1,2,3,4,5,6)
print(a.__dict__)
{'b': 2, 'f': 6, 'a': 1, 'd': 4, 'c': 3, 'e': 5}

for-else语句

for循环还有一个else从句,我们大多数人并不熟悉。这个else从句会在循环正常结束时执行。这意味着,循环没有遇到任何break. 一旦你掌握了何时何地使用它,它真的会非常有用。

In [64]:
for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, 'equals', x, '*', n / x)
            break
    else:
        # loop fell through without finding a factor
        print(n, 'is a prime number')
2 is a prime number
3 is a prime number
4 equals 2 * 2.0
5 is a prime number
6 equals 2 * 3.0
7 is a prime number
8 equals 2 * 4.0
9 equals 3 * 3.0
/usr/local/lib/python3.5/dist-packages/ipython_memory_usage/ipython_memory_usage.py SOMETHING WEIRD HAPPENED AND THIS RAN FOR TOO LONG, THIS THREAD IS KILLING ITSELF

使用C扩展

使用C扩展可以极大的提高python程序的性能,常见的使用C扩展的原因:

  • 你要提升代码的运行速度,而且你知道C要比Python快50倍以上
  • C语言中有很多传统类库,而且有些正是你想要的,但你又不想用Python去重写它们
  • 想对从内存到文件接口这样的底层资源进行访问
  • 不需要理由,就是想这样做

开发者有三种方法可以在自己的Python代码中来调用C编写的函数:

  • ctypes
  • SWIG
  • Python/C API
  • Cpython

Ctypes

Python中的ctypes模块可能是Python调用C方法中最简单的一种。ctypes模块提供了和C语言兼容的数据类型和函数来加载dll文件,因此在调用时不需对源文件做任何的修改。也正是如此奠定了这种方法的简单性。 接下来将C文件编译为.so文件(windows下为DLL)。

具体使用略

SWIG

SWIG是Simplified Wrapper and Interface Generator的缩写。是Python中调用C代码的另一种方法。在这个方法中,开发人员必须编写一个额外的接口文件来作为SWIG(终端工具)的入口。
Python开发者一般不会采用这种方法,因为大多数情况它会带来不必要的复杂。而当你有一个C/C++代码库需要被多种语言调用时,这将是个非常不错的选择。

Cpython

Python调用C代码的另一种方式便是使用Cython让Python编译的更快。但是Cython和传统的Python比起来可以将它理解为另一种语言,所以我们就不在这里过多描述了。

Python/C API

Python/C API可能是被最广泛使用的方法。它不仅简单,而且可以在C代码中操作你的Python对象。

这种方法需要以特定的方式来编写C代码以供Python去调用它。所有的Python对象都被表示为一种叫做PyObject的结构体,并且Python.h头文件中提供了各种操作它的函数。大部分对Python原生对象的基础函数和操作在Python.h头文件中都能找到。

注:这种方法很重要,但是我目前的学习阶段用不到,以后用到时候再回头看这部分内容

协程

Python中的协程和生成器很相似但又稍有不同。主要区别在于:

  • 生成器是数据的生产者
  • 协程则是数据的消费者
In [4]:
# 生成器generator用例
def fib():
    a, b = 0, 1
    for i in range(8):
        yield a
        a, b = b, a+b
            
for i in fib():
    print(i)
0
1
1
2
3
5
8
13

这样做不仅快而且不会给内存带来压力,因为我们所需要的值都是动态生成的而不是将他们存储在一个列表中。更概括的说如果现在我们在上面的例子中使用yield便可获得了一个协程。协程会消费掉发送给它的值。Python实现的grep就是个很好的例子:

In [6]:
def grep(pattern):
    print("Searching for", pattern)
    while True:
        line = (yield)
        if pattern in line:
            print(line)
            
            
search = grep('coroutine')
next(search)
#output: Searching for coroutine
search.send("I love you")
search.send("I love coroutine instead!")
search.send("Don't you love me?")
Searching for coroutine
I love coroutine instead!

发送的值会被yield接收。我们为什么要运行next()方法呢?这样做正是为了启动一个协程。就像协程中包含的生成器并不是立刻执行,而是通过next()方法来响应send()方法。因此,你必须通过next()方法来执行yield表达式。
我们可以通过调用close()方法来关闭一个协程。像这样:

In [7]:
search = grep('coroutine')
search.close()

Function caching函数缓存

函数缓存允许我们将一个函数对于给定参数的返回值缓存起来。
当一个I/O密集的函数被频繁使用相同的参数调用的时候,函数缓存可以节约时间。

我们来实现一个斐波那契计算器,并使用lru_cache。

In [8]:
from functools import lru_cache

@lru_cache(maxsize=32)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

print([fib(n) for n in range(10)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

那个maxsize参数是告诉lru_cache,最多缓存最近多少个返回值。 我们也可以轻松地对返回值清空缓存,通过这样:

In [9]:
fib.cache_clear()

Context managers上下文管理器

上下文管理器允许你在有需要的时候,精确地分配和释放资源。

上下文管理器的一个常见用例,是资源的加锁和解锁,以及关闭已打开的文件(就像我已经展示给你看的)。
使用上下文管理器最广泛的案例就是with语句了。 想象下你有两个需要结对执行的相关操作,然后还要在它们中间放置一段代码。 上下文管理器就是专门让你做这种事情的。举个例子:

with open('some_file', 'w') as opened_file:
    opened_file.write('Hola!')

基于类的实现

一个上下文管理器的类,最起码要定义enterexit方法。

In [10]:
class File(object):
    def __init__(self, file_name, method):
        self.file_obj = open(file_name, method)
    def __enter__(self):
        return self.file_obj
    def __exit__(self, type, value, traceback):
        self.file_obj.close()

通过定义enterexit方法,我们可以在with语句里使用它。我们来试试:

with File('demo.txt', 'w') as opened_file:
    opened_file.write('Hola!')

我们的__exit__函数接受三个参数。这些参数对于每个上下文管理器类中的exit方法都是必须的。我们来谈谈在底层都发生了什么。

with语句先暂存了File类的__exit__方法
然后它调用File类的__enter__方法
__enter__方法打开文件并返回给with语句
打开的文件句柄被传递给opened_file参数
我们使用.write()来写文件
with语句调用之前暂存的__exit__方法
__exit__方法关闭了文件

处理异常

如果发生异常,Python会将异常的type,value和traceback传递给exit方法。 它让exit方法来决定如何关闭文件以及是否需要其他步骤。
举个例子:

with File('demo.txt', 'w') as opened_file:
    opened_file.undefined_function('Hola!')

我们来列一下,当异常发生时,with语句会采取哪些步骤。

  1. 它把异常的type,value和traceback传递给__exit__方法
  2. 它让__exit__方法来处理异常
  3. 如果__exit__返回的是True,那么这个异常就被优雅地处理了。
  4. 如果__exit__返回的是True以外的任何东西,那么这个异常将被with语句抛出。

我们试着在__exit__方法中处理异常

In [12]:
class File(object):
    def __init__(self, file_name, method):
        self.file_obj = open(file_name, method)
    def __enter__(self):
        return self.file_obj
    def __exit__(self, type, value, traceback):
        print("Exception has been handled")
        print(type,value,traceback)
        self.file_obj.close()
        return True

with File('demo.txt', 'w') as opened_file:
    opened_file.undefined_function()
Exception has been handled
 '_io.TextIOWrapper' object has no attribute 'undefined_function' 

我们的exit方法返回了True,因此没有异常会被with语句抛出。

基于生成器的实现

我们还可以用装饰器(decorators)和生成器(generators)来实现上下文管理器。 Python有个contextlib模块专门用于这个目的。我们可以使用一个生成器函数来实现一个上下文管理器,而不是使用一个类。
< 细节略 >

In [13]:
# 简单用法
from contextlib import contextmanager

@contextmanager
def open_file(name):
    f = open(name, 'w')
    yield f
    f.close()

with open_file('some_file') as f:
    f.write('hola!')

The END

Comments