diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..28ec9fe --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.md linguist-language=Python diff --git a/.gitignore b/.gitignore index 2ad78eb..7cc593b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.py[cod] .Ulysses* _book +node_modules/* diff --git a/Advanced-Features/README.md b/Advanced-Features/README.md index e4b9e2f..79a5aee 100644 --- a/Advanced-Features/README.md +++ b/Advanced-Features/README.md @@ -1,2 +1,9 @@ # 高级特性 +本章主要介绍: + +- [迭代器](./iterator.md) +- [生成器](./generator.md) +- [上下文管理器](./context.md) + + diff --git a/Advanced-Features/context.md b/Advanced-Features/context.md index 1c3b538..6807d9c 100644 --- a/Advanced-Features/context.md +++ b/Advanced-Features/context.md @@ -1,2 +1,177 @@ # 上下文管理器 +什么是上下文?其实我们可以简单地把它理解成环境。从一篇文章中抽出一句话,让你来理解,我们会说这是断章取义。为什么?因为我们压根就没考虑到这句话的上下文是什么。编程中的上下文也与此类似,比如『进程上下文』,指的是一个进程在执行的时候,CPU 的所有寄存器中的值、进程的状态以及堆栈上的内容等,当系统需要切换到其他进程时,系统会保留当前进程的上下文,也就是运行时的环境,以便再次执行该进程。 + +迭代器有[迭代器协议(Iterator Protocol)](./iterator.md),上下文管理器(Context manager)也有上下文管理协议(Context Management Protocol)。 + + - **上下文管理器协议**,是指要实现对象的 `__enter__()` 和 `__exit__()` 方法。 + - **上下文管理器**也就是支持上下文管理器协议的对象,也就是实现了 `__enter__()` 和 `__exit__()` 方法。 + +这里先构造一个简单的上下文管理器的例子,以理解 `__enter__()` 和 `__exit__()` 方法。 + +```python +from math import sqrt, pow + +class Point(object): + def __init__(self, x, y): + print 'initialize x and y' + self.x, self.y = x, y + + def __enter__(self): + print "Entering context" + return self + + def __exit__(self, type, value, traceback): + print "Exiting context" + + def get_distance(self): + distance = sqrt(pow(self.x, 2) + pow(self.y, 2)) + return distance +``` + +上面的代码定义了一个 `Point` 类,并实现了 `__enter__()` 和 `__exit__()` 方法,我们还定义了 `get_distance` 方法,用于返回点到原点的距离。 + +通常,我们使用 `with` 语句调用上下文管理器: + +```python +with Point(3, 4) as pt: + print 'distance: ', pt.get_distance() + +# output +initialize x and y # 调用了 __init__ 方法 +Entering context # 调用了 __enter__ 方法 +distance: 5.0 # 调用了 get_distance 方法 +Exiting context # 调用了 __exit__ 方法 +``` + +上面的 `with` 语句执行过程如下: + +- Point(3, 4) 生成了一个上下文管理器; +- 调用上下文管理器的 `__enter__()` 方法,并将 `__enter__()` 方法的返回值赋给 as 字句中的变量 pt; +- 执行**语句体**(指 with 语句包裹起来的代码块)内容,输出 distance; +- 不管执行过程中是否发生异常,都执行上下文管理器的 `__exit__()` 方法。`__exit__()` 方法负责执行『清理』工作,如释放资源,关闭文件等。如果执行过程没有出现异常,或者语句体中执行了语句 break/continue/return,则以 None 作为参数调用 `__exit__(None, None, None) `;如果执行过程中出现异常,则使用 `sys.exc_info` 得到的异常信息为参数调用 `__exit__(exc_type, exc_value, exc_traceback)`; +- 出现异常时,如果 `__exit__(type, value, traceback)` 返回 False 或 None,则会重新抛出异常,让 with 之外的语句逻辑来处理异常;如果返回 True,则忽略异常,不再对异常进行处理; + +上面的 with 语句执行过程没有出现异常,我们再来看出现异常的情形: + +```python +with Point(3, 4) as pt: + pt.get_length() # 访问了对象不存在的方法 + +# output +initialize x and y +Entering context +Exiting context +--------------------------------------------------------------------------- +AttributeError Traceback (most recent call last) + in () + 1 with Point(3, 4) as pt: +----> 2 pt.get_length() + +AttributeError: 'Point' object has no attribute 'get_length' +``` + +在我们的例子中,`__exit__` 方法返回的是 None(如果没有 return 语句那么方法会返回 None)。因此,with 语句抛出了那个异常。我们对 `__exit__` 方法做一些改动,让它返回 True。 + +```python +from math import sqrt, pow + +class Point(object): + def __init__(self, x, y): + print 'initialize x and y' + self.x, self.y = x, y + + def __enter__(self): + print "Entering context" + return self + + def __exit__(self, type, value, traceback): + print "Exception has been handled" + print "Exiting context" + return True + + def get_distance(self): + distance = sqrt(pow(self.x, 2) + pow(self.y,2 )) + return distance + +with Point(3, 4) as pt: + pt.get_length() # 访问了对象不存在的方法 + +# output +initialize x and y +Entering context +Exception has been handled +Exiting context +``` + +可以看到,由于 `__exit__` 方法返回了 True,因此没有异常会被 with 语句抛出。 + +# 内建对象使用 with 语句 + +除了自定义上下文管理器,Python 中也提供了一些内置对象,可直接用于 with 语句中,比如最常见的文件操作。 + +传统的文件操作经常使用 `try/finally` 的方式,比如: + +```python +file = open('somefile', 'r') +try: + for line in file: + print line +finally: + file.close() # 确保关闭文件 +``` + +将上面的代码改用 with 语句: + +```python +with open('somefile', 'r') as file: + for line in file: + print line +``` + +可以看到,通过使用 with,代码变得很简洁,而且即使处理过程发生异常,with 语句也会确保我们的文件被关闭。 + +# contextlib 模块 + +除了在类中定义 `__enter__` 和 `__exit__` 方法来实现上下文管理器,我们还可以通过生成器函数(也就是带有 yield 的函数)结合装饰器来实现上下文管理器,Python 中自带的 contextlib 模块就是做这个的。 + +contextlib 模块提供了三个对象:装饰器 contextmanager、函数 nested 和上下文管理器 closing。其中,contextmanager 是一个装饰器,用于装饰生成器函数,并返回一个上下文管理器。需要注意的是,被装饰的生成器函数只能产生一个值,否则会产生 RuntimeError 异常。 + +下面我们看一个简单的例子: + +```python +from contextlib import contextmanager + +@contextmanager +def point(x, y): + print 'before yield' + yield x * x + y * y + print 'after yield' + +with point(3, 4) as value: + print 'value is: %s' % value + +# output +before yield +value is: 25 +after yield +``` + +可以看到,yield 产生的值赋给了 as 子句中的 value 变量。 + +另外,需要强调的是,虽然通过使用 contextmanager 装饰器,我们可以不必再编写 `__enter__` 和 `__exit__` 方法,但是『获取』和『清理』资源的操作仍需要我们自己编写:『获取』资源的操作定义在 yield 语句之前,『释放』资源的操作定义在 yield 语句之后。 + +# 小结 + +- 上下文管理器是支持上下文管理协议的对象,也就是实现了 `__enter__` 和 `__exit__` 方法。 +- 通常,我们使用 `with` 语句调用上下文管理器。with 语句尤其适用于对资源进行访问的场景,确保执行过程中出现异常情况时也可以对资源进行回收,比如自动关闭文件等。 +- `__enter__` 方法在 with 语句体执行前调用,with 语句将该方法的返回值赋给 as 字句中的变量,如果有 as 字句的话。 +- `__exit__` 方法在退出`运行时上下文`时被调用,它负责执行『清理』工作,比如关闭文件,释放资源等。如果退出时没有发生异常,则 `__exit__` 的三个参数,即 type, value 和 traceback 都为 None。如果发生异常,返回 True 表示不处理异常,否则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。 + +# 参考资料 + +- [编程中什么是「Context(上下文)」? - 知乎](https://www.zhihu.com/question/26387327) +- [浅谈 Python 的 with 语句](https://www.ibm.com/developerworks/cn/opensource/os-cn-pythonwith/) +- [上下文管理器 · Python进阶](https://eastlakeside.gitbooks.io/interpy-zh/content/context_managers/) + + diff --git a/Advanced-Features/generator.md b/Advanced-Features/generator.md index 47aae31..9e330da 100644 --- a/Advanced-Features/generator.md +++ b/Advanced-Features/generator.md @@ -1,2 +1,237 @@ -# 生成器 +生成器 +==== + +生成器(generator)也是一种迭代器,在每次迭代时返回一个值,直到抛出 `StopIteration` 异常。它有两种构造方式: + +- 生成器表达式 + +和列表推导式的定义类似,生成器表达式使用 `()` 而不是 `[]`,比如: + +``` +numbers = (x for x in range(5)) # 注意是(),而不是[] +for num in numbers: + print num +``` + +- 生成器函数 + +含有 `yield` 关键字的函数,调用该函数时会返回一个生成器。 + +本文主要讲**生成器函数**。 + +# 生成器函数 + +先来看一个简单的例子: + +```python +>>> def generator_function(): +... print 'hello 1' +... yield 1 +... print 'hello 2' +... yield 2 +... print 'hello 3' +>>> +>>> g = generator_function() # 函数没有立即执行,而是返回了一个生成器,当然也是一个迭代器 +>>> g.next() # 当使用 next() 方法,或使用 next(g) 的时候开始执行,遇到 yield 暂停 +hello 1 +1 +>>> g.next() # 从原来暂停的地方继续执行 +hello 2 +2 +>>> g.next() # 从原来暂停的地方继续执行,没有 yield,抛出异常 +hello 3 +Traceback (most recent call last): + File "", line 1, in +StopIteration +``` + +可以看到,上面的函数没有使用 `return` 语句返回值,而是使用了 `yield`『生出』一个值。一个带有 `yield` 的函数就是一个生成器函数,当我们使用 `yield` 时,它帮我们自动创建了 `__iter__()` 和 `next()` 方法,而且在没有数据时,也会抛出 `StopIteration` 异常,也就是我们不费吹灰之力就获得了一个迭代器,非常简洁和高效。 + +带有 `yield` 的函数执行过程比较特别: + +- 调用该函数的时候不会立即执行代码,而是返回了一个生成器对象; +- 当使用 `next()` (在 for 循环中会自动调用 `next()`) 作用于返回的生成器对象时,函数开始执行,在遇到 `yield` 的时候会『暂停』,并返回当前的迭代值; +- 当再次使用 `next()` 的时候,函数会从原来『暂停』的地方继续执行,直到遇到 `yield` 语句,如果没有 `yield` 语句,则抛出异常; + +整个过程看起来就是不断地 `执行->中断->执行->中断` 的过程。一开始,调用生成器函数的时候,函数不会立即执行,而是返回一个生成器对象;然后,当我们使用 `next()` 作用于它的时候,它开始执行,遇到 `yield` 语句的时候,执行被中断,并返回当前的迭代值,要注意的是,此刻会记住中断的位置和所有的变量值,也就是执行时的上下文环境被保留起来;当再次使用 `next()` 的时候,从原来中断的地方继续执行,直至遇到 `yield`,如果没有 `yield`,则抛出异常。 + +**简而言之,就是 `next` 使函数执行,`yield` 使函数暂停。** + +# 例子 + +看一个 Fibonacci 数列的例子,如果使用自定义迭代器的方法,是这样的: + +```python +>>> class Fib(object): +... def __init__(self): +... self.a, self.b = 0, 1 +... def __iter__(self): +... return self +... def next(self): +... self.a, self.b = self.b, self.a + self.b +... return self.a +... +>>> f = Fib() +>>> for item in f: +... if item > 10: +... break +... print item +... +1 +1 +2 +3 +5 +8 +``` + +而使用生成器的方法,是这样的: + +```python +>>> def fib(): +... a, b = 0, 1 +... while True: +... a, b = b, a + b +... yield a +... +>>> f = fib() +>>> for item in f: +... if item > 10: +... break +... print item +... +1 +1 +2 +3 +5 +8 +``` + +可以看到,使用生成器的方法非常简洁,不用自定义 `__iter__()` 和 `next()` 方法。 + +另外,在处理大文件的时候,我们可能无法一次性将其载入内存,这时可以通过构造固定长度的缓冲区,来不断读取文件内容。有了 `yield`,我们就不用自己实现读文件的迭代器了,比如下面的实现: + +```python +def read_in_chunks(file_object, chunk_size=1024): + """Lazy function (generator) to read a file piece by piece. + Default chunk size: 1k.""" + while True: + data = file_object.read(chunk_size) + if not data: + break + yield data + +f = open('really_big_file.dat') +for piece in read_in_chunks(f): + process_data(piece) +``` + +# 进阶使用 + +我们除了能对生成器进行迭代使它返回值外,还能: + +- 使用 `send()` 方法给它发送消息; +- 使用 `throw()` 方法给它发送异常; +- 使用 `close()` 方法关闭生成器; + +## send() 方法 + +看一个简单的例子: + +``` +>>> def generator_function(): +... value1 = yield 0 +... print 'value1 is ', value1 +... value2 = yield 1 +... print 'value2 is ', value2 +... value3 = yield 2 +... print 'value3 is ', value3 +... +>>> g = generator_function() +>>> g.next() # 调用 next() 方法开始执行,返回 0 +0 +>>> g.send(2) +value1 is 2 +1 +>>> g.send(3) +value2 is 3 +2 +>>> g.send(4) +value3 is 4 +Traceback (most recent call last): + File "", line 1, in +StopIteration +``` + +在上面的代码中,我们先调用 `next()` 方法,使函数开始执行,代码执行到 `yield 0` 的时候暂停,返回了 0;接着,我们执行了 `send()` 方法,它会恢复生成器的运行,并将发送的值赋给上次中断时 yield 表达式的执行结果,也就是 value1,这时控制台打印出 value1 的值,并继续执行,直到遇到 yield 后暂停,此时返回 1;类似地,再次执行 `send()` 方法,将值赋给 value2。 + +简单地说,`send()` 方法就是 `next()` 的功能,加上传值给 `yield`。 + +## throw() 方法 + +除了可以给生成器传值,我们还可以给它传异常,比如: + +```python +>>> def generator_function(): +... try: +... yield 'Normal' +... except ValueError: +... yield 'Error' +... finally: +... print 'Finally' +... +>>> g = generator_function() +>>> g.next() +'Normal' +>>> g.throw(ValueError) +'Error' +>>> g.next() +Finally +Traceback (most recent call last): + File "", line 1, in +StopIteration +``` + +可以看到,`throw()` 方法向生成器函数传递了 `ValueError` 异常,此时代码进入 except ValueError 语句,遇到 yield 'Error',暂停并返回 Error 字符串。 + +简单的说,`throw()` 就是 `next()` 的功能,加上传异常给 `yield`。 + +## close() 方法 + +我们可以使用 `close()` 方法来关闭一个生成器。生成器被关闭后,再次调用 next() 方法,不管能否遇到 yield 关键字,都会抛出 StopIteration 异常,比如: + +```python +>>> def generator_function(): +... yield 1 +... yield 2 +... yield 3 +... +>>> g = generator_function() +>>> +>>> g.next() +1 +>>> g.close() # 关闭生成器 +>>> g.next() +Traceback (most recent call last): + File "", line 1, in +StopIteration +``` + +# 小结 + +- yield 把函数变成了一个生成器。 +- 生成器函数的执行过程看起来就是不断地 `执行->中断->执行->中断` 的过程。 + - 一开始,调用生成器函数的时候,函数不会立即执行,而是返回一个生成器对象; + - 然后,当我们使用 `next()` 作用于它的时候,它开始执行,遇到 `yield` 语句的时候,执行被中断,并返回当前的迭代值,要注意的是,此刻会记住中断的位置和所有的数据,也就是执行时的上下文环境被保留起来; + - 当再次使用 `next()` 的时候,从原来中断的地方继续执行,直至遇到 `yield`,如果没有 `yield`,则抛出异常。 + + +# 参考资料 + +- [Python yield 使用浅析](https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/) +- [谈谈Python的生成器 – 思诚之道](http://www.bjhee.com/python-yield.html) +- [Function vs Generator in Python](https://code-maven.com/function-vs-generator-in-python) +- [Lazy Method for Reading Big File in Python? - Stack Overflow](http://stackoverflow.com/questions/519633/lazy-method-for-reading-big-file-in-python) + diff --git a/Advanced-Features/iterator.md b/Advanced-Features/iterator.md index 86f8cba..2ea86a9 100644 --- a/Advanced-Features/iterator.md +++ b/Advanced-Features/iterator.md @@ -1,2 +1,214 @@ +迭代器 (Iterator) +==== + +# 迭代和可迭代 + +迭代器这个概念在很多语言中(比如 C++,Java)都是存在的,但是不同语言实现迭代器的方式各不相同。**在 Python 中,迭代器是指遵循迭代器协议(iterator protocol)的对象。**至于什么是迭代器协议,稍后自然会说明。为了更好地理解迭代器,我先介绍和迭代器相关的两个概念: + +- 迭代(Iteration) +- 可迭代对象(Iterable) + +你可能会觉得这是在玩文字游戏,但这确实是要搞清楚的。 + +> 当我们用一个循环(比如 for 循环)来遍历容器(比如列表,元组)中的元素时,这种遍历的过程就叫***迭代***。 + +在 Python 中,我们使用 `for...in...` 进行迭代。比如,遍历一个 list: + +```python +numbers = [1, 2, 3, 4] +for num in numbers: + print num +``` + +像上面这种可以使用 `for` 循环进行迭代的对象,就是可迭代对象,它的定义如下: + +> 含有 `__iter__()` 方法或 `__getitem__()` 方法的对象称之为***可迭代对象***。 + +我们可以使用 Python 内置的 `hasattr()` 函数来判断一个对象是不是可迭代的: + +```python +>>> hasattr((), '__iter__') +True +>>> hasattr([], '__iter__') +True +>>> hasattr({}, '__iter__') +True +>>> hasattr(123, '__iter__') +False +>>> hasattr('abc', '__iter__') +False +>>> hasattr('abc', '__getitem__') +True +``` + +另外,我们也可使用 `isinstance()` 进行判断: + +```python +>>> from collections import Iterable + +>>> isinstance((), Iterable) # 元组 +True +>>> isinstance([], Iterable) # 列表 +True +>>> isinstance({}, Iterable) # 字典 +True +>>> isinstance('abc', Iterable) # 字符串 +True +>>> isinstance(100, Iterable) # 数字 +False +``` + +可见,我们熟知的字典(dict)、元组(tuple)、集合(set)和字符串对象都是可迭代的。 + # 迭代器 +现在,让我们看看什么是迭代器(Iterator)。上文说过,**迭代器是指遵循迭代器协议(iterator protocol)的对象。**从这句话我们可以知道,迭代器是一个对象,但比较特别,它需要遵循迭代器协议,那什么是迭代器协议呢? + +> ***迭代器协议(iterator protocol)***是指要实现对象的 `__iter()__` 和 `next()` 方法(注意:Python3 要实现 `__next__()` 方法),其中,`__iter()__` 方法返回迭代器对象本身,`next()` 方法返回容器的下一个元素,在没有后续元素时抛出 `StopIteration` 异常。 + +接下来讲讲迭代器的例子,有什么常见的迭代器呢?列表是迭代器吗?字典是迭代器吗?我们使用 `hasattr()` 进行判断: + +``` +>>> hasattr((1, 2, 3), '__iter__') +True +>>> hasattr((1, 2, 3), 'next') # 有 __iter__ 方法但是没有 next 方法,不是迭代器 +False +>>> +>>> hasattr([1, 2, 3], '__iter__') +True +>>> hasattr([1, 2, 3], 'next') +False +>>> +>>> hasattr({'a': 1, 'b': 2}, '__iter__') +True +>>> hasattr({'a': 1, 'b': 2}, 'next') +False +``` + +同样,我们也可以使用 `isinstance()` 进行判断: + +``` +>>> from collections import Iterator +>>> +>>> isinstance((), Iterator) +False +>>> isinstance([], Iterator) +False +>>> isinstance({}, Iterator) +False +>>> isinstance('', Iterator) +False +>>> isinstance(123, Iterator) +False +``` + +可见,**虽然元组、列表和字典等对象是可迭代的,但它们却不是迭代器!**对于这些可迭代对象,可以使用 Python 内置的 `iter()` 函数获得它们的迭代器对象,看下面的使用: + +``` +>>> from collections import Iterator +>>> isinstance(iter([1, 2, 3]), Iterator) # 使用 iter() 函数,获得迭代器对象 +True +>>> isinstance(iter('abc'), Iterator) +True +>>> +>>> my_str = 'abc' +>>> next(my_str) # my_str 不是迭代器,不能使用 next(),因此出错 +--------------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in () +----> 1 next(my_str) + +TypeError: str object is not an iterator +>>> +>>> my_iter = iter(my_str) # 获得迭代器对象 +>>> isinstance(my_iter, Iterator) +True +>>> next(my_iter) # 可使用内置的 next() 函数获得下一个元素 +'a' +``` + +事实上,Python 的 `for` 循环就是先通过内置函数 `iter()` 获得一个迭代器,然后再不断调用 `next()` 函数实现的,比如: + +``` +for x in [1, 2, 3]: + print i +``` + +等价于 + +```python +# 获得 Iterator 对象 +it = iter([1, 2, 3]) + +# 循环 +while True: + try: + # 获得下一个值 + x = next(it) + print x + except StopIteration: + # 没有后续元素,退出循环 + break +``` + +# 斐波那契数列迭代器 + +现在,让我们来自定义一个迭代器:斐波那契(Fibonacci)数列迭代器。根据迭代器的定义,我们需要实现 `__iter()__` 和 `next()` 方法(在 Python3 中是 `__next__()` 方法)。先看代码: + +```python +# -*- coding: utf-8 -*- + +from collections import Iterator + +class Fib(object): + def __init__(self): + self.a, self.b = 0, 1 + + # 返回迭代器对象本身 + def __iter__(self): + return self + + # 返回容器下一个元素 + def next(self): + self.a, self.b = self.b, self.a + self.b + return self.a + +def main(): + fib = Fib() # fib 是一个迭代器 + print 'isinstance(fib, Iterator): ', isinstance(fib, Iterator) + + for i in fib: + if i > 10: + break + print i + +if __name__ == '__main__': + main() +``` + +在上面的代码中,我们定义了一个 Fib 类,用于生成 Fibonacci 数列。在类的实现中,我们定义了 `__iter__` 方法,它返回对象本身,这个方法会在遍历时被 Python 内置的 `iter()` 函数调用,返回一个迭代器。类中的 `next()` 方法用于返回容器的下一个元素,当使用 `for` 循环进行遍历的时候,就会使用 Python 内置的 `next()` 函数调用对象的 `next` 方法(在 Python3 中是 `__next__` 方法)对迭代器进行遍历。 + +运行上面的代码,可得到如下结果: + +``` +isinstance(fib, Iterator): True +1 +1 +2 +3 +5 +8 +``` + +# 小结 + +- 元组、列表、字典和字符串对象是可迭代的,但不是迭代器,不过我们可以通过 `iter()` 函数获得一个迭代器对象; +- Python 的 `for` 循环实质上是先通过内置函数 `iter()` 获得一个迭代器,然后再不断调用 `next()` 函数实现的; +- 自定义迭代器需要实现对象的 `__iter()__` 和 `next()` 方法(注意:Python3 要实现 `__next__()` 方法),其中,`__iter()__` 方法返回迭代器对象本身,`next()` 方法返回容器的下一个元素,在没有后续元素时抛出 `StopIteration` 异常。 + +# 参考资料 + +- [Callback or Iterator in Python](https://code-maven.com/callback-or-iterator-in-python) +- [迭代器 - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00143178254193589df9c612d2449618ea460e7a672a366000) + + diff --git a/Basic/README.md b/Basic/README.md new file mode 100644 index 0000000..df716ec --- /dev/null +++ b/Basic/README.md @@ -0,0 +1,10 @@ +# 基础 + +本章主要介绍两个方面的内容: + +- [字符编码](./character_encoding.md) +- [输入和输出](./input_output.md) + +其中,**字符编码**的概念很重要,不管你用的是 Python2 还是 Python3,亦或是 C++ 等其他编程语言,希望读者厘清这个概念,当遇到 UnicodeEncodeError 和 UnicodeDecodeError 时才能从容应对,而不是到处查找资料。 + + diff --git a/Basic/input_output.md b/Basic/input_output.md new file mode 100644 index 0000000..31a61f7 --- /dev/null +++ b/Basic/input_output.md @@ -0,0 +1,215 @@ +# 输入和输出 + +Python2 提供了 `input`,`raw_input`,`print` 等用于输入输出,但在 Python3 中发生了一些改变,`raw_input` 已经没有了,`input` 的用法发生了变化,`print` 也从原来的语句变成了一个函数。本文将对这两种情况进行介绍。 + +## 输入 + +- 首先看 Python2 中的 `raw_input`,它的用法如下: + +``` +raw_input(prompt) +``` + +其中,prompt 表示输入提示。`raw_input` 会读取控制台的输入,并返回字符串类型。 + +让我们看几个例子: + +```python +>>> name = raw_input('please enter your name: ') +please enter your name: ethan # 输入一个字符串 +>>> name +'ethan' +>>> type(name) + +>>> +>>> num = raw_input('please enter your id: ') +please enter your id: 12345 # 输入一个数值 +>>> num +'12345' +>>> type(num) + +>>> +>>> sum = raw_input('please enter a+b: ') +please enter a+b: 3+6 # 输入一个表达式 +>>> sum +'3+6' +>>> type(sum) + +``` + +可以看到,不管我们输入一个字符串、数值还是表达式,`raw_input` 都直接返回一个字符串。 + +- 现在看一下 Pythn2 中的 `input`。 + +`input` 的用法跟 `raw_input` 类似,形式如下: + +```python +input(prompt) +``` + +事实上,`input` 本质上是使用 `raw_input` 实现的,如下: + +```python +def input(prompt): + return (eval(raw_input(prompt))) +``` + +也就是说,调用 `input` 实际上是通过调用 `raw_input` 再调用 `eval` 函数实现的。 + +这里的 `eval` 通常用来执行一个字符串表达式,并返回表达式的值,它的基本用法如下: + +``` +>>> eval('1+2') +3 +>>> a = 1 +>>> eval('a+9') +10 +``` + +现在,让我们看看 `input` 的用法: + +```python +>>> name = input('please input your name: ') +please input your name: ethan # 输入字符串如果没加引号会出错 +Traceback (most recent call last): + File "", line 1, in + File "", line 1, in +NameError: name 'ethan' is not defined +>>> +>>> name = input('please input your name: ') +please input your name: 'ethan' # 添加引号 +>>> name +'ethan' +>>> +>>> num = input('please input your id: ') +please input your id: 12345 # 输入数值 +>>> num # 注意返回的是数值类型,而不是字符串 +12345 +>>> type(num) + +>>> +>>> sum = input('please enter a+b: ') # 输入数字表达式,会对表达式求值 +please enter a+b: 3+6 +>>> sum +9 +>>> type(sum) + +>>> +>>> sum = input('please enter a+b: ') # 输入字符串表达式,会字符串进行运算 +please enter a+b: '3'+'6' +>>> sum +'36' +``` + +可以看到,使用 `input` 的时候,如果输入的是字符串,必须使用引号把它们括起来;如果输入的是数值类型,则返回的也是数值类型;如果输入的是表达式,会对表达式进行运算。 + +- 再来看一下 Python3 中的 `input`。 + +事实上,Python3 中的 `input` 就是 Python2 中的 `raw_input`,也就是说,原 Python2 中的 `raw_input` 被重命名为 `input` 了。那如果我们想使用原 Python2 的 `input` 功能呢?你可以这样做: + +```python +eval(input()) +``` + +也就是说,手动添加 `eval` 函数。 + +## 输出 + +Python2 中的 `print` 是一个语句(statement),而 Python3 中的 `print` 是一个函数。 + +### Python2 中的 print + +- 简单输出 + +使用 `print` 最简单的方式就是直接在 `print` 后面加上数字、字符串、列表等对象,比如: + +```python +# Python 2.7.11 (default, Feb 24 2016, 10:48:05) +>>> print 123 +123 +>>> print 'abc' +abc +>>> x = 10 +>>> print x +10 +>>> d = {'a': 1, 'b': 2} +>>> print d +{'a': 1, 'b': 2} +>>> +>>> print(123) +123 +>>> print('abc') +abc +>>> print(x) +10 +>>> print(d) +{'a': 1, 'b': 2} +``` + +在 Python2 中,使用 `print` 时可以加括号,也可以不加括号。 + +- 格式化输出 + +有时,我们需要对输出进行一些格式化,比如限制小数的精度等,直接看几个例子: + +``` +>>> s = 'hello' +>>> l = len(s) +>>> print('the length of %s is %d' % (s, l)) +the length of hello is 5 +>>> +>>> pi = 3.14159 +>>> print('%10.3f' % pi) # 字段宽度 10,精度 3 + 3.142 +>>> print('%010.3f' % pi) # 用 0 填充空白 +000003.142 +>>> print('%+f' % pi) # 显示正负号 ++3.141590 +``` + +- 换行输出 + +print 默认是换行输出的,如果不想换行,可以在末尾加上一个 `,',比如: + +```python +>>> for i in range(0, 3): +... print i +... +0 +1 +2 +>>> for i in range(0, 3): +... print i, # 加了 , +... +0 1 2 # 注意会加上一个空格 +``` + +### Python3 中的 print + +在 Python3 中使用 print 跟 Python2 差别不大,不过要注意的是在 Python3 中使用 print 必须加括号,否则会抛出 SyntaxError。 + +另外,如果不想 print 换行输出,可以参考下面的方式: + +```python +>>> for i in range(0, 3): +... print(i) +... +0 +1 +2 +>>> for i in range(0, 3): +... print(i, end='') # 加上一个 end 参数 +... +012 +``` + +# 小结 + +- 在 Python2 中,`raw_input` 会读取控制台的输入,并返回字符串类型。 +- 在 Python2 中,如无特殊要求建议使用 raw_input() 来与用户交互。 +- 在 Python3 中,使用 `input` 处理输入,如有特殊要求,可以考虑加上 `eval`。 + +# 参考资料 + +- [python - What's the difference between raw_input() and input() in python3.x? - Stack Overflow](http://stackoverflow.com/questions/4915361/whats-the-difference-between-raw-input-and-input-in-python3-x) + diff --git a/Class/README.md b/Class/README.md index 091b6ff..b6e2323 100644 --- a/Class/README.md +++ b/Class/README.md @@ -1,2 +1,22 @@ # 类 +Python 是一门面向对象编程(Object Oriented Programming, OOP)的语言,这里的**对象**可以看做是由数据(或者说特性)以及一系列可以存取、操作这些数据的方法所组成的集合。面向对象编程主要有以下特点: + +- 多态(Polymorphism):不同类(Class)的对象对同一消息会做出不同的响应。 +- 封装(Encapsulation):对外部世界隐藏对象的工作细节。 +- 继承(Inheritance):以已有的类(父类)为基础建立专门的类对象。 + +在 Python 中,元组、列表和字典等数据类型是对象,函数也是对象。那么,我们能创建自己的对象吗?答案是肯定的。跟其他 OOP 语言类似,我们使用**类**来自定义对象。 + +本章主要介绍以下几个方面: + +* [类和实例](./class_and_object.md) +* [继承和多态](./inheritance_and_polymorphism.md) +* [类方法和静态方法](./method.md) +* [定制类和魔法方法](./magic_method.md) +* [slots 魔法](./slots.md) +* [使用 @property](./property.md) +* [你不知道的 super](./super.md) +* [元类](./metaclass.md) + + diff --git a/Class/class_and_object.md b/Class/class_and_object.md index 416f2bb..1f3e6b9 100644 --- a/Class/class_and_object.md +++ b/Class/class_and_object.md @@ -30,7 +30,7 @@ class Animal(object): 然后,在创建实例的时候,传入参数: ```python ->>> animal = Aniaml('dog1') # 传入参数 'dog1' +>>> animal = Animal('dog1') # 传入参数 'dog1' >>> animal.name # 访问对象的 name 属性 'dog1' ``` diff --git a/Class/magic_method.md b/Class/magic_method.md index 23033fe..96cb8d7 100644 --- a/Class/magic_method.md +++ b/Class/magic_method.md @@ -1,15 +1,63 @@ # 定制类和魔法方法 -在 Python 中,我们可以经常看到以双下划线 `__` 包裹起来的方法,比如最常见的 `__init__`,这些方法被称为**魔法方法(magic method)或特殊方法(special method)**。简单地说,这些方法可以给 Python 的类提供特殊功能,方便我们定制一个类,比如 `__init__` 方法可以对类属性进行初始化。 +在 Python 中,我们可以经常看到以双下划线 `__` 包裹起来的方法,比如最常见的 `__init__`,这些方法被称为**魔法方法(magic method)或特殊方法(special method)**。简单地说,这些方法可以给 Python 的类提供特殊功能,方便我们定制一个类,比如 `__init__` 方法可以对实例属性进行初始化。 -完整的特殊方法列表可在[这里](https://docs.python.org/2/reference/datamodel.html#special-method-names)查看,本文只介绍以下一些常用的特殊方法: +完整的特殊方法列表可在[这里](https://docs.python.org/2/reference/datamodel.html#special-method-names)查看,本文介绍部分常用的特殊方法: +- `__new__` - `__str__` , `__repr__` - `__iter__` - `__getitem__` , `__setitem__` , `__delitem__` - `__getattr__` , `__setattr__` , `__delattr__` - `__call__` +# new + +在 Python 中,当我们创建一个类的实例时,类会先调用 `__new__(cls[, ...])` 来创建实例,然后 `__init__` 方法再对该实例(self)进行初始化。 + +关于 `__new__` 和 `__init__` 有几点需要注意: + +- `__new__` 是在 `__init__` 之前被调用的; +- `__new__` 是类方法,`__init__` 是实例方法; +- 重载 `__new__` 方法,需要返回类的实例; + +一般情况下,我们不需要重载 `__new__` 方法。但在某些情况下,我们想**控制实例的创建过程**,这时可以通过重载 `__new_` 方法来实现。 + +让我们看一个例子: + +```python +class A(object): + _dict = dict() + + def __new__(cls): + if 'key' in A._dict: + print "EXISTS" + return A._dict['key'] + else: + print "NEW" + return object.__new__(cls) + + def __init__(self): + print "INIT" + A._dict['key'] = self +``` + +在上面,我们定义了一个类 `A`,并重载了 `__new__` 方法:当 `key` 在 `A._dict` 中时,直接返回 `A._dict['key']`,否则创建实例。 + +执行情况: + +``` +>>> a1 = A() +NEW +INIT +>>> a2 = A() +EXISTS +INIT +>>> a3 = A() +EXISTS +INIT +``` + # str & repr 先看一个简单的例子: @@ -318,14 +366,15 @@ True # 小结 +- `__new__` 在 `__init__` 之前被调用,用来创建实例。 - `__str__` 是用 print 和 str 显示的结果,`__repr__` 是直接显示的结果。 - `__getitem__` 用类似 `obj[key]` 的方式对对象进行取值 - `__getattr__` 用于获取不存在的属性 obj.attr - `__call__` 使得可以对实例进行调用 - # 参考资料 +- [design patterns - Python's use of __new__ and __init__? - Stack Overflow](http://stackoverflow.com/questions/674304/pythons-use-of-new-and-init) - [定制类](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013946328809098c1be08a2c7e4319bd60269f62be04fa000) - [__setitem__ implementation in Python for Point(x,y) class - Stack Overflow](http://stackoverflow.com/questions/15774804/setitem-implementation-in-python-for-pointx-y-class) - [Python对象的特殊属性和方法 | Hom](http://gohom.win/2015/10/09/pySpecialObjMethod/) diff --git a/Class/metaclass.md b/Class/metaclass.md index 580b2b5..b4893b6 100644 --- a/Class/metaclass.md +++ b/Class/metaclass.md @@ -1,2 +1,325 @@ -# 元类 +# 陌生的 metaclass + +Python 中的**元类(metaclass)**是一个深度魔法,平时我们可能比较少接触到元类,本文将通过一些简单的例子来理解这个魔法。 + +## 类也是对象 + +在 Python 中,一切皆对象。字符串,列表,字典,函数是对象,**类也是一个对象**,因此你可以: + +- 把类赋值给一个变量 +- 把类作为函数参数进行传递 +- 把类作为函数的返回值 +- 在运行时动态地创建类 + +看一个简单的例子: + +```python +class Foo(object): + foo = True + +class Bar(object): + bar = True + +def echo(cls): + print cls + +def select(name): + if name == 'foo': + return Foo # 返回值是一个类 + if name == 'bar': + return Bar + +>>> echo(Foo) # 把类作为参数传递给函数 echo + +>>> cls = select('foo') # 函数 select 的返回值是一个类,把它赋给变量 cls +>>> cls +__main__.Foo +``` + +## 熟悉又陌生的 type + +在日常使用中,我们经常使用 `object` 来派生一个类,事实上,在这种情况下,Python 解释器会调用 `type` 来创建类。 + +这里,出现了 `type`,没错,是你知道的 `type`,我们经常使用它来判断一个对象的类型,比如: + +```python +class Foo(object): + Foo = True + +>>> type(10) + +>>> type('hello') + +>>> type(Foo()) + +>>> type(Foo) + +``` + +**事实上,`type` 除了可以返回对象的类型,它还可以被用来动态地创建类(对象)**。下面,我们看几个例子,来消化一下这句话。 + +使用 `type` 来创建类(对象)的方式如下: + +> type(类名, 父类的元组(针对继承的情况,可以为空),包含属性和方法的字典(名称和值)) + +### 最简单的情况 + +假设有下面的类: + +```python +class Foo(object): + pass +``` + +现在,我们不使用 `class` 关键字来定义,而使用 `type`,如下: + +```python +Foo = type('Foo', (object, ), {}) # 使用 type 创建了一个类对象 +``` + +上面两种方式是等价的。我们看到,`type` 接收三个参数: + +- 第 1 个参数是字符串 'Foo',表示类名 +- 第 2 个参数是元组 (object, ),表示所有的父类 +- 第 3 个参数是字典,这里是一个空字典,表示没有定义属性和方法 + +在上面,我们使用 `type()` 创建了一个名为 Foo 的类,然后把它赋给了变量 Foo,我们当然可以把它赋给其他变量,但是,此刻没必要给自己找麻烦。 + +接着,我们看看使用: + +```python +>>> print Foo + +>>> print Foo() +<__main__.Foo object at 0x10c34f250> +``` + +### 有属性和方法的情况 + +假设有下面的类: + +```python +class Foo(object): + foo = True + def greet(self): + print 'hello world' + print self.foo +``` + +用 `type` 来创建这个类,如下: + +```python +def greet(self): + print 'hello world' + print self.foo + +Foo = type('Foo', (object, ), {'foo': True, 'greet': greet}) +``` + +上面两种方式的效果是一样的,看下使用: + +```python +>>> f = Foo() +>>> f.foo +True +>>> f.greet +> +>>> f.greet() +hello world +True +``` + +### 继承的情况 + +再来看看继承的情况,假设有如下的父类: + +```python +class Base(object): + pass +``` + +我们用 Base 派生一个 Foo 类,如下: + +```python +class Foo(Base): + foo = True +``` + +改用 `type` 来创建,如下: + +```python +Foo = type('Foo', (Base, ), {'foo': True}) +``` + +## 什么是元类(metaclass) + +**元类(metaclass)是用来创建类(对象)的可调用对象。**这里的可调用对象可以是函数或者类等。但一般情况下,我们使用类作为元类。对于实例对象、类和元类,我们可以用下面的图来描述: + +```python +类是实例对象的模板,元类是类的模板 + ++----------+ +----------+ +----------+ +| | | | | | +| | instance of | | instance of | | +| instance +------------>+ class +------------>+ metaclass| +| | | | | | +| | | | | | ++----------+ +----------+ +----------+ +``` + +我们在前面使用了 `type` 来创建类(对象),事实上,**`type` 就是一个元类**。 + +那么,元类到底有什么用呢?要你何用... + +**元类的主要目的是为了控制类的创建行为。**我们还是先来看看一些例子,以消化这句话。 + +## 元类的使用 + +先从一个简单的例子开始,假设有下面的类: + +```python +class Foo(object): + name = 'foo' + def bar(self): + print 'bar' +``` + +现在我们想给这个类的方法和属性名称前面加上 `my_` 前缀,即 name 变成 my_name,bar 变成 my_bar,另外,我们还想加一个 echo 方法。当然,有很多种做法,这里展示用元类的做法。 + +1.首先,定义一个元类,按照默认习惯,类名以 Metaclass 结尾,代码如下: + +```python +class PrefixMetaclass(type): + def __new__(cls, name, bases, attrs): + # 给所有属性和方法前面加上前缀 my_ + _attrs = (('my_' + name, value) for name, value in attrs.items()) + + _attrs = dict((name, value) for name, value in _attrs) # 转化为字典 + _attrs['echo'] = lambda self, phrase: phrase # 增加了一个 echo 方法 + + return type.__new__(cls, name, bases, _attrs) # 返回创建后的类 +``` + +上面的代码有几个需要注意的点: + +- PrefixMetaClass 从 `type` 继承,这是因为 PrefixMetaclass 是用来创建类的 +- `__new__` 是在 `__init__` 之前被调用的特殊方法,它用来创建对象并返回创建后的对象,对它的参数解释如下: + - cls:当前准备创建的类 + - name:类的名字 + - bases:类的父类集合 + - attrs:类的属性和方法,是一个字典 + +2.接着,我们需要指示 Foo 使用 PrefixMetaclass 来定制类。 + +在 Python2 中,我们只需在 Foo 中加一个 `__metaclass__` 的属性,如下: + +```python +class Foo(object): + __metaclass__ = PrefixMetaclass + name = 'foo' + def bar(self): + print 'bar' +``` + +在 Python3 中,这样做: + +```python +class Foo(metaclass=PrefixMetaclass): + name = 'foo' + def bar(self): + print 'bar' +``` + +现在,让我们看看使用: + +```python +>>> f = Foo() +>>> f.name # name 属性已经被改变 +--------------------------------------------------------------------------- +AttributeError Traceback (most recent call last) + in () +----> 1 f.name + +AttributeError: 'Foo' object has no attribute 'name' +>>> +>>> f.my_name +'foo' +>>> f.my_bar() +bar +>>> f.echo('hello') +'hello' +``` + +可以看到,Foo 原来的属性 name 已经变成了 my_name,而方法 bar 也变成了 my_bar,这就是元类的魔法。 + +再来看一个继承的例子,下面是完整的代码: + +```python +class PrefixMetaclass(type): + def __new__(cls, name, bases, attrs): + # 给所有属性和方法前面加上前缀 my_ + _attrs = (('my_' + name, value) for name, value in attrs.items()) + + _attrs = dict((name, value) for name, value in _attrs) # 转化为字典 + _attrs['echo'] = lambda self, phrase: phrase # 增加了一个 echo 方法 + + return type.__new__(cls, name, bases, _attrs) + +class Foo(object): + __metaclass__ = PrefixMetaclass # 注意跟 Python3 的写法有所区别 + name = 'foo' + def bar(self): + print 'bar' + +class Bar(Foo): + prop = 'bar' +``` + +其中,PrefixMetaclass 和 Foo 跟前面的定义是一样的,只是新增了 Bar,它继承自 Foo。先让我们看看使用: + +```python +>>> b = Bar() +>>> b.prop # 发现没这个属性 +--------------------------------------------------------------------------- +AttributeError Traceback (most recent call last) + in () +----> 1 b.prop + +AttributeError: 'Bar' object has no attribute 'prop' +>>> b.my_prop +'bar' +>>> b.my_name +'foo' +>>> b.my_bar() +bar +>>> b.echo('hello') +'hello' +``` + +我们发现,Bar 没有 prop 这个属性,但是有 my_prop 这个属性,这是为什么呢? + +原来,当我们定义 `class Bar(Foo)` 时,Python 会首先在当前类,即 Bar 中寻找 `__metaclass__`,如果没有找到,就会在父类 Foo 中寻找 `__metaclass__`,如果找不到,就继续在 Foo 的父类寻找,如此继续下去,如果在任何父类都找不到 `__metaclass__`,就会到模块层次中寻找,如果还是找不到,就会用 type 来创建这个类。 + +这里,我们在 Foo 找到了 `__metaclass__`,Python 会使用 PrefixMetaclass 来创建 Bar,也就是说,元类会隐式地继承到子类,虽然没有显示地在子类使用 `__metaclass__`,这也解释了为什么 Bar 的 prop 属性被动态修改成了 my_prop。 + +写到这里,不知道你理解元类了没?希望理解了,如果没理解,就多看几遍吧~ + +# 小结 + +- 在 Python 中,类也是一个对象。 +- 类创建实例,元类创建类。 +- 元类主要做了三件事: + - 拦截类的创建 + - 修改类的定义 + - 返回修改后的类 +- 当你创建类时,解释器会调用元类来生成它,定义一个继承自 object 的普通类意味着调用 type 来创建它。 + +# 参考资料 + +- [oop - What is a metaclass in Python? - Stack Overflow](http://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python) +- [深刻理解Python中的元类(metaclass) - 伯乐在线](http://blog.jobbole.com/21351/) +- [使用元类 - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/0014319106919344c4ef8b1e04c48778bb45796e0335839000) +- [Python基础:元类](http://www.cnblogs.com/russellluo/p/3409602.html) +- [在Python中使用class decorator和metaclass](http://blog.zhangyu.so/python/2016/02/19/class-decorator-and-metaclass-in-python/) + diff --git a/Class/method.md b/Class/method.md index 385e697..07f3165 100644 --- a/Class/method.md +++ b/Class/method.md @@ -1,2 +1,63 @@ # 类方法和静态方法 +在讲类方法和静态方法之前,先来看一个简单的例子: + +```python +class A(object): + def foo(self): + print 'Hello ', self + +>>> a = A() +>>> a.foo() +Hello, <__main__.A object at 0x10c37a450> +``` + +在上面,我们定义了一个类 A,它有一个方法 foo,然后我们创建了一个对象 a,并调用方法 foo。 + +## 类方法 + +如果我们想通过类来调用方法,而不是通过实例,那应该怎么办呢? + +Python 提供了 `classmethod` 装饰器让我们实现上述功能,看下面的例子: + +```python +class A(object): + bar = 1 + @classmethod + def class_foo(cls): + print 'Hello, ', cls + print cls.bar + +>>> A.class_foo() # 直接通过类来调用方法 +Hello, +1 +``` + +在上面,我们使用了 `classmethod` 装饰方法 `class_foo`,它就变成了一个类方法,`class_foo` 的参数是 cls,代表类本身,当我们使用 `A.class_foo()` 时,cls 就会接收 A 作为参数。另外,被 `classmethod` 装饰的方法由于持有 cls 参数,因此我们可以在方法里面调用类的属性、方法,比如 `cls.bar`。 + +## 静态方法 + +在类中往往有一些方法跟类有关系,但是又不会改变类和实例状态的方法,这种方法是**静态方法**,我们使用 `staticmethod` 来装饰,比如下面的例子: + +```python +class A(object): + bar = 1 + @staticmethod + def static_foo(): + print 'Hello, ', A.bar + +>>> a = A() +>>> a.static_foo() +Hello, 1 +>>> A.static_foo() +Hello, 1 +``` + +可以看到,静态方法没有 self 和 cls 参数,可以把它看成是一个普通的函数,我们当然可以把它写到类外面,但这是不推荐的,因为这不利于代码的组织和命名空间的整洁。 + +# 小结 + +- 类方法使用 `@classmethod` 装饰器,可以使用类(也可使用实例)来调用方法。 +- 静态方法使用 `@staticmethod` 装饰器,它是跟类有关系但在运行时又不需要实例和类参与的方法,可以使用类和实例来调用。 + + diff --git a/Class/property.md b/Class/property.md index 679b8c2..5ea1c1b 100644 --- a/Class/property.md +++ b/Class/property.md @@ -1,2 +1,92 @@ # 使用 @property +在使用 `@property` 之前,让我们先来看一个简单的例子: + +```python +class Exam(object): + def __init__(self, score): + self._score = score + + def get_score(self): + return self._score + + def set_score(self, val): + if val < 0: + self._score = 0 + elif val > 100: + self._score = 100 + else: + self._score = val + +>>> e = Exam(60) +>>> e.get_score() +60 +>>> e.set_score(70) +>>> e.get_score() +70 +``` + +在上面,我们定义了一个 Exam 类,为了避免直接对 `_score` 属性操作,我们提供了 get_score 和 set_score 方法,这样起到了封装的作用,把一些不想对外公开的属性隐蔽起来,而只是提供方法给用户操作,在方法里面,我们可以检查参数的合理性等。 + +这样做没什么问题,但是我们有更简单的方式来做这件事,Python 提供了 `property` 装饰器,被装饰的方法,我们可以将其『当作』属性来用,看下面的例子: + +```python +class Exam(object): + def __init__(self, score): + self._score = score + + @property + def score(self): + return self._score + + @score.setter + def score(self, val): + if val < 0: + self._score = 0 + elif val > 100: + self._score = 100 + else: + self._score = val + +>>> e = Exam(60) +>>> e.score +60 +>>> e.score = 90 +>>> e.score +90 +>>> e.score = 200 +>>> e.score +100 +``` + +在上面,我们给方法 score 加上了 `@property`,于是我们可以把 score 当成一个属性来用,此时,又会创建一个新的装饰器 `score.setter`,它可以把被装饰的方法变成属性来赋值。 + +另外,我们也不一定要使用 `score.setter` 这个装饰器,这时 score 就变成一个只读属性了: + +```python +class Exam(object): + def __init__(self, score): + self._score = score + + @property + def score(self): + return self._score + +>>> e = Exam(60) +>>> e.score +60 +>>> e.score = 200 # score 是只读属性,不能设置值 +--------------------------------------------------------------------------- +AttributeError Traceback (most recent call last) + in () +----> 1 e.score = 200 + +AttributeError: can't set attribute + +``` + +# 小结 + +- `@property` 把方法『变成』了属性。 + + diff --git a/Class/singleton.md b/Class/singleton.md new file mode 100644 index 0000000..b88cee6 --- /dev/null +++ b/Class/singleton.md @@ -0,0 +1,126 @@ +# 单例模式 + +**单例模式(Singleton Pattern)**是一种常用的软件设计模式,该模式的主要目的是确保**某一个类只有一个实例存在**。当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。 + +比如,某个服务器程序的配置信息存放在一个文件中,客户端通过一个 AppConfig 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig 对象的实例,这就导致系统中存在多个 AppConfig 的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 AppConfig 这样的类,我们希望在程序运行期间只存在一个实例对象。 + +在 Python 中,我们可以用多种方法来实现单例模式: + +- 使用模块 +- 使用 `__new__` +- 使用装饰器(decorator) +- 使用元类(metaclass) + +# 使用模块 + +其实,**Python 的模块就是天然的单例模式**,因为模块在第一次导入时,会生成 `.pyc` 文件,当第二次导入时,就会直接加载 `.pyc` 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。如果我们真的想要一个单例类,可以考虑这样做: + +```python +# mysingleton.py +class My_Singleton(object): + def foo(self): + pass + +my_singleton = My_Singleton() +``` + +将上面的代码保存在文件 `mysingleton.py` 中,然后这样使用: + +``` +from mysingleton import my_singleton + +my_singleton.foo() +``` + +# 使用 `__new__` + +为了使类只能出现一个实例,我们可以使用 `__new__` 来控制实例的创建过程,代码如下: + +```python +class Singleton(object): + _instance = None + def __new__(cls, *args, **kw): + if not cls._instance: + cls._instance = super(Singleton, cls).__new__(cls, *args, **kw) + return cls._instance + +class MyClass(Singleton): + a = 1 +``` + +在上面的代码中,我们将类的实例和一个类变量 `_instance` 关联起来,如果 `cls._instance` 为 None 则创建实例,否则直接返回 `cls._instance`。 + +执行情况如下: + +``` +>>> one = MyClass() +>>> two = MyClass() +>>> one == two +True +>>> one is two +True +>>> id(one), id(two) +(4303862608, 4303862608) +``` + +# 使用装饰器 + +我们知道,装饰器(decorator)可以动态地修改一个类或函数的功能。这里,我们也可以使用装饰器来装饰某个类,使其只能生成一个实例,代码如下: + +``` +from functools import wraps + +def singleton(cls): + instances = {} + @wraps(cls) + def getinstance(*args, **kw): + if cls not in instances: + instances[cls] = cls(*args, **kw) + return instances[cls] + return getinstance + +@singleton +class MyClass(object): + a = 1 +``` + +在上面,我们定义了一个装饰器 `singleton`,它返回了一个内部函数 `getinstance`,该函数会判断某个类是否在字典 `instances` 中,如果不存在,则会将 `cls` 作为 key,`cls(*args, **kw)` 作为 value 存到 `instances` 中,否则,直接返回 `instances[cls]`。 + +# 使用 metaclass + +元类(metaclass)可以控制类的创建过程,它主要做三件事: + +- 拦截类的创建 +- 修改类的定义 +- 返回修改后的类 + +使用元类实现单例模式的代码如下: + +```python +class Singleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + +# Python2 +class MyClass(object): + __metaclass__ = Singleton + +# Python3 +# class MyClass(metaclass=Singleton): +# pass +``` + +# 小结 + +- Python 的模块是天然的单例模式,这在大部分情况下应该是够用的,当然,我们也可以使用装饰器、元类等方法 + +# 参考资料 + +- [Creating a singleton in Python - Stack Overflow](http://stackoverflow.com/questions/6760685/creating-a-singleton-in-python) +- [深入浅出单实例Singleton设计模式 | 酷 壳](http://coolshell.cn/articles/265.html) +- [design patterns - Python's use of __new__ and __init__? - Stack Overflow](http://stackoverflow.com/questions/674304/pythons-use-of-new-and-init) + + diff --git a/Class/slots.md b/Class/slots.md index 8ec84d2..a0fd476 100644 --- a/Class/slots.md +++ b/Class/slots.md @@ -1,2 +1,58 @@ # slots 魔法 +在 Python 中,我们在定义类的时候可以定义属性和方法。当我们创建了一个类的实例后,我们还可以给该实例绑定任意新的属性和方法。 + +看下面一个简单的例子: + +```python +class Point(object): + def __init__(self, x=0, y=0): + self.x = x + self.y = y + +>>> p = Point(3, 4) +>>> p.z = 5 # 绑定了一个新的属性 +>>> p.z +5 +>>> p.__dict__ +{'x': 3, 'y': 4, 'z': 5} +``` + +在上面,我们创建了实例 p 之后,给它绑定了一个新的属性 z,这种动态绑定的功能虽然很有用,但它的代价是消耗了更多的内存。 + +因此,为了不浪费内存,可以使用 `__slots__` 来告诉 Python 只给一个固定集合的属性分配空间,对上面的代码做一点改进,如下: + +```python +class Point(object): + __slots__ = ('x', 'y') # 只允许使用 x 和 y + + def __init__(self, x=0, y=0): + self.x = x + self.y = y +``` + +上面,我们给 `__slots__` 设置了一个元组,来限制类能添加的属性。现在,如果我们想绑定一个新的属性,比如 z,就会出错了,如下: + +```python +>>> p = Point(3, 4) +>>> p.z = 5 +--------------------------------------------------------------------------- +AttributeError Traceback (most recent call last) + in () +----> 1 p.z = 5 + +AttributeError: 'Point' object has no attribute 'z' +``` + +使用 `__slots__` 有一点需要注意的是,`__slots__` 设置的属性仅对当前类有效,对继承的子类不起效,除非子类也定义了 `__slots__`,这样,子类允许定义的属性就是自身的 slots 加上父类的 slots。 + +# 小结 + +- __slots__ 魔法:限定允许绑定的属性. +- `__slots__` 设置的属性仅对当前类有效,对继承的子类不起效,除非子类也定义了 slots,这样,子类允许定义的属性就是自身的 slots 加上父类的 slots。 + +# 参考资料 + +- [slots魔法 · Python进阶](https://eastlakeside.gitbooks.io/interpy-zh/content/slots_magic/) + + diff --git a/Class/super.md b/Class/super.md index 915de13..4001455 100644 --- a/Class/super.md +++ b/Class/super.md @@ -1,4 +1,4 @@ -# super() 的使用 +# 你不知道的 super 在类的继承中,如果重定义某个方法,该方法会覆盖父类的同名方法,但有时,我们希望能同时实现父类的功能,这时,我们就需要调用父类的方法了,可通过使用 `super` 来实现,比如: diff --git a/Conclusion/README.md b/Conclusion/README.md index 9c5a300..6f7d00e 100644 --- a/Conclusion/README.md +++ b/Conclusion/README.md @@ -1,2 +1,10 @@ # 结束语 +到这里,虽然本书结束了,但对于 Python 的学习和实践还远远没结束,后面我也会持续更新本书。虽然 Python 的语法相比 C++ 等语言比较简洁,但想熟练运用,仍需在实际的项目中多多实践,而不只是停留在简单的概念学习中。 + +这里主要推荐 Python 相关的一些学习资源,同时也列出本书的主要参考资料。 + +- [资源推荐](./resource_recommendation.md) +- [参考资料](./reference_material.md) + + diff --git a/Conclusion/resource_recommendation.md b/Conclusion/resource_recommendation.md index 20b90bd..5c5beee 100644 --- a/Conclusion/resource_recommendation.md +++ b/Conclusion/resource_recommendation.md @@ -1,2 +1,53 @@ # 资源推荐 +这里列出了 Python 相关的一些资源,欢迎读者补充。 + +- [vinta/awesome-python: A curated list of awesome Python frameworks, libraries, software and resources](https://github.com/vinta/awesome-python) + + 包含了 Python 框架、Python 库和软件的 awesome 列表。 + +- [aosabook/500lines: 500 Lines or Less](https://github.com/aosabook/500lines) + + Python 神书,里面有若干个项目,每个项目都是由业内大神所写,每个项目代码在 500 行左右。 + +- [Python Module of the Week - PyMOTW 2](https://pymotw.com/2/) + + 自 2007 年以来,[Doug Hellmann](http://www.doughellmann.com/) 在他的博客上发表了颇受关注的「Python Module of the Week」系列,计划每周介绍一个 Python 标准库的使用。上面的链接是介绍 Python2 中的标准库,同样也有 Python3 的:[Python 3 Module of the Week — PyMOTW 3](https://pymotw.com/3/#python-3-module-of-the-week)。 + +- [Transforming Code into Beautiful, Idiomatic Python](https://gist.github.com/JeffPaine/6213790) + + 写出简洁的、优雅的 Python 代码。 + +- [jobbole/awesome-python-cn: Python资源大全中文版](https://github.com/jobbole/awesome-python-cn) + + Python 资源大全,包含:Web 框架、网络爬虫、模板引擎和数据库等,由[伯乐在线](https://github.com/jobbole)更新。 + +- [Pycoder's Weekly | A Weekly Python E-Mail Newsletter](http://pycoders.com/) + + 优秀的免费邮件 Python 新闻周刊。 + +- [Python 初学者的最佳学习资源](http://python.jobbole.com/82399/) + + 伯乐在线翻译的 Python 学习资源。 + +- [Full Stack Python](http://www.fullstackpython.com/) + + Python 资源汇总,从基础入门到各种 Web 开发框架,再到高级的 ORM,Docker 等等。 + +- [The Hitchhiker’s Guide to Python!](http://docs.python-guide.org/en/latest/) + + [Requests](https://github.com/kennethreitz/requests) 作者 kennethreitz 的一本开源书籍,介绍 Python 的最佳实践。 + +- [Welcome to Python for you and me](http://pymbook.readthedocs.io/en/latest/) + + 介绍 Python 的基本语法,特点等。 + +- [District Data Labs - How to Develop Quality Python Code](https://districtdatalabs.silvrback.com/how-to-develop-quality-python-code) + + 开发高质量的 Python 代码。 + +- [A "Best of the Best Practices" (BOBP) guide to developing in Python.](https://gist.github.com/sloria/7001839) + + Python 最佳实践。 + + diff --git a/Datatypes/README.md b/Datatypes/README.md index 97aa321..397145e 100644 --- a/Datatypes/README.md +++ b/Datatypes/README.md @@ -1,2 +1,217 @@ -# 基本数据类型 +# 常用数据类型 + +在介绍 Python 的常用数据类型之前,我们先看看 Python 最基本的数据结构 - **序列(sequence)**。 + +**序列**的一个特点就是根据索引(index,即元素的位置)来获取序列中的元素,第一个索引是 0,第二个索引是 1,以此类推。 + +所有序列类型都可以进行某些通用的操作,比如: + +- 索引(indexing) +- 分片(slicing) +- 迭代(iteration) +- 加(adding) +- 乘(multiplying) + +除了上面这些,我们还可以检查某个元素是否属于序列的成员,计算序列的长度等等。 + +说完序列,我们接下来看看 Python 中常用的数据类型,如下: + +- [列表(list)](./list.md) +- [元组(tuple)](./tuple.md) +- [字符串(string)](./string.md) +- [字典(dict)](./dict.md) +- [集合(set)](./set.md) + +其中,**列表、元组和字符串都属于序列类型,它们可以进行某些通用的操作**,比如索引、分片等;字典属于**映射**类型,每个元素由键(key)和值(value)构成;集合是一种特殊的类型,它所包含的元素是不重复的。 + +# 通用的序列操作 + +## 索引 + +序列中的元素可以通过索引获取,索引从 0 开始。看看下面的例子: + +``` +>>> nums = [1, 2, 3, 4, 5] # 列表 +>>> nums[0] +1 +>>> nums[1] +2 +>>> nums[-1] # 索引 -1 表示最后一个元素 +5 +>>> s = 'abcdef' # 字符串 +>>> s[0] +'a' +>>> s[1] +'b' +>>> +>>> a = (1, 2, 3) # 元组 +>>> a[0] +1 +>>> a[1] +2 +``` + +注意到,-1 则代表序列的最后一个元素,-2 代表倒数第二个元素,以此类推。 + +## 分片 + +**索引**用于获取序列中的单个元素,而**分片**则用于获取序列的部分元素。分片操作需要提供两个索引作为边界,中间用冒号相隔,比如: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6] +>>> numbers[0:2] # 列表分片 +[1, 2] +>>> numbers[2:5] +[3, 4, 5] +>>> s = 'hello, world' # 字符串分片 +>>> s[0:5] +'hello' +>>> a = (2, 4, 6, 8, 10) # 元组分片 +>>> a[2:4] +(6, 8) +``` + +**这里需要特别注意的是,分片有两个索引,第 1 个索引的元素是包含在内的,而第 2 个元素的索引则不包含在内**,也就是说,numbers[2:5] 获取的是 numbers[2], numbers[3], numbers[4],没有包括 numbers[5]。 + +下面列举使用分片的一些技巧。 + +- 访问最后几个元素 + +假设需要访问序列的最后 3 个元素,我们当然可以像下面这样做: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6] +>>> numbers[3:6] +[4, 5, 6] +``` + +有没有更简洁的方法呢?想到可以使用负数形式的索引,你可能会尝试这样做: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6] +>>> numbers[-3:-1] # 实际取出的是 numbers[-3], numbers[-2] +[4, 5] +>>> numbers[-3:0] # 左边索引的元素比右边索引出现得晚,返回空序列 +[] +``` + +上面的两种使用方式并不能正确获取序列的最后 3 个元素,Python 提供了一个捷径: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8] +>>> numbers[-3:] +[6, 7, 8] +>>> numbers[5:] +[6, 7, 8] +``` + +也就是说,如果希望分片包含最后一个元素,可将第 2 个索引置为空。 + +**如果要复制整个序列,可以将两个索引都置为空**: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8] +>>> nums = numbers[:] +>>> nums +[1, 2, 3, 4, 5, 6, 7, 8] +``` + +- 使用步长 + +使用分片的时候,步长默认是 1,即逐个访问,我们也可以自定义步长,比如: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8] +>>> numbers[0:4] +[1, 2, 3, 4] +>>> numbers[0:4:1] # 步长为 1,不写也可以,默认为 1 +[1, 2, 3, 4] +>>> numbers[0:4:2] # 步长为 2,取出 numbers[0], numbers[2] +[1, 3] +>>> numbers[::3] # 等价于 numbers[0:8:3],取出索引为 0, 3, 6 的元素 +[1, 4, 7] +``` + +另外,步长也可以是负数,表示从右到左取元素: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6, 7, 8] +>>> numbers[0:4:-1] +[] +>>> numbers[4:0:-1] # 取出索引为 4, 3, 2, 1 的元素 +[5, 4, 3, 2] +>>> numbers[4:0:-2] # 取出索引为 4, 2 的元素 +[5, 3] +>>> numbers[::-1] # 从右到左取出所有元素 +[8, 7, 6, 5, 4, 3, 2, 1] +>>> numbers[::-2] # 取出索引为 7, 5, 3, 1 的元素 +[8, 6, 4, 2] +>>> numbers[6::-2] # 取出索引为 6, 4, 2, 0 的元素 +[7, 5, 3, 1] +>>> numbers[:6:-2] # 取出索引为 7 的元素 +[8] +``` + +这里总结一下使用分片操作的一些方法,分片的使用形式是: + +```python +# 左索引:右索引:步长 +left_index:right_index:step +``` + +要牢牢记住的是: + +- 左边索引的元素包括在结果之中,右边索引的元素不包括在结果之中; +- 当使用一个负数作为步长时,必须让左边索引大于右边索引; +- 对正数步长,从左向右取元素;对负数步长,从右向左取元素; + +## 加 + +序列可以进行「加法」操作,如下: + +```python +>>> [1, 2, 3] + [4, 5, 6] # 「加法」效果其实就是连接在一起 +[1, 2, 3, 4, 5, 6] +>>> (1, 2, 3) + (4, 5, 6) +(1, 2, 3, 4, 5, 6) +>>> 'hello, ' + 'world!' +'hello, world!' +>>> [1, 2, 3] + 'abc' +Traceback (most recent call last): + File "", line 1, in +TypeError: can only concatenate list (not "str") to list +``` + +这里需要注意的是:两种相同类型的序列才能「加法」操作。 + +## 乘 + +序列可以进行「乘法」操作,比如: + +```python +>>> 'abc' * 3 +'abcabcabc' +>>> [0] * 3 +[0, 0, 0] +>>> [1, 2, 3] * 3 +[1, 2, 3, 1, 2, 3, 1, 2, 3] +``` + +## in + +为了检查一个值是否在序列中,可以使用 `in` 运算符,比如: + +```python +>>> 'he' in 'hello' +True +>>> 'hl' in 'hello' +False +>>> 10 in [6, 8, 10] +True +``` + +# 参考资料 + +- 《Python 基础教程》 + diff --git a/Datatypes/dict.md b/Datatypes/dict.md index 103e645..01785ba 100644 --- a/Datatypes/dict.md +++ b/Datatypes/dict.md @@ -1,2 +1,432 @@ # 字典 +字典是 Python 中唯一的映射类型,每个元素由键(key)和值(value)构成,键必须是不可变类型,比如数字、字符串和元组。 + +# 字典基本操作 + +这里先介绍字典的几个基本操作,后文再介绍字典的常用方法。 + +- 创建字典 +- 遍历字典 +- 判断键是否在字典里面 + +## 创建字典 + +字典可以通过下面的方式创建: + +```python +>>> d0 = {} # 空字典 +>>> d0 +{} +>>> d1 = {'name': 'ethan', 'age': 20} +>>> d1 +{'age': 20, 'name': 'ethan'} +>>> d1['age'] = 21 # 更新字典 +>>> d1 +{'age': 21, 'name': 'ethan'} +>>> d2 = dict(name='ethan', age=20) # 使用 dict 函数 +>>> d2 +{'age': 20, 'name': 'ethan'} +>>> item = [('name', 'ethan'), ('age', 20)] +>>> d3 = dict(item) +>>> d3 +{'age': 20, 'name': 'ethan'} +``` + +## 遍历字典 + +遍历字典有多种方式,这里先介绍一些基本的方式,后文会介绍一些高效的遍历方式。 + +```python +>>> d = {'name': 'ethan', 'age': 20} +>>> for key in d: +... print '%s: %s' % (key, d[key]) +... +age: 20 +name: ethan +>>> d['name'] +'ethan' +>>> d['age'] +20 +>>> for key in d: +... if key == 'name': +... del d[key] # 要删除字典的某一项 +... +Traceback (most recent call last): + File "", line 1, in +RuntimeError: dictionary changed size during iteration +>>> +>>> for key in d.keys(): # python2 应该使用这种方式, python3 使用 list(d.keys()) +... if key == 'name': +... del d[key] +... +>>> d +{'age': 20} +``` + +在上面,我们介绍了两种遍历方式:`for key in d` 和 `for key in d.keys()`,如果在遍历的时候,要删除键为 key 的某项,使用第一种方式会抛出 RuntimeError,使用第二种方式则不会。 + +## 判断键是否在字典里面 + +有时,我们需要判断某个键是否在字典里面,这时可以用 `in` 进行判断,如下: + +```python +>>> d = {'name': 'ethan', 'age': 20} +>>> 'name' in d +True +>>> d['score'] # 访问不存在的键,会抛出 KeyError +Traceback (most recent call last): + File "", line 1, in +KeyError: 'score' +>>> 'score' in d # 使用 in 判断 key 是否在字典里面 +False +``` + +# 字典常用方法 + +字典有自己的一些操作方法,这里只介绍部分常用的方法: + +- clear +- copy +- get +- setdefault +- update +- pop +- popitem +- keys/iterkeys +- values/itervalues +- items/iteritems +- fromkeys + +## clear + +clear 方法用于清空字典中的所有项,这是个原地操作,所以无返回值(或者说是 None)。 + +看看例子: + +```python +>>> d = {'name': 'ethan', 'age': 20} +>>> rv = d.clear() +>>> d +{} +>>> print rv +None +``` + +再看看一个例子: + +```python +>>> d1 = {} +>>> d2 = d1 +>>> d2['name'] = 'ethan' +>>> d1 +{'name': 'ethan'} +>>> d2 +{'name': 'ethan'} +>>> d1 = {} # d1 变为空字典 +>>> d2 +{'name': 'ethan'} # d2 不受影响 +``` + +在上面,d1 和 d2 最初对应同一个字典,而后我们使用 `d1 = {}` 使其变成一个空字典,但此时 d2 不受影响。如果希望 d1 变成空字典之后,d2 也变成空字典,则可以使用 clear 方法: + +```python +>>> d1 = {} +>>> d2 = d1 +>>> d2['name'] = 'ethan' +>>> d1 +{'name': 'ethan'} +>>> d2 +{'name': 'ethan'} +>>> d1.clear() # d1 清空之后,d2 也为空 +>>> d1 +{} +>>> d2 +{} +``` + +## copy + +copy 方法实现的是浅复制(shallow copy)。它具有以下特点: + +- 对可变对象的修改保持同步; +- 对不可变对象的修改保持独立; + +看看例子: + +```python +# name 的值是不可变对象,books 的值是可变对象 +>>> d1 = {'name': 'ethan', 'books': ['book1', 'book2', 'book3']} +>>> d2 = d1.copy() +>>> d2['name'] = 'peter' # d2 对不可变对象的修改不会改变 d1 +>>> d2 +{'books': ['book1', 'book2', 'book3'], 'name': 'peter'} +>>> d1 +{'books': ['book1', 'book2', 'book3'], 'name': 'ethan'} +>>> d2['books'].remove('book2') # d2 对可变对象的修改会影响 d1 +>>> d2 +{'books': ['book1', 'book3'], 'name': 'peter'} +>>> d1 +{'books': ['book1', 'book3'], 'name': 'ethan'} +>>> d1['books'].remove('book3') # d1 对可变对象的修改会影响 d2 +>>> d1 +{'books': ['book1'], 'name': 'ethan'} +>>> d2 +{'books': ['book1'], 'name': 'peter'} +``` + +和浅复制对应的是深复制(deep copy),它会创造出一个副本,跟原来的对象没有关系,可以通过 copy 模块的 deepcopy 函数来实现: + +```python +>>> from copy import deepcopy +>>> d1 = {'name': 'ethan', 'books': ['book1', 'book2', 'book3']} +>>> d2 = deepcopy(d1) # 创造出一个副本 +>>> +>>> d2['books'].remove('book2') # 对 d2 的任何修改不会影响到 d1 +>>> d2 +{'books': ['book1', 'book3'], 'name': 'ethan'} +>>> d1 +{'books': ['book1', 'book2', 'book3'], 'name': 'ethan'} +>>> +>>> d1['books'].remove('book3') # 对 d1 的任何修改也不会影响到 d2 +>>> d1 +{'books': ['book1', 'book2'], 'name': 'ethan'} +>>> d2 +{'books': ['book1', 'book3'], 'name': 'ethan'} +``` + +## get + +当我们试图访问字典中不存在的项时会出现 KeyError,但使用 get 就可以避免这个问题。 + +看看例子: + +```python +>>> d = {} +>>> d['name'] +Traceback (most recent call last): + File "", line 1, in +KeyError: 'name' +>>> print d.get('name') +None +>>> d.get('name', 'ethan') # 'name' 不存在,使用默认值 'ethan' +'ethan' +>>> d +{} +``` + +## setdefault + +setdefault 方法用于对字典设定键值。使用形式如下: + +``` +dict.setdefault(key, default=None) +``` + +看看例子: + +```python +>>> d = {} +>>> d.setdefault('name', 'ethan') # 返回设定的默认值 'ethan' +'ethan' +>>> d # d 被更新 +{'name': 'ethan'} +>>> d['age'] = 20 +>>> d +{'age': 20, 'name': 'ethan'} +>>> d.setdefault('age', 18) # age 已存在,返回已有的值,不会更新字典 +20 +>>> d +{'age': 20, 'name': 'ethan'} +``` + +可以看到,当键不存在的时候,setdefault 返回设定的默认值并且更新字典。当键存在的时候,会返回已有的值,但不会更新字典。 + +## update + +update 方法用于将一个字典添加到原字典,如果存在相同的键则会进行覆盖。 + +看看例子: + +```python +>>> d = {} +>>> d1 = {'name': 'ethan'} +>>> d.update(d1) # 将字典 d1 添加到 d +>>> d +{'name': 'ethan'} +>>> d2 = {'age': 20} +>>> d.update(d2) # 将字典 d2 添加到 d +>>> d +{'age': 20, 'name': 'ethan'} +>>> d3 = {'name': 'michael'} # 将字典 d3 添加到 d,存在相同的 key,则覆盖 +>>> d.update(d3) +>>> d +{'age': 20, 'name': 'michael'} +``` +## items/iteritems + +items 方法将所有的字典项以列表形式返回,这些列表项的每一项都来自于(键,值)。我们也经常使用这个方法来对字典进行遍历。 + +看看例子: + +```python +>>> d = {'name': 'ethan', 'age': 20} +>>> d.items() +[('age', 20), ('name', 'ethan')] +>>> for k, v in d.items(): +... print '%s: %s' % (k, v) +... +age: 20 +name: ethan +``` + +iteritems 的作用大致相同,但会返回一个迭代器对象而不是列表,同样,我们也可以使用这个方法来对字典进行遍历,而且这也是推荐的做法: + +```python +>>> d = {'name': 'ethan', 'age': 20} +>>> d.iteritems() + +>>> for k, v in d.iteritems(): +... print '%s: %s' % (k, v) +... +age: 20 +name: ethan +``` + +## keys/iterkeys + +keys 方法将字典的键以列表形式返回,iterkeys 则返回针对键的迭代器。 + +看看例子: + +```python +>>> d = {'name': 'ethan', 'age': 20} +>>> d.keys() +['age', 'name'] +>>> d.iterkeys() + +``` + +## values/itervalues + +values 方法将字典的值以列表形式返回,itervalues 则返回针对值的迭代器。 + +看看例子: + +```python +>>> d = {'name': 'ethan', 'age': 20} +>>> d.values() +[20, 'ethan'] +>>> d.itervalues() + +``` + +## pop + +pop 方法用于将某个键值对从字典移除,并返回给定键的值。 + +看看例子: + +```python +>>> d = {'name': 'ethan', 'age': 20} +>>> d.pop('name') +'ethan' +>>> d +{'age': 20} +``` + +## popitem + +popitem 用于随机移除字典中的某个键值对。 + +看看例子: + +```python +>>> d = {'id': 10, 'name': 'ethan', 'age': 20} +>>> d.popitem() +('age', 20) +>>> d +{'id': 10, 'name': 'ethan'} +>>> d.popitem() +('id', 10) +>>> d +{'name': 'ethan'} +``` + +# 对元素为字典的列表排序 + +事实上,我们很少直接对字典进行排序,而是对元素为字典的列表进行排序。 + +比如,存在下面的 students 列表,它的元素是字典: + +```python +students = [ + {'name': 'john', 'score': 'B', 'age': 15}, + {'name': 'jane', 'score': 'A', 'age': 12}, + {'name': 'dave', 'score': 'B', 'age': 10}, + {'name': 'ethan', 'score': 'C', 'age': 20}, + {'name': 'peter', 'score': 'B', 'age': 20}, + {'name': 'mike', 'score': 'C', 'age': 16} +] +``` + +- 按 score 从小到大排序 + +```python +>>> sorted(students, key=lambda stu: stu['score']) +[{'age': 12, 'name': 'jane', 'score': 'A'}, + {'age': 15, 'name': 'john', 'score': 'B'}, + {'age': 10, 'name': 'dave', 'score': 'B'}, + {'age': 20, 'name': 'peter', 'score': 'B'}, + {'age': 20, 'name': 'ethan', 'score': 'C'}, + {'age': 16, 'name': 'mike', 'score': 'C'}] +``` + +需要注意的是,这里是按照字母的 ascii 大小排序的,所以 score 从小到大,即从 'A' 到 'C'。 + + +- 按 score 从大到小排序 + +```python +>>> sorted(students, key=lambda stu: stu['score'], reverse=True) # reverse 参数 +[{'age': 20, 'name': 'ethan', 'score': 'C'}, + {'age': 16, 'name': 'mike', 'score': 'C'}, + {'age': 15, 'name': 'john', 'score': 'B'}, + {'age': 10, 'name': 'dave', 'score': 'B'}, + {'age': 20, 'name': 'peter', 'score': 'B'}, + {'age': 12, 'name': 'jane', 'score': 'A'}] +``` + +- 按 score 从小到大,再按 age 从小到大 + +```python +>>> sorted(students, key=lambda stu: (stu['score'], stu['age'])) +[{'age': 12, 'name': 'jane', 'score': 'A'}, + {'age': 10, 'name': 'dave', 'score': 'B'}, + {'age': 15, 'name': 'john', 'score': 'B'}, + {'age': 20, 'name': 'peter', 'score': 'B'}, + {'age': 16, 'name': 'mike', 'score': 'C'}, + {'age': 20, 'name': 'ethan', 'score': 'C'}] +``` + +- 按 score 从小到大,再按 age 从大到小 + +```python +>>> sorted(students, key=lambda stu: (stu['score'], -stu['age'])) +[{'age': 12, 'name': 'jane', 'score': 'A'}, + {'age': 20, 'name': 'peter', 'score': 'B'}, + {'age': 15, 'name': 'john', 'score': 'B'}, + {'age': 10, 'name': 'dave', 'score': 'B'}, + {'age': 20, 'name': 'ethan', 'score': 'C'}, + {'age': 16, 'name': 'mike', 'score': 'C'}] +``` + +# 参考资料 + +- 《Python 基础教程》 +- [python字典和集合 | Alex's Blog](http://codingnow.cn/language/353.html) +- [Python中实现多属性排序 | 酷 壳 - CoolShell.cn](http://coolshell.cn/articles/435.html) +- [HowTo/Sorting - Python Wiki](https://wiki.python.org/moin/HowTo/Sorting) +- [How do I sort a list of dictionaries by values of the dictionary in Python? - Stack Overflow](http://stackoverflow.com/questions/72899/how-do-i-sort-a-list-of-dictionaries-by-values-of-the-dictionary-in-python) + + diff --git a/Datatypes/list.md b/Datatypes/list.md index ecc4092..0377f2c 100644 --- a/Datatypes/list.md +++ b/Datatypes/list.md @@ -1,2 +1,261 @@ # 列表 +字符串和元组是不可变的,而列表是可变(mutable)的,可以对它进行随意修改。我们还可以将字符串和元组转换成一个列表,只需使用 `list` 函数,比如: + +```python +>>> s = 'hello' +>>> list(s) +['h', 'e', 'l', 'l', 'o'] +>>> a = (1, 2, 3) +>>> list(a) +[1, 2, 3] +``` + +本文主要介绍常用的列表方法: + +- index +- count +- append +- extend +- insert +- pop +- remove +- reverse +- sort + +# index + +index 方法用于从列表中找出某个元素的位置,如果有多个相同的元素,则返回第一个元素的位置。 + +看看例子: + +```python +>>> numbers = [1, 2, 3, 4, 5, 5, 7, 8] +>>> numbers.index(5) # 列表有两个 5,返回第一个元素的位置 +4 +>>> numbers.index(2) +1 +>>> words = ['hello', 'world', 'you', 'me', 'he'] +>>> words.index('me') +3 +>>> words.index('her') # 如果没找到元素,则会抛出异常 +Traceback (most recent call last): + File "", line 1, in +ValueError: 'her' is not in list +``` + +# count + +count 方法用于统计某个元素在列表中出现的次数。 + +看看例子: + +```python +>>> numbers = [1, 2, 3, 4, 5, 5, 6, 7] +>>> numbers.count(2) # 出现一次 +1 +>>> numbers.count(5) # 出现了两次 +2 +>>> numbers.count(9) # 没有该元素,返回 0 +0 +``` + +# append + +append 方法用于在列表末尾增加新的元素。 + +看看例子: + +```python +>>> numbers = [1, 2, 3, 4, 5, 5, 6, 7] +>>> numbers.append(8) # 增加 8 这个元素 +>>> numbers +[1, 2, 3, 4, 5, 5, 6, 7, 8] +>>> numbers.append([9, 10]) # 增加 [9, 10] 这个元素 +>>> numbers +[1, 2, 3, 4, 5, 5, 6, 7, 8, [9, 10]] +``` + +# extend + +extend 方法将一个新列表的元素添加到原列表中。 + +看看例子: + +```python +>>> a = [1, 2, 3] +>>> b = [4, 5, 6] +>>> a.extend(b) +>>> a +[1, 2, 3, 4, 5, 6] +>>> +>>> a.extend(3) +Traceback (most recent call last): + File "", line 1, in +TypeError: 'int' object is not iterable +>>> a.extend([3]) +>>> a +[1, 2, 3, 4, 5, 6, 3] +``` + +注意到,虽然 append 和 extend 可接收一个列表作为参数,但是 append 方法是将其作为一个元素添加到列表中,而 extend 则是将新列表的元素逐个添加到原列表中。 + +# insert + +insert 方法用于将某个元素添加到某个位置。 + +看看例子: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6] +>>> numbers.insert(3, 9) +>>> numbers +[1, 2, 3, 9, 4, 5, 6] +``` + +# pop + +pop 方法用于移除列表中的一个元素(默认是最后一个),并且返回该元素的值。 + +看看例子: + +```python +>>> numbers = [1, 2, 3, 4, 5, 6] +>>> numbers.pop() +6 +>>> numbers +[1, 2, 3, 4, 5] +>>> numbers.pop(3) +4 +>>> numbers +[1, 2, 3, 5] +``` + +# remove + +remove 方法用于移除列表中的某个匹配元素,如果有多个匹配,则移除第一个。 + +看看例子: + +```python +>>> numbers = [1, 2, 3, 5, 6, 7, 5, 8] +>>> numbers.remove(5) # 有两个 5,移除第 1 个 +>>> numbers +[1, 2, 3, 6, 7, 5, 8] +>>> numbers.remove(9) # 没有匹配的元素,抛出异常 +Traceback (most recent call last): + File "", line 1, in +ValueError: list.remove(x): x not in list +``` + +# reverse + +reverse 方法用于将列表中的元素进行反转。 + +看看例子: + +```python +>>> numbers = [1, 2, 3, 5, 6, 7, 5, 8] +>>> numbers.reverse() +>>> numbers +[8, 5, 7, 6, 5, 3, 2, 1] +``` + +# sort + +sort 方法用于对列表进行排序,注意该方法会改变原来的列表,而不是返回新的排序列表,另外,sort 方法的返回值是空。 + +看看例子: + +```python +>>> a = [4, 3, 6, 8, 9, 1] +>>> b = a.sort() +>>> b == None # 返回值为空 +True +>>> a +[1, 3, 4, 6, 8, 9] # 原列表已经发生改变 +``` + +如果我们不想改变原列表,而是希望返回一个排序后的列表,可以使用 sorted 函数,如下: + +```python +>>> a = [4, 3, 6, 8, 9, 1] +>>> b = sorted(a) # 返回一个排序后的列表 +>>> a +[4, 3, 6, 8, 9, 1] # 原列表没有改变 +>>> b +[1, 3, 4, 6, 8, 9] # 这是对原列表排序后的列表 +``` + +注意到,不管是 sort 方法还是 sorted 函数,默认排序都是升序排序。如果你想要降序排序,就需要指定排序参数了。比如,对 sort 方法,可以添加一个 reverse 关键字参数,如下: + +```python +>>> a = [4, 3, 6, 8, 9, 1] +>>> a.sort(reverse=True) # 反向排序 +>>> a +[9, 8, 6, 4, 3, 1] +``` + +该参数对 sorted 函数同样适用: + +```python +>>> a = [4, 3, 6, 8, 9, 1] +>>> sorted(a, reverse=True) +[9, 8, 6, 4, 3, 1] +``` + +除了 reverse 关键字参数,还可以指定 key 关键字参数,它为每个元素创建一个键,然后所有元素按照这个键来排序,比如我们想根据元素的长度来排序: + +```python +>>> s = ['ccc', 'a', 'bb', 'dddd'] +>>> s.sort(key=len) # 使用 len 作为键函数,根据元素长度排序 +>>> s +['a', 'bb', 'ccc', 'dddd'] +``` + +另外,我们还可以使用 sorted 进行多列(属性)排序。 + +看看例子: + +```python +>>> students = [ + ('john', 'B', 15), + ('jane', 'A', 12), + ('dave', 'B', 10), + ('ethan', 'C', 20), + ('peter', 'B', 20), + ('mike', 'C', 16) + ] +>>> +# 对第 3 列排序 (从小到大) +>>> sorted(students, key=lambda student: student[2]) +[('dave', 'B', 10), + ('jane', 'A', 12), + ('john', 'B', 15), + ('mike', 'C', 16), + ('ethan', 'C', 20), + ('peter', 'B', 20)] + +# 对第 2 列排序(从小到大),再对第 3 列从大到小排序 +>>> sorted(students, key=lambda student: (student[1], -student[2])) +[('jane', 'A', 12), + ('peter', 'B', 20), + ('john', 'B', 15), + ('dave', 'B', 10), + ('ethan', 'C', 20), + ('mike', 'C', 16)] +``` + +如果你想了解更多关于排序的知识,可以参考[此文](https://wiki.python.org/moin/HowTo/Sorting)。 + +# 小结 + +- 列表是可变的。 +- 列表常用的方法有 index, count, append, extend 等。 + +# 参考资料 + +- 《Python 基础教程》 +- [HowTo/Sorting - Python Wiki](https://wiki.python.org/moin/HowTo/Sorting) + + diff --git a/Datatypes/number.md b/Datatypes/number.md deleted file mode 100644 index 7579199..0000000 --- a/Datatypes/number.md +++ /dev/null @@ -1,2 +0,0 @@ -# 数字 - diff --git a/Datatypes/set.md b/Datatypes/set.md index 0045d0e..4feb79a 100644 --- a/Datatypes/set.md +++ b/Datatypes/set.md @@ -1,2 +1,109 @@ # 集合 +集合(set)和字典(dict)类似,它是一组 key 的集合,但不存储 value。集合的特性就是:key 不能重复。 + +# 集合常用操作 + +## 创建集合 + +set 的创建可以使用 `{}` 也可以使用 set 函数: + +```python +>>> s1 = {'a', 'b', 'c', 'a', 'd', 'b'} # 使用 {} +>>> s1 +set(['a', 'c', 'b', 'd']) +>>> +>>> s2 = set('helloworld') # 使用 set(),接收一个字符串 +>>> s2 +set(['e', 'd', 'h', 'l', 'o', 'r', 'w']) +>>> +>>> s3 = set(['.mp3', '.mp4', '.rmvb', '.mkv', '.mp3']) # 使用 set(),接收一个列表 +>>> s3 +set(['.mp3', '.mkv', '.rmvb', '.mp4']) +``` + +## 遍历集合 + +```python +>>> s = {'a', 'b', 'c', 'a', 'd', 'b'} +>>> for e in s: +... print e +... +a +c +b +d +``` + +## 添加元素 + +`add()` 方法可以将元素添加到 set 中,可以重复添加,但没有效果。 + +```python +>>> s = {'a', 'b', 'c', 'a', 'd', 'b'} +>>> s +set(['a', 'c', 'b', 'd']) +>>> s.add('e') +>>> s +set(['a', 'c', 'b', 'e', 'd']) +>>> s.add('a') +>>> s +set(['a', 'c', 'b', 'e', 'd']) +>>> s.add(4) +>>> s +set(['a', 'c', 'b', 4, 'd', 'e']) +``` + +## 删除元素 + +`remove()` 方法可以删除集合中的元素, 但是删除不存在的元素,会抛出 KeyError,可改用 `discard()`。 + +看看例子: + +```python +>>> s = {'a', 'b', 'c', 'a', 'd', 'b'} +>>> s +set(['a', 'c', 'b', 'd']) +>>> s.remove('a') # 删除元素 'a' +>>> s +set(['c', 'b', 'd']) +>>> s.remove('e') # 删除不存在的元素,会抛出 KeyError +Traceback (most recent call last): + File "", line 1, in +KeyError: 'e' +>>> s.discard('e') # 删除不存在的元素, 不会抛出 KeyError +``` + +## 交集/并集/差集 + +Python 中的集合也可以看成是数学意义上的无序和无重复元素的集合,因此,我们可以对两个集合作交集、并集等。 + +看看例子: + +```python +>>> s1 = {1, 2, 3, 4, 5, 6} +>>> s2 = {3, 6, 9, 10, 12} +>>> s3 = {2, 3, 4} +>>> s1 & s2 # 交集 +set([3, 6]) +>>> s1 | s2 # 并集 +set([1, 2, 3, 4, 5, 6, 9, 10, 12]) +>>> s1 - s2 # 差集 +set([1, 2, 4, 5]) +>>> s3.issubset(s1) # s3 是否是 s1 的子集 +True +>>> s3.issubset(s2) # s3 是否是 s2 的子集 +False +>>> s1.issuperset(s3) # s1 是否是 s3 的超集 +True +>>> s1.issuperset(s2) # s1 是否是 s2 的超集 +False +``` + +# 参考资料 + +- [使用dict和set - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013868193482529754158abf734c00bba97c87f89a263b000) +- [python字典和集合 | Alex's Blog](http://codingnow.cn/language/353.html) +- [python - Why is it possible to replace set() with {}? - Stack Overflow](http://stackoverflow.com/questions/36674083/why-is-it-possible-to-replace-set-with) + + diff --git a/Datatypes/string.md b/Datatypes/string.md index 9d51a75..3911358 100644 --- a/Datatypes/string.md +++ b/Datatypes/string.md @@ -1,2 +1,178 @@ # 字符串 +字符串也是一种序列,因此,通用的序列操作,比如索引,分片,加法,乘法等对它同样适用。比如: + +```python +>>> s = 'hello, ' +>>> s[0] # 索引 +'h' +>>> s[1:3] # 分片 +'el' +>>> s + 'world' # 加法 +'hello, world' +>>> s * 2 # 乘法 +'hello, hello, ' +``` + +但需要注意的是,字符串和元组一样,也是不可变的,所以你不能对它进行赋值等操作: + +```python +>>> s = 'hello' +>>> s[1] = 'ab' # 不能对它进行赋值 +Traceback (most recent call last): + File "", line 1, in +TypeError: 'str' object does not support item assignment +``` + +除了通用的序列操作,字符串还有自己的方法,比如 join, lower, upper 等。字符串的方法特别多,这里只介绍一些常用的方法,如下: + +- find +- split +- join +- strip +- replace +- translate +- lower/upper + +# find + +find 方法用于在一个字符串中查找子串,它返回子串所在位置的最左端索引,如果没有找到,则返回 -1。 + +看看例子: + +```python +>>> motto = "to be or not to be, that is a question" +>>> motto.find('be') # 返回 'b' 所在的位置,即 3 +3 +>>> motto.find('be', 4) # 指定从起始位置开始找,找到的是第 2 个 'be' +16 +>>> motto.find('be', 4, 7) # 指定起始位置和终点位置,没有找到,返回 -1 +-1 +``` + +# split + +split 方法用于将字符串分割成序列。 + +看看例子: + +```python +>>> '/user/bin/ssh'.split('/') # 使用 '/' 作为分隔符 +['', 'user', 'bin', 'ssh'] +>>> '1+2+3+4+5'.split('+') # 使用 '+' 作为分隔符 +['1', '2', '3', '4', '5'] +>>> 'that is a question'.split() # 没有提供分割符,默认使用所有空格作为分隔符 +['that', 'is', 'a', 'question'] +``` + +需要注意的是,如果不提供分隔符,则默认会使用所有空格作为分隔符(空格、制表符、换行等)。 + +# join + +join 方法可以说是 split 的逆方法,它用于将序列中的元素连接起来。 + +看看例子: + +```python +>>> '/'.join(['', 'user', 'bin', 'ssh']) +'/user/bin/ssh' +>>> +>>> '+'.join(['1', '2', '3', '4', '5']) +'1+2+3+4+5' +>>> ' '.join(['that', 'is', 'a', 'question']) +'that is a question' +>>> ''.join(['h', 'e', 'll', 'o']) +'hello' +>>> '+'.join([1, 2, 3, 4, 5]) # 不能是数字 +Traceback (most recent call last): + File "", line 1, in +TypeError: sequence item 0: expected string, int found +``` + +# strip + +strip 方法用于移除字符串左右两侧的空格,但不包括内部,当然也可以指定需要移除的字符串。 + +看看例子: + +```python +>>> ' hello world! '.strip() # 移除左右两侧空格 +'hello world!' +>>> '%%% hello world!!! ####'.strip('%#') # 移除左右两侧的 '%' 或 '#' +' hello world!!! ' +>>> '%%% hello world!!! ####'.strip('%# ') # 移除左右两侧的 '%' 或 '#' 或空格 +'hello world!!!' +``` + +# replace + +replace 方法用于替换字符串中的**所有**匹配项。 + +看看例子: + +```python +>>> motto = 'To be or not To be, that is a question' +>>> motto.replace('To', 'to') # 用 'to' 替换所有的 'To',返回了一个新的字符串 +'to be or not to be, that is a question' +>>> motto # 原字符串保持不变 +'To be or not To be, that is a question' +``` + +# translate + +translate 方法和 replace 方法类似,也可以用于替换字符串中的某些部分,但 **translate 方法只处理单个字符**。 + +translate 方法的使用形式如下: + +```python +str.translate(table[, deletechars]); +``` + +其中,table 是一个包含 256 个字符的转换表,可通过 maketrans 方法转换而来,deletechars 是字符串中要过滤的字符集。 + +看看例子: + +```python +>>> from string import maketrans +>>> table = maketrans('aeiou', '12345') +>>> motto = 'to be or not to be, that is a question' +>>> motto.translate(table) +'t4 b2 4r n4t t4 b2, th1t 3s 1 q52st34n' +>>> motto +'to be or not to be, that is a question' +>>> motto.translate(table, 'rqu') # 移除所有的 'r', 'q', 'u' +'t4 b2 4 n4t t4 b2, th1t 3s 1 2st34n' +``` + +可以看到,maketrans 接收两个参数:两个等长的字符串,表示第一个字符串的每个字符用第二个字符串对应位置的字符替代,在上面的例子中,就是 'a' 用 '1' 替代,'e' 用 '2' 替代,等等,注意,是单个字符的代替,而不是整个字符串的替代。因此,motto 中的 o 都被替换为 4,e 都被替换为 2,等等。 + +# lower/upper + +lower/upper 用于返回字符串的大写或小写形式。 + +看看例子: + +```python +>>> x = 'PYTHON' +>>> x.lower() +'python' +>>> x +'PYTHON' +>>> +>>> y = 'python' +>>> y.upper() +'PYTHON' +>>> y +'python' +``` + +# 小结 + +- 字符串是不可变对象,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回。 +- translate 针对单个字符进行替换。 + +# 参考资料 + +- 《python 基础教程》 + + diff --git a/Datatypes/tuple.md b/Datatypes/tuple.md index f2c1dc9..53235bc 100644 --- a/Datatypes/tuple.md +++ b/Datatypes/tuple.md @@ -1,2 +1,51 @@ # 元组 +在 Python 中,**元组是一种不可变序列**,它使用圆括号来表示: + +```python +>>> a = (1, 2, 3) # a 是一个元组 +>>> a +(1, 2, 3) +>>> a[0] = 6 # 元组是不可变的,不能对它进行赋值操作 +Traceback (most recent call last): + File "", line 1, in +TypeError: 'tuple' object does not support item assignment +``` + +# 空元组 + +创建一个空元组可以用没有包含内容的圆括号来表示: + +```python +>>> a = () +>>> a +() +``` + +# 一个值的元组 + +创建一个值的元组需要在值后面再加一个逗号,这个比较特殊,需要牢牢记住: + +```python +>>> a = (12,) # 在值后面再加一个逗号 +>>> a +(12,) +>>> type(a) + +>>> +>>> b = (12) # 只是使用括号括起来,而没有加逗号,不是元组,本质上是 b = 12 +>>> b +12 +>>> type(b) + +``` + +# 元组操作 + +元组也是一种序列,因此也可以对它进行索引、分片等。由于它是不可变的,因此就没有类似列表的 append, extend, sort 等方法。 + +# 小结 + +- 元组是不可变的。 +- 创建一个值的元组需要在值后面再加一个逗号。 + diff --git a/Exception/README.md b/Exception/README.md index bd4ea6d..3ba210e 100644 --- a/Exception/README.md +++ b/Exception/README.md @@ -1,2 +1,316 @@ -# 错误和异常 +# 异常处理 + +我们在编写程序的时候,经常需要对异常情况做处理。比如,当一个数试图除以 0 时,我们需要捕获这个异常情况并做处理。你可能会使用类似 if/else 的条件语句来对异常情况做判断,比如,判断除法的分母是否为零,如果为零,则打印错误信息。 + +这在某些简单的情况下是可以的,但是,在大多数时候,我们应该使用 Python 的**异常处理**机制。这主要有两方面的好处: + +- 一方面,你可以选择忽略某些不重要的异常事件,或在需要的时候自己引发异常; +- 另一方面,异常处理不会搞乱原来的代码逻辑,但如果使用一大堆 if/else 语句,不仅会没效率和不灵活,而且会让代码相当难读; + +# 异常对象 + +Python 用**异常对象(exception object)**来表示异常情况。当程序在运行过程中遇到错误时,会引发异常。如果异常对象未被处理或捕捉,程序就会用所谓的回溯(Traceback,一种错误信息)终止执行。 + +比如: + +```python +>>> 1/0 +Traceback (most recent call last): + File "", line 1, in +ZeroDivisionError: integer division or modulo by zero +``` + +上面的 `ZeroDivisionError` 就是一个异常类,相应的异常对象就是该类的实例。Python 中所有的异常类都是从 `BaseException` 类派生的,常见的异常类型可以在[这里][1]查看。 + +# 使用 try/except 捕捉异常 + +在编写程序的时候,如果我们知道某段代码可能会导致某种异常,而又不希望程序以堆栈跟踪的形式终止,这时我们可以根据需要添加 `try/except` 或者 `try/finally` 语句(或者它们的组合)进行处理。一般来说,有以下使用形式: + +``` +try...except... +try...except...else... +try...except...else...finally... +try...except...except...else...finally... +try...finally... +``` + +## 基本形式 + +捕捉异常的基本形式是 `try...except`。 + +让我们看看第一个例子: + +```python +try: + x = input('Enter x: ') + y = input('Enter y: ') + print x / y +except ZeroDivisionError as e: + print 'Error:',e + +print 'hello world' +``` + +当 y = 0 时,看看执行结果: + +```python +Enter x: 3 +Enter y: 0 +Error: integer division or modulo by zero +hello world +``` + +可以看到,我们的程序正确捕获了`除以零`的异常,而且程序没有以堆栈跟踪的形式终止,而是继续执行后面的代码,打印出 'hello world'。 + +## 多个 except 子句 + +有时,我们的程序可能会出现多个异常,这时可以用多个 except 子句来处理这种情况。 + +让我们继续看第一个例子,如果 y 输入的是一个非数字的值,就会产生另外一个异常: + +```python +Enter x: 2 +Enter y: 'a' # y 的输入是一个字符 +---------------------------------------------------------------------- +TypeError Traceback (most recent call last) + in () + 2 x = input('Enter x: ') + 3 y = input('Enter y: ') +----> 4 print x / y + 5 except ZeroDivisionError as e: + 6 print e + +TypeError: unsupported operand type(s) for /: 'int' and 'str' +``` + +可以看到,当 y 输入一个字符 'a' 之后,程序产生了一个 `TypeError` 异常,并且终止,这是因为我们的 except 子句只是捕获了 `ZeroDivisionError` 异常,为了能捕获`TypeError` 异常,我们可以再加一个 except 子句,完整代码如下: + +```python +try: + x = input('Enter x: ') + y = input('Enter y: ') + print x / y +except ZeroDivisionError as e: # 处理 ZeroDivisionError 异常 + print 'ZeroDivisionError:',e +except TypeError as e: # 处理 TypeError 异常 + print 'TypeError:',e + +print 'hello world' +``` + +当 y 输入 'a' 时,看看执行结果: + +```python +Enter x: 3 +Enter y: 'a' +TypeError: unsupported operand type(s) for /: 'int' and 'str' +hello world +``` + +## 捕捉未知异常 + +事实上,在编写程序的时候,我们很难预料到程序的所有异常情况。比如,对于第一个例子,我们可以预料到一个 `ZeroDivisionError` 异常,如果细心一点,也会预料到一个 `TypeError` 异常,可是,还是有一些其他情况我们没有考虑到,比如在输入 x 的时候,我们直接按回车键,这时又会引发一个异常,程序也会随之挂掉: + +```python +Enter x: # 这里输入回车键 +Traceback (most recent call last): + File "", line 2, in + File "", line 0 + + ^ +SyntaxError: unexpected EOF while parsing +``` + +那么,我们应该怎么在程序捕获某些难以预料的异常呢?我们在上文说过,Python 中所有的异常类都是从 `BaseException` 类派生的,也就是说,`ZeroDivisionError`、`SyntaxError` 等都是它的子类,因此,对于某些难以预料的异常,我们就可以使用 `BaseException` 来捕获,在大部分情况下,我们也可以使用 `Exception` 来捕获,因为 `Exception` 是大部分异常的父类,可以到[这里][1]查看所有异常类的继承关系。 + +因此,对于第一个例子,我们可以把程序做一些修改,使其更加健壮: + +```python +try: + x = input('Enter x: ') + y = input('Enter y: ') + print x / y +except ZeroDivisionError as e: # 捕获 ZeroDivisionError 异常 + print 'ZeroDivisionError:',e +except TypeError as e: # 捕获 TypeError 异常 + print 'TypeError:',e +except BaseException as e: # 捕获其他异常 + print 'BaseException:',e + +print 'hello world' +``` + +注意到,我们把 `BaseException` 写在了最后一个 except 子句。如果你把它写在了第一个 except 子句,由于 `BaseException` 是所有异常的父类,那么程序的所有异常都会被第一个 except 子句捕获。 + +## else 子句 + +我们可以在 except 子句后面加一个 else 子句。当没有异常发生时,会自动执行 else 子句。 + +对第一个例子,加入 else 子句: + +```python +try: + x = input('Enter x: ') + y = input('Enter y: ') + print x / y +except ZeroDivisionError as e: + print 'ZeroDivisionError:',e +except TypeError as e: + print 'TypeError:',e +except BaseException as e: + print 'BaseException:',e +else: + print 'no error!' + +print 'hello world' +``` + +看看执行结果: + +``` +Enter x: 6 +Enter y: 2 +3 +no error! +hello world +``` + +## finally 子句 + +finally 子句不管有没有出现异常都会被执行。 + +看看例子: + +```python +try: + x = 1/0 + print x +finally: + print 'DONE' +``` + +执行结果: + +``` +DONE +Traceback (most recent call last): + File "", line 2, in +ZeroDivisionError: integer division or modulo by zero +``` + +再看一个例子: + +```python +try: + x = 1/0 + print x +except ZeroDivisionError as e: + print 'ZeroDivisionError:',e +finally: + print 'DONE' +``` + +执行结果: + +``` +ZeroDivisionError: integer division or modulo by zero +DONE +``` + +## 使用 raise 手动引发异常 + +有时,我们使用 except 捕获了异常,又想把异常抛出去,这时可以使用 raise 语句。 + +看看例子: + +```python +try: + x = input('Enter x: ') + y = input('Enter y: ') + print x / y +except ZeroDivisionError as e: + print 'ZeroDivisionError:',e +except TypeError as e: + print 'TypeError:',e +except BaseException as e: + print 'BaseException:',e + raise # 使用 raise 抛出异常 +else: + print 'no error!' + +print 'hello world' +``` + +运行上面代码,当 x 输入一个回车键时,错误会被打印出来,并被抛出: + +```python +Enter x: # 这里输入回车键 +BaseException: unexpected EOF while parsing (, line 0) +Traceback (most recent call last): + File "", line 2, in + File "", line 0 + + ^ +SyntaxError: unexpected EOF while parsing +``` + +上面的 raise 语句是不带参数的,它会把当前错误原样抛出。事实上,我们也创建自己的异常类,并抛出自定义的异常。 + +**创建自定义的异常类需要从 Exception 类继承**,可以间接继承或直接继承,也就是可以继承其他的内建异常类。比如: + +```python +# 自定义异常类 +class SomeError(Exception): + pass + +try: + x = input('Enter x: ') + y = input('Enter y: ') + print x / y +except ZeroDivisionError as e: + print 'ZeroDivisionError:',e +except TypeError as e: + print 'TypeError:',e +except BaseException as e: + print 'BaseException:',e + raise SomeError('invalid value') # 抛出自定义的异常 +else: + print 'no error!' + +print 'hello world' +``` + +运行上面代码,当 x 输入一个回车键时,错误被打印出来,并抛出我们自定义的异常: + +``` +Enter x: +BaseException: unexpected EOF while parsing (, line 0) +---------------------------------------------------------------------- +SomeError Traceback (most recent call last) + in () + 12 except BaseException as e: + 13 print 'BaseException:',e +---> 14 raise SomeError('invalid value') + 15 else: + 16 print 'no error!' + +SomeError: invalid value +``` + +# 小结 + +- Python 中所有的异常类都是从 `BaseException` 类派生的。 +- 通过 try/except 来捕捉异常,可以使用多个 except 子句来分别处理不同的异常。 +- else 子句在主 try 块没有引发异常的情况下被执行。 +- finally 子句不管是否发生异常都会被执行。 +- 通过继承 Exception 类可以创建自己的异常类。 + +# 参考资料 + +- 《Python 基础教程》 +- [Python 异常处理](http://www.runoob.com/python/python-exceptions.html) + + +[1]: https://docs.python.org/2/library/exceptions.html#exception-hierarchy + diff --git a/File-Directory/README.md b/File-Directory/README.md index 2d21ddc..35f46a8 100644 --- a/File-Directory/README.md +++ b/File-Directory/README.md @@ -1,2 +1,9 @@ # 文件和目录 +本章主要介绍文件和目录操作,包含以下部分: + +* [读写文本文件](./text_file_io.md) +* [读写二进制文件](./binary_file_io.md) +* [os 模块](./os.md) + + diff --git a/File-Directory/binary_file_io.md b/File-Directory/binary_file_io.md new file mode 100644 index 0000000..61a1e76 --- /dev/null +++ b/File-Directory/binary_file_io.md @@ -0,0 +1,57 @@ +# 读写二进制文件 + +Python 不仅支持文本文件的读写,也支持二进制文件的读写,比如图片,声音文件等。 + +# 读取二进制文件 + +读取二进制文件使用 'rb' 模式。 + +这里以图片为例: + +```python +with open('test.png', 'rb') as f: + image_data = f.read() # image_data 是字节字符串格式的,而不是文本字符串 +``` + +这里需要注意的是,在读取二进制数据时,返回的数据是字节字符串格式的,而不是文本字符串。一般情况下,我们可能会对它进行编码,比如 [base64](https://en.wikipedia.org/wiki/Base64) 编码,可以这样做: + +```python +import base64 + +with open('test.png', 'rb') as f: + image_data = f.read() + base64_data = base64.b64encode(image_data) # 使用 base64 编码 + print base64_data +``` + +下面是执行结果的一部分: + +```python +iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAACGFjVEw +``` + +# 写入二进制文件 + +写入二进制文件使用 'wb' 模式。 + +以图片为例: + +```python +with open('test.png', 'rb') as f: + image_data = f.read() + +with open('/Users/ethan/test2.png', 'wb') as f: + f.write(image_data) +``` + + +# 小结 + +- 读取二进制文件使用 'rb' 模式。 +- 写入二进制文件使用 'wb' 模式。 + +# 参考资料 + +- [读写字节数据 — python3-cookbook 2.0.0 文档](http://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p04_read_write_binary_data.html) + + diff --git a/File-Directory/os.md b/File-Directory/os.md new file mode 100644 index 0000000..75cac12 --- /dev/null +++ b/File-Directory/os.md @@ -0,0 +1,137 @@ +# os 模块 + +Python 的 os 模块封装了常见的文件和目录操作,本文只列出部分常用的方法,更多的方法可以查看[官方文档](https://docs.python.org/3/library/os.path.html)。 + +下面是部分常见的用法: + +| 方法 | 说明 | +| :-: | :-: | +|  os.mkdir |  创建目录 | +| os.rmdir |  删除目录 | +|  os.rename |  重命名 | +| os.remove | 删除文件 | +|  os.getcwd | 获取当前工作路径 | +| os.walk | 遍历目录 | +| os.path.join | 连接目录与文件名 | +| os.path.split | 分割文件名与目录 | +| os.path.abspath | 获取绝对路径 | +| os.path.dirname | 获取路径 | +| os.path.basename | 获取文件名或文件夹名 | +| os.path.splitext | 分离文件名与扩展名 | +| os.path.isfile | 判断给出的路径是否是一个文件 | +| os.path.isdir | 判断给出的路径是否是一个目录 | + +# 例子 + +后文的例子以下面的目录结构为参考,工作目录为 `/Users/ethan/coding/python`。 + +``` +Users/ethan +└── coding + └── python + ├── hello.py - 文件 + └── web - 目录 +``` + +看看例子: + +- os.path.abspath:获取文件或目录的绝对路径 + +```python +$ pwd +/Users/ethan/coding/python +$ python +>>> import os # 记得导入 os 模块 +>>> os.path.abspath('hello.py') +'/Users/ethan/coding/python/hello.py' +>>> os.path.abspath('web') +'/Users/ethan/coding/python/web' +>>> os.path.abspath('.') # 当前目录的绝对路径 +'/Users/ethan/coding/python' +``` + +- os.path.dirname:获取文件或文件夹的路径 + +```python +>>> os.path.dirname('/Users/ethan/coding/python/hello.py') +'/Users/ethan/coding/python' +>>> os.path.dirname('/Users/ethan/coding/python/') +'/Users/ethan/coding/python' +>>> os.path.dirname('/Users/ethan/coding/python') +'/Users/ethan/coding' +``` + +- os.path.basename:获取文件名或文件夹名 + +```python +>>> os.path.basename('/Users/ethan/coding/python/hello.py') +'hello.py' +>>> os.path.basename('/Users/ethan/coding/python/') +'' +>>> os.path.basename('/Users/ethan/coding/python') +'python' +``` + +- os.path.splitext:分离文件名与扩展名 + +```python +>>> os.path.splitext('/Users/ethan/coding/python/hello.py') +('/Users/ethan/coding/python/hello', '.py') +>>> os.path.splitext('/Users/ethan/coding/python') +('/Users/ethan/coding/python', '') +>>> os.path.splitext('/Users/ethan/coding/python/') +('/Users/ethan/coding/python/', '') +``` + +- os.path.split:分离目录与文件名 + +```python +>>> os.path.split('/Users/ethan/coding/python/hello.py') +('/Users/ethan/coding/python', 'hello.py') +>>> os.path.split('/Users/ethan/coding/python/') +('/Users/ethan/coding/python', '') +>>> os.path.split('/Users/ethan/coding/python') +('/Users/ethan/coding', 'python') +``` + +- os.path.isfile/os.path.isdir + +```python +>>> os.path.isfile('/Users/ethan/coding/python/hello.py') +True +>>> os.path.isdir('/Users/ethan/coding/python/') +True +>>> os.path.isdir('/Users/ethan/coding/python') +True +>>> os.path.isdir('/Users/ethan/coding/python/hello.py') +False +``` + +- os.walk + +os.walk 是遍历目录常用的模块,它返回一个包含 3 个元素的元组:(dirpath, dirnames, filenames)。dirpath 是以 string 字符串形式返回该目录下所有的绝对路径;dirnames 是以列表 list 形式返回每一个绝对路径下的文件夹名字;filenames 是以列表 list 形式返回该路径下所有文件名字。 + +```python +>>> for root, dirs, files in os.walk('/Users/ethan/coding'): +... print root +... print dirs +... print files +... +/Users/ethan/coding +['python'] +[] +/Users/ethan/coding/python +['web2'] +['hello.py'] +/Users/ethan/coding/python/web2 +[] +[] +``` + +# 参考资料 + +- [关于python文件操作 - Rollen Holt](http://www.cnblogs.com/rollenholt/archive/2012/04/23/2466179.html) +- [操作文件和目录 - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013868321590543ff305fb9f9949f08d760883cc243812000) +- [Python os.walk的用法与举例](http://blog.csdn.net/bagboy_taobao_com/article/details/893) + + diff --git a/File-Directory/text_file_io.md b/File-Directory/text_file_io.md new file mode 100644 index 0000000..9e11972 --- /dev/null +++ b/File-Directory/text_file_io.md @@ -0,0 +1,220 @@ +# 读写文本文件 + +读写文件是最常见的 IO 操作。通常,我们使用 `input` 从控制台读取输入,使用 `print` 将内容输出到控制台。实际上,我们也经常从文件读取输入,将内容写到文件。 + +# 读文件 + +在 Python 中,读文件主要分为三个步骤: + +- 打开文件 +- 读取内容 +- 关闭文件 + +一般使用形式如下: + +```python +try: + f = open('/path/to/file', 'r') # 打开文件 + data = f.read() # 读取文件内容 +finally: + if f: + f.close() # 确保文件被关闭 +``` + +注意到,我们在代码中加了 `try...finally`,这是因为,如果打开和读取文件时出现错误,文件就没有被关闭。为了确保在任何情况下,文件都能被关闭,我们加了 `try...finally`。 + +上面的代码中,'r' 模式表示读模式,`open` 函数的常用模式主要有: + +| ‘r' | 读模式 | +| :-: | :-: | +| ‘w' | 写模式 | +| ‘a' | 追加模式 | +| ‘b' | 二进制模式(可添加到其他模式中使用) | +| ‘+' | 读/写模式(可添加到其他模式中使用) | + +上面的读文件做法很繁琐,我们可以使用 Python 的 `with` 语句来帮我们自动调用 `close` 方法: + +```python +with open('/path/to/file', 'r') as f: + data = f.read() +``` + +可以看到,这种方式很简洁,而且还能在出现异常的情况下自动关闭文件。 + +通常而言,读取文件有以下几种方式: + +- 一次性读取所有内容,使用 `read()` 或 `readlines()`; +- 按字节读取,使用 `read(size)`; +- 按行读取,使用 `readline()`; + +## 读取所有内容 + +读取所有内容可以使用 `read()` 或 `readlines()`。我们在上面已经介绍过 `read()` 了,现在,让我们看看 `readlines()`。 + +`readlines()` 方法会把文件读入一个字符串列表,在列表中每个字符串就是一行。 + +假设有一个文件 data.txt,它的文件内容如下(数字之间的间隔符是'\t'): + +```python +10 1 9 9 +6 3 2 8 +20 10 3 23 +1 4 1 10 +10 8 6 3 +10 2 1 6 +``` + +我们使用 `readlines()` 将文件读入一个字符串列表: + +```python +with open('data.txt', 'r') as f: + lines = f.readlines() + line_num = len(lines) + print lines + print line_num +``` + +执行结果: + +``` +['10\t1\t9\t9\n', '6\t3\t2\t8\n', '20\t10\t3\t23\n', '1\t4\t1\t10\n', '10\t8\t6\t3\n', '10\t2\t1\t6'] +6 +``` + +可以看到,列表中的每个元素都是一个字符串,刚好对应文件中的每一行。 + +## 按字节读取 + +如果文件较小,一次性读取所有内容确实比较方便。但是,如果文件很大,比如有 100G,那就不能一次性读取所有内容了。这时,我们构造一个固定长度的缓冲区,来不断读取文件内容。 + +看看例子: + +```python +with open('path/to/file', 'r') as f: + while True: + piece = f.read(1024) # 每次读取 1024 个字节(即 1 KB)的内容 + if not piece: + break + print piece +``` + +在上面,我们使用 `f.read(1024)` 来每次读取 1024 个字节(1KB) 的文件内容,将其存到 piece,再对 piece 进行处理。 + +事实上,我们还可以结合 yield 来使用: + +```python +def read_in_chunks(file_object, chunk_size=1024): + """Lazy function (generator) to read a file piece by piece. + Default chunk size: 1k.""" + while True: + data = file_object.read(chunk_size) + if not data: + break + yield data + +with open('path/to/file', 'r') as f: + for piece in read_in_chunks(f): + print piece +``` + +## 逐行读取 + +在某些情况下,我们希望逐行读取文件,这时可以使用 `readline()` 方法。 + +看看例子: + +```python +with open('data.txt', 'r') as f: + while True: + line = f.readline() # 逐行读取 + if not line: + break + print line, # 这里加了 ',' 是为了避免 print 自动换行 +``` + +执行结果: + +``` +10 1 9 9 +6 3 2 8 +20 10 3 23 +1 4 1 10 +10 8 6 3 +10 2 1 6 +``` + +## 文件迭代器 + +在 Python 中,**文件对象是可迭代的**,这意味着我们可以直接在 for 循环中使用它们,而且是逐行迭代的,也就是说,效果和 `readline()` 是一样的,而且更简洁。 + +看看例子: + +```python +with open('data.txt', 'r') as f: + for line in f: + print line, +``` + +在上面的代码中,f 就是一个文件迭代器,因此我们可以直接使用 `for line in f`,它是逐行迭代的。 + +看看执行结果: + +```python +10 1 9 9 +6 3 2 8 +20 10 3 23 +1 4 1 10 +10 8 6 3 +10 2 1 6 +``` + +再看一个例子: + +```python +with open(file_path, 'r') as f: + lines = list(f) + print lines +``` + +执行结果: + +```python +['10\t1\t9\t9\n', '6\t3\t2\t8\n', '20\t10\t3\t23\n', '1\t4\t1\t10\n', '10\t8\t6\t3\n', '10\t2\t1\t6'] +``` + +可以看到,我们可以对文件迭代器执行和普通迭代器相同的操作,比如上面使用 `list(open(filename))` 将 f 转为一个字符串列表,这样所达到的效果和使用 `readlines` 是一样的。 + +# 写文件 + +写文件使用 `write` 方法,如下: + +```python +with open('/Users/ethan/data2.txt', 'w') as f: + f.write('one\n') + f.write('two') +``` + +- 如果上述文件已存在,则会清空原内容并覆盖掉; +- 如果上述路径是正确的(比如存在 /Users/ethan 的路径),但是文件不存在(data2.txt 不存在),则会新建一个文件,并写入上述内容; +- 如果上述路径是不正确的(比如将路径写成 /Users/eth ),这时会抛出 IOError; + +如果我们想往已存在的文件追加内容,可以使用 'a' 模式,如下: + +```python +with open('/Users/ethan/data2.txt', 'a') as f: + f.write('three\n') + f.write('four') +``` + +# 小结 + +- 推荐使用 with 语句操作文件 IO。 +- 如果文件较大,可以按字节读取或按行读取。 +- 使用文件迭代器进行逐行迭代。 + +# 参考资料 + +- 《Python 基础教程》 +- [读写文本数据 — python3-cookbook 2.0.0 文档](http://python3-cookbook.readthedocs.io/zh_CN/latest/c05/p01_read_write_text_data.html) + + diff --git a/Function/README.md b/Function/README.md index e2dcbf2..96a81e7 100644 --- a/Function/README.md +++ b/Function/README.md @@ -1,2 +1,7 @@ # 函数 +本章讲解函数,包含以下部分: + +* [定义函数](./func_definition.md) +* [函数参数](./func_parameter.md) + diff --git a/Functional/decorator.md b/Functional/decorator.md index ad388e8..d6c4309 100644 --- a/Functional/decorator.md +++ b/Functional/decorator.md @@ -313,7 +313,7 @@ def hello(): 'wrapped' ``` -为了消除这样的副作用,Python 中的 functool 包提供了一个 wraps 的装饰器: +为了消除这样的副作用,Python 中的 functools 包提供了一个 wraps 的装饰器: ```python from functools import wraps diff --git a/Functional/partial.md b/Functional/partial.md new file mode 100644 index 0000000..b4fc18f --- /dev/null +++ b/Functional/partial.md @@ -0,0 +1,87 @@ +# partial 函数 + +Python 提供了一个 functools 的模块,该模块为高阶函数提供支持,partial 就是其中的一个函数,该函数的形式如下: + +```python +functools.partial(func[,*args][, **kwargs]) +``` + +这里先举个例子,看看它是怎么用的。 + +假设有如下函数: + +```python +def multiply(x, y): + return x * y +``` + +现在,我们想返回某个数的双倍,即: + +``` +>>> multiply(3, y=2) +6 +>>> multiply(4, y=2) +8 +>>> multiply(5, y=2) +10 +``` + +上面的调用有点繁琐,每次都要传入 `y=2`,我们想到可以定义一个新的函数,把 `y=2` 作为默认值,即: + +```python +def double(x, y=2): + return multiply(x, y) +``` + +现在,我们可以这样调用了: + +``` +>>> double(3) +6 +>>> double(4) +8 +>>> double(5) +10 +``` + +事实上,我们可以不用自己定义 `double`,利用 `partial`,我们可以这样: + +```python +from functools import partial + +double = partial(multiply, y=2) +``` + +`partial` 接收函数 `multiply` 作为参数,固定 `multiply` 的参数 `y=2`,并返回一个新的函数给 `double`,这跟我们自己定义 `double` 函数的效果是一样的。 + +所以,简单而言,`partial` 函数的功能就是:把一个函数的某些参数给固定住,返回一个新的函数。 + +需要注意的是,我们上面是固定了 `multiply` 的关键字参数 `y=2`,如果直接使用: + +```python +double = partial(multiply, 2) +``` + +则 `2` 是赋给了 `multiply` 最左边的参数 `x`,不信?我们可以验证一下: + +```python +from functools import partial + +def subtraction(x, y): + return x - y + +f = partial(subtraction, 4) # 4 赋给了 x +>>> f(10) # 4 - 10 +-6 +``` + +# 小结 + +- partial 的功能:固定函数参数,返回一个新的函数。 +- 当函数参数太多,需要固定某些参数时,可以使用 `functools.partial` 创建一个新的函数。 + +# 参考资料 + +- [偏函数 - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386819893624a7edc0e3e3df4d5d852a352b037c93ec000) + + diff --git a/HTTP/HTTP.md b/HTTP/HTTP.md new file mode 100644 index 0000000..3da8960 --- /dev/null +++ b/HTTP/HTTP.md @@ -0,0 +1,280 @@ +# HTTP 协议简介 + +**HTTP** (HyperText Transfer Protocol, 超文本传输协议)是互联网上应用最为广泛的一种网络协议,它是基于 [TCP][tcp] 的**应用层**协议,简单地说就是客户端和服务器进行通信的一种规则,它的模式非常简单,就是**客户端发起请求,服务器响应请求**,如下图所示: + +![](https://ofaatpail.qnssl.com/2016-11-29-http.png-480) + +HTTP 最早于 1991 年发布,是 0.9 版,不过目前该版本已不再用。HTTP 目前在使用的版本主要有: + +- HTTP/1.0,于 1996 年 5 月发布,引入了多种功能,至今仍在使用当中。 +- HTTP/1.1,于 1997 年 1 月发布,持久连接被默认采用,是目前最流行的版本。 +- HTTP/2 ,于 2015 年 5 月发布,引入了服务器推送等多种功能,是目前最新的版本。 + +# HTTP 请求 + +HTTP 请求由三部分组成: + +- **请求行**:包含请求方法、请求地址和 HTTP 协议版本 +- **消息报头**:包含一系列的键值对 +- **请求正文(可选)**:注意和消息报头之间有一个空行 + +如图所示: + +![](https://ooo.0o0.ooo/2016/12/05/58456e61d5d47.png) + +下面是一个 HTTP GET 请求的例子: + +``` +GET / HTTP/1.1 +Host: httpbin.org +Connection: keep-alive +Cache-Control: max-age=0 +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 +Accept-Encoding: gzip, deflate, sdch, br +Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4 +Cookie: _ga=GA1.2.475070272.1480418329; _gat=1 +``` + +上面的第一行就是一个**请求行**: + +``` +GET / HTTP/1.1 +``` + +其中,`GET` 是请求方法,表示从服务器获取资源;`/` 是一个请求地址;`HTTP/1.1` 表明 HTTP 的版本是 1.1。 + +请求行后面的一系列键值对就是**消息报头**: + +``` +Host: httpbin.org +Connection: keep-alive +Cache-Control: max-age=0 +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 +Accept-Encoding: gzip, deflate, sdch, br +Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4 +Cookie: _ga=GA1.2.475034215.1480418329; _gat=1 +``` + +其中: + +- Host 是请求报头域,用于指定被请求资源的 Internet 主机和端口号,它通常从 HTTP URL 中提取出来; +- Connection 表示连接状态,keep-alive 表示该连接是持久连接(persistent connection),即 TCP 连接默认不关闭,可以被多个请求复用,如果客户端和服务器发现对方有一段时间没有活动,就可以主动关闭连接; +- Cache-Control 用于指定缓存指令,它的值有 no-cache, no-store, max-age 等,`max-age=秒`表示资源在本地缓存多少秒; +- User-Agent 用于标识请求者的一些信息,比如浏览器类型和版本,操作系统等; +- Accept 用于指定客户端希望接受哪些类型的信息,比如 text/html, image/gif 等; +- Accept-Encoding 用于指定可接受的内容编码; +- Accept-Language 用于指定可接受的自然语言; +- Cookie 用于维护状态,可做用户认证,服务器检验等,它是浏览器储存在用户电脑上的文本片段; + +## HTTP 请求方法 + +HTTP 通过不同的请求方法以多种方式来操作指定的资源,常用的请求方法如下表: + +| 方法 | 描述 | +| :-- | :-- | +| GET | 从服务器获取指定(请求地址)的资源的信息,它通常只用于读取数据,就像数据库查询一样,不会对资源进行修改。 | +| POST | 向指定资源提交数据(比如提交表单,上传文件),请求服务器进行处理。数据被包含在请求正文中,这个请求可能会创建新的资源或更新现有的资源。 | +| PUT | 通过指定资源的唯一标识(在服务器上的具体存放位置),请求服务器创建或更新资源。 | +| DELETE | 请求服务器删除指定资源。 | +| HEAD | 与 GET 方法类似,从服务器获取资源信息,和 GET 方法不同的是,HEAD 不含有呈现数据,仅仅是 HTTP 头信息。HEAD 的好处在于,使用这个方法可以在不必传输全部内容的情况下,就可以获得资源的元信息(或元数据)。 | +| OPTIONS | 该方法可使服务器传回资源所支持的所有 HTTP 请求方法。 | + +# HTTP 响应 + +HTTP 响应与 HTTP 请求相似,由三部分组成: + +- **状态行**:包含 HTTP 协议版本、状态码和状态描述,以空格分隔 +- **响应头**:即消息报头,包含一系列的键值对 +- **响应正文**:返回内容,注意和响应头之间有一个空行 + +如图所示: + +![](https://ooo.0o0.ooo/2016/12/05/58456e62d49d6.png) + +下面是一个 HTTP GET 请求的响应结果: + +``` +HTTP/1.1 200 OK +Server: nginx +Date: Tue, 29 Nov 2016 13:08:38 GMT +Content-Type: application/json +Content-Length: 203 +Connection: close +Access-Control-Allow-Origin: * +Access-Control-Allow-Credentials: true + +{ + "args": {}, + "headers": { + "Host": "httpbin.org", + "User-Agent": "Paw/2.3.1 (Macintosh; OS X/10.11.3) GCDHTTPRequest" + }, + "origin": "13.75.42.240", + "url": "https://httpbin.org/get" +} +``` + +上面的第一行就是一个**状态行**: + +``` +HTTP/1.1 200 OK +``` + +其中,`200` 是状态码,表示客户端请求成功,`OK` 是相应的状态描述。 + +状态码是一个三位的数字,常见的状态码有以下几类: + +- 1XX 消息 -- 请求已被服务接收,继续处理 +- 2XX 成功 -- 请求已成功被服务器接收、理解、并接受 + - 200 OK + - 201 Created 已创建 + - 202 Accepted 接收 + - 203 Non-Authoritative Information 非认证信息 + - 204 No Content 无内容 +- 3XX 重定向 -- 需要后续操作才能完成这一请求 + - 301 Moved Permanently 请求永久重定向 + - 302 Moved Temporarily 请求临时重定向 + - 304 Not Modified 文件未修改,可以直接使用缓存的文件 + - 305 Use Proxy 使用代理 +- 4XX 请求错误 -- 请求含有词法错误或者无法被执行 + - 400 Bad Request 由于客户端请求有语法错误,不能被服务器所理解 + - 401 Unauthorized 请求未经授权。这个状态代码必须和WWW-Authenticate报头域一起使用 + - 403 Forbidden 服务器收到请求,但是拒绝提供服务。服务器通常会在响应正文中给出不提供服务的原因 + - 404 Not Found 请求的资源不存在,例如,输入了错误的URL +- 5XX 服务器错误 -- 服务器在处理某个正确请求时发生错误 + - 500 Internal Server Error 服务器发生不可预期的错误,导致无法完成客户端的请求 + - 503 Service Unavailable 服务器当前不能够处理客户端的请求,在一段时间之后,服务器可能会恢复正常 + - 504 Gateway Time-out 网关超时 + +状态行后面的一系列键值对就是消息报头,即响应头: + +``` +Server: nginx +Date: Tue, 29 Nov 2016 13:08:38 GMT +Content-Type: application/json +Content-Length: 203 +Connection: close +Access-Control-Allow-Origin: * +Access-Control-Allow-Credentials: true +``` + +其中: + +- Server 包含了服务器用来处理请求的软件信息,跟请求报头域 User-Agent 相对应; +- Content-Type 用于指定发送给接收者(比如浏览器)的响应正文的媒体类型,比如 text/html, text/css, image/png, image/jpeg, video/mp4, application/pdf, application/json 等; +- Content-Length 指明本次回应的数据长度; + +# 再议 POST 和 PUT + +注意到,POST 和 PUT 都可用于创建或更新资源,然而,它们之间还是有比较大的区别: + +- POST 所对应的 URI 并非创建的资源本身,而是资源的接收者,资源本身的存放位置由服务器决定;而 PUT 所对应的 URI 是要创建或更新的资源本身,它指明了具体的存放位置 + +比如,往某个站点添加一篇文章,如果**使用 POST 来创建资源**,可类似这样: + +``` +POST /articles HTTP/1.1 + +{ + "author": "ethan", + "title": "hello world", + "content": "hello world" +} +``` + +在上面,POST 对应的 URI 是 `/articles`,它是资源的接收者,而非资源的标识,如果资源被成功创建,服务器可以返回 `201 Created` 状态以及新建资源的位置,比如: + +``` +HTTP/1.1 201 Created +Location: /articles/abcdef123 +``` + +我们如果知道新建资源的标识符,可以**使用 PUT 来创建资源**,比如: + +``` +PUT /articles/abcdef234 HTTP/1.1 + +{ + "author": "peter", + "title": "hello world", + "content": "hello world" +} +``` + +在上面,PUT 对应的 URI 是 `/articles/abcdef234`,它指明了资源的存放位置,如果资源被成功创建,服务器可以返回 `201 Created` 状态以及新建资源的位置,比如: + +``` +HTTP/1.1 201 Created +Location: /articles/abcdef234 +``` + +- 使用 PUT 更新某一资源,需要更新资源的全部属性;而使用 POST,可以更新全部或一部分值 + +比如使用 PUT 更新地址为 `/articles/abcdef234` 的文章的标题,我们需要发送所有值: + +``` +PUT /articles/abcdef234 HTTP/1.1 + +{ + "author": "peter", + "title": "hello python", + "content": "hello world" +} +``` + +而使用 POST,可以更新某个域的值: + +``` +POST /articles/abcdef234 HTTP/1.1 + +{ + "title": "hello python" +} +``` + +- POST 是不幂等的,PUT 是幂等的,这是一个很重要的区别 + +> HTTP 方法的幂等性是指一次和多次请求某一个资源应该具有同样的**副作用**,注意这里是副作用,而不是返回结果。 + +**GET** 方法用于获取资源,不会改变资源的状态,不论调用一次还是多次都没有副作用,因此它是幂等的;**DELETE** 方法用于删除资源,有副作用,但调用一次或多次都是删除同个资源,产生的副作用是相同的,因此也是幂等的;**POST** 是不幂等的,因为两次相同的 POST 请求会在服务器创建两份资源,它们具有不同的 URI;**PUT** 是幂等的,对同一 URI 进行多次 PUT 的副作用和一次 PUT 是相同的。 + +# HTTP 特点 + +- 客户端/服务器模式 +- 简单快速:客户端向服务器请求服务时,通过传送请求方法、请求地址和数据体(可选)即可 +- 灵活:允许传输任意类型的数据对象,通过 Content-Type 标识 +- 无状态:对事物处理没记忆能力 + +# 小结 + +- HTTP 是在网络上传输 HTML 的协议,用于浏览器和服务器的通信,默认使用 80 端口。 +- URL 地址用于定位资源,HTTP 中的 GET, POST, PUT, DELETE 用于操作资源,比如查询,增加,更新等。 +- GET, PUT, DELETE 是幂等的,POST 是不幂等的。 +- POST VS PUT + - 使用 PUT 创建资源需要提供资源的唯一标识(具体存放位置),POST 不需要,POST 的数据存放位置由服务器自己决定 + - 使用 PUT 更新某一资源,需要更新资源的全部属性;而使用 POST,可以更新全部或一部分值 + - POST 是不幂等的,PUT 是幂等的,这是一个很重要的区别 +- GET 可提交的数据量受到 URL 长度的限制,HTTP 协议规范没有对 URL 长度进行限制,这个限制是特定的浏览器及服务器对它的限制。 +- 理论上讲,POST 是没有大小限制的,HTTP 协议规范也没有进行大小限制,出于安全考虑,服务器软件在实现时会做一定限制。 + +# 参考资料 + +- [超文本传输协议 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE#HTTP.2F1.1) +- [HTTP 协议入门 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2016/08/http.html) +- [HTTP幂等性概念和应用 | 酷 壳 - CoolShell.cn](http://coolshell.cn/articles/4787.html) +- [Http协议详解 - 简书](http://www.jianshu.com/p/e83d323c6bcc) +- [When to use PUT or POST - The RESTful cookbook](http://restcookbook.com/HTTP%20Methods/put-vs-post/) +- [To PUT or POST?](https://stormpath.com/blog/put-or-post) +- [全面解读HTTP Cookie](http://www.webryan.net/2011/08/wiki-of-http-cookie/) +- [HTTP cookies 详解 | bubkoo](http://bubkoo.com/2014/04/21/http-cookies-explained/) +- [HTTP 接口设计指北](https://github.com/bolasblack/http-api-guide) + + + +[tcp]: https://zh.wikipedia.org/wiki/%E4%BC%A0%E8%BE%93%E6%8E%A7%E5%88%B6%E5%8D%8F%E8%AE%AE + + diff --git a/HTTP/README.md b/HTTP/README.md index ade5b64..d0feb90 100644 --- a/HTTP/README.md +++ b/HTTP/README.md @@ -1,2 +1,8 @@ # HTTP 服务 +本章主要介绍: + +- [HTTP 协议](./HTTP.md) +- [Requests 库的使用](./Requests.md) + + diff --git a/HTTP/Requests.md b/HTTP/Requests.md new file mode 100644 index 0000000..be2efc2 --- /dev/null +++ b/HTTP/Requests.md @@ -0,0 +1,566 @@ +# Requests 库的使用 + +Python 的标准库 urllib 提供了大部分 HTTP 功能,但使用起来较繁琐。通常,我们会使用另外一个优秀的第三方库:[Requests](https://github.com/kennethreitz/requests),它的标语是:**Requests: HTTP for Humans**。 + +Requests 提供了很多功能特性,几乎涵盖了当今 Web 服务的需求,比如: + +- 浏览器式的 SSL 验证 +- 身份认证 +- Keep-Alive & 连接池 +- 带持久 Cookie 的会话 +- 流下载 +- 文件分块上传 + +下面,我们将从以下几个方面介绍 Requests 库: + +- HTTP 请求 +- HTTP 响应 +- cookie +- 会话对象 +- 代理 +- 身份认证 + +# HTTP 请求 + +我们知道,一个 HTTP 请求由三部分构成: + +- **请求行**:包含请求方法(比如 GET, POST)、请求地址和 HTTP 协议版本 +- **请求头**:包含一系列的键值对 +- **请求正文(可选)** + +如图所示: + +![](https://ooo.0o0.ooo/2016/12/05/58456e61d5d47.png) + +Requests 提供了几乎所有 HTTP 动词的功能:GET、OPTIONS、HEAD、POST、PUT、PATCH、DELETE,另外,它提供了 `headers` 参数让我们根据需求定制请求头。 + +使用 Requests 发送一个请求很方便,比如: + +``` +import requests + +r = requests.get("http://httpbin.org/get") +r = requests.post("http://httpbin.org/post") +r = requests.put("http://httpbin.org/put") +r = requests.delete("http://httpbin.org/delete") +r = requests.head("http://httpbin.org/get") +r = requests.options("http://httpbin.org/get") +``` + +下面,我们重点讲一下 GET 请求,POST 请求和定制请求头。 + +## GET 请求 + +使用 Requests 发送 GET 请求非常简单,如下: + +```python +import requests + +r = requests.get("http://httpbin.org/get") +``` + +在有些情况下,URL 会带参数,比如 `https://segmentfault.com/blogs?page=2`,这个 URL 有一个参数 page,值为 2。Requests 提供了 `params` 关键字参数,允许我们以一个字典来提供这些参数,比如: + +```python +import requests + +payload = {'page': '1', 'per_page': '10'} +r = requests.get("http://httpbin.org/get", params=payload) +``` + +通过打印该 URL,我们可以看到 URL 已被正确编码: + +```python +>>> print r.url +http://httpbin.org/get?per_page=10&page=1 +``` + +需要注意的是字典里值为 None 的键不会被添加到 URL 的查询字符串中。 + +## POST 请求 + +使用 Requests 发送 POST 请求也很简单,如下: + +```python +import requests + +r = requests.post("http://httpbin.org/post") +``` + +通常,我们在发送 POST 请求时还会附上数据,比如发送编码为表单形式的数据或编码为 JSON 形式的数据,这时,我们可以使用 Requests 提供的 `data` 参数。 + +- 发送编码为表单形式的数据 + +通过给 `data` 参数传递一个 `dict`,我们的数据字典在发出请求时会被自动编码为表单形式,比如: + +```python +import requests + +payload = {'page': 1, 'per_page': 10} +r = requests.post("http://httpbin.org/post", data=payload) +``` + +看看返回的内容(省略了部分数据): + +```python +>>> print r.text +{ + ... + "form": { + "page": "1", + "per_page": "10" + }, + ... +} +``` + +- 发送编码为 JSON 形式的数据 + +如果给 `data` 参数传递一个 `string`,我们的数据会被直接发布出去,比如: + +```python +import json +import requests + +payload = {'page': 1, 'per_page': 10} +r = requests.post("http://httpbin.org/post", data=json.dumps(payload)) +``` + +看看返回: + +```python +>>> print r.text +{ + "args": {}, + "data": "{\"per_page\": 10, \"page\": 1}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Content-Length": "27", + "Host": "httpbin.org", + "User-Agent": "python-requests/2.9.1" + }, + "json": { + "page": 1, + "per_page": 10 + }, + "origin": "13.75.42.240", + "url": "http://httpbin.org/post" +} +``` + +在上面,我们自行对 `dict` 进行了编码,这种方式等价于使用 `json` 参数,而给它传递 `dict`,如下: + +```python +import requests + +payload = {'page': 1, 'per_page': 10} +r = requests.post("http://httpbin.org/post", json=payload) +``` + +这种做法跟上面的做法是等价的,数据在发出时会被自动编码。 + +## 请求头 + +有时,我们需要为请求添加 HTTP 头部,我们可以**通过传递一个 `dict` 给 `headers` 参数来实现**。比如: + +```python +import requests + +url = 'http://httpbin.org/post' +payload = {'page': 1, 'per_page': 10} +headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} + +r = requests.post("http://httpbin.org/post", json=payload, headers=headers) +``` + +发送到服务器的请求的头部可以通过 `r.request.headers` 访问: + +``` +>>> print r.request.headers +{'Content-Length': '27', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)', 'Connection': 'keep-alive', 'Content-Type': 'application/json'} +``` + +服务器返回给我们的响应头部信息可以通过 `r.headers` 访问: + +``` +>>> print r.headers +{'Content-Length': '462', 'Server': 'nginx', 'Connection': 'close', 'Access-Control-Allow-Credentials': 'true', 'Date': 'Mon, 05 Dec 2016 15:41:05 GMT', 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json'} +``` + +# HTTP 响应 + +HTTP 响应与 HTTP 请求相似,由三部分组成: + +- **状态行**:包含 HTTP 协议版本、状态码和状态描述,以空格分隔 +- **响应头**:包含一系列的键值对 +- **响应正文** + +如图所示: + +![](https://ooo.0o0.ooo/2016/12/05/58456e62d49d6.png) + +当我们使用 `requests.*` 发送请求时,Requests 做了两件事: + +- 构建一个 Request 对象,该对象会根据请求方法或相关参数发起 HTTP 请求 +- 一旦服务器返回响应,就会产生一个 Response 对象,该响应对象包含服务器返回的所有信息,也包含你原来创建的 Request 对象 + +对于响应状态码,我们可以访问响应对象的 `status_code` 属性: + +```python +import requests + +r = requests.get("http://httpbin.org/get") +print r.status_code + +# 输出 +200 +``` + +对于响应正文,我们可以通过多种方式读取,比如: + +- 普通响应,使用 `r.text` 获取 +- JSON 响应,使用 `r.json()` 获取 +- 二进制响应,使用 `r.content` 获取 +- 原始响应,使用 `r.raw` 获取 + +## 普通响应 + +我们可以使用 `r.text` 来读取 unicode 形式的响应,看看例子: + +```python +import requests + +r = requests.get("https://github.com/timeline.json") +print r.text +print r.encoding + +# 输出 +{"message":"Hello there, wayfaring stranger. If you’re reading this then you probably didn’t see our blog post a couple of years back announcing that this API would go away: http://git.io/17AROg Fear not, you should be able to get what you need from the shiny new Events API instead.","documentation_url":"https://developer.github.com/v3/activity/events/#list-public-events"} +utf-8 +``` + +Requests 会自动解码来自服务器的内容,大多数 unicode 字符集都能被正确解码。 + +## JSON 响应 + +对于 JSON 响应的内容,我们可以使用 `json()` 方法把返回的数据解析成 Python 对象。 + +看看例子: + +```python +import requests + +r = requests.get("https://github.com/timeline.json") + +if r.status_code == 200: + print r.headers.get('content-type') + print r.json() + +# 输出 +application/json; charset=utf-8 +{u'documentation_url': u'https://developer.github.com/v3/activity/events/#list-public-events', u'message': u'Hello there, wayfaring stranger. If you\u2019re reading this then you probably didn\u2019t see our blog post a couple of years back announcing that this API would go away: http://git.io/17AROg Fear not, you should be able to get what you need from the shiny new Events API instead.'} +``` + +如果 JSON 解码失败,`r.json()` 就会抛出异常,比如: + +```python +import requests + +r = requests.get("https://www.baidu.com") + +if r.status_code == 200: + print r.headers.get('content-type') + print r.json() + +# 输出 +text/html +--------------------------------------------------------------------------- +ValueError Traceback (most recent call last) + in () + 1 if r.status_code == 200: + 2 print r.headers.get('content-type') +----> 3 print r.json() + 4 +.... +.... +ValueError: No JSON object could be decoded +``` + +## 二进制响应 + +我们也可以以字节的方式访问响应正文,访问 `content` 属性可以获取二进制数据,比如用返回的二进制数据创建一张图片: + +```python +import requests + +url = 'https://github.com/reactjs/redux/blob/master/logo/logo.png?raw=true' +r = requests.get(url) +image_data = r.content # 获取二进制数据 + +with open('/Users/Ethan/Downloads/redux.png', 'wb') as fout: + fout.write(image_data) +``` + +## 原始响应 + +在少数情况下,我们可能想获取来自服务器的原始套接字响应,这可以通过访问响应对象的 `raw` 属性来实现,但要确保在初始请求中设置了 `stream=True`,比如: + +```python +import requests + +url = 'https://github.com/reactjs/redux/blob/master/logo/logo.png?raw=true' +r = requests.get(url, stream=True) +print r.raw +r.raw.read(10) + +# 输出 + +'\x89PNG\r\n\x1a\n\x00\x00' +``` + +# 重定向 + +默认情况下,除了 HEAD,Requests 会自动处理所有重定向。我们可以使用响应对象的 `history` 属性来追踪重定向,**Response.history** 是一个 Response 对象的列表,这个对象列表按照从最老到最近的请求进行排序。 + +比如,点击某些网站的链接,它会将页面重定向到其他网站: + +```python +>>> import requests + +>>> headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} +>>> r = requests.get('https://toutiao.io/k/c32y51', headers=headers) + +>>> r.status_code +200 + +>>> r.url # 发生了重定向,响应对象的 url,跟请求对象不一样 +u'http://www.jianshu.com/p/490441391db6?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io' + +>>> r.history +[] + +>>> r.history[0].text +u'You are being redirected.' +``` + +可以看到,我们访问网址 `https://toutiao.io/k/c32y51` 被重定向到了下面的链接: + +```python +http://www.jianshu.com/p/490441391db6?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io' +``` + +我们还看到 `r.history` 包含了一个 Response 对象列表,我们可以用它来追踪重定向。 + +如果请求方法是 GET、POST、PUT、OPTIONS、PATCH 或 DELETE,我们可以通过 `all_redirects` 参数禁止重定向: + +```python +>>> import requests + +>>> headers = {'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'} +>>> r = requests.get('https://toutiao.io/k/c32y51', headers=headers, allow_redirects=False) +>>> r.url # 禁止重定向,响应对象的 url 跟请求对象一致 +u'https://toutiao.io/k/c32y51' +>>> r.history +[] +>>> r.text +u'You are being redirected.' +``` + +# Cookie + +- 如果某个响应包含一些 cookie,我们可以直接访问它们,比如: + +```python +>>> import requests + +>>> url = 'http://exmaple.com/some/cookie/setting/url' +>>> r = requests.get(url) + +>>> r.cookies['some_key'] +'some_value' +``` + +- 发送 cookies 到服务器,可以使用 `cookies` 参数: + +```python +>>> import requests + +>>> url = 'http://httpbin.org/cookies' +>>> cookies = dict(key1='value1') + +>>> r = requests.get(url, cookies=cookies) +>>> r.text +u'{\n "cookies": {\n "key1": "value1"\n }\n}\n' +>>> print r.text +{ + "cookies": { + "key1": "value1" + } +} +``` + +# 会话对象 + +我们知道,HTTP 协议是无状态的,这意味着每个请求都是独立的,如果后续的处理需要用到前面的信息,则数据需要重传,为了解决这个问题,我们可以使用 Cookie 或 Session 来存储某些特定的信息,比如用户名、密码等,这样,当用户在不同 Web 页面跳转或再次登录网站时,就不用重新输入用户名和密码(当然,如果 Cookie 被删除和 Session 过期则需要重新输入)。 + +Requests 提供了会话对象让我们能够跨请求保持某些参数,也可以在同一个 Session 实例发出的所有请求之间保持 Cookie。 + +下面,我们看看会话对象的使用。 + +下面是一个跨请求保持 Cookie 的例子: + +```python +>>> import requests +>>> s = requests.Session() +>>> s.get('http://httpbin.org/cookies/set/sessioncookie/123456789') + +>>> r = s.get("http://httpbin.org/cookies") +>>> print r.text +{ + "cookies": { + "sessioncookie": "123456789" + } +} +``` + +会话还可用来为请求方法提供缺省数据,通过设置会话对象的属性来实现: + +```python +import requests + +s = requests.Session() +s.auth = ('user', 'pass') +s.headers.update({'x-test': 'true'}) + +# x-test 和 x-test2 都会被发送 +s.get('http://httpbin.org/headers', headers={'x-test2': 'true'}) +``` + +# 代理 + +Requests 支持基本的 HTTP 代理 和 SOCKS 代理(2.10.0 新增功能)。 + +## HTTP 代理 + +如果需要使用 HTTP 代理,我们可以为任意请求方法提供 `proxies` 参数,如下: + +```python +import requests + +proxies = { + "http": "http://10.10.1.10:3128", + "https": "http://10.10.1.10:1080", +} + +requests.get("http://example.org", proxies=proxies) +``` + +我们也可以通过设置环境变量 `HTTP_PROXY` 和 `HTTPS_PROXY` 来配置代理: + +```python +$ export HTTP_PROXY="http://10.10.1.10:3128" +$ export HTTPS_PROXY="http://10.10.1.10:1080" + +$ python +>>> import requests +>>> requests.get("http://example.org") +``` + +## SOCKS 代理 + +Requests 自 2.10.0 版起,开始支持 SOCKS 协议的代理,如果要使用,我们还需安装一个第三方库: + +``` +$ pip install requests[socks] +``` + +SOCKS 代理的使用和 HTTP 代理类似: + +```python +import requests + +proxies = { + "http": "socks5://user:pass@host:port", + "https": "socks5://user:pass@host:port", +} + +requests.get("http://example.org", proxies=proxies) +``` + +# 身份认证 + +大部分 Web 服务都需要身份认证,并且有多种不同的认证类型,比如: + +- 基本身份认证 +- 摘要式身份认证 +- OAuth 认证 + +下面介绍一下基本身份认证和 OAuth 认证。 + +## 基本身份认证 + +基本身份认证(HTTP Basic Auth)是最简单的一种身份认证,一般需要身份认证的 Web 服务也都接受 HTTP Basic Auth,Requests 提供了非常简单的形式让我们使用 HTTP Basic Auth: + +```python +>>> from requests.auth import HTTPBasicAuth +>>> requests.get('https://api.github.com/user', auth=HTTPBasicAuth('user', 'pass')) +``` + +由于 HTTP Basic Auth 非常常见,Requests 提供了一种简写的形式: + +```python +requests.get('https://api.github.com/user', auth=('user', 'pass')) +``` + +## OAuth 2 认证 + +OAuth 是一种常见的 Web API 认证方式,目前的版本是 2.0。Requests 并不直接支持 OAuth 认证,而是要配合另外一个库一起使用,该库是 [requests-oauthlib](https://github.com/requests/requests-oauthlib)。 + +下面以 GitHub 为例,介绍一下 OAuth 2 认证。 + +```python +>>> # Credentials you get from registering a new application +>>> client_id = '' +>>> client_secret = '' + +>>> # OAuth endpoints given in the GitHub API documentation +>>> authorization_base_url = 'https://github.com/login/oauth/authorize' +>>> token_url = 'https://github.com/login/oauth/access_token' + +>>> from requests_oauthlib import OAuth2Session +>>> github = OAuth2Session(client_id) + +>>> # Redirect user to GitHub for authorization +>>> authorization_url, state = github.authorization_url(authorization_base_url) +>>> print 'Please go here and authorize,', authorization_url + +>>> # Get the authorization verifier code from the callback url +>>> redirect_response = raw_input('Paste the full redirect URL here:') + +>>> # Fetch the access token +>>> github.fetch_token(token_url, client_secret=client_secret, +>>> authorization_response=redirect_response) + +>>> # Fetch a protected resource, i.e. user profile +>>> r = github.get('https://api.github.com/user') +>>> print r.content +``` + +更多关于 OAuth 工作流程的信息,可以参考 [OAuth 官方网站](https://oauth.net/),关于 `requests-oauthlib` 库的使用,可以参考[官方文档](https://requests-oauthlib.readthedocs.io/en/latest/#)。 + + +# 小结 + +- 任何时候调用 requests.*() 你都在做两件主要的事情。其一,你在构建一个 Request 对象, 该对象将被发送到某个服务器请求或查询一些资源。其二,一旦 requests 得到一个从 服务器返回的响应就会产生一个 Response 对象。该响应对象包含服务器返回的所有信息,也包含你原来创建的 Request 对象。 + +# 参考资料 + +- [Requests: Requests 2.10.0 文档](http://docs.python-requests.org/zh_CN/latest/) +- [如何理解HTTP协议的 “无连接,无状态” 特点?](http://blog.csdn.net/tennysonsky/article/details/44562435) +- [理解OAuth 2.0 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) +- [作为客户端与HTTP服务交互 — python3-cookbook 2.0.0 文档](http://python3-cookbook.readthedocs.io/zh_CN/latest/c11/p01_interact_with_http_services_as_client.html) + + diff --git a/PYTHONroadmap.png b/PYTHONroadmap.png new file mode 100644 index 0000000..8a2e097 Binary files /dev/null and b/PYTHONroadmap.png differ diff --git a/Process-Thread-Coroutine/process.md b/Process-Thread-Coroutine/process.md index b468711..8861568 100644 --- a/Process-Thread-Coroutine/process.md +++ b/Process-Thread-Coroutine/process.md @@ -62,7 +62,7 @@ I am child process (86646) and my parent is (86645). # 多进程 -Python 提供了一个 [multiprocessing][mp] 模块,利用它,我们可以来编写跨平台的多进程程序。 +Python 提供了一个 [multiprocessing][mp] 模块,利用它,我们可以来编写跨平台的多进程程序,但需要注意的是 multiprocessing 在 Windows 和 Linux 平台的不一致性:一样的代码在 Windows 和 Linux 下运行的结果可能不同。因为 Windows 的进程模型和 Linux 不一样,Windows 下没有 fork。 我们先来看一个简单的例子,该例子演示了在主进程中启动一个子进程,并等待其结束,代码如下: @@ -94,6 +94,64 @@ Run child process test (10075)... Process end. ``` +## multiprocessing 与平台有关 + +``` python +import random +import os +from multiprocessing import Process + +num = random.randint(0, 100) + +def show_num(): + print("pid:{}, num is {}".format(os.getpid(), num)) + +if __name__ == "__main__": + print("pid:{}, num is {}".format(os.getpid(), num)) + p = Process(target=show_num) + p.start() + p.join() +``` + +在 Windows 下运行以上代码,输出的结果如下(你得到不一样的结果也是对的): + +``` +pid:6504, num is 25 +pid:6880, num is 6 +``` + +我们发现,num 的值是不一样的! + +在 Linux 下运行以上代码,可以看到 num 的值是一样的: + +``` +pid:11747, num is 13 +pid:11748, num is 13 +``` + +这是因为根据不同的平台,multiprocessing支持三种启动进程的办法: +#### spawn +父进程会启动一个新的解释器,子进程只会继承run()所需的资源。 +不必要的文件描述符和句柄(一种指针)不会被继承。 +该方法和fork,forkserver相比,启动进程较慢。 + +可在Unix和Windows上使用。 Windows上的默认设置。 + +#### fork +父进程使用 os.fork() 来产生 Python 解释器分叉。 +子进程在开始时实际上与父进程相同,并且会继承父进程的所有资源。 +多线程的安全是有问题的。 + +只能在Unix上使用。Unix上的默认设置。 + +#### forkserver +程序启动并选择forkserver启动方法时,将启动一个服务器进程。 +之后每当需要一个新进程时,父进程就会连接到服务器并请求它分叉一个新进程。 +分叉服务器进程是单线程的,因此使用 os.fork() 是安全的。 +没有不必要的资源被继承。 + +可在Unix平台上使用,并支持通过Unix管道传递文件描述符。 + ## 使用进程池创建多个进程 在上面,我们只是创建了一个进程,如果要创建多个进程呢?Python 提供了**进程池**的方式,让我们批量创建子进程,让我们看一个简单的示例: @@ -140,7 +198,7 @@ All subprocesses done. # 进程间通信 -进程间的通信可以通过管道(Pipe),队列(Queue)等多种方式来实现。Python 的 multiprocessing 模块封装了底层的实现机制,让我们可以很容器地实现进程间的通信。 +进程间的通信可以通过管道(Pipe),队列(Queue)等多种方式来实现。Python 的 multiprocessing 模块封装了底层的实现机制,让我们可以很容易地实现进程间的通信。 下面以队列(Queue)为例,在父进程中创建两个子进程,一个往队列写数据,一个从对列读数据,代码如下: @@ -219,9 +277,7 @@ DONE - [编程中的进程、线程、协程、同步、异步、回调 · 浮生半日闲](https://wangdashuaihenshuai.github.io/2015/10/17/%E7%BC%96%E7%A8%8B%E4%B8%AD%E7%9A%84%E8%BF%9B%E7%A8%8B%E3%80%81%E7%BA%BF%E7%A8%8B%E3%80%81%E5%8D%8F%E7%A8%8B%E3%80%81%E5%90%8C%E6%AD%A5%E3%80%81%E5%BC%82%E6%AD%A5%E3%80%81%E5%9B%9E%E8%B0%83/) - [python中多进程以及多线程编程的总结 - Codefly](http://sunms.codefly.top/2016/11/05/python%E4%B8%AD%E5%A4%9A%E8%BF%9B%E7%A8%8B%E4%BB%A5%E5%8F%8A%E5%A4%9A%E7%BA%BF%E7%A8%8B%E7%BC%96%E7%A8%8B%E7%9A%84%E6%80%BB%E7%BB%93/) - [multithreading - Python multiprocessing.Pool: when to use apply, apply_async or map? - Stack Overflow](http://stackoverflow.com/questions/8533318/python-multiprocessing-pool-when-to-use-apply-apply-async-or-map) - +- [multiprocessing --- 基于进程的并行¶](https://docs.python.org/zh-cn/3/library/multiprocessing.html) [mp]: https://docs.python.org/2/library/multiprocessing.html - - diff --git a/README.md b/README.md index e69de29..e33cca7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,89 @@ +![cover](https://i.loli.net/2020/03/23/9B1oWLeu6Q3Hbrz.png) + +Python 之旅 +=== + +![Version](https://img.shields.io/badge/version-1.0-brightgreen.svg) +[![License: CC BY-NC-ND 4.0](https://img.shields.io/badge/License-CC%20BY--NC--ND%204.0-brightgreen.svg)](https://raw.githubusercontent.com/ethan-funny/explore-python/master/LICENSE) + +## Python 简介 + +Python 诞生于 1989 年的圣诞期间,由 [Guido van Rossum](https://gvanrossum.github.io/) 开发而成,目前 Guido 仍然是 Python 的主要开发者,主导着 Python 的发展方向,Python 社区经常称呼他为『仁慈的独裁者』。 + +Python 是一门面向对象,解释型的高级程序设计语言,它的语法非常简洁、优雅,而这也是 Python 的一些设计哲学。Python 自带了很完善的库,涵盖了数据库,网络,文件处理,GUI 等方方面面,通过这些库,我们可以比较快速地解决一些棘手问题,也可以将其作为基础库,开发出一些高级库。 + +目前 Python 在大部分领域都占有一席之地,比如 Web 开发,机器学习,科学计算等。不少大型网站都是使用 Python 作为后台开发语言的,比如 [YouTube](https://www.youtube.com/)、[Pinterest](https://www.pinterest.com/)、国内的[豆瓣](https://www.douban.com/)和[知乎](http://www.zhihu.com/)等。 + +另外,有不少知名的机器学习库也是使用 Python 开发的,比如,[scikit-learn](http://scikit-learn.org/stable/) 是一个强大的机器学习库,[PyTorch](https://pytorch.org/) 是一个成熟的深度学习库。 + +当然了,Python 也有一些缺点。Python 经常被人们吐槽的一点就是:运行速度慢,和 C/C++ 相比非常慢。但是,除了像视频高清解码等**计算密集型任务**对运行速度有较高的要求外,在大部分时候,我们可能并不需要非常快的运行速度。比如,一个程序使用 C 来实现,运行时间只需 0.01 秒,而使用 Python 来实现,需要 0.1 秒,虽然 Python 的运行时间是 C 的 10 倍,显然很慢,但对我们而言,这压根不是问题。 + +## 关于本书 + +本书是我学习和使用 Python 的总结。在学习和使用 Python 的过程中,我作了不少笔记,并对一些笔记进行了加工和完善,发表在博客上。随着笔记的增加,我就萌生了写一本书的想法,希望能比较系统地总结相关知识,巩固自己的知识体系,而不是停留在『感觉好像懂了』的状态中。 + +有了想法之后,接下来就要开始写了。当然,从产生想法到付诸实践还是纠结了一段时间,毕竟,作笔记和写书很不一样啊。思想斗争过后,我下定决心要把它写出来。 + +首先,我参考一些相关的书籍,作了一个基础的思维导图,如下: + +![思维导图](https://i.loli.net/2020/03/23/uZN8aehmwl14XcG.png) +![Eng journy](PYTHONroadmap.png) + +接下来,就要开始写作了,这也是最艰难的一关。 + +我没有按照从头到尾的顺序写,而是从最感兴趣的知识点入手,比如函数式编程、类的使用等等。就这样,一点一点地写,实在不想写了,就先搁置一下,过两天继续写。 + +我在写作的过程中,给自己提了一个要求:**尽量深入浅出,条理清晰**。至于是否达到了,希望读者们多多批评指正,并给我提意见和建议。 + +本书的每章基本上都是独立的,读者可以挑选感兴趣的章节进行阅读。目前本书有 15 个章节: + +- 第 1 章:介绍一些基础知识,包括 Python 中的输入和输出,字符编码。 +- 第 2 章:介绍常用数据类型,比如字符串、列表和字典等。 +- 第 3 章:介绍函数的定义和函数参数魔法。 +- 第 4 章:介绍 Python 中的函数式编程,包括匿名函数、闭包和装饰器等。 +- 第 5 章:介绍 Python 中类的使用,包括类方法、静态方法、super 和元类的使用等。 +- 第 6 章:介绍 Python 中的高级特性,比如生成器,上下文管理器。 +- 第 7 章:介绍文件和目录操作,os 的使用。 +- 第 8 章:介绍使用 Python 处理进程、线程和协程。 +- 第 9 章:异常处理。 +- 第 10 章:单元测试。 +- 第 11 章:正则表达式,re 模块的使用。 +- 第 12 章:HTTP 服务,requests 模块的使用。 +- 第 13 章:一些标准模块的使用,比如 argparse、collections 和 datetime 等。 +- 第 14 章:一些第三方模块的使用。 +- 第 15 章:结束语。 + +本书的编码环境: + +- Python 版本以 2.7 为主,同时也会指出在 Python3 中的相应变化 +- 操作系统使用 macOS,代码结果,尤其是内存地址等由于运行环境的不同会存在差异 + +本书将会持续进行修订和更新,读者如果遇到问题,请及时向我反馈,我会在第一时间加以解决。 + + +## 声明 + +Creative Commons License + +本书由 [Ethan](https://github.com/ethan-funny) 编写,采用 [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.zh) 协议发布。 + +这意味着你可以在非商业性使用的前提下自由转载,但必须: + +1. 保持署名 +2. 不对本书进行修改 + +## 更新记录 + +| 时间 | 说明 | +| :---: | :---: | +| 2017-01-03 | 发布版本 v1.0 | +| 2019-02-09 | fix typo | + + +## 支持我 + +如果你觉得本书对你有所帮助,不妨请我喝杯咖啡,感谢支持! + + + + diff --git a/Regular-Expressions/re.md b/Regular-Expressions/re.md new file mode 100644 index 0000000..77a6832 --- /dev/null +++ b/Regular-Expressions/re.md @@ -0,0 +1,594 @@ +# re 模块 + +在 Python 中,我们可以使用内置的 re 模块来使用正则表达式。 + +有一点需要特别注意的是,正则表达式使用 `\` 对特殊字符进行转义,比如,为了匹配字符串 'python.org',我们需要使用正则表达式 `'python\.org'`,而 Python 的字符串本身也用 `\` 转义,所以上面的正则表达式在 Python 中应该写成 `'python\\.org'`,这会很容易陷入 `\` 的困扰中,因此,我们建议使用 Python 的原始字符串,只需加一个 r 前缀,上面的正则表达式可以写成: + +``` +r'python\.org' +``` + +re 模块提供了不少有用的函数,用以匹配字符串,比如: + +- compile 函数 +- match 函数 +- search 函数 +- findall 函数 +- finditer 函数 +- split 函数 +- sub 函数 +- subn 函数 + +re 模块的一般使用步骤如下: + +- 使用 compile 函数将正则表达式的字符串形式编译为一个 Pattern 对象 +- 通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果(一个 Match 对象) +- 最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作 + +# compile 函数 + +**compile 函数用于编译正则表达式,生成一个 Pattern 对象**,它的一般使用形式如下: + +``` +re.compile(pattern[, flag]) +``` + +其中,pattern 是一个字符串形式的正则表达式,flag 是一个可选参数,表示匹配模式,比如忽略大小写,多行模式等。 + +下面,让我们看看例子。 + +```python +import re + +# 将正则表达式编译成 Pattern 对象 +pattern = re.compile(r'\d+') +``` + +在上面,我们已将一个正则表达式编译成 Pattern 对象,接下来,我们就可以利用 pattern 的一系列方法对文本进行匹配查找了。Pattern 对象的一些常用方法主要有: + +- match 方法 +- search 方法 +- findall 方法 +- finditer 方法 +- split 方法 +- sub 方法 +- subn 方法 + +## match 方法 + +match 方法用于查找字符串的头部(也可以指定起始位置),它是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果。它的一般使用形式如下: + +``` +match(string[, pos[, endpos]]) +``` + +其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。因此,**当你不指定 pos 和 endpos 时,match 方法默认匹配字符串的头部**。 + +当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None。 + +看看例子。 + +```python +>>> import re +>>> pattern = re.compile(r'\d+') # 用于匹配至少一个数字 +>>> m = pattern.match('one12twothree34four') # 查找头部,没有匹配 +>>> print m +None +>>> m = pattern.match('one12twothree34four', 2, 10) # 从'e'的位置开始匹配,没有匹配 +>>> print m +None +>>> m = pattern.match('one12twothree34four', 3, 10) # 从'1'的位置开始匹配,正好匹配 +>>> print m # 返回一个 Match 对象 +<_sre.SRE_Match object at 0x10a42aac0> +>>> m.group(0) # 可省略 0 +'12' +>>> m.start(0) # 可省略 0 +3 +>>> m.end(0) # 可省略 0 +5 +>>> m.span(0) # 可省略 0 +(3, 5) +``` + +在上面,当匹配成功时返回一个 Match 对象,其中: + +- `group([group1, …])` 方法用于获得一个或多个分组匹配的字符串,当要获得整个匹配的子串时,可直接使用 `group()` 或 `group(0)`; +- `start([group])` 方法用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0; +- `end([group])` 方法用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0; +- `span([group])` 方法返回 `(start(group), end(group))`。 + +再看看一个例子: + +```python +>>> import re +>>> pattern = re.compile(r'([a-z]+) ([a-z]+)', re.I) # re.I 表示忽略大小写 +>>> m = pattern.match('Hello World Wide Web') +>>> print m # 匹配成功,返回一个 Match 对象 +<_sre.SRE_Match object at 0x10bea83e8> +>>> m.group(0) # 返回匹配成功的整个子串 +'Hello World' +>>> m.span(0) # 返回匹配成功的整个子串的索引 +(0, 11) +>>> m.group(1) # 返回第一个分组匹配成功的子串 +'Hello' +>>> m.span(1) # 返回第一个分组匹配成功的子串的索引 +(0, 5) +>>> m.group(2) # 返回第二个分组匹配成功的子串 +'World' +>>> m.span(2) # 返回第二个分组匹配成功的子串 +(6, 11) +>>> m.groups() # 等价于 (m.group(1), m.group(2), ...) +('Hello', 'World') +>>> m.group(3) # 不存在第三个分组 +Traceback (most recent call last): + File "", line 1, in +IndexError: no such group +``` + +## search 方法 + +search 方法用于查找字符串的任何位置,它也是一次匹配,只要找到了一个匹配的结果就返回,而不是查找所有匹配的结果,它的一般使用形式如下: + +``` +search(string[, pos[, endpos]]) +``` + +其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。 + +当匹配成功时,返回一个 Match 对象,如果没有匹配上,则返回 None。 + +让我们看看例子: + +```python +>>> import re +>>> pattern = re.compile('\d+') +>>> m = pattern.search('one12twothree34four') # 这里如果使用 match 方法则不匹配 +>>> m +<_sre.SRE_Match object at 0x10cc03ac0> +>>> m.group() +'12' +>>> m = pattern.search('one12twothree34four', 10, 30) # 指定字符串区间 +>>> m +<_sre.SRE_Match object at 0x10cc03b28> +>>> m.group() +'34' +>>> m.span() +(13, 15) +``` + +再来看一个例子: + +```python +# -*- coding: utf-8 -*- + +import re + +# 将正则表达式编译成 Pattern 对象 +pattern = re.compile(r'\d+') + +# 使用 search() 查找匹配的子串,不存在匹配的子串时将返回 None +# 这里使用 match() 无法成功匹配 +m = pattern.search('hello 123456 789') + +if m: + # 使用 Match 获得分组信息 + print 'matching string:',m.group() + print 'position:',m.span() +``` + +执行结果: + +``` +matching string: 123456 +position: (6, 12) +``` + +## findall 方法 + +上面的 match 和 search 方法都是一次匹配,只要找到了一个匹配的结果就返回。然而,在大多数时候,我们需要搜索整个字符串,获得所有匹配的结果。 + +findall 方法的使用形式如下: + +``` +findall(string[, pos[, endpos]]) +``` + +其中,string 是待匹配的字符串,pos 和 endpos 是可选参数,指定字符串的起始和终点位置,默认值分别是 0 和 len (字符串长度)。 + +findall 以列表形式返回全部能匹配的子串,如果没有匹配,则返回一个空列表。 + +看看例子: + +```python +import re + +pattern = re.compile(r'\d+') # 查找数字 +result1 = pattern.findall('hello 123456 789') +result2 = pattern.findall('one1two2three3four4', 0, 10) + +print result1 +print result2 +``` + +执行结果: + +``` +['123456', '789'] +['1', '2'] +``` + +## finditer 方法 + +finditer 方法的行为跟 findall 的行为类似,也是搜索整个字符串,获得所有匹配的结果。但它返回一个顺序访问每一个匹配结果(Match 对象)的迭代器。 + +看看例子: + +```python +# -*- coding: utf-8 -*- + +import re + +pattern = re.compile(r'\d+') + +result_iter1 = pattern.finditer('hello 123456 789') +result_iter2 = pattern.finditer('one1two2three3four4', 0, 10) + +print type(result_iter1) +print type(result_iter2) + +print 'result1...' +for m1 in result_iter1: # m1 是 Match 对象 + print 'matching string: {}, position: {}'.format(m1.group(), m1.span()) + +print 'result2...' +for m2 in result_iter2: + print 'matching string: {}, position: {}'.format(m2.group(), m2.span()) +``` + +执行结果: + +``` + + +result1... +matching string: 123456, position: (6, 12) +matching string: 789, position: (13, 16) +result2... +matching string: 1, position: (3, 4) +matching string: 2, position: (7, 8) +``` + +## split 方法 + +split 方法按照能够匹配的子串将字符串分割后返回列表,它的使用形式如下: + +``` +split(string[, maxsplit]) +``` + +其中,maxsplit 用于指定最大分割次数,不指定将全部分割。 + +看看例子: + +```python +import re + +p = re.compile(r'[\s\,\;]+') +print p.split('a,b;; c d') +``` + +执行结果: + +``` +['a', 'b', 'c', 'd'] +``` + +## sub 方法 + +sub 方法用于替换。它的使用形式如下: + +``` +sub(repl, string[, count]) +``` + +其中,repl 可以是字符串也可以是一个函数: + +- 如果 repl 是字符串,则会使用 repl 去替换字符串每一个匹配的子串,并返回替换后的字符串,另外,repl 还可以使用 `\id` 的形式来引用分组,但不能使用编号 0; +- 如果 repl 是函数,这个方法应当只接受一个参数(Match 对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。 + +count 用于指定最多替换次数,不指定时全部替换。 + +看看例子: + +```python +import re + +p = re.compile(r'(\w+) (\w+)') +s = 'hello 123, hello 456' + +def func(m): + return 'hi' + ' ' + m.group(2) + +print p.sub(r'hello world', s) # 使用 'hello world' 替换 'hello 123' 和 'hello 456' +print p.sub(r'\2 \1', s) # 引用分组 +print p.sub(func, s) +print p.sub(func, s, 1) # 最多替换一次 +``` + +执行结果: + +``` +hello world, hello world +123 hello, 456 hello +hi 123, hi 456 +hi 123, hello 456 +``` + +## subn 方法 + +subn 方法跟 sub 方法的行为类似,也用于替换。它的使用形式如下: + +``` +subn(repl, string[, count]) +``` + +它返回一个元组: + +``` +(sub(repl, string[, count]), 替换次数) +``` + +元组有两个元素,第一个元素是使用 sub 方法的结果,第二个元素返回原字符串被替换的次数。 + +看看例子: + +``` +import re + +p = re.compile(r'(\w+) (\w+)') +s = 'hello 123, hello 456' + +def func(m): + return 'hi' + ' ' + m.group(2) + +print p.subn(r'hello world', s) +print p.subn(r'\2 \1', s) +print p.subn(func, s) +print p.subn(func, s, 1) +``` + +执行结果: + +``` +('hello world, hello world', 2) +('123 hello, 456 hello', 2) +('hi 123, hi 456', 2) +('hi 123, hello 456', 1) +``` + +# 其他函数 + +事实上,使用 compile 函数生成的 Pattern 对象的一系列方法跟 re 模块的多数函数是对应的,但在使用上有细微差别。 + +## match 函数 + +match 函数的使用形式如下: + +``` +re.match(pattern, string[, flags]): +``` + +其中,pattern 是正则表达式的字符串形式,比如 `\d+`, `[a-z]+`。 + +而 Pattern 对象的 match 方法使用形式是: + +``` +match(string[, pos[, endpos]]) +``` + +可以看到,match 函数不能指定字符串的区间,它只能搜索头部,看看例子: + +```python +import re + +m1 = re.match(r'\d+', 'One12twothree34four') +if m1: + print 'matching string:',m1.group() +else: + print 'm1 is:',m1 + +m2 = re.match(r'\d+', '12twothree34four') +if m2: + print 'matching string:', m2.group() +else: + print 'm2 is:',m2 +``` + +执行结果: + +``` +m1 is: None +matching string: 12 +``` + +## search 函数 + +search 函数的使用形式如下: + +``` +re.search(pattern, string[, flags]) +``` + +search 函数不能指定字符串的搜索区间,用法跟 Pattern 对象的 search 方法类似。 + +## findall 函数 + +findall 函数的使用形式如下: + +``` +re.findall(pattern, string[, flags]) +``` + +findall 函数不能指定字符串的搜索区间,用法跟 Pattern 对象的 findall 方法类似。 + +看看例子: + +```python +import re + +print re.findall(r'\d+', 'hello 12345 789') + +# 输出 +['12345', '789'] +``` + +## finditer 函数 + +finditer 函数的使用方法跟 Pattern 的 finditer 方法类似,形式如下: + +``` +re.finditer(pattern, string[, flags]) +``` + +## split 函数 + +split 函数的使用形式如下: + +``` +re.split(pattern, string[, maxsplit]) +``` + +## sub 函数 + +sub 函数的使用形式如下: + +``` +re.sub(pattern, repl, string[, count]) +``` + +## subn 函数 + +subn 函数的使用形式如下: + +``` +re.subn(pattern, repl, string[, count]) +``` + +# 到底用哪种方式 + +从上文可以看到,使用 re 模块有两种方式: + +- 使用 re.compile 函数生成一个 Pattern 对象,然后使用 Pattern 对象的一系列方法对文本进行匹配查找; +- 直接使用 re.match, re.search 和 re.findall 等函数直接对文本匹配查找; + +下面,我们用一个例子展示这两种方法。 + +先看第 1 种用法: + +```python +import re + +# 将正则表达式先编译成 Pattern 对象 +pattern = re.compile(r'\d+') + +print pattern.match('123, 123') +print pattern.search('234, 234') +print pattern.findall('345, 345') +``` + +再看第 2 种用法: + +```python +import re + +print re.match(r'\d+', '123, 123') +print re.search(r'\d+', '234, 234') +print re.findall(r'\d+', '345, 345') +``` + +如果一个正则表达式需要用到多次(比如上面的 `\d+`),在多种场合经常需要被用到,出于效率的考虑,我们应该预先编译该正则表达式,生成一个 Pattern 对象,再使用该对象的一系列方法对需要匹配的文件进行匹配;而如果直接使用 re.match, re.search 等函数,每次传入一个正则表达式,它都会被编译一次,效率就会大打折扣。 + +因此,我们推荐使用第 1 种用法。 + +# 匹配中文 + +在某些情况下,我们想匹配文本中的汉字,有一点需要注意的是,[中文的 unicode 编码范围](http://blog.oasisfeng.com/2006/10/19/full-cjk-unicode-range/) 主要在 `[\u4e00-\u9fa5]`,这里说主要是因为这个范围并不完整,比如没有包括全角(中文)标点,不过,在大部分情况下,应该是够用的。 + +假设现在想把字符串 `title = u'你好,hello,世界'` 中的中文提取出来,可以这么做: + +```python +# -*- coding: utf-8 -*- + +import re + +title = u'你好,hello,世界' +pattern = re.compile(ur'[\u4e00-\u9fa5]+') +result = pattern.findall(title) + +print result +``` + +注意到,我们在正则表达式前面加上了两个前缀 `ur`,其中 `r` 表示使用原始字符串,`u` 表示是 unicode 字符串。 + +执行结果: + +``` +[u'\u4f60\u597d', u'\u4e16\u754c'] +``` + +# 贪婪匹配 + +在 Python 中,正则匹配默认是**贪婪匹配**(在少数语言中可能是非贪婪),也就是**匹配尽可能多的字符**。 + +比如,我们想找出字符串中的所有 `div` 块: + +```python +import re + +content = 'aa
test1
bb
test2
cc' +pattern = re.compile(r'
.*
') +result = pattern.findall(content) + +print result +``` + +执行结果: + +``` +['
test1
bb
test2
'] +``` + +由于正则匹配是贪婪匹配,也就是尽可能多的匹配,因此,在成功匹配到第一个 `` 时,它还会向右尝试匹配,查看是否还有更长的可以成功匹配的子串。 + +如果我们想非贪婪匹配,可以加一个 `?`,如下: + +```python +import re + +content = 'aa
test1
bb
test2
cc' +pattern = re.compile(r'
.*?
') # 加上 ? +result = pattern.findall(content) + +print result +``` + +结果: + +``` +['
test1
', '
test2
'] +``` + +# 小结 + +- re 模块的一般使用步骤如下: + - 使用 compile 函数将正则表达式的字符串形式编译为一个 Pattern 对象; + - 通过 Pattern 对象提供的一系列方法对文本进行匹配查找,获得匹配结果(一个 Match 对象); + - 最后使用 Match 对象提供的属性和方法获得信息,根据需要进行其他的操作; +- Python 的正则匹配默认是贪婪匹配。 + +# 参考资料 + +- [Python正则表达式指南](http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html) +- [正则表达式 - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/001386832260566c26442c671fa489ebc6fe85badda25cd000) + + diff --git a/SUMMARY.md b/SUMMARY.md index fa61037..dc8cb78 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,57 +1,67 @@ # Summary * [前言](README.md) -* [基础](Basis/README.md) - * [输入和输出](Basis/input_output.md) - * [字符编码](Basis/character_encoding.md) -* [基本数据类型](Datatypes/README.md) - * [数字](Datatypes/number.md) - * [字符串](Datatypes/string.md) - * [元组](Datatypes/tuple.md) +* [基础](Basic/README.md) + * [字符编码](Basic/character_encoding.md) + * [输入和输出](Basic/input_output.md) +* [常用数据类型](Datatypes/README.md) * [列表](Datatypes/list.md) + * [元组](Datatypes/tuple.md) + * [字符串](Datatypes/string.md) * [字典](Datatypes/dict.md) * [集合](Datatypes/set.md) * [函数](Function/README.md) * [定义函数](Function/func_definition.md) - * [函数返回](Function/func_return.md) - * [函数参数](Function/func_parameter.md) + * [函数参数魔法](Function/func_parameter.md) * [函数式编程](Functional/README.md) * [高阶函数](Functional/high_order_func.md) * [map/reduce/filter](Functional/map_reduce_filter.md) * [匿名函数](Functional/anonymous_func.md) - * [闭包](Functional/closure.md) - * [装饰器](Functional/decorator.md) - * [functools](Functional/functools.md) + * [携带状态的闭包](Functional/closure.md) + * [会打扮的装饰器](Functional/decorator.md) + * [partial 函数](Functional/partial.md) * [类](Class/README.md) * [类和实例](Class/class_and_object.md) * [继承和多态](Class/inheritance_and_polymorphism.md) - * [super() 函数](Class/super.md) * [类方法和静态方法](Class/method.md) - * [定制类和特殊方法](Class/special_method.md) + * [定制类和魔法方法](Class/magic_method.md) * [slots 魔法](Class/slots.md) * [使用 @property](Class/property.md) - * [元类](Class/metaclass.md) + * [你不知道的 super](Class/super.md) + * [陌生的 metaclass](Class/metaclass.md) * [高级特性](Advanced-Features/README.md) * [迭代器](Advanced-Features/iterator.md) * [生成器](Advanced-Features/generator.md) * [上下文管理器](Advanced-Features/context.md) * [文件和目录](File-Directory/README.md) - * 文件操作 - * 读写图像 - * 读写大文件 + * [读写文本文件](File-Directory/text_file_io.md) + * [读写二进制文件](File-Directory/binary_file_io.md) + * [os 模块](File-Directory/os.md) * [进程、线程和协程](Process-Thread-Coroutine/README.md) - * [进程](Process-Thread-Coroutine/Process.md) - * [线程](Process-Thread-Coroutine/Thread.md) - * [ThreadLocal](Process-Thread-Coroutine/ThreadLocal.md) - * [协程](Process-Thread-Coroutine/Coroutine.md) -* [错误和异常](Exception/README.md) + * [进程](Process-Thread-Coroutine/process.md) + * [线程](Process-Thread-Coroutine/thread.md) + * [ThreadLocal](Process-Thread-Coroutine/threadlocal.md) + * [协程](Process-Thread-Coroutine/coroutine.md) +* [异常处理](Exception/README.md) +* [单元测试](Testing/README.md) * [正则表达式](Regular-Expressions/README.md) + * [re 模块](Regular-Expressions/re.md) * [HTTP 服务](HTTP/README.md) -* [测试](Testing/README.md) - * [单元测试](Testing/unit_testing.md) -* [常用标准模块](Standard-Modules/README.md) + * [HTTP 协议简介](HTTP/HTTP.md) + * [Requests 库的使用](HTTP/Requests.md) +* [标准模块](Standard-Modules/README.md) + * [argparse](Standard-Modules/argparse.md) + * [base64](Standard-Modules/base64.md) + * [collections](Standard-Modules/collections.md) + * [itertools](Standard-Modules/itertools.md) + * [datetime](Standard-Modules/datetime.md) + * [hashlib](Standard-Modules/hashlib.md) + * [hmac](Standard-Modules/hmac.md) * [第三方模块](Third-Party-Modules/README.md) + * [celery](Third-Party-Modules/celery.md) + * [click](Third-Party-Modules/click.md) * [结束语](Conclusion/README.md) - * [参考资料](Conclusion/reference_material.md) * [资源推荐](Conclusion/resource_recommendation.md) + * [参考资料](Conclusion/reference_material.md) + diff --git a/Standard-Modules/README.md b/Standard-Modules/README.md index f2c218e..375df38 100644 --- a/Standard-Modules/README.md +++ b/Standard-Modules/README.md @@ -1,2 +1,20 @@ -# 常用标准模块 +# 标准模块 + +前面我们介绍了 os 模块和 re 模块,本章再介绍 Python 常用的一些标准模块: + +- [argparse](./argparse.md) +- [base64](./base64.md) +- [collections](./collections.md) +- [datetime](./datetime.md) +- [hashlib](./hashlib.md) +- [hmac](./hmac.md) + +其中: + +- argparse 是用于创建命令行的库; +- base64 是用于 base64 编码和解码的库; +- collections 模块提供了 5 个高性能的数据类型,如 `Counter`,`OrderedDict` 等; +- datetime 是用于处理日期时间的模块; +- hashlib 模块提供了常见的摘要算法,比如 MD5,SHA1 等; +- hmac 模块提供了 HMAC 哈希算法; diff --git a/Standard-Modules/argparse.md b/Standard-Modules/argparse.md new file mode 100644 index 0000000..ac99dfd --- /dev/null +++ b/Standard-Modules/argparse.md @@ -0,0 +1,183 @@ +# argparse + +argparse 是 Python 内置的一个用于命令项选项与参数解析的模块,通过在程序中定义好我们需要的参数,argparse 将会从 sys.argv 中解析出这些参数,并自动生成帮助和使用信息。当然,Python 也有第三方的库可用于命令行解析,而且功能也更加强大,比如 [docopt](http://docopt.org/),[Click](http://click.pocoo.org/5/)。 + +# argparse 使用 + +## 简单示例 + +我们先来看一个简单示例。主要有三个步骤: + +- 创建 ArgumentParser() 对象 +- 调用 add_argument() 方法添加参数 +- 使用 parse_args() 解析添加的参数 + +```python +# -*- coding: utf-8 -*- + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('integer', type=int, help='display an integer') +args = parser.parse_args() + +print args.integer +``` + +将上面的代码保存为文件 `argparse_usage.py`,在终端运行,结果如下: + +``` +$ python argparse_usage.py +usage: argparse_usage.py [-h] integer +argparse_usage.py: error: too few arguments + +$ python argparse_usage.py abcd +usage: argparse_usage.py [-h] integer +argparse_usage.py: error: argument integer: invalid int value: 'abcd' + +$ python argparse_usage.py -h +usage: argparse_usage.py [-h] integer + +positional arguments: + integer display an integer + +optional arguments: + -h, --help show this help message and exit + +$ python argparse_usage.py 10 +10 +``` + +## 定位参数 + +上面的示例,其实就展示了定位参数的使用,我们再来看一个例子 - 计算一个数的平方: + +```py +# -*- coding: utf-8 -*- + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("square", help="display a square of a given number", type=int) +args = parser.parse_args() +print args.square**2 +``` + +将上面的代码保存为文件 `argparse_usage.py`,在终端运行,结果如下: + +``` +$ python argparse_usage.py 9 +81 +``` + +## 可选参数 + +现在看下可选参数的用法,所谓可选参数,也就是命令行参数是可选的,废话少说,看下面例子: + +``` +# -*- coding: utf-8 -*- + +import argparse + +parser = argparse.ArgumentParser() + +parser.add_argument("--square", help="display a square of a given number", type=int) +parser.add_argument("--cubic", help="display a cubic of a given number", type=int) + +args = parser.parse_args() + +if args.square: + print args.square**2 + +if args.cubic: + print args.cubic**3 +``` + +将上面的代码保存为文件 `argparse_usage.py`,在终端运行,结果如下: + +``` +$ python argparse_usage.py --h +usage: argparse_usage.py [-h] [--square SQUARE] [--cubic CUBIC] + +optional arguments: + -h, --help show this help message and exit + --square SQUARE display a square of a given number + --cubic CUBIC display a cubic of a given number + +$ python argparse_usage.py --square 8 +64 + +$ python argparse_usage.py --cubic 8 +512 + +$ python argparse_usage.py 8 +usage: argparse_usage.py [-h] [--square SQUARE] [--cubic CUBIC] +argparse_usage.py: error: unrecognized arguments: 8 + +$ python argparse_usage.py # 没有输出 +``` + +## 混合使用 + +定位参数和选项参数可以混合使用,看下面一个例子,给一个整数序列,输出它们的和或最大值(默认): + +```py +import argparse + +parser = argparse.ArgumentParser(description='Process some integers.') +parser.add_argument('integers', metavar='N', type=int, nargs='+', + help='an integer for the accumulator') +parser.add_argument('--sum', dest='accumulate', action='store_const', + const=sum, default=max, + help='sum the integers (default: find the max)') + +args = parser.parse_args() +print args.accumulate(args.integers) +``` + +结果: + +```py +$ python argparse_usage.py +usage: argparse_usage.py [-h] [--sum] N [N ...] +argparse_usage.py: error: too few arguments +$ python argparse_usage.py 1 2 3 4 +4 +$ python argparse_usage.py 1 2 3 4 --sum +10 +``` + +## add_argument() 方法 + +add_argument() 方法定义如何解析命令行参数: + +``` +ArgumentParser.add_argument(name or flags...[, action][, nargs][, const][, default][, type][, choices][, required][, help][, metavar][, dest]) +``` + +每个参数解释如下: + +- name or flags - 选项字符串的名字或者列表,例如 foo 或者 -f, --foo。 +- action - 命令行遇到参数时的动作,默认值是 store。 + - store_const,表示赋值为const; + - append,将遇到的值存储成列表,也就是如果参数重复则会保存多个值; + - append_const,将参数规范中定义的一个值保存到一个列表; + - count,存储遇到的次数;此外,也可以继承 argparse.Action 自定义参数解析; +- nargs - 应该读取的命令行参数个数,可以是具体的数字,或者是?号,当不指定值时对于 Positional argument 使用 default,对于 Optional argument 使用 const;或者是 * 号,表示 0 或多个参数;或者是 + 号表示 1 或多个参数。 +- const - action 和 nargs 所需要的常量值。 +- default - 不指定参数时的默认值。 +- type - 命令行参数应该被转换成的类型。 +- choices - 参数可允许的值的一个容器。 +- required - 可选参数是否可以省略 (仅针对可选参数)。 +- help - 参数的帮助信息,当指定为 `argparse.SUPPRESS` 时表示不显示该参数的帮助信息. +- metavar - 在 usage 说明中的参数名称,对于必选参数默认就是参数名称,对于可选参数默认是全大写的参数名称. +- dest - 解析后的参数名称,默认情况下,对于可选参数选取最长的名称,中划线转换为下划线. + +# 参考资料 + +- [Argparse Tutorial — Python 2.7.12 documentation](https://docs.python.org/2/howto/argparse.html) +- [Argparse – Command line option and argument parsing](https://pymotw.com/2/argparse/) +- [Argparse — Parser for command-line options, arguments and sub-commands](http://python.usyiyi.cn/python_278/library/argparse.html) +- [Python 中的命令行解析工具介绍](http://lingxiankong.github.io/blog/2014/01/14/command-line-parser/) + + diff --git a/Standard-Modules/base64.md b/Standard-Modules/base64.md new file mode 100644 index 0000000..7815c69 --- /dev/null +++ b/Standard-Modules/base64.md @@ -0,0 +1,61 @@ +# Base64 + +**Base64,简单地讲,就是用 64 个字符来表示二进制数据的方法**。这 64 个字符包含小写字母 a-z、大写字母 A-Z、数字 0-9 以及符号"+"、"/",其实还有一个 "=" 作为后缀用途,所以实际上有 65 个字符。 + +本文主要介绍如何使用 Python 进行 Base64 编码和解码,关于 Base64 编码转换的规则可以参考 [Base64 笔记](http://www.ruanyifeng.com/blog/2008/06/base64.html)。 + +Python 内置了一个用于 Base64 编解码的库:`base64`: + +- 编码使用 `base64.b64encode()` +- 解码使用 `base64.b64decode()` + +下面,我们介绍文本和图片的 Base64 编解码。 + +# 对文本进行 Base64 编码和解码 + +```python +>>> import base64 +>>> str = 'hello world' +>>> +>>> base64_str = base64.b64encode(str) # 编码 +>>> print base64_str +aGVsbG8gd29ybGQ= +>>> +>>> ori_str = base64.b64decode(base64_str) # 解码 +>>> print ori_str +hello world +``` + +# 对图片进行 Base64 编码和解码 + +```python +def convert_image(): + # 原始图片 ==> base64 编码 + with open('/path/to/alpha.png', 'r') as fin: + image_data = fin.read() + base64_data = base64.b64encode(image_data) + + fout = open('/path/to/base64_content.txt', 'w') + fout.write(base64_data) + fout.close() + + # base64 编码 ==> 原始图片 + with open('/path/to/base64_content.txt', 'r') as fin: + base64_data = fin.read() + ori_image_data = base64.b64decode(base64_data) + + fout = open('/path/to/beta.png', 'wb'): + fout.write(ori_image_data) + fout.close() +``` + +# 小结 + +- Base64 可以将任意二进制数据编码到文本字符串,常用于在 URL、Cookie 和网页中传输少量二进制数据。 + +# 参考资料 + +- [Base64 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/Base64) +- [Base64笔记 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2008/06/base64.html) + + diff --git a/Standard-Modules/collections.md b/Standard-Modules/collections.md new file mode 100644 index 0000000..48ad146 --- /dev/null +++ b/Standard-Modules/collections.md @@ -0,0 +1,205 @@ +# collections + +我们知道,Python 的数据类型有 list, tuple, dict, str 等,**collections 模块**提供了额外 5 个高性能的数据类型: + +- `Counter`: 计数器 +- `OrderedDict`: 有序字典 +- `defaultdict`: 带有默认值的字典 +- `namedtuple`: 生成可以通过属性访问元素内容的 tuple 子类 +- `deque`: 双端队列,能够在队列两端添加或删除元素 + +# Counter + +`Counter` 是一个简单的计数器,可用于统计字符串、列表等的元素个数。 + +看看例子: + +```python +>>> from collections import Counter +>>> +>>> s = 'aaaabbbccd' +>>> c = Counter(s) # 创建了一个 Counter 对象 +>>> c +Counter({'a': 4, 'b': 3, 'c': 2, 'd': 1}) +>>> isinstance(c, dict) # c 其实也是一个字典对象 +True +>>> c.get('a') +4 +>>> c.most_common(2) # 获取出现次数最多的前两个元素 +[('a', 4), ('b', 3)] +``` + +在上面,我们使用 `Counter()` 创建了一个 `Counter` 对象 `c`,`Counter` 其实是 dict 的一个子类,我们可以使用 `get` 方法来获取某个元素的个数。`Counter` 对象有一个 `most_common` 方法,允许我们获取出现次数最多的前几个元素。 + +另外,两个 `Counter` 对象还可以做运算: + +```python +>>> from collections import Counter +>>> +>>> s1 = 'aaaabbbccd' +>>> c1 = Counter(s1) +>>> c1 +Counter({'a': 4, 'b': 3, 'c': 2, 'd': 1}) +>>> +>>> s2 = 'aaabbef' +>>> c2 = Counter(s2) +>>> c2 +Counter({'a': 3, 'b': 2, 'e': 1, 'f': 1}) +>>> +>>> c1 + c2 # 两个计数结果相加 +Counter({'a': 7, 'b': 5, 'c': 2, 'e': 1, 'd': 1, 'f': 1}) +>>> c1 - c2 # c2 相对于 c1 的差集 +Counter({'c': 2, 'a': 1, 'b': 1, 'd': 1}) +>>> c1 & c2 # c1 和 c2 的交集 +Counter({'a': 3, 'b': 2}) +>>> c1 | c2 # c1 和 c2 的并集 +Counter({'a': 4, 'b': 3, 'c': 2, 'e': 1, 'd': 1, 'f': 1}) +``` + +# OrderedDict + +Python 中的 dict 是无序的: + +```python +>>> dict([('a', 10), ('b', 20), ('c', 15)]) +{'a': 10, 'c': 15, 'b': 20} +``` + +有时,我们希望保持 key 的顺序,这时可以用 OrderedDict: + +```python +>>> from collections import OrderedDict +>>> OrderedDict([('a', 10), ('b', 20), ('c', 15)]) +OrderedDict([('a', 10), ('b', 20), ('c', 15)]) +``` + +# defaultdict + +在 Python 中使用 dict 时,如果访问了不存在的 key,会抛出 KeyError 异常,因此,在访问之前,我们经常需要对 key 作判断,比如: + +```python +>>> d = dict() +>>> s = 'aaabbc' +>>> for char in s: +... if char in d: +... d[char] += 1 +... else: +... d[char] = 1 +... +>>> d +{'a': 3, 'c': 1, 'b': 2} +``` + +使用 defaultdict,我们可以给字典中的 key 提供一个默认值。访问 defaultdict 中的 key,如果 key 存在,就返回 key 对应的 value,如果 key 不存在,就返回默认值。 + +```python +>>> from collections import defaultdict +>>> d = defaultdict(int) # 默认的 value 值是 0 +>>> s = 'aaabbc' +>>> for char in s: +... d[char] += 1 +... +>>> d +defaultdict(, {'a': 3, 'c': 1, 'b': 2}) +>>> d.get('a') +3 +>>> d['z'] +0 +``` + +使用 defaultdict 时,我们可以传入一个工厂方法来指定默认值,比如传入 int,表示默认值是 0,传入 list,表示默认是 `[]`: + +```python +>>> from collections import defaultdict +>>> +>>> d1 = defaultdict(int) +>>> d1['a'] +0 +>>> d2 = defaultdict(list) +>>> d2['b'] +[] +>>> d3 = defaultdict(str) +>>> d3['a'] +'' +``` + +我们还可以自定义默认值,通过 `lambda` 函数来实现: + +```python +>>> from collections import defaultdict +>>> +>>> d = defaultdict(lambda: 10) +>>> d['a'] +10 +``` + +# namedtuple + +我们经常用 tuple (元组) 来表示一个不可变对象,比如用一个 `(姓名, 学号, 年龄)` 的元组来表示一个学生: + +```python +>>> stu = ('ethan', '001', 20) +>>> stu[0] +'ethan' +``` + +这里使用 tuple 没什么问题,但可读性比较差,我们必须清楚索引代表的含义,比如索引 0 表示姓名,索引 1 表示学号。如果用类来定义,就可以通过设置属性 name, id, age 来表示,但就有些小题大作了。 + +我们可以通过 namedtuple 为元组的每个索引设置名称,然后通过「属性名」来访问: + +```python +>>> from collections import namedtuple +>>> Student = namedtuple('Student', ['name', 'id', 'age']) # 定义了一个 Student 元组 +>>> +>>> stu = Student('ethan', '001', 20) +>>> stu.name +'ethan' +>>> stu.id +'001' +``` + +# deque + +deque 是双端队列,允许我们在队列两端添加或删除元素。 + +```python +>>> from collections import deque + +>>> q = deque(['a', 'b', 'c', 'd']) +>>> q.append('e') # 添加到尾部 +>>> q +deque(['a', 'b', 'c', 'd', 'e']) +>>> q.appendleft('o') # 添加到头部 +>>> q +deque(['o', 'a', 'b', 'c', 'd', 'e']) +>>> q.pop() # 从尾部弹出元素 +'e' +>>> q +deque(['o', 'a', 'b', 'c', 'd']) +>>> q.popleft() # 从头部弹出元素 +'o' +>>> q +deque(['a', 'b', 'c', 'd']) +>>> q.extend('ef') # 在尾部 extend 元素 +>>> q +deque(['a', 'b', 'c', 'd', 'e', 'f']) +>>> q.extendleft('uv') # 在头部 extend 元素,注意顺序 +>>> q +deque(['v', 'u', 'a', 'b', 'c', 'd', 'e', 'f']) +>>> +>>> q.rotate(2) # 将尾部的两个元素移动到头部 +>>> q +deque(['e', 'f', 'v', 'u', 'a', 'b', 'c', 'd']) +>>> q.rotate(-2) # 将头部的两个元素移动到尾部 +>>> q +deque(['v', 'u', 'a', 'b', 'c', 'd', 'e', 'f']) +``` + +其中,`rotate` 方法用于旋转,如果旋转参数 n 大于 0,表示将队列右端的 n 个元素移动到左端,否则相反。 + + +# 参考资料 + +- [collections — High-performance container datatypes — Python 2.7.13 documentation](https://docs.python.org/2/library/collections.html#module-collections) + + diff --git a/Standard-Modules/datetime.md b/Standard-Modules/datetime.md new file mode 100644 index 0000000..b9793f6 --- /dev/null +++ b/Standard-Modules/datetime.md @@ -0,0 +1,153 @@ +# datetime + +Python 提供了两个标准库用于处理跟时间相关的问题,一个是 `time`,另一个是 `datetime`,`datetime` 对 `time` 进行了封装,提供了更多实用的函数。本文介绍 `datetime` 库的简单使用。 + +# 当前时间 + +获取当前时间可以使用 `now()` 或 `utcnow()` 方法,其中,`now()` 用于获取当地时间,而 `utcnow()` 用于获取 UTC 时间。 + +```python +>>> from datetime import datetime + +>>> datetime.now() # 返回一个 datetime 对象,这里是当地时间 +datetime.datetime(2016, 12, 10, 11, 32, 43, 806970) + +>>> datetime.utcnow() # 返回一个 datetime 对象,这里是 UTC 时间 +datetime.datetime(2016, 12, 10, 3, 32, 49, 999423) + +>>> datetime.now().year, datetime.now().month, datetime.now().day # 年月日 +(2016, 12, 10) + +>>> datetime.now().hour, datetime.now().minute, datetime.now().second # 时分秒 +(11, 35, 37) +``` + +# 时间格式化 + +有时,我们需要对时间做格式化处理,可以使用 `strftime()` 或 `strptime()` 方法,其中,`strftime` 用于对 `datetime` 对象进行格式化,`strptime` 用于对字符串对象进行格式化。 + +```python +>>> from datetime import datetime + +# 获取当前当地时间 +>>> now = datetime.now() +>>> now +datetime.datetime(2016, 12, 10, 11, 46, 24, 432168) + +# 对 datetime 对象进行格式化,转为字符串格式 +>>> now_str = now.strftime('%Y-%m-%d %H:%M:%S.%f') +>>> now_str +'2016-12-10 11:46:24.432168' + +# 对字符串对象进行格式化,转为 datetime 对象 +>>> datetime.strptime(now_str, '%Y-%m-%d %H:%M:%S.%f') +datetime.datetime(2016, 12, 10, 11, 46, 24, 432168) +``` + +# 时间戳 + +[Unix 时间戳](http://funhacks.net/2015/04/29/Unix-timestamp/)根据精度的不同,有 10 位(秒级),13 位(毫秒级),16 位(微妙级)和 19 位(纳秒级)。 + +要注意的是,由于每个时区都有自己的本地时间(北京在东八区),因此也产生了世界标准时间(UTC, Universal Time Coordinated)。所以,在将一个时间戳转换为普通时间(比如 2016-01-01 12:00:00)时,要注意是要本地时区的时间还是世界时间等。 + +- 获取当前当地时间戳 + +```python +>>> import time +>>> from datetime import datetime + +# 获取当前当地时间,返回一个 datetime 对象 +>>> now = datetime.now() +>>> now +datetime.datetime(2016, 12, 9, 11, 56, 47, 632778) + +# 13 位的毫秒时间戳 +>>> long(time.mktime(now.timetuple()) * 1000.0 + now.microsecond / 1000.0) +1481255807632L + +# 10 位的时间戳 +>>> int(time.mktime(now.timetuple())) +1481255807 +``` + +- 获取当前 UTC 时间戳 + +```python +>>> import calendar +>>> from datetime import datetime + +# 获取当前的 UTC 时间,返回 datetime 对象 +>>> utc_now = datetime.utcnow() +>>> utc_now +datetime.datetime(2016, 12, 9, 4, 0, 53, 356641) + +# 13 位的时间戳 +>>> long(calendar.timegm(utc_now.timetuple()) * 1000.0 + utc_now.microsecond / 1000.0) +1481256053356L + +# 10 位的时间戳 +>>> calendar.timegm(utc_now.timetuple()) +1481256053 +``` + +- 将时间戳转为字符串形式 + +```python +>>> from datetime import datetime + +# 13 位的毫秒时间戳 +>>> timestamp = 1456402864242 + +# 根据时间戳构建当地时间 +>>> datetime.fromtimestamp(timestamp / 1000.0).strftime('%Y-%m-%d %H:%M:%S.%f') +'2016-02-25 20:21:04.242000' + +# 根据时间戳构建 UTC 时间 +>>> datetime.utcfromtimestamp(timestamp / 1000.0).strftime('%Y-%m-%d %H:%M:%S.%f') +'2016-02-25 12:21:04.242000' + +# 10 位的时间戳 +>>> timestamp = 1456402864 + +# 根据时间戳构建当地时间 +>>> datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') +'2016-02-25 20:21:04' + +# 根据时间戳构建 UTC 时间 +>>> datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') +'2016-02-25 12:21:04' +``` + +- 将时间戳转为 datetime 形式 + +```python +>>> from datetime import datetime + +# 13 位的毫秒时间戳 +>>> timestamp = 1456402864242 + +# 根据时间戳构建当地时间 +>>> datetime.fromtimestamp(timestamp / 1000.0) +datetime.datetime(2016, 2, 25, 20, 21, 4, 242000) + +# 根据时间戳构建 UTC 时间 +>>> datetime.utcfromtimestamp(timestamp / 1000.0) +datetime.datetime(2016, 2, 25, 12, 21, 4, 242000) + +# 10 位的时间戳 +>>> timestamp = 1456402864 + +# 根据时间戳构建当地时间 +>>> datetime.fromtimestamp(timestamp) +datetime.datetime(2016, 2, 25, 20, 21, 4) + +# 根据时间戳构建 UTC 时间 +>>> datetime.utcfromtimestamp(timestamp) +datetime.datetime(2016, 2, 25, 12, 21, 4) +``` + +# 参考资料 + +- [python-datetime-time-conversions](http://www.saltycrane.com/blog/2008/11/python-datetime-time-conversions/) + + diff --git a/Standard-Modules/hashlib.md b/Standard-Modules/hashlib.md new file mode 100644 index 0000000..7b841d4 --- /dev/null +++ b/Standard-Modules/hashlib.md @@ -0,0 +1,57 @@ +# hashlib + +Python 内置的 hashlib 模块提供了常见的**摘要算法**(或称哈希算法,散列算法),如 MD5,SHA1, SHA256 等。**摘要算法的基本原理是:将数据(如一段文字)运算变为另一固定长度值**。 + +MD5 (Message-Digest Algorithm 5, 消息摘要算法),是一种被广泛使用的密码散列函数,可以产生出一个 128 位(16 字节)的散列值(hash value),用于确保信息传输完整一致。 + +SHA1 (Secure Hash Algorithm, 安全哈希算法) 是 SHA 家族的其中一个算法,它经常被用作数字签名。 + +# MD5 + +hashlib 模块提供了 `md5` 函数,我们可以很方便地使用它: + +```python +>>> import hashlib +>>> +>>> m = hashlib.md5('md5 test in Python!') +>>> m.digest() +'\xad\xc0\x99\x01\x12\xc7&\xb5~\xb0\xaf \x974\x11\xab' +>>> m.hexdigest() # 使用一个 32 位的 16 进制字符串表示 +'adc0990112c726b57eb0af20973411ab' +``` + +上面,我们是直接把数据传入 `md5()` 函数,我们也可以通过一次或多次使用 `update` 来实现: + +```python +>>> import hashlib +>>> m = hashlib.md5() +>>> m.update('md5 test ') +>>> m.update('in Python!') +>>> m.hexdigest() +'adc0990112c726b57eb0af20973411ab' +``` + +# SHA1 + +SHA1 的使用和 MD5 的使用类似: + +```python +>>> import hashlib +>>> +>>> sha1 = hashlib.sha1() +>>> sha1.update('md5 test ') +>>> sha1.update('in Python!') +>>> sha1.hexdigest() +'698a8b18d5f99a140520475c342af455183c58a3' +``` + +# 小结 + +- MD5 以前经常用来做用户密码的存储。2004年,它被证明无法防止碰撞,因此无法适用于安全性认证,但仍广泛应用于普通数据的错误检查领域。如果你需要储存用户密码,你应该去了解一下诸如 [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) 或 [bcrypt](https://en.wikipedia.org/wiki/Bcrypt) 之类的算法。而 SHA1 则经常用作数字签名。 + +# 参考资料 + +- [MD5 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/MD5) +- [SHA家族 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/SHA%E5%AE%B6%E6%97%8F) + + diff --git a/Standard-Modules/hmac.md b/Standard-Modules/hmac.md new file mode 100644 index 0000000..36a2ab7 --- /dev/null +++ b/Standard-Modules/hmac.md @@ -0,0 +1,58 @@ +# hmac + +HMAC 是用于消息认证的加密哈希算法,全称是 keyed-Hash Message Authentication Code。**HMAC 利用哈希算法,以一个密钥和一个消息作为输入,生成一个加密串作为输出**。HMAC 可以有效防止类似 MD5 的彩虹表等攻击,比如将常见密码的 MD5 值存入数据库,可能被反向破解。 + +Python 的 `hmac` 模块提供了 HMAC 算法,它的使用形式是: + +```python +hmac.new(key[, msg[, digestmod]]) +``` + +其中,key 是一个密钥;msg 是消息,可选,如果给出 msg,则调用方法 `update(msg)`;digestmod 是 HMAC 对象使用的摘要构造函数或模块,默认为 `hashlib.md5` 构造函数。 + +HMAC 对象常用的方法有: + +- HMAC.update(msg) + +用字符串 msg 更新 HMAC 对象,重复的调用等同于一次调用所有参数的组合,即: + +``` +m.update(a); +m.update(b); +``` + +相当于 + +``` +m.update(a+b) +``` + +- HMAC.digest() + +返回目前传递给 `update()` 方法的字符串的摘要。此字符串长度将与给构造函数的摘要的 digest_size 相同。它可能包含非 ASCII 字符,包括 NULL 字节。 + +- HMAC.hexdigest() + +类似 digest(),但是返回的摘要的字符串的长度翻倍,且只包含十六进制数字。 + +现在,让我们看一个简单的例子: + +```python +>>> from datetime import datetime +>>> import hashlib +>>> import hmac + +>>> key = 'you-never-know' +>>> msg = datetime.utcnow().strftime('%Y-%m-%d') + +>>> m = hmac.new(key, msg, hashlib.sha1) +>>> signature = m.hexdigest() +>>> signature +'fdb2087a66a2f00afbc1884738467ba089782779' +``` + +# 参考资料 + +- [hmac — 用于消息认证的加密哈希算法](http://python.usyiyi.cn/python_278/library/hmac.html) + + diff --git a/Standard-Modules/itertools.md b/Standard-Modules/itertools.md new file mode 100644 index 0000000..a3eadf8 --- /dev/null +++ b/Standard-Modules/itertools.md @@ -0,0 +1,545 @@ +# itertools + +我们知道,迭代器的特点是:**惰性求值**(Lazy evaluation),即只有当迭代至某个值时,它才会被计算,这个特点使得迭代器特别适合于遍历大文件或无限集合等,因为我们不用一次性将它们存储在内存中。 + +Python 内置的 itertools 模块包含了一系列用来产生不同类型迭代器的函数或类,这些函数的返回都是一个迭代器,我们可以通过 for 循环来遍历取值,也可以使用 `next()` 来取值。 + +itertools 模块提供的迭代器函数有以下几种类型: + +- 无限迭代器:生成一个无限序列,比如自然数序列 `1, 2, 3, 4, ...`; +- 有限迭代器:接收一个或多个序列(sequence)作为参数,进行组合、分组和过滤等; +- 组合生成器:序列的排列、组合,求序列的笛卡儿积等; + +# 无限迭代器 + +itertools 模块提供了三个函数(事实上,它们是类)用于生成一个无限序列迭代器: + +- count(firstval=0, step=1) + + 创建一个从 firstval (默认值为 0) 开始,以 step (默认值为 1) 为步长的的无限整数迭代器 + +- cycle(iterable) + + 对 iterable 中的元素反复执行循环,返回迭代器 + +- repeat(object [,times] + + 反复生成 object,如果给定 times,则重复次数为 times,否则为无限 + +下面,让我们看看一些例子。 + +## count + +`count()` 接收两个参数,第一个参数指定开始值,默认为 0,第二个参数指定步长,默认为 1: + +```python +>>> import itertools +>>> +>>> nums = itertools.count() +>>> for i in nums: +... if i > 6: +... break +... print i +... +0 +1 +2 +3 +4 +5 +6 +>>> nums = itertools.count(10, 2) # 指定开始值和步长 +>>> for i in nums: +... if i > 20: +... break +... print i +... +10 +12 +14 +16 +18 +20 +``` + +## cycle + +`cycle()` 用于对 iterable 中的元素反复执行循环: + +```python +>>> import itertools +>>> +>>> cycle_strings = itertools.cycle('ABC') +>>> i = 1 +>>> for string in cycle_strings: +... if i == 10: +... break +... print i, string +... i += 1 +... +1 A +2 B +3 C +4 A +5 B +6 C +7 A +8 B +9 C +``` + +## repeat + +`repeat()` 用于反复生成一个 object: + +```python +>>> import itertools +>>> +>>> for item in itertools.repeat('hello world', 3): +... print item +... +hello world +hello world +hello world +>>> +>>> for item in itertools.repeat([1, 2, 3, 4], 3): +... print item +... +[1, 2, 3, 4] +[1, 2, 3, 4] +[1, 2, 3, 4] +``` + +# 有限迭代器 + +itertools 模块提供了多个函数(类),接收一个或多个迭代对象作为参数,对它们进行组合、分组和过滤等: + +- chain() +- compress() +- dropwhile() +- groupby() +- ifilter() +- ifilterfalse() +- islice() +- imap() +- starmap() +- tee() +- takewhile() +- izip() +- izip_longest() + +## chain + +`chain` 的使用形式如下: + +``` +chain(iterable1, iterable2, iterable3, ...) +``` + +`chain` 接收多个可迭代对象作为参数,将它们『连接』起来,作为一个新的迭代器返回。 + +```python +>>> from itertools import chain +>>> +>>> for item in chain([1, 2, 3], ['a', 'b', 'c']): +... print item +... +1 +2 +3 +a +b +c +``` + +`chain` 还有一个常见的用法: + +``` +chain.from_iterable(iterable) +``` + +接收一个可迭代对象作为参数,返回一个迭代器: + +```python +>>> from itertools import chain +>>> +>>> string = chain.from_iterable('ABCD') +>>> string.next() +'A' +``` + +## compress + +`compress` 的使用形式如下: + +``` +compress(data, selectors) +``` + +`compress` 可用于对数据进行筛选,当 selectors 的某个元素为 true 时,则保留 data 对应位置的元素,否则去除: + +```python +>>> from itertools import compress +>>> +>>> list(compress('ABCDEF', [1, 1, 0, 1, 0, 1])) +['A', 'B', 'D', 'F'] +>>> list(compress('ABCDEF', [1, 1, 0, 1])) +['A', 'B', 'D'] +>>> list(compress('ABCDEF', [True, False, True])) +['A', 'C'] +``` + +## dropwhile + +`dropwhile` 的使用形式如下: + +``` +dropwhile(predicate, iterable) +``` + +其中,predicate 是函数,iterable 是可迭代对象。对于 iterable 中的元素,如果 predicate(item) 为 true,则丢弃该元素,否则返回该项及所有后续项。 + +```python +>>> from itertools import dropwhile +>>> +>>> list(dropwhile(lambda x: x < 5, [1, 3, 6, 2, 1])) +[6, 2, 1] +>>> +>>> list(dropwhile(lambda x: x > 3, [2, 1, 6, 5, 4])) +[2, 1, 6, 5, 4] +``` + +## groupby + +`groupby` 用于对序列进行分组,它的使用形式如下: + +``` +groupby(iterable[, keyfunc]) +``` + +其中,iterable 是一个可迭代对象,keyfunc 是分组函数,用于对 iterable 的连续项进行分组,如果不指定,则默认对 iterable 中的连续相同项进行分组,返回一个 `(key, sub-iterator)` 的迭代器。 + +```python +>>> from itertools import groupby +>>> +>>> for key, value_iter in groupby('aaabbbaaccd'): +... print key, ':', list(value_iter) +... +a : ['a', 'a', 'a'] +b : ['b', 'b', 'b'] +a : ['a', 'a'] +c : ['c', 'c'] +d : ['d'] +>>> +>>> data = ['a', 'bb', 'ccc', 'dd', 'eee', 'f'] +>>> for key, value_iter in groupby(data, len): # 使用 len 函数作为分组函数 +... print key, ':', list(value_iter) +... +1 : ['a'] +2 : ['bb'] +3 : ['ccc'] +2 : ['dd'] +3 : ['eee'] +1 : ['f'] +>>> +>>> data = ['a', 'bb', 'cc', 'ddd', 'eee', 'f'] +>>> for key, value_iter in groupby(data, len): +... print key, ':', list(value_iter) +... +1 : ['a'] +2 : ['bb', 'cc'] +3 : ['ddd', 'eee'] +1 : ['f'] +``` + +## ifilter + +`ifilter` 的使用形式如下: + +``` +ifilter(function or None, sequence) +``` + +将 iterable 中 function(item) 为 True 的元素组成一个迭代器返回,如果 function 是 None,则返回 iterable 中所有计算为 True 的项。 + +```python +>>> from itertools import ifilter +>>> +>>> list(ifilter(lambda x: x < 6, range(10))) +[0, 1, 2, 3, 4, 5] +>>> +>>> list(ifilter(None, [0, 1, 2, 0, 3, 4])) +[1, 2, 3, 4] +``` + +## ifilterfalse + +`ifilterfalse` 的使用形式和 `ifilter` 类似,它将 iterable 中 function(item) 为 False 的元素组成一个迭代器返回,如果 function 是 None,则返回 iterable 中所有计算为 False 的项。 + +```python +>>> from itertools import ifilterfalse +>>> +>>> list(ifilterfalse(lambda x: x < 6, range(10))) +[6, 7, 8, 9] +>>> +>>> list(ifilter(None, [0, 1, 2, 0, 3, 4])) +[0, 0] +``` + +## islice + +`islice` 是切片选择,它的使用形式如下: + +``` +islice(iterable, [start,] stop [, step]) +``` + +其中,iterable 是可迭代对象,start 是开始索引,stop 是结束索引,step 是步长,start 和 step 可选。 + +```python +>>> from itertools import count, islice +>>> +>>> list(islice([10, 6, 2, 8, 1, 3, 9], 5)) +[10, 6, 2, 8, 1] +>>> +>>> list(islice(count(), 6)) +[0, 1, 2, 3, 4, 5] +>>> +>>> list(islice(count(), 3, 10)) +[3, 4, 5, 6, 7, 8, 9] +>>> list(islice(count(), 3, 10 ,2)) +[3, 5, 7, 9] +``` + +## imap + +`imap` 类似 `map` 操作,它的使用形式如下: + +``` +imap(func, iter1, iter2, iter3, ...) +``` + +`imap` 返回一个迭代器,元素为 `func(i1, i2, i3, ...)`,`i1`,`i2` 等分别来源于 `iter`, `iter2`。 + +```python +>>> from itertools import imap +>>> +>>> imap(str, [1, 2, 3, 4]) + +>>> +>>> list(imap(str, [1, 2, 3, 4])) +['1', '2', '3', '4'] +>>> +>>> list(imap(pow, [2, 3, 10], [4, 2, 3])) +[16, 9, 1000] +``` + +## tee + +`tee` 的使用形式如下: + +``` +tee(iterable [,n]) +``` + +`tee` 用于从 iterable 创建 n 个独立的迭代器,以元组的形式返回,n 的默认值是 2。 + +```python +>>> from itertools import tee +>>> +>>> tee('abcd') # n 默认为 2,创建两个独立的迭代器 +(, ) +>>> +>>> iter1, iter2 = tee('abcde') +>>> list(iter1) +['a', 'b', 'c', 'd', 'e'] +>>> list(iter2) +['a', 'b', 'c', 'd', 'e'] +>>> +>>> tee('abc', 3) # 创建三个独立的迭代器 +(, , ) +``` + +## takewhile + +`takewhile` 的使用形式如下: + +``` +takewhile(predicate, iterable) +``` + +其中,predicate 是函数,iterable 是可迭代对象。对于 iterable 中的元素,如果 predicate(item) 为 true,则保留该元素,只要 predicate(item) 为 false,则立即停止迭代。 + +```python +>>> from itertools import takewhile +>>> +>>> list(takewhile(lambda x: x < 5, [1, 3, 6, 2, 1])) +[1, 3] +>>> list(takewhile(lambda x: x > 3, [2, 1, 6, 5, 4])) +[] +``` + +## izip + +`izip` 用于将多个可迭代对象**对应位置**的元素作为一个元组,将所有元组『组成』一个迭代器,并返回。它的使用形式如下: + +``` +izip(iter1, iter2, ..., iterN) +``` + +如果某个可迭代对象不再生成值,则迭代停止。 + +```python +>>> from itertools import izip +>>> +>>> for item in izip('ABCD', 'xy'): +... print item +... +('A', 'x') +('B', 'y') +>>> for item in izip([1, 2, 3], ['a', 'b', 'c', 'd', 'e']): +... print item +... +(1, 'a') +(2, 'b') +(3, 'c') +``` + +## izip_longest + +`izip_longest` 跟 `izip` 类似,但迭代过程会持续到所有可迭代对象的元素都被迭代完。它的形式如下: + +``` +izip_longest(iter1, iter2, ..., iterN, [fillvalue=None]) +``` + +如果有指定 fillvalue,则会用其填充缺失的值,否则为 None。 + +```python +>>> from itertools import izip_longest +>>> +>>> for item in izip_longest('ABCD', 'xy'): +... print item +... +('A', 'x') +('B', 'y') +('C', None) +('D', None) +>>> +>>> for item in izip_longest('ABCD', 'xy', fillvalue='-'): +... print item +... +('A', 'x') +('B', 'y') +('C', '-') +('D', '-') +``` + +# 组合生成器 + +itertools 模块还提供了多个组合生成器函数,用于求序列的排列、组合等: + +- product +- permutations +- combinations +- combinations_with_replacement + +## product + +`product` 用于求多个可迭代对象的笛卡尔积,它跟嵌套的 for 循环等价。它的一般使用形式如下: + +``` +product(iter1, iter2, ... iterN, [repeat=1]) +``` + +其中,repeat 是一个关键字参数,用于指定重复生成序列的次数, + + +```python +>>> from itertools import product +>>> +>>> for item in product('ABCD', 'xy'): +... print item +... +('A', 'x') +('A', 'y') +('B', 'x') +('B', 'y') +('C', 'x') +('C', 'y') +('D', 'x') +('D', 'y') +>>> +>>> list(product('ab', range(3))) +[('a', 0), ('a', 1), ('a', 2), ('b', 0), ('b', 1), ('b', 2)] +>>> +>>> list(product((0,1), (0,1), (0,1))) +[(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)] +>>> +>>> list(product('ABC', repeat=2)) +[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')] +>>> +``` + +## permutations + +`permutations` 用于生成一个排列,它的一般使用形式如下: + +``` +permutations(iterable[, r]) +``` + +其中,r 指定生成排列的元素的长度,如果不指定,则默认为可迭代对象的元素长度。 + +```python +>>> from itertools import permutations +>>> +>>> permutations('ABC', 2) + +>>> +>>> list(permutations('ABC', 2)) +[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')] +>>> +>>> list(permutations('ABC')) +[('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')] +>>> +``` + +## combinations + +`combinations` 用于求序列的组合,它的使用形式如下: + +``` +combinations(iterable, r) +``` + +其中,r 指定生成组合的元素的长度。 + +```python +>>> from itertools import combinations +>>> +>>> list(combinations('ABC', 2)) +[('A', 'B'), ('A', 'C'), ('B', 'C')] +``` + +## combinations_with_replacement + +`combinations_with_replacement` 和 `combinations` 类似,但它生成的组合包含自身元素。 + +```python +>>> from itertools import combinations_with_replacement +>>> +>>> list(combinations_with_replacement('ABC', 2)) +[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')] +``` + +# 小结 + +- itertools 模块提供了很多用于产生多种类型迭代器的函数,它们的返回值不是 list,而是迭代器。 + +# 参考链接 + +- [itertools — Functions creating iterators for efficient looping](https://docs.python.org/2/library/itertools.html) +- [itertools – Iterator functions for efficient looping - Python Module of the Week](https://pymotw.com/2/itertools/) + + diff --git a/Testing/README.md b/Testing/README.md index 17c4efc..6a200d9 100644 --- a/Testing/README.md +++ b/Testing/README.md @@ -1,2 +1,180 @@ -# 测试 +# 单元测试 + +软件系统的开发是一个很复杂的过程,随着系统复杂性的提高,代码中隐藏的 bug 也可能变得越来越多。为了保证软件的质量,测试是一个必不可少的部分,甚至还有**测试驱动开发(Test-driven development, TDD)**的理念,也就是先测试再编码。 + +在计算机编程中,**单元测试(Unit Testing)**又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作,所谓的单元是指一个函数,一个模块,一个类等。 + +在 Python 中,我们可以使用 `unittest` 模块编写单元测试。 + +下面以[官方文档](https://docs.python.org/2/library/unittest.html)的例子进行介绍,该例子对字符串的一些方法进行测试: + +```python +# -*- coding: utf-8 -*- + +import unittest + +class TestStringMethods(unittest.TestCase): + + def test_upper(self): + self.assertEqual('foo'.upper(), 'FOO') # 判断两个值是否相等 + + def test_isupper(self): + self.assertTrue('FOO'.isupper()) # 判断值是否为 True + self.assertFalse('Foo'.isupper()) # 判断值是否为 False + + def test_split(self): + s = 'hello world' + self.assertEqual(s.split(), ['hello', 'world']) + # check that s.split fails when the separator is not a string + with self.assertRaises(TypeError): # 检测异常 + s.split(2) +``` + +在上面,我们定义了一个 TestStringMethods 类,它从 `unittest.TestCase` 继承。注意到,我们的方法名都是以 `test` 开头,表明该方法是测试方法,不以 `test` 开头的方法测试的时候不会被执行。 + +在方法里面,我们使用了`断言(assert)`判断程序运行的结果是否和预期相符。其中: + +- `assertEqual` 用于判断两个值是否相等; +- `assertTrue/assertFalse` 用于判断表达式的值是 True 还是 False; +- `assertRaises` 用于检测异常; + +断言方法主要有三种类型: + +- 检测两个值的大小关系:相等,大于,小于等 +- 检查逻辑表达式的值:True/Flase +- 检查异常 + +下面列举了部分常用的断言方法: + +| Method | Checks that | +| :--- | :--- | +| assertEqual(a, b) | a == b | +| assertNotEqual(a, b) | a != b | +| assertGreater(a, b) | a > b | +| assertGreaterEqual(a, b) | a >= b | +| assertLess(a, b) | a < b | +| assertLessEqual(a, b) | a <= b | +| assertTrue(x) | bool(x) is True | +| assertFalse(x) | bool(x) is False | +| assertIs(a, b) | a is b | +| assertIsNot(a, b) | a is not b | +| assertIsNone(x) | x is None | +| assertIsNotNone(x) | x is not None | +| assertIn(a, b) | a in b | +| assertNotIn(a, b) | a not in b | +| assertIsInstance(a, b) | isinstance(a, b) | +| assertNotIsInstance(a, b) | not isinstance(a, b) | + +现在,让我们来运行上面的单元测试,将上面的代码保存为文件 `mytest.py`,通过 `-m unittest` 参数运行单元测试: + +``` +$ python -m unittest mytest +test_isupper (mytest.TestStringMethods) ... ok +test_split (mytest.TestStringMethods) ... ok +test_upper (mytest.TestStringMethods) ... ok +``` + +执行结果: + +```python +... +---------------------------------------------------------------------- +Ran 3 tests in 0.000s + +OK +``` + +上面的结果表明测试通过,我们也可以加 `-v` 参数得到更加详细的测试结果: + +```python +$ python -m unittest -v mytest +test_isupper (mytest.TestStringMethods) ... ok +test_split (mytest.TestStringMethods) ... ok +test_upper (mytest.TestStringMethods) ... ok + +---------------------------------------------------------------------- +Ran 3 tests in 0.000s + +OK +``` + +上面这种运行单元测试的方法是我们推荐的做法,当然,你也可以在代码的最后添加两行: + +```python +if __name__ == '__main__': + unittest.main() +``` + +然后再直接运行: + +```python +$ python mytest.py +``` + +# setUp 和 tearDown + +在某些情况下,我们需要在每个测试方法执行前和执行后做一些相同的操作,比如我们想在每个测试方法执行前连接数据库,执行后断开数据库连接,为了避免在每个测试方法中编写同样的代码,我们可以使用 setUp 和 tearDown 方法,比如: + +```python +# -*- coding: utf-8 -*- + +import unittest + +class TestStringMethods(unittest.TestCase): + + def setUp(self): # 在每个测试方法执行前被调用 + print 'setUp, Hello' + + def tearDown(self): # 在每个测试方法执行后被调用 + print 'tearDown, Bye!' + + def test_upper(self): + self.assertEqual('foo'.upper(), 'FOO') # 判断两个值是否相等 + + def test_isupper(self): + self.assertTrue('FOO'.isupper()) # 判断值是否为 True + self.assertFalse('Foo'.isupper()) # 判断值是否为 False + + def test_split(self): + s = 'hello world' + self.assertEqual(s.split(), ['hello', 'world']) + # check that s.split fails when the separator is not a string + with self.assertRaises(TypeError): # 检测异常 + s.split(2) +``` + +看看执行结果: + +``` +$ python -m unittest -v mytest +test_isupper (mytest.TestStringMethods) ... setUp, Hello +tearDown, Bye! +ok +test_split (mytest.TestStringMethods) ... setUp, Hello +tearDown, Bye! +ok +test_upper (mytest.TestStringMethods) ... setUp, Hello +tearDown, Bye! +ok + +---------------------------------------------------------------------- +Ran 3 tests in 0.000s + +OK +``` + +# 小结 + +- 通过从 `unittest.TestCase` 继承来编写测试类。 +- 使用断言方法判断程序运行的结果是否和预期相符。 +- setUp 在每个测试方法执行前被调用,tearDown 在每个测试方法执行后被调用。 + +# 参考资料 + +- [unittest — Unit testing framework — Python 2.7.12 documentation](https://docs.python.org/2/library/unittest.html) +- [python单元测试unittest | Lucia Garden](http://luciastar.com/2016/05/16/python%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95unittest/) +- [单元测试 - 廖雪峰的官方网站](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/00140137128705556022982cfd844b38d050add8565dcb9000) +- [单元测试 - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95) +- [“单元测试要做多细?” | 酷 壳 - CoolShell.cn](http://coolshell.cn/articles/8209.html) + diff --git a/Testing/unit_testing.md b/Testing/unit_testing.md deleted file mode 100644 index bbc5cdc..0000000 --- a/Testing/unit_testing.md +++ /dev/null @@ -1,2 +0,0 @@ -# 单元测试 - diff --git a/Third-Party-Modules/README.md b/Third-Party-Modules/README.md index 6ea6395..db1ae0b 100644 --- a/Third-Party-Modules/README.md +++ b/Third-Party-Modules/README.md @@ -1,2 +1,13 @@ # 第三方模块 +在前面,我们介绍了一个优秀的第三方库 -- requests,本章再介绍两个第三方库: + +- [celery](./celery.md) +- [click](./click.md) + +其中: + +- celery 是一个强大的分布式任务队列,通常用于实现异步任务; +- click 是快速创建命令行的神器; + + diff --git a/Third-Party-Modules/celery.md b/Third-Party-Modules/celery.md new file mode 100644 index 0000000..cb52fcb --- /dev/null +++ b/Third-Party-Modules/celery.md @@ -0,0 +1,406 @@ +# Celery + +在程序的运行过程中,我们经常会碰到一些耗时耗资源的操作,为了避免它们阻塞主程序的运行,我们经常会采用多线程或异步任务。比如,在 Web 开发中,对新用户的注册,我们通常会给他发一封激活邮件,而发邮件是个 IO 阻塞式任务,如果直接把它放到应用当中,就需要等邮件发出去之后才能进行下一步操作,此时用户只能等待再等待。更好的方式是在业务逻辑中触发一个发邮件的异步任务,而主程序可以继续往下运行。 + +[Celery][0] 是一个强大的分布式任务队列,它可以让任务的执行完全脱离主程序,甚至可以被分配到其他主机上运行。我们通常使用它来实现异步任务(async task)和定时任务(crontab)。它的架构组成如下图: + +![Celery_framework](https://ooo.0o0.ooo/2016/12/10/584bbf78e1783.png) + +可以看到,Celery 主要包含以下几个模块: + +- 任务模块 + + 包含异步任务和定时任务。其中,**异步任务通常在业务逻辑中被触发并发往任务队列,而定时任务由 Celery Beat 进程周期性地将任务发往任务队列**。 + +- 消息中间件 Broker + + Broker,即为任务调度队列,**接收任务生产者发来的消息(即任务),将任务存入队列**。Celery 本身不提供队列服务,官方推荐使用 RabbitMQ 和 Redis 等。 + +- 任务执行单元 Worker + + Worker 是执行任务的处理单元,**它实时监控消息队列,获取队列中调度的任务,并执行它**。 + +- 任务结果存储 Backend + + Backend 用于**存储任务的执行结果**,以供查询。同消息中间件一样,存储也可使用 RabbitMQ, Redis 和 MongoDB 等。 + +# 异步任务 + +使用 Celery 实现异步任务主要包含三个步骤: + +1. 创建一个 Celery 实例 +2. 启动 Celery Worker +3. 应用程序调用异步任务 + +## 快速入门 + +为了简单起见,对于 Broker 和 Backend,这里都使用 redis。在运行下面的例子之前,请确保 redis 已正确安装,并开启 redis 服务,当然,celery 也是要安装的。可以使用下面的命令来安装 celery 及相关依赖: + +``` +$ pip install 'celery[redis]' +``` + +### 创建 Celery 实例 + +将下面的代码保存为文件 `tasks.py`: + +```python +# -*- coding: utf-8 -*- + +import time +from celery import Celery + +broker = 'redis://127.0.0.1:6379' +backend = 'redis://127.0.0.1:6379/0' + +app = Celery('my_task', broker=broker, backend=backend) + +@app.task +def add(x, y): + time.sleep(5) # 模拟耗时操作 + return x + y +``` + +上面的代码做了几件事: + +- 创建了一个 Celery 实例 app,名称为 `my_task`; +- 指定消息中间件用 redis,URL 为 `redis://127.0.0.1:6379`; +- 指定存储用 redis,URL 为 `redis://127.0.0.1:6379/0`; +- 创建了一个 Celery 任务 `add`,当函数被 `@app.task` 装饰后,就成为可被 Celery 调度的任务; + +### 启动 Celery Worker + +在当前目录,使用如下方式启动 Celery Worker: + +``` +$ celery worker -A tasks --loglevel=info +``` + +其中: + +- 参数 `-A` 指定了 Celery 实例的位置,本例是在 `tasks.py` 中,Celery 会自动在该文件中寻找 Celery 对象实例,当然,我们也可以自己指定,在本例,使用 `-A tasks.app`; +- 参数 `--loglevel` 指定了日志级别,默认为 warning,也可以使用 `-l info` 来表示; + +在生产环境中,我们通常会使用 [Supervisor](http://supervisord.org/) 来控制 Celery Worker 进程。 + +启动成功后,控制台会显示如下输出: + +![celery](https://ooo.0o0.ooo/2016/12/10/584b7da9f2c17.png) + +### 调用任务 + +现在,我们可以在应用程序中使用 `delay()` 或 `apply_async()` 方法来调用任务。 + +在当前目录打开 Python 控制台,输入以下代码: + +```python +>>> from tasks import add +>>> add.delay(2, 8) + +``` + +在上面,我们从 `tasks.py` 文件中导入了 `add` 任务对象,然后使用 `delay()` 方法将任务发送到消息中间件(Broker),Celery Worker 进程监控到该任务后,就会进行执行。我们将窗口切换到 Worker 的启动窗口,会看到多了两条日志: + +``` +[2016-12-10 12:00:50,376: INFO/MainProcess] Received task: tasks.add[2272ddce-8be5-493f-b5ff-35a0d9fe600f] +[2016-12-10 12:00:55,385: INFO/PoolWorker-4] Task tasks.add[2272ddce-8be5-493f-b5ff-35a0d9fe600f] succeeded in 5.00642602402s: 10 +``` + +这说明任务已经被调度并执行成功。 + +另外,我们如果想获取执行后的结果,可以这样做: + +```python +>>> result = add.delay(2, 6) +>>> result.ready() # 使用 ready() 判断任务是否执行完毕 +False +>>> result.ready() +False +>>> result.ready() +True +>>> result.get() # 使用 get() 获取任务结果 +8 +``` + +在上面,我们是在 Python 的环境中调用任务。事实上,我们通常在应用程序中调用任务。比如,将下面的代码保存为 `client.py`: + +```python +# -*- coding: utf-8 -*- + +from tasks import add + +# 异步任务 +add.delay(2, 8) + +print 'hello world' +``` + +运行命令 `$ python client.py`,可以看到,虽然任务函数 `add` 需要等待 5 秒才返回执行结果,但由于它是一个异步任务,不会阻塞当前的主程序,因此主程序会往下执行 `print` 语句,打印出结果。 + +## 使用配置 + +在上面的例子中,我们直接把 Broker 和 Backend 的配置写在了程序当中,更好的做法是将配置项统一写入到一个配置文件中,通常我们将该文件命名为 `celeryconfig.py`。Celery 的配置比较多,可以在[官方文档][1]查询每个配置项的含义。 + +下面,我们再看一个例子。项目结构如下: + +``` +celery_demo # 项目根目录 + ├── celery_app # 存放 celery 相关文件 + │   ├── __init__.py + │   ├── celeryconfig.py # 配置文件 + │   ├── task1.py # 任务文件 1 + │   └── task2.py # 任务文件 2 + └── client.py # 应用程序 +``` + +`__init__.py` 代码如下: + +```python +# -*- coding: utf-8 -*- + +from celery import Celery + +app = Celery('demo') # 创建 Celery 实例 +app.config_from_object('celery_app.celeryconfig') # 通过 Celery 实例加载配置模块 +``` + +`celeryconfig.py` 代码如下: + +```python +BROKER_URL = 'redis://127.0.0.1:6379' # 指定 Broker +CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' # 指定 Backend + +CELERY_TIMEZONE='Asia/Shanghai' # 指定时区,默认是 UTC +# CELERY_TIMEZONE='UTC' + +CELERY_IMPORTS = ( # 指定导入的任务模块 + 'celery_app.task1', + 'celery_app.task2' +) +``` + +`task1.py` 代码如下: + +```python +import time +from celery_app import app + +@app.task +def add(x, y): + time.sleep(2) + return x + y +``` + +`task2.py` 代码如下: + +```python +import time +from celery_app import app + +@app.task +def multiply(x, y): + time.sleep(2) + return x * y +``` + +`client.py` 代码如下: + +```python +# -*- coding: utf-8 -*- + +from celery_app import task1 +from celery_app import task2 + +task1.add.apply_async(args=[2, 8]) # 也可用 task1.add.delay(2, 8) +task2.multiply.apply_async(args=[3, 7]) # 也可用 task2.multiply.delay(3, 7) + +print 'hello world' +``` + +现在,让我们启动 Celery Worker 进程,在项目的根目录下执行下面命令: + +``` +celery_demo $ celery -A celery_app worker --loglevel=info +``` + +接着,运行 `$ python client.py`,它会发送两个异步任务到 Broker,在 Worker 的窗口我们可以看到如下输出: + +``` +[2016-12-10 13:51:58,939: INFO/MainProcess] Received task: celery_app.task1.add[9ccffad0-aca4-4875-84ce-0ccfce5a83aa] +[2016-12-10 13:51:58,941: INFO/MainProcess] Received task: celery_app.task2.multiply[64b1f889-c892-4333-bd1d-ac667e677a8a] +[2016-12-10 13:52:00,948: INFO/PoolWorker-3] Task celery_app.task1.add[9ccffad0-aca4-4875-84ce-0ccfce5a83aa] succeeded in 2.00600231002s: 10 +[2016-12-10 13:52:00,949: INFO/PoolWorker-4] Task celery_app.task2.multiply[64b1f889-c892-4333-bd1d-ac667e677a8a] succeeded in 2.00601326401s: 21 +``` + +## delay 和 apply_async + +在前面的例子中,我们使用 `delay()` 或 `apply_async()` 方法来调用任务。事实上,`delay` 方法封装了 `apply_async`,如下: + +```python +def delay(self, *partial_args, **partial_kwargs): + """Shortcut to :meth:`apply_async` using star arguments.""" + return self.apply_async(partial_args, partial_kwargs) +``` + +也就是说,`delay` 是使用 `apply_async` 的快捷方式。`apply_async` 支持更多的参数,它的一般形式如下: + +```python +apply_async(args=(), kwargs={}, route_name=None, **options) +``` + +apply_async 常用的参数如下: + +- countdown:指定多少秒后执行任务 + +``` +task1.apply_async(args=(2, 3), countdown=5) # 5 秒后执行任务 +``` + +- eta (estimated time of arrival):指定任务被调度的具体时间,参数类型是 datetime + +```python +from datetime import datetime, timedelta + +# 当前 UTC 时间再加 10 秒后执行任务 +task1.multiply.apply_async(args=[3, 7], eta=datetime.utcnow() + timedelta(seconds=10)) +``` + +- expires:任务过期时间,参数类型可以是 int,也可以是 datetime + +```python +task1.multiply.apply_async(args=[3, 7], expires=10) # 10 秒后过期 +``` + +更多的参数列表可以在[官方文档](http://docs.celeryproject.org/en/latest/reference/celery.app.task.html#celery.app.task.Task.apply_async)中查看。 + +# 定时任务 + +Celery 除了可以执行**异步任务**,也支持执行**周期性任务(Periodic Tasks)**,或者说定时任务。Celery Beat 进程通过读取配置文件的内容,周期性地将定时任务发往任务队列。 + +让我们看看例子,项目结构如下: + +``` +celery_demo # 项目根目录 + ├── celery_app # 存放 celery 相关文件 +    ├── __init__.py +    ├── celeryconfig.py # 配置文件 +    ├── task1.py # 任务文件 +    └── task2.py # 任务文件 +``` + +`__init__.py` 代码如下: + +```python +# -*- coding: utf-8 -*- + +from celery import Celery + +app = Celery('demo') +app.config_from_object('celery_app.celeryconfig') +``` + +`celeryconfig.py` 代码如下: + +```python +# -*- coding: utf-8 -*- + +from datetime import timedelta +from celery.schedules import crontab + +# Broker and Backend +BROKER_URL = 'redis://127.0.0.1:6379' +CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0' + +# Timezone +CELERY_TIMEZONE='Asia/Shanghai' # 指定时区,不指定默认为 'UTC' +# CELERY_TIMEZONE='UTC' + +# import +CELERY_IMPORTS = ( + 'celery_app.task1', + 'celery_app.task2' +) + +# schedules +CELERYBEAT_SCHEDULE = { + 'add-every-30-seconds': { + 'task': 'celery_app.task1.add', + 'schedule': timedelta(seconds=30), # 每 30 秒执行一次 + 'args': (5, 8) # 任务函数参数 + }, + 'multiply-at-some-time': { + 'task': 'celery_app.task2.multiply', + 'schedule': crontab(hour=9, minute=50), # 每天早上 9 点 50 分执行一次 + 'args': (3, 7) # 任务函数参数 + } +} +``` + +`task1.py` 代码如下: + +```python +import time +from celery_app import app + +@app.task +def add(x, y): + time.sleep(2) + return x + y +``` + +`task2.py` 代码如下: + +```python +import time +from celery_app import app + +@app.task +def multiply(x, y): + time.sleep(2) + return x * y +``` + +现在,让我们启动 Celery Worker 进程,在项目的根目录下执行下面命令: + +``` +celery_demo $ celery -A celery_app worker --loglevel=info +``` + +接着,启动 Celery Beat 进程,定时将任务发送到 Broker,在项目根目录下执行下面命令: + +``` +celery_demo $ celery beat -A celery_app +celery beat v4.0.1 (latentcall) is starting. +__ - ... __ - _ +LocalTime -> 2016-12-11 09:48:16 +Configuration -> + . broker -> redis://127.0.0.1:6379// + . loader -> celery.loaders.app.AppLoader + . scheduler -> celery.beat.PersistentScheduler + . db -> celerybeat-schedule + . logfile -> [stderr]@%WARNING + . maxinterval -> 5.00 minutes (300s) +``` + +之后,在 Worker 窗口我们可以看到,任务 `task1` 每 30 秒执行一次,而 `task2` 每天早上 9 点 50 分执行一次。 + +在上面,我们用两个命令启动了 Worker 进程和 Beat 进程,我们也可以将它们放在一个命令中: + +``` +$ celery -B -A celery_app worker --loglevel=info +``` + +Celery 周期性任务也有多个配置项,可参考[官方文档](http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html)。 + +# 参考资料 + +- [Celery - Distributed Task Queue — Celery 4.0.1 documentation](http://docs.celeryproject.org/en/latest/index.html) +- [使用Celery - Python之美](https://zhuanlan.zhihu.com/p/22304455) +- [分布式任务队列Celery的介绍 – 思诚之道](http://www.bjhee.com/celery.html) +- [异步任务神器 Celery 简明笔记](http://www.jianshu.com/p/1840035cb510) + + +[0]: https://github.com/celery/celery +[1]: http://docs.celeryproject.org/en/latest/userguide/configuration.html + + diff --git a/Third-Party-Modules/click.md b/Third-Party-Modules/click.md new file mode 100644 index 0000000..0318fd6 --- /dev/null +++ b/Third-Party-Modules/click.md @@ -0,0 +1,426 @@ +# Click + +[Click][0] 是 [Flask][1] 的开发团队 [Pallets][2] 的另一款开源项目,它是用于快速创建命令行的第三方模块。我们知道,Python 内置了一个 [Argparse][3] 的标准库用于创建命令行,但使用起来有些繁琐,[Click][0] 相比于 [Argparse][3],就好比 [requests][4] 相比于 [urllib][5]。 + +# 快速使用 + +Click 的使用大致有两个步骤: + +1. 使用 `@click.command()` 装饰一个函数,使之成为命令行接口; +2. 使用 `@click.option()` 等装饰函数,为其添加命令行选项等。 + +它的一种典型使用形式如下: + +```python +import click + +@click.command() +@click.option('--param', default=default_value, help='description') +def func(param): + pass +``` + +下面,让我们看一下[官方文档][6]的入门例子: + +```python +import click + +@click.command() +@click.option('--count', default=1, help='Number of greetings.') +@click.option('--name', prompt='Your name', help='The person to greet.') +def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for x in range(count): + click.echo('Hello %s!' % name) + +if __name__ == '__main__': + hello() +``` + +在上面的例子中,函数 `hello` 有两个参数:count 和 name,它们的值从命令行中获取。 + +- `@click.command()` 使函数 `hello` 成为命令行接口; +- `@click.option` 的第一个参数指定了命令行选项的名称,不难猜到,count 的默认值是 1,name 的值从输入获取; +- 使用 `click.echo` 进行输出是为了获得更好的兼容性,因为 `print` 在 Python2 和 Python3 的用法有些差别。 + +看看执行情况: + +``` +$ python hello.py +Your name: Ethan # 这里会显示 'Your name: '(对应代码中的 prompt),接受用户输入 +Hello Ethan! + +$ python hello.py --help # click 帮我们自动生成了 `--help` 用法 +Usage: hello.py [OPTIONS] + + Simple program that greets NAME for a total of COUNT times. + +Options: + --count INTEGER Number of greetings. + --name TEXT The person to greet. + --help Show this message and exit. + +$ python hello.py --count 3 --name Ethan # 指定 count 和 name 的值 +Hello Ethan! +Hello Ethan! +Hello Ethan! + +$ python hello.py --count=3 --name=Ethan # 也可以使用 `=`,和上面等价 +Hello Ethan! +Hello Ethan! +Hello Ethan! + +$ python hello.py --name=Ethan # 没有指定 count,默认值是 1 +Hello Ethan! +``` + +# click.option + +option 最基本的用法就是通过指定命令行选项的名称,从命令行读取参数值,再将其传递给函数。在上面的例子,我们看到,除了设置命令行选项的名称,我们还会指定默认值,help 说明等,option 常用的设置参数如下: + +- default: 设置命令行参数的默认值 +- help: 参数说明 +- type: 参数类型,可以是 string, int, float 等 +- prompt: 当在命令行中没有输入相应的参数时,会根据 prompt 提示用户输入 +- nargs: 指定命令行参数接收的值的个数 + +下面,我们再看看相关的例子。 + +## 指定 type + +我们可以使用 `type` 来指定参数类型: + +```python +import click + +@click.command() +@click.option('--rate', type=float, help='rate') # 指定 rate 是 float 类型 +def show(rate): + click.echo('rate: %s' % rate) + +if __name__ == '__main__': + show() +``` + +执行情况: + +``` +$ python click_type.py --rate 1 +rate: 1.0 +$ python click_type.py --rate 0.66 +rate: 0.66 +``` + +## 可选值 + +在某些情况下,一个参数的值只能是某些可选的值,如果用户输入了其他值,我们应该提示用户输入正确的值。在这种情况下,我们可以通过 `click.Choice()` 来限定: + +```python +import click + +@click.command() +@click.option('--gender', type=click.Choice(['man', 'woman'])) # 限定值 +def choose(gender): + click.echo('gender: %s' % gender) + +if __name__ == '__main__': + choose() +``` + +执行情况: + +``` +$ python click_choice.py --gender boy +Usage: click_choice.py [OPTIONS] + +Error: Invalid value for "--gender": invalid choice: boy. (choose from man, woman) + +$ python click_choice.py --gender man +gender: man +``` + +## 多值参数 + +有时,一个参数需要接收多个值。**option 支持设置固定长度的参数值**,通过 `nargs` 指定。 + +看看例子就明白了: + +```python +import click + +@click.command() +@click.option('--center', nargs=2, type=float, help='center of the circle') +@click.option('--radius', type=float, help='radius of the circle') +def circle(center, radius): + click.echo('center: %s, radius: %s' % (center, radius)) + +if __name__ == '__main__': + circle() +``` + +在上面的例子中,option 指定了两个参数:center 和 radius,其中,center 表示二维平面上一个圆的圆心坐标,接收两个值,以元组的形式将值传递给函数,而 radius 表示圆的半径。 + +执行情况: + +``` +$ python click_multi_values.py --center 3 4 --radius 10 +center: (3.0, 4.0), radius: 10.0 + +$ python click_multi_values.py --center 3 4 5 --radius 10 +Usage: click_multi_values.py [OPTIONS] + +Error: Got unexpected extra argument (5) +``` + +## 输入密码 + +有时,在输入密码的时候,我们希望能隐藏显示。option 提供了两个参数来设置密码的输入:hide_input 和 confirmation_promt,其中,hide_input 用于隐藏输入,confirmation_promt 用于重复输入。 + +看看例子: + +```python +import click + +@click.command() +@click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True) +def input_password(password): + click.echo('password: %s' % password) + + +if __name__ == '__main__': + input_password() +``` + +执行情况: + +``` +$ python click_password.py +Password: # 不会显示密码 +Repeat for confirmation: # 重复一遍 +password: 666666 +``` + +由于上面的写法有点繁琐,click 也提供了一种快捷的方式,通过使用 `@click.password_option()`,上面的代码可以简写成: + +```python +import click + +@click.command() +@click.password_option() +def input_password(password): + click.echo('password: %s' % password) + +if __name__ == '__main__': + input_password() +``` + +## 改变命令行程序的执行 + +有些参数会改变命令行程序的执行,比如在终端输入 `python` 是进入 python 控制台,而输入 `python --version` 是打印 python 版本。Click 提供 eager 标识对参数名进行标识,如果输入该参数,则会拦截既定的命令行执行流程,跳转去执行一个回调函数。 + +让我们看看例子: + +```python +import click + +def print_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + click.echo('Version 1.0') + ctx.exit() + +@click.command() +@click.option('--version', is_flag=True, callback=print_version, + expose_value=False, is_eager=True) +@click.option('--name', default='Ethan', help='name') +def hello(name): + click.echo('Hello %s!' % name) + +if __name__ == '__main__': + hello() +``` + +其中: + +- `is_eager=True` 表明该命令行选项优先级高于其他选项; +- `expose_value=False` 表示如果没有输入该命令行选项,会执行既定的命令行流程; +- `callback` 指定了输入该命令行选项时,要跳转执行的函数; + +执行情况: + +``` +$ python click_eager.py +Hello Ethan! + +$ python click_eager.py --version # 拦截既定的命令行执行流程 +Version 1.0 + +$ python click_eager.py --name Michael +Hello Michael! + +$ python click_eager.py --version --name Ethan # 忽略 name 选项 +Version 1.0 +``` + +# click.argument + +我们除了使用 `@click.option` 来添加**可选参数**,还会经常使用 `@click.argument` 来添加**固定参数**。它的使用和 option 类似,但支持的功能比 option 少。 + +## 入门使用 + +下面是一个简单的例子: + +```python +import click + +@click.command() +@click.argument('coordinates') +def show(coordinates): + click.echo('coordinates: %s' % coordinates) + +if __name__ == '__main__': + show() +``` + +看看执行情况: + +```click_argument +$ python click_argument.py # 错误,缺少参数 coordinates +Usage: click_argument.py [OPTIONS] COORDINATES + +Error: Missing argument "coordinates". + +$ python click_argument.py --help # argument 指定的参数在 help 中没有显示 +Usage: click_argument.py [OPTIONS] COORDINATES + +Options: + --help Show this message and exit. + +$ python click_argument.py --coordinates 10 # 错误用法,这是 option 参数的用法 +Error: no such option: --coordinates + +$ python click_argument.py 10 # 正确,直接输入值即可 +coordinates: 10 +``` + +## 多个 argument + +我们再来看看多个 argument 的例子: + +```python +import click + +@click.command() +@click.argument('x') +@click.argument('y') +@click.argument('z') +def show(x, y, z): + click.echo('x: %s, y: %s, z:%s' % (x, y, z)) + +if __name__ == '__main__': + show() +``` + +执行情况: + +``` +$ python click_argument.py 10 20 30 +x: 10, y: 20, z:30 + +$ python click_argument.py 10 +Usage: click_argument.py [OPTIONS] X Y Z + +Error: Missing argument "y". + +$ python click_argument.py 10 20 +Usage: click_argument.py [OPTIONS] X Y Z + +Error: Missing argument "z". + +$ python click_argument.py 10 20 30 40 +Usage: click_argument.py [OPTIONS] X Y Z + +Error: Got unexpected extra argument (40) +``` + +## 不定参数 + +argument 还有另外一种常见的用法,就是接收不定量的参数,让我们看看例子: + +```python +import click + +@click.command() +@click.argument('src', nargs=-1) +@click.argument('dst', nargs=1) +def move(src, dst): + click.echo('move %s to %s' % (src, dst)) + +if __name__ == '__main__': + move() +``` + +其中,`nargs=-1` 表明参数 `src` 接收不定量的参数值,参数值会以 tuple 的形式传入函数。如果 `nargs` 大于等于 1,表示接收 `nargs` 个参数值,上面的例子中,`dst` 接收一个参数值。 + +让我们看看执行情况: + +``` +$ python click_argument.py file1 trash # src=('file1',) dst='trash' +move (u'file1',) to trash + +$ python click_argument.py file1 file2 file3 trash # src=('file1', 'file2', 'file3') dst='trash' +move (u'file1', u'file2', u'file3') to trash +``` + +# 彩色输出 + +在前面的例子中,我们使用 `click.echo` 进行输出,如果配合 [colorama][7] 这个模块,我们可以使用 `click.secho` 进行彩色输出,在使用之前,使用 pip 安装 colorama: + +``` +$ pip install colorama +``` + +看看例子: + +```python +import click + +@click.command() +@click.option('--name', help='The person to greet.') +def hello(name): + click.secho('Hello %s!' % name, fg='red', underline=True) + click.secho('Hello %s!' % name, fg='yellow', bg='black') + +if __name__ == '__main__': + hello() +``` + +其中: + +- `fg` 表示前景颜色(即字体颜色),可选值有:BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE 等; +- `bg` 表示背景颜色,可选值有:BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE 等; +- `underline` 表示下划线,可选的样式还有:`dim=True`,`bold=True` 等; + +# 小结 + +- 使用 `click.command()` 装饰一个函数,使其成为命令行接口。 +- 使用 `click.option()` 添加可选参数,支持设置固定长度的参数值。 +- 使用 `click.argument()` 添加固定参数,支持设置不定长度的参数值。 + +# 参考资料 + +- [Click Documentation (6.0)](http://click.pocoo.org/6/) +- [Python Click 学习笔记 | I sudo X](https://isudox.com/2016/09/03/learning-python-package-click/) +- [click模块 - cdwanze](http://cdwanze.github.io/%E7%94%B5%E8%84%91/python/%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A8%A1%E5%9D%97/click%E6%A8%A1%E5%9D%97.html) + + +[0]: https://github.com/pallets/click +[1]: https://github.com/pallets/flask +[2]: https://github.com/pallets +[3]: https://docs.python.org/2/howto/argparse.html +[4]: https://github.com/kennethreitz/requests +[5]: https://docs.python.org/2/library/urllib.html +[6]: http://click.pocoo.org/6/ +[7]: https://pypi.python.org/pypi/colorama + + diff --git a/book.json b/book.json index 6dcb6ce..0520c60 100644 --- a/book.json +++ b/book.json @@ -1,5 +1,5 @@ { - "author": "ethan-funny", + "author": "Ethan", "description": "Notes for Learning Python", "extension": null, "generator": "site", @@ -12,8 +12,7 @@ "weibo": null }, "sidebar": { - "Blog": "https://funhacks.net", - "Github": "https://github.com/ethan-funny/explore-python/" + "书籍源码": "https://github.com/ethan-funny/explore-python/" } }, "output": null, @@ -28,10 +27,10 @@ "right": 62, "top": 36 }, - "pageNumbers": false, + "pageNumbers": true, "paperSize": "a4" }, - "plugins": [], + "plugins": ["splitter"], "pluginsConfig": { "theme-default": { "showLevel": false diff --git a/config.json b/config.json new file mode 100644 index 0000000..7b54f47 --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "name": "Python 之旅", + "introduction":"Explore Python,Python 学习之旅。", + "path": { + "toc": "SUMMARY.md" + } +} diff --git a/cover.png b/cover.png new file mode 100644 index 0000000..6b46fca Binary files /dev/null and b/cover.png differ diff --git a/cover/background.jpg b/cover/background.jpg new file mode 100644 index 0000000..bdf1798 Binary files /dev/null and b/cover/background.jpg differ diff --git a/cover/cover.png b/cover/cover.png new file mode 100644 index 0000000..6b46fca Binary files /dev/null and b/cover/cover.png differ diff --git a/cover/logo.png b/cover/logo.png new file mode 100644 index 0000000..608a4c0 Binary files /dev/null and b/cover/logo.png differ